This service signs for the UTEXO bridge. It runs inside an AWS Nitro Enclave.
The enclave makes its own keys. It checks each bridge operation itself. It signs only when all checks pass. The host is not trusted.
The bridge has two primary flows:
| Flow | Direction | User does | Enclave signs | Signer image |
|---|---|---|---|---|
| Mint | EVM -> RGB | Locks ERC-20 tokens on EVM | A Bitcoin PSBT that mints RGB units | mint-signer |
| Burn | RGB -> EVM | Burns RGB units on Bitcoin | An EIP-712 fundsOut release, and its gas transaction |
burn-signer |
Each flow has its own enclave image, its own seed and its own PCR0. A mint signer refuses burn requests. A burn signer refuses mint requests.
Read these first:
docs/mint-flow.md- the mint flow, step by step.docs/burn-flow.md- the burn flow, step by step.
Reference material:
docs/tee-spec.md- the full specification: trust model, signing rules, limits.docs/pubkey-attestation.md- how to prove that a signing key belongs to attested enclave code. Theattest-verifyCLI.docs/verify-a-signer.md- how anyone checks a published burn-signer attestation bundle and the on-chain signer list.docs/kms-persistence.md- how the mint signer keeps its seed with AWS KMS and S3.docs/parent-mtls.md- the required mTLS between the parent and its clients. RPC roles. Host rollout.docs/clone-cli-results.md- result codes of thecloneCLI command.docs/diagrams/- Mermaid diagrams.enclave-proto/README.md- where the wire schema comes from, and how to sync it.
- The listener (
federated-signer-node) sends a signing request to the parent over gRPC with mTLS. - The parent runs on the EC2 host. It is not trusted. It sends the request to the enclave over vsock.
- The enclave holds the keys. It checks the request and signs, or refuses.
- The enclave reads Bitcoin data (Electrum) and EVM data (JSON-RPC) through
host vsock proxies. EVM TLS ends inside the enclave. Electrum uses TLS
only when the configured URL uses
ssl://. - The parent sends Bitcoin block headers to the enclave. On mainnet, the enclave checks proof of work and maintains its own header chain. Signet and regtest use weaker checks. See the specification.
See the component diagram and the deployment diagram.
| Crate | Binary | Description |
|---|---|---|
enclave/ |
utexo-bridge-enclave |
Runs inside the Nitro Enclave. Keys, checks, signing, SPV chain, cloning, attestation. |
enclave-proto/ |
- | Vendored enclave protobuf package. Pre-generated Rust, no build.rs. |
attestation-verify/ |
- | Nitro attestation verifier (COSE_Sign1, cert chain to the AWS root, PCRs). Also the encoding of the security-policy commitment. The enclave and the parent both use it. |
parent/ |
utexo-bridge-parent |
gRPC server on the EC2 host. Translates ParentService RPCs to the enclave wire protocol. Syncs Bitcoin headers. |
parent/ |
utexo-bridge-parent-cli |
Direct enclave client: endpoints, key init, cloning, keys, header sync, manual signing, health. |
parent/ |
attest-verify |
Gets an attested public key through the parent and verifies it end to end. |
parent/ is a separate cargo workspace with its own Cargo.lock. See
Proto source for the reason.
The user locks tokens on EVM. The bridge mints the same value of RGB units to the user. The mint signer signs the Bitcoin transaction that does the mint.
Before it signs, the mint signer:
- Gets the EVM deposit receipt itself. The receipt must be a success and at
least
EVM_MIN_CONFIRMATIONSblocks deep (default 12). - Finds exactly one
BridgeFundsInevent from the pinnedFUNDS_IN_CONTRACT. TheoperationId, amount and commission must match the request. - Validates the RGB consignment (full RGB consensus). The asset must be the
pinned
RGB_ASSET_ID. Each BFABridge(mint) transition must have a verifiedFundsInlock behind it. - Binds the PSBT to the consignment. The txid, the inputs and the sighash
types must match. The minted units must equal
amount - commissionexactly. - Binds the recipient. The v2 Bridge deposit names the mint OpId, and the mint commits to the user's seal. A legacy deposit has an RGB invoice. Then the one blinded seal in the consignment must equal that invoice.
- Checks the Bitcoin fee and the sats that leave the bridge.
- Signs Taproot key-path inputs with keys from the colored BIP-86 account. A Tapret or script-tree root can be part of the output-key tweak.
Full detail: docs/mint-flow.md.
The user burns RGB units on Bitcoin. The bridge releases the locked tokens on
EVM. The burn signer signs the fundsOut release. MultisigProxy needs M of
N signatures.
Before it signs, the burn signer:
- Verifies each EVM deposit behind the burned units (the mint ancestry).
- Validates the RGB consignment. The last transition must be a BFA
Burn. - Checks that each Bitcoin transaction of the consignment is in its own header chain, at least 6 blocks deep. The chain tip must be less than 2 hours old.
- Decodes the
fundsOutcalldata. The encoding must be canonical. Chain, contract and deadline must be correct. - Checks
sourceChainId,sourceAddressandburnId. - Checks the BtcRelay finality proof against its own header chain.
- Binds the release to the burn: amount,
sourceBurnTxId, recipient andsettlementData. - Signs the EIP-712
TeeFundsOut(orTeeLzFundsOut) digest over the decoded fields.
The burn signer also signs the gas transaction that sends the release to EVM
(SignRawDigest). An attested allowlist limits that transaction.
Full detail: docs/burn-flow.md.
The mint signer and the burn signer get their seed in different ways:
- Mint signer (
kms-persistence): the enclave makes or recovers its seed through attested AWS KMS. It keeps the encrypted seed in S3. Mint replicas recover the same seed. Peer cloning is off. Seedocs/kms-persistence.md. - Burn signer: the enclave makes a BIP-39 mnemonic from OS entropy. Replicas get the seed through the attested cloning handshake.
Both keep the 64-byte seed in a SecretBox (zeroized on drop). The enclave
never exports the plaintext seed. For mint signers, KMS also handles the
plaintext seed during generation and decryption. Mnemonic or raw-seed import
exists only with
allow-seed-import (dev builds).
Key paths:
| Key | Path | Use |
|---|---|---|
| EVM bridge key | m/44'/60'/0'/0/0 |
Signs fundsOut (burn). Its address is the cluster identity. |
| EVM gas-tx key | m/44'/60'/0'/0/1 |
Signs the outer gas transaction (burn). |
| BTC legacy key | m/84'/0'/0'/0/0 |
Public key only. Signs nothing. |
| Vanilla taproot account | m/86'/<coin>'/0' (coin 0 mainnet, 1 other) |
Plain-BTC SignBtc only. |
| Colored taproot account | m/86'/<rgb_coin>'/0' (827166 mainnet, 827167 other) |
Mint PSBTs only. |
| Concordium key | m/44'/919'/0'/0'/0' (Ed25519, SLIP-0010) |
SignCcd. |
The enclave returns the master fingerprint and both account xpubs. The bridge
wallet uses them for its watch-only descriptors. A cloned enclave must derive
the same EVM address before it goes Active.
The enclave resolves one SecurityPolicy when the operator sets the
endpoints at launch. The policy comes from build flags, image pins and the
endpoints. It is Production { ... } or Development { reason }.
- A release bridge build refuses to boot if its pins do not make a valid
Productionpolicy. - It also refuses endpoints that do not make a valid
Productionpolicy. - The policy goes into the attestation
user_data, with the signer role. attest-verifybuilds the expected policy and fails on any difference.
See docs/pubkey-attestation.md.
- The parent implements
parent.ParentServicefromfederated-signer-proto(proto/enclave/parent.proto):Sign,PublicKey,Initialize,Clone,GetLastSavedBlock,SubmitHeaders,AttestedPublicKey. - mTLS is required. A client certificate ACL gives each caller a role
(
listener,clone-operator,observer). Seedocs/parent-mtls.md. Signroutes bydata_type:TRANSACTION-> enclaveSign,EVM_GAS_TX->SignRawDigest,BTC_UTXO->SignBtc.SubmitHeadersanswersPERMISSION_DENIEDto every caller. The parent's header sync writes headers through the direct enclave protocol. Other direct enclave clients can also submit headers. The sync reads headers fromHEADER_ELECTRUM_URLand sends them to its enclave.- The parent opens one new TCP or vsock connection per enclave RPC, with a 30 s timeout.
GET /healthon a loopback port tells if the enclave can sign now. See Readiness endpoint.
Wire format: [4-byte little-endian length][protobuf EnclaveRequest]. One
request per connection. Frame cap 24 MiB. Schema:
enclave-proto/proto/enclave.proto.
| Request | Phase | Signer | Description |
|---|---|---|---|
SetEndpoints |
any | all | Sets the chain endpoints (and the KMS values on mint). Once per boot. |
InitializeKey |
Initial | all | Mint: recover or create the KMS seed. Burn: make a seed from OS entropy, with an optional donor cloning_secret. |
GetPublicKey |
Active | all | EVM address and keys, gas-tx key, BTC key and xpubs, fingerprint, CCD key, boot pins. |
GetAttestedPublicKey |
Active | all | The same bundle, plus an NSM attestation document bound to the nonce, the bundle and the policy. |
Sign |
Active | mint, burn | Bridge signing. Mint signer: EVM -> RGB only. Burn signer: RGB -> EVM only. |
SignBtc |
Active | mint | Plain-BTC PSBT on the vanilla account. Off unless the attested policy turns it on. |
SignRawDigest |
Active | burn | Gas transaction under the attested allowlist. |
SignCcd |
Active | ccd builds |
Ed25519 over a 32-byte hash. |
SubmitHeaders |
any | RGB builds | Bitcoin headers (<= 10 000 per call, <= 100 000 per 60 s). The parent denies this gRPC method to clients. Direct enclave callers can still submit headers. |
GetLastSavedBlock |
any | RGB builds | Header-chain tip (the checkpoint when empty). |
InitiateCloning |
Initial or expired Cloning | burn | Starts a requester session or replaces an expired session. |
GetClone |
Active | burn | Donor side. Verifies the requester attestation and seals the seed. |
SetClone |
Cloning | burn | Requester installs the sealed seed and goes Active. |
SignRawMessage |
- | - | Removed. Always refused. |
Health |
any | all | Readiness: endpoints set and key loaded. Builds with the RGB -> EVM path also require a ready header chain. |
ProxyFederation |
- | - | Not implemented. Always refused (code 1). |
The "Signer" column is for the mint/burn images. Other builds (combined, CCD) keep both directions where their features allow it.
Error codes in ErrorResponse, with the gRPC status the parent gives each:
1all other errors (INTERNAL).2not ready (UNAVAILABLE).3cross-check or SPV failure, or aGetClonedonor without keys (FAILED_PRECONDITION).GetCloneonly:4invalid input (INVALID_ARGUMENT),5refused credential, attestation, PCR or pubkey (PERMISSION_DENIED),6replayed attestation nonce (ALREADY_EXISTS),7seed-export hard cap (RESOURCE_EXHAUSTED).
- Rust 1.96.1 - pinned in
rust-toolchain.toml. The exact patch version matters for reproducible PCR0. - SSH deploy keys - the workspace currently pins the RGB crates to private
BFA mirrors (
rgb-consensus-s-bfa,rgb-ops-s-bfa,rgb-schemas-s-bfa) through thegithub-rgb-*SSH host aliases inCargo.toml. Every build, including the enclave, needs read access to them.consignment-utilsis public and is fetched over HTTPS without a key. The parent additionally needsfederated-signer-proto. CI wires the aliases in.github/workflows/ci.yml; copy that~/.ssh/configshape locally. External users cannot build without read access to all three private repos. Public source builds require public access to their pinned revisions. Access alone does not prove that a build reproduces the approved PCR0. - Docker +
nitro-clifor the EIF. Any x86_64 Linux host with Docker can build an EIF and read its PCRs; Nitro hardware is needed only to run it.
git clone git@github.com:UTEXO-Protocol/enclave-signer
cd enclave-signer
cargo build # enclave workspace
cargo build --manifest-path parent/Cargo.toml # parent workspaceThe production images are the mint signer and the burn signer. Each image
has one role, its own PCR0 and its own seed. Use --no-default-features:
# Mint signer (EVM -> RGB)
cargo build --release -p utexo-bridge-enclave --no-default-features --features vsock,rgb,mint-signer
# Burn signer (RGB -> EVM)
cargo build --release -p utexo-bridge-enclave --no-default-features --features vsock,rgb,burn-signerOther builds stay in the tree. They are not the production bridge:
# Combined (what build/Dockerfile.enclave ships). Uses the retired swap flow.
cargo build --release -p utexo-bridge-enclave --no-default-features --features vsock,rgb,rgb-swap,ccd,bfa-validation
# RGB send/receive (swap) only. Retired flow.
cargo build --release -p utexo-bridge-enclave --no-default-features --features vsock,rgb,rgb-swap,bfa-validation
# Concordium only
cargo build --release -p utexo-bridge-enclave --no-default-features --features vsock,ccd
# Local TCP dev build (debug profile, seed import allowed)
cargo build -p utexo-bridge-enclave --no-default-features --features allow-seed-import,spv,rgb-swap,ccd,evm-rpcCompile-time guards in enclave/src/lib.rs: rgb-validation requires spv;
exactly one of rgb-swap / rgb-mint-burn whenever rgb-validation is on;
exactly one of mint-signer / burn-signer whenever rgb-mint-burn is on;
kms-persistence requires mint-signer;
allow-seed-import and mock-attestation do not compile in a release
profile. CI asserts every guard fires.
No image takes a KMS value: the mint enclave gets them at launch. See mint KMS setup for the parent and policy requirements.
RGB_ASSET_ID="rgb:<approved-bfa-contract-id>" DOCKERFILE=Dockerfile.enclave.mint ./build/build-enclave.sh # mint signer
RGB_ASSET_ID="rgb:<approved-bfa-contract-id>" DOCKERFILE=Dockerfile.enclave.burn ./build/build-enclave.sh # burn signer
RGB_ASSET_ID="rgb:<approved-swap-contract-id>" ./build/build-enclave.sh # combined
RGB_ASSET_ID="rgb:<approved-swap-contract-id>" DOCKERFILE=Dockerfile.enclave.rgb ./build/build-enclave.sh # swap (retired)
DOCKERFILE=Dockerfile.enclave.ccd ./build/build-enclave.shDockerfile.enclave.mint and Dockerfile.enclave.burn build the BFA signer images.
Each signer role enables bfa-mint. This enables rgb-mint-burn and
bfa-validation. BFA validation also enables evm-rpc.
Run the two roles as separate enclaves with independent seeds. Mint signers use KMS persistence. Burn signers use OS entropy or peer cloning with matching PCRs.
Set RGB_ASSET_ID when you use the build helper. For a direct Docker build,
pass --build-arg RGB_ASSET_ID=rgb:<contract-id>. Use the approved BFA contract
id for mint and burn images. There is no default asset id.
The helper and Dockerfile reject an empty value. They do not validate the id or reject a value that contains only spaces. The image contains the asset pin. A runtime environment override is not the provisioning procedure.
The EIF workflow gets this value from the repository variable BFA_RGB_ASSET_ID.
It records the asset pin from the built image as rgb_asset_id in metadata.json.
This file accompanies the EIF in the Actions artifact and S3 bundle. CCD images
have no asset pin and record null.
To reproduce a published EIF, use its recorded asset id, not the current
repository variable. Older bundles may lack this field; obtain the original
build value from the release owner. Include metadata.json when distributing
a release. An internal S3 upload alone does not make these inputs public.
Before deploying, record the image/EIF checksum, approved asset, measured PCRs, registered key, and Parent endpoint together. Verify a genuine BFA request succeeds and an opposite-flow request is rejected.
All Dockerfiles resolve private dependencies. Supply either a GitHub token with read access to those repositories, or the same per-repository deploy keys used by Rust CI. Credentials are mounted as BuildKit secrets during Cargo's build step; they are not copied into image layers.
# GITHUB_TOKEN must already be exported; the value is not a build argument.
docker build --secret id=github_token,env=GITHUB_TOKEN \
-f build/Dockerfile.enclave-dev -t utexo-bridge-enclave-dev .
# EIF: uses GITHUB_TOKEN, or PRIVATE_DEPS_DIR if no token is set.
RGB_ASSET_ID="rgb:<approved-swap-contract-id>" \
PRIVATE_DEPS_DIR=/absolute/path/to/private-deps ./build/build-enclave.shThe key directory contains consensus_key, ops_key, and schemas_key;
parent builds also need federated_key. Keep it outside the
checkout, with directory mode 700 and key files 600. For a direct Docker
build with keys, pass each file as --secret id=<name>,src=<absolute-path>.
make build_* uses the token option by default; DOCKER_AUTH_ARGS can override
it with those key-file arguments.
CD and EIF workflows reuse the four deploy-key secrets configured for Rust CI:
RGB_CONSENSUS_BFA_DEPLOY_KEY,
RGB_OPS_BFA_DEPLOY_KEY, RGB_SCHEMAS_BFA_DEPLOY_KEY, and
FEDERATED_SIGNER_PROTO_DEPLOY_KEY. The workflow's automatic GITHUB_TOKEN
is used for image publishing, not cross-repository dependency access.
The script builds the Docker image with SOURCE_DATE_EPOCH set to the commit
time, converts it with nitro-cli build-enclave, and writes the EIF,
PCR.json and SHA256SUMS to build/. Reproducibility inputs: pinned
toolchain, digest-pinned base images, --locked, CARGO_INCREMENTAL=0,
path-prefix remapping, pre-generated proto code. All five EIF recipes use
digest-pinned Debian 13 Trixie images for both build and runtime stages.
The builder uses Rust 1.96.1, matching rust-toolchain.toml.
Both stages install packages from the signed Debian snapshot
20261007T000000Z over HTTPS. APT checks repository signatures.
The expiry check is disabled because a fixed snapshot must remain usable after
its Release metadata expires. CMake comes from this snapshot instead of PyPI.
Only the parent and dev image recipes still use live APT repositories.
CI pins nitro-cli and its kernel/init blobs to 1.4.5. Docker, Buildx and
BuildKit versions are not pinned in CI, so PCR reproducibility still requires
verification.
Each runtime checks the binary with its dynamic loader before EIF conversion. This rejects missing libraries and incompatible symbol versions. The base-image update changes enclave measurements. Build new EIFs and approve their PCRs before updating attestation allowlists or KMS policies. Existing measurements do not apply to these images.
.github/workflows/build-eif.yml builds the combined, rgb,
rgb-mint, rgb-burn and ccd variants on a plain runner with nitro-cli 1.4.5
and uploads EIF + PCRs + host binaries to s3://<bucket>/eif/<git_sha>/.
release-eif.yml deploys the combined EIF only: deploy/deploy-host.sh
fetches eif/<git_sha>/utexo-bridge-enclave.eif and runs it on every CID.
The rgb-mint and rgb-burn EIFs have no release path yet. The cd-*.yml workflows push container images for
the parent and the dev enclave images only (utexo-bridge-enclave-mint
and utexo-bridge-enclave-burn, both from Dockerfile.enclave-dev.bfa with a
SIGNER_ROLE build arg).
The production Dockerfiles bake the bridge pins as ENV (EVM_CHAIN_ID,
EVM_PROXY_CONTRACT_ADDRESS, RGB_ASSET_ID, FUNDS_IN_CONTRACT,
TOKEN_CONTRACT, GAS_TX_ALLOWED_TO, BTC_MAX_TOTAL_SATS, ...), so they are
measured into PCR0. The cloning secret is never baked. The chain endpoints and
the KMS values are not in the image: anyone can rebuild the EIF and get the
same PCR0 without knowing them.
# Enclave on 127.0.0.1:5000
RUST_LOG=debug cargo run -p utexo-bridge-enclave
# Parent gRPC server (GRPC_PORT defaults to 5000; pick another port when both run on one host)
RUST_LOG=debug GRPC_PORT=50051 GRPC_ALLOW_INSECURE_LOOPBACK=true cargo run --manifest-path parent/Cargo.toml
# Or: parent reachable from other hosts, plaintext, no mTLS (see "insecure-dev" below)
RUST_LOG=debug GRPC_HOST=0.0.0.0 GRPC_PORT=50051 \
cargo run --manifest-path parent/Cargo.toml --features insecure-dev
# CLI (shell function works in bash and zsh)
cli() { cargo run --manifest-path parent/Cargo.toml --bin utexo-bridge-parent-cli -- "$@"; }
cli set-endpoints --electrum-url tcp://<host>:<port> # once, before any signature (debug build; release needs ssl://)
cli init
cli get-keys
cli get-last-saved-block
cli --help--addr host:port or --addr vsock://<cid>:<port> selects the enclave.
insecure-dev (parent cargo feature, dev only): the parent serves plaintext gRPC
with no client auth on any GRPC_HOST, and clone / attest-verify accept
http:// to any host. Any GRPC_TLS_* setting is an error. A release build with
this feature does not compile. Dev image: make build_parent_dev.
Initialize once: use cli init --cloning-secret-file <file> instead of
cli init to configure a donor. Use a fresh requester for cli clone; initialization
and cloning are alternative ways to enter Active. Signing subcommands require
complete proofs and configured pins; see their --help and the spec.
nitro-cli run-enclave --cpu-count 2 --memory 3072 --enclave-cid 16 \
--eif-path build/utexo-bridge-enclave.eif
# Host-side proxies (allowlist each upstream)
vsock-proxy 8001 <electrum-host> 50002 # ELECTRUM_URL upstream
vsock-proxy 8002 <EVM_RPC_HOST> 443 # EVM JSON-RPC over TLS to the pinned host (see deploy/host-prep-evmrpc.sh)
# Set the endpoints once. The enclave refuses a second set, and signs nothing
# and opens no chain connection before it. A restart needs a new set.
cli --addr vsock://16:5000 set-endpoints --electrum-url ssl://<electrum-host>:50002 \
--evm-rpc-host <EVM_RPC_HOST> --evm-rpc-tls-port 443 --evm-rpc-ca-der-file ca.der \
--kms-key-arn <KMS_KEY_ARN> --kms-region <KMS_REGION> --kms-seed-id <KMS_SEED_ID> # mint only
GRPC_HOST=0.0.0.0 GRPC_PORT=50051 USE_VSOCK=true ENCLAVE_VSOCK_CID=16 \
GRPC_TLS_CERT_FILE=server.pem GRPC_TLS_KEY_FILE=server.key \
GRPC_TLS_CLIENT_CA_FILE=client-ca.pem GRPC_TLS_ACL_FILE=clients.acl \
./utexo-bridge-parentdeploy/deploy-host.sh installs the systemd units for a three-enclave host:
CIDs 16 / 18 / 20 with parents on ports 50051 / 50052 / 50053. It verifies the
EIF checksum and PCR0 against the S3 manifest before and after start.
A restart removes the in-memory keys. Initialize or clone each enclave again.
Mint signers recover their saved KMS seed through InitializeKey. They require
the verified address pin and do not support peer cloning.
deploy/systemd/utexo-enclave-ctl.sh start terminates the enclave it
launched if set-endpoints or the launch check fails. That enclave has no key
yet. Run start again.
After a manual set-endpoints with no reply, do not send it again blindly:
the enclave refuses a second set. Read the enclave state first:
cli --addr vsock://16:5000 health # read "Endpoints set" and "Key loaded"; exit code 1 only means "not ready"Endpoints set |
Key loaded |
Action |
|---|---|---|
| false | any | The set did not take effect. A refused set keeps the slot empty. Send set-endpoints again. |
| true | false | The set took effect. Run verify-launch with the same values. On a mismatch, terminate the enclave and start it again. It has no key to lose. |
| true | true | The set took effect. Do not run verify-launch: it refuses an enclave with keys. Do not restart: a restart removes the keys. Check the attested values with attest-verify, the release PCRs and the --expect-* values of this launch, through the parent with an observer certificate. On a mismatch, stop and report. |
nitro-cli run-enclave ... --debug-mode zeroes PCR0/1/2, so attestation
against pinned PCRs fails. Use it only to read logs:
nitro-cli console --enclave-id $(nitro-cli describe-enclaves | jq -r '.[0].EnclaveID')
nitro-cli describe-enclaves
nitro-cli terminate-enclave --enclave-id <id>Bridge pins (all three required for a Production policy):
| Variable | Default | Description |
|---|---|---|
EVM_CHAIN_ID |
0 |
Pinned chain id. Must match the destination chain and the direct-route destinationChainId. |
EVM_PROXY_CONTRACT_ADDRESS |
zero | MultisigProxy address: EIP-712 verifyingContract and the to of the payable lzFundsOutCall carve-out. Attested as bridge_contract. |
RGB_ASSET_ID |
empty | Pinned RGB contract id. Enforced on every bridge PSBT, and on fundsOut when the bridge is configured. |
FUNDS_IN_CONTRACT |
falls back to the proxy | Attested emitter of FundsIn / BridgeFundsIn. It must resolve to a non-zero address in production. |
TOKEN_CONTRACT |
zero | The ERC-20 the Bridge releases (Bridge.TOKEN). A burnId preimage input: the enclave recomputes burnId from it and refuses a mismatch. Attested; must be non-zero in production. |
BTC_RELAY_MODE |
required |
required: every fundsOut proof must carry the two BtcRelay commitment words, and each must equal the relay record the enclave rebuilds from its own chain; a zero word is refused. none: the stand has no BtcRelay (route verifier NullVerifier), the bridge sends both words as zero and the enclave requires exactly that, still binding heights, anchor and freshness. A production policy refuses to boot on none. Any other value is treated as required with a boot warning. |
Value bounds (fail closed while unset in a production build):
| Variable | Default | Description |
|---|---|---|
BTC_MAX_TOTAL_SATS |
0 |
Cap on total input value of one plain-BTC (SignBtc) transaction. Non-zero also flips allow_vanilla_psbt in the attested policy. |
BTC_MAX_UNOWNED_SATS |
0 |
Plain-BTC output budget for scripts the enclave does not prove it controls. Outputs repaying a signed input or landing on the enclave's own BIP-86 key-path addresses (singlesig change, create_utxo allocations) are proven and do not count. |
RGB_MAX_UNOWNED_SATS |
0 |
Bridge-PSBT output budget for sats the enclave cannot prove it controls. Size it from the bridge's witnessed satoshi amount. |
GAS_TX_ALLOWED_TO |
unset | Only to a gas tx may target. |
GAS_TX_MAX_GAS_LIMIT |
0 |
Ceiling on gasLimit. |
GAS_TX_MAX_FEE_PER_GAS |
0 |
Ceiling (wei) on maxFeePerGas / maxPriorityFeePerGas / legacy gasPrice. |
GAS_TX_ALLOWED_SELECTORS |
empty | Comma-separated 4-byte selectors a gas tx may call. Empty calldata is refused. Malformed entries are dropped with a warning. |
GAS_TX_MAX_VALUE_WEI |
unset | Ceiling on native value; only lzFundsOutCall to the pinned proxy may carry value. |
The gas-tx rule is part of the attested policy. Unset pins commit as zero.
Mint signer KMS custody is set at launch (see the launch table below).
With KMS_EXPECTED_EVM_ADDRESS set, initialization recovers the saved ciphertext
and checks the derived address. Without the pin, initialization creates a seed
only when storage reports no saved object. It rejects an existing object.
There is no separate creation-mode setting. See the deployment and recovery
procedure for parent storage and relay configuration.
Data sources and transport:
| Variable | Default | Description |
|---|---|---|
BITCOIN_NETWORK |
bitcoin |
bitcoin, testnet, signet, regtest. Selects the SPV checkpoint, coin types and xpub prefix. Baked into the image. |
ESPLORA_VSOCK_PORT |
8001 |
Host vsock-proxy port for the Electrum resolver. |
EVM_RPC_VSOCK_PORT |
8002 |
Host vsock-proxy port for the EVM RPC. |
EVM_MIN_CONFIRMATIONS |
12 |
Attested minimum depth of a FundsIn receipt; zero is rejected at production boot. |
ENCLAVE_LISTEN_ADDR |
127.0.0.1:5000 |
TCP listen address, non-vsock builds only. |
RUST_LOG |
unset | Log filter. |
Chain endpoints and KMS values, set once at launch with cli set-endpoints
(SetEndpoints), never in the image. The attested policy commits the Electrum
host, the EVM RPC host, the SHA-256 of the CA and the four KMS values. A build
requires the values it uses and refuses the others:
| Value (CLI flag / env) | Build | Description |
|---|---|---|
--electrum-url / ELECTRUM_URL |
rgb-validation |
ssl://host:port (Electrum) or https://host[:port] (Esplora, for the custom dev signet). TLS terminates inside the enclave. The forwarder listens on that port and pins host to loopback in /etc/hosts. A test, debug, mock-attestation or allow-seed-import build also accepts plaintext tcp://host:port and http://host[:port]; a release image refuses them. |
--evm-rpc-host / EVM_RPC_HOST |
evm-rpc |
TLS host name of the EVM RPC. No scheme, path, port or IP literal. The JSON-RPC is served at /. |
--evm-rpc-tls-port / EVM_RPC_TLS_PORT |
evm-rpc |
TLS port, 1-65535, not the Electrum port. The forwarder listens on it. |
--evm-rpc-ca-der-file / EVM_RPC_TLS_CA_DER_FILE |
evm-rpc |
DER of the only CA the EVM RPC TLS trusts. |
--kms-key-arn / KMS_KEY_ARN |
kms-persistence |
Full symmetric KMS key ARN; aliases are rejected. |
--kms-region / KMS_REGION |
kms-persistence |
Commercial AWS region matching the key ARN. The KMS forwarder listens on 127.0.0.2:443. |
--kms-seed-id / KMS_SEED_ID |
kms-persistence |
Stable signer identity used in the KMS encryption context and storage namespace. |
--kms-expected-evm-address / KMS_EXPECTED_EVM_ADDRESS |
kms-persistence, optional |
Identity pin: 40 hex digits with optional 0x. Pin the verified signer before funding; missing ciphertext then fails without replacement. |
Limits and dev knobs:
| Variable | Default | Description |
|---|---|---|
MAX_CONSIGNMENT_BYTES |
8 MiB | Consignment size cap. Sized for a history of 10,000 transitions. |
MAX_MERKLE_PROOFS |
16384 |
Proof-count cap per request. A burn needs one proof for each witness tx in its history. |
MAX_TOTAL_PROOF_BYTES |
8 MiB | Aggregate proof-bytes cap per request. |
SPV_CHECKPOINT |
unset | Dev builds only: height:hash[:bits:time[:chainwork]] moves the SPV anchor forward. Without chainwork (Core's getblockheader value) every fundsOut is refused under BTC_RELAY_MODE=required; none needs no chainwork. A production-shaped build refuses to boot when set. |
UTEXO_CLONING_SECRET |
unset | Legacy donor secret; ignored with kms-persistence, which rejects cloning. Otherwise prefer init --cloning-secret-file at runtime. |
| Variable | Default | Description |
|---|---|---|
GRPC_HOST |
127.0.0.1 |
Bind address. The deploy script defaults to the private EC2 address. |
GRPC_PORT |
5000 |
gRPC port. Deployments use 50051-50053. |
ENCLAVE_ADDR |
127.0.0.1:5000 |
Enclave TCP address (dev). |
USE_VSOCK |
false |
true / 1 selects vsock (Linux only). |
ENCLAVE_VSOCK_CID |
16 |
Enclave CID. |
ENCLAVE_VSOCK_PORT |
5000 |
Enclave vsock port. |
HEALTH_HOST |
127.0.0.1 |
Bind host for GET /health. Keep on loopback - unlike GRPC_HOST, do not set to 0.0.0.0 |
HEALTH_PORT |
5001 |
Port for GET /health |
HEADER_ELECTRUM_URL |
unset | ssl://host:port (WebPKI roots, host name checked), or tcp:// to a loopback IP. The parent's header sync reads Bitcoin headers here. Unset: header_sync.state is unconfigured. Malformed: the parent still serves, header_sync.state is unconfigured and last_error says why. Deploy passes it through; it does not copy ELECTRUM_URL, whose enclave-side rules differ. |
HEADER_SYNC_INTERVAL_SECS |
10 |
Seconds between header sync steps, 1..=600. A malformed value stops the parent at boot. |
RUST_LOG |
unset | Log filter. |
Use GET /health to check readiness before you continue a rollout.
deploy/deploy-host.sh performs a cold deployment. It does not initialize keys
or implement a rolling restart.
curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:5001/health200- endpoints are set and the signing key is loaded. Builds with the RGB -> EVM path also require a fresh header chain with at least six headers above the checkpoint (assert_chain_ready). Mint signers do not require SPV readiness, although they reportspv_synced.503- the enclave is not ready, cannot be reached, or does not answer within the five-second probe timeout.
A 200 response does not prove that a signing request will succeed. The
request must still pass its policy, data-source, and validation checks.
The body includes readiness, key, phase, and SPV diagnostic fields. Use them to investigate a failed deployment. It also includes the parent's header-sync status. Example fragment:
{"header_sync": {"state": "synced", "source_tip": 412345, "enclave_tip": 412345, "lag_blocks": 0, "tip_age_secs": 41, "last_ok_unix": 1790000000, "last_error": null}}state is synced, syncing, stalled (three failed steps in a row), off
(a build with no header chain) or unconfigured (no HEADER_ELECTRUM_URL).
The header-sync state does not change the HTTP code. A rollout can check it
separately when the selected signer needs Bitcoin headers. The deploy script
sets ports 50061, 50062, and 50063 for the three parents. The parent
Docker image uses the same probe in its HEALTHCHECK.
This operations probe binds to loopback by default. HEALTH_HOST can change
the address. Keep the endpoint inaccessible from other hosts.
The same answer is available from the CLI, for debugging from the host shell:
utexo-bridge-parent-cli --addr vsock://16 healthcargo test # enclave workspace, default features
cargo test -p utexo-bridge-enclave --no-default-features --features rgb,mint-signer,mock-attestation,allow-seed-import
cargo test -p utexo-bridge-enclave --no-default-features --features rgb,burn-signer,mock-attestation,allow-seed-import
cargo test -p utexo-bridge-enclave --features evm-rpc
cargo test -p utexo-bridge-enclave --features mock-attestation,allow-seed-import
cargo test --manifest-path parent/Cargo.toml # gRPC bridge + attest-verify e2eCoverage: key derivation and fingerprints, framing, EIP-712 digests checked against
contract fixtures, calldata canonicalisation, gas-tx allowlist, consignment
fixtures per flow, PSBT binding and fee gate, SPV chain / reorg / Merkle,
attestation verification incl. crafted cert chains, cloning handshake, wire
roundtrips over TCP, gRPC translation with a mock enclave, vendored-proto
provenance. build/smoke-test.sh drives a live enclave through the CLI.
| Feature | Implies | Description |
|---|---|---|
rgb |
spv |
RGB / Bitcoin bridge stack. |
ccd |
- | Concordium stack (Ed25519 is always compiled; this gates the handlers). |
rgb-swap |
rgb |
Retired RGB flow: send/receive with BFA Transfer. In the default set. Not in production. |
kms-persistence |
- | Attested KMS seed generation/recovery with encrypted S3 persistence. Requires mint-signer; custody context is rgb-mint. |
rgb-mint-burn |
rgb |
RGB flow: deposits mint with BFA Bridge, withdrawals Burn. Needs --no-default-features. |
bfa-mint |
rgb-mint-burn, bfa-validation |
Mint/burn flow with BFA consensus and settlement checks against verified FundsIn locks. |
mint-signer |
bfa-mint, kms-persistence |
Mint/burn signer role: EVM -> RGB only (mint PSBT, SignBtc). Exactly one role per mint/burn build. |
burn-signer |
bfa-mint |
Mint/burn signer role: RGB -> EVM only (fundsOut, gas tx). Exactly one role per mint/burn build. |
bfa-validation |
evm-rpc |
Runs BFA consensus with verified mint ancestry. Implied by bfa-mint. |
spv |
rgb-validation |
In-enclave Bitcoin header chain and witness inclusion proofs. |
rgb-validation |
rgb crates | In-enclave consignment validation. Requires spv. |
evm-rpc |
rgb-validation |
In-enclave FundsIn verification over JSON-RPC, TLS to a pinned host and CA. Without it the enclave refuses every bridge PSBT. |
vsock |
- | vsock listener and forwarders (Linux). |
allow-seed-import |
- | Mnemonic / raw-seed import. Dev only, does not compile in release. |
mock-attestation |
- | Raw-CBOR attestation with zero PCRs. Dev only, does not compile in release. |
| Enclave | Parent | |
|---|---|---|
| Schema | enclave-proto/, vendored in-tree |
federated-signer-proto, git dep over SSH |
| Packages | enclave only |
bridge / node / orchestrator / parent / signer |
| Workspace | repo root | parent/ (own root + lockfile) |
Cargo materialises every git source in a workspace before it knows which
crates a -p build compiles. Keeping parent/ out of the root workspace is
what keeps its private proto dep out of the enclave dependency graph. Build
the parent with --manifest-path parent/Cargo.toml, never -p.
The generated Rust is committed because protoc / prost-build versions change
the output and would otherwise enter PCR0. Both sides pin the same upstream
commit; enclave-proto/tests/vendored_provenance.rs fails when they drift.
Re-syncing changes PCR0. Procedure in
enclave-proto/README.md.
- Untrusted host. Requests from the parent, listener and backend are checked inside the enclave. For RGB-source requests, Bitcoin witness inclusion is checked against its header chain. Concordium source validation still trusts the listener; see the spec for network-specific limits.
- Pinned EVM RPC trust (accepted by design). TLS ends inside the enclave and authenticates the configured RPC hostname against the pinned CA. The host relay cannot alter authenticated responses without detection. The enclave checks successful receipts, unique expected events from the pinned contract, operation IDs, amounts and depth relative to the provider-reported chain head. These checks do not prove EVM consensus: an approved provider can return a self-consistent false deposit history. Trust in the provider's data is an explicit design assumption. Verifiers must compare the attested host and CA SHA-256 with independently approved values. See the EVM RPC trust boundary.
- Attested posture. Build flags and pins resolve to one
SecurityPolicycommitted into the attestation. Policy matching checks only the committed fields. The Electrum/Esplora scheme is not committed: a release image accepts only TLS (ssl://,https://), so PCR0 covers it. Endpoint ports are not committed. See the policy scope. - Fail closed. Missing feature, missing pin, missing receipt, missing proof, zero inputs signed: refuse, never sign with less verification.
- Limits. Bitcoin confirmation depth, freshness, reorg/retention caps and
connection limits are compiled in.
EVM_MIN_CONFIRMATIONSand request-size caps are read from environment; image-baked values are measured with the EIF. - Key custody. Seed and keys in
SecretBox, zeroized on drop.#![deny(unsafe_code)]. Withkms-persistence, seeds persist as KMS ciphertext in S3. The parent receives no plaintext seed. KMS handles the seed during generation and decryption. Derived signing keys stay in enclave memory. - Cloning (without KMS persistence). X25519 + HKDF-SHA256 + ChaCha20-Poly1305, mutual attestation with PCR equality, shared secret, replay guard recorded only after authentication.
- Release hardening.
opt-level = "z", LTO, stripped,panic = "abort", single codegen unit. Dev features arecompile_error!in release.
Known limitations are listed in
docs/tee-spec.md.