Signer image: burn-signer (--no-default-features --features vsock,rgb,burn-signer).
Checked against repository code: 2026-11-02, rgb-lib-bfa 0a9d352f
(RGB-Tools bfa 1ae1c160).
Deployment was not checked.
This document tells how the burn signer checks and signs a release. The spec describes the rules and known limits. If the code and this document differ, investigate the difference. Either can contain an error.
- The user burns RGB units on Bitcoin.
- The burn records how many units it destroys (
burnedAsset) and which destination gets the tokens (burnReason: an EVM address, or the hash of a destination record, see burn-destination.md). A burn has no outputs: rgb-lib first splits the units in a transfer of the same bundle, which keeps the change, and the burn destroys the split's other output. - The burn signer checks the burn.
- The burn signer signs an EIP-712 release for
MultisigProxy. MultisigProxyneeds M of N such signatures. Then the Bridge contract releases the tokens to the user.
The burn signer signs nothing on Bitcoin.
| Part | Where | Trusted for | Job |
|---|---|---|---|
| User | Bitcoin | - | Burns RGB units. |
| Backend and listener | Operator servers | Nothing | Build the fundsOut calldata. Send the request. |
| Parent | EC2 host | Nothing | Moves bytes. Sends Bitcoin headers to the enclave. |
| Burn signer | Nitro Enclave | Checks and keys | Checks the burn. Signs the release and the gas transaction. |
MultisigProxy and Bridge |
EVM | Quorum and replay | Count signatures. Use the nonce. Check burnId again. |
The burn signer checks request claims before it signs. It derives the burn
amount from the consignment. It reads no Bitcoin indexer, and reads the EVM
RPC only for an asset whose genesis names a reject list (Stage 3b): the
deposits behind the burn are derived, and the witnesses are checked on chain against
the enclave's own header chain. One call of rgb-lib's
validate_consignment_with_resolver runs RGB consensus. It takes each witness
transaction from the consignment, but the enclave's resolver treats it as mined
only if a Merkle proof in the request places it at least
SPV_MIN_CONFIRMATIONS deep in that chain. An unmined witness anywhere in the
history fails consensus. So two conflicting
off-chain transfers of the same units cannot both back a burn: only one of
them can be mined.
On mainnet and testnet4, SPV checks witness inclusion against the enclave header chain with proof of work. On testnet4, min-difficulty blocks count as confirmations and freshness, for testing convenience: testnets carry no production value. Signet and regtest do not enforce proof of work. See Bitcoin network limits.
sequenceDiagram
participant U as User
participant BT as Bitcoin
participant L as Listener
participant P as Parent (untrusted)
participant E as Burn signer (enclave)
participant C as MultisigProxy + Bridge (EVM)
U->>BT: burn transaction
loop every 10 s
P->>E: SubmitHeaders (own header sync)
end
L->>P: Sign(RgbSource -> EvmDestination)
P->>E: forward over vsock
E->>E: Stage 1: payload caps
E->>E: Stage 2: work caps, then rgb-lib consensus: mint anchors bound to<br/>derived deposits, each witness mined per its SPV proof, reject list
E->>E: Stage 3: SPV freshness, proof coverage, block pins
E->>E: Stage 4: decode and check calldata
E->>E: Stage 5: bind release to burn
E->>E: Stage 6: EIP-712 digest, sign
E-->>P: signature + calldata (unchanged)
P-->>L: signature
L->>P: Sign(EVM_GAS_TX)
P->>E: SignRawDigest
E-->>L: gas transaction signature
L->>L: assemble signed gas transaction
L->>C: send transaction with M of N signatures
C-->>U: tokens released
| Name | Value | Meaning |
|---|---|---|
TS_BURN |
8010 | Burn transition. The only settling transition the burn signer accepts. |
TS_MINT |
8013 | Mint transition. It shows in the burn's history (the ancestry). |
GS_BURNED_ASSET |
2015 | Burn global: the number of units destroyed (u64). Required. |
GS_REJECT_LIST |
3010 | Genesis global: where the reject list is. The enclave reads only an IRgbRejectList contract on EVM_CHAIN_ID (B3.7). |
GS_BURN_REASON |
3009 | Burn global: 32 bytes. A left-padded EVM address (V0), or the hash of a destination record (V1). See burn-destination.md. Required. |
RGB_CHAIN_ID |
827166 | The bridge's id for the RGB network. Compiled in. |
SPV_MIN_CONFIRMATIONS |
6 (default) | Minimum depth of each Bitcoin transaction. Image env, attested. Zero stops the boot. |
SPV_MAX_TIP_AGE_SECS |
7200 | Maximum age of the header-chain tip. |
SPV_MAX_TIP_FUTURE_SECS |
7200 | Maximum time the tip can be in the future. |
MAX_RELAY_TIP_LAG_BLOCKS |
100 | Maximum distance from the BtcRelay tip to the enclave tip. |
fundsOut selector |
0x340276aa |
Pools route. |
lzFundsOut selector |
from the enclave ABI | LayerZero route. |
| Calldata cap | 96 KiB | Maximum calldata size. |
| EIP-712 domain | ("MultisigProxy", "1", chainId, verifyingContract) |
Tests compare it with contract fixtures. |
If one check fails, the burn signer stops. It signs nothing. It returns an error.
- B0.1 The endpoints must be set (
SetEndpoints). - B0.2 The source must be RGB and the destination must be EVM. The burn signer refuses a mint request.
- B0.3 The enclave must be
Active. While the keys are not ready, the burn signer refuses the request before any validation.
- B1.1 The consignment must not be larger than
MAX_CONSIGNMENT_BYTES(default 12 MiB). - B1.2 Proof count must be at most
MAX_MERKLE_PROOFS(default 16384). Proof bytes must be at mostMAX_TOTAL_PROOF_BYTES(default 8 MiB). - B1.3
keccak256(consignment)must equalconsignment_hash. This check finds a damaged copy only. It is not a safety proof.
- B2.1 The bytes must be a V1 transfer file (
TFR). The genesis schema must be BFA. If the genesis names a reject list (GS_REJECT_LIST), it must be an EVM contract; a URL list is refused. - B2.2 Before rgb-lib runs, the contract id must pass the asset bind
(B2.7), so a foreign genesis never reaches rgb-lib, and no foreign contract
is ever read as its list. Then the enclave bounds rgb-lib's work on the
parsed transfer (
validation/cost.rs). In phase 1, before any witness is checked, rgb-lib checks each transition of every BFA against the whole witness tx, anchor and input map of its bundle. So the sum over bundles of transitions x (tx bytes + Tapret partner leaf bytes) plus seals x tx inputs must be at mostMAX_WITNESS_CHECK_COST, and the sum over bundles of (transitions + 1) x input map entries at mostMAX_BUNDLE_ID_COST. The operations DAG rgb-lib builds in the same pass costs one step per spent opout since rgb-consensus af0b751d, so it has no cap of its own: the time budget covers it. Each opout costs rgb-lib far more than its bytes, since it hashes every opout into its operation id more than once, so the budget counts opouts on their own. It also hashes every genesis global and assignment into the contract id once per operation (validate_state), so the budget counts those leaves times the operations. If the genesis names a reject list, the enclave also counts rgb-lib's reject walk (B3.10), one walk per origin with no list entry to cut it short, which must be at mostMAX_REJECT_WALK_COST. Once validation passed, rgb-lib's inspection reads the summary from the same file (B2.8): it reads the file again, hashes every operation id three times more and each witness tx once per transition. The caps add up on top of the file's own cost, so the modeled rgb-lib time of validation (the file bytes, its opouts, the genesis hashes and the three costs) and of the inspection (the file bytes, its opouts, its operations and the witness checks) together must be at mostMAX_RGB_LIB_NS(18 s). The caps and the timings behind them are in Work caps. - B2.3 One call of rgb-lib's
validate_consignment_with_resolverruns consensus phase 1 over the whole history under the BFA rules fromrgb-schemas, then phase 2 through three resolvers of the enclave: the witness resolver (B2.9), the anchor resolver (B2.4, B2.5) and the reject-list resolver (Stage 3b). Nosafe_heightis passed: the witness resolver already enforces the depth. rgb-lib must report the transfer the enclave parsed (same consignment id) and no warning; neither of its warnings can arise with this resolver, so one refuses. Each mint leaves an anchor: its OpId and itsissuedSupply. - B2.4 The anchor resolver asks the binder once with no anchor: the
genesis
bridgeLocationmust name the pinnedFUNDS_IN_CONTRACTonEVM_CHAIN_ID, and rgb-lib compares that chain id with the genesis one. Then it asks once per anchor. For each anchor, the burn signer derives theoperationIdof the one deposit that can back it:keccak256(abi.encode(RGB_MINT_DEPOSIT_TYPEHASH, FUNDS_IN_CONTRACT, EVM_CHAIN_ID, TOKEN_CONTRACT, RGB_CHAIN_ID, mint OpId, minted units)). It reads nothing from the chain.TOKEN_CONTRACTandEVM_CHAIN_IDmust be pinned. - B2.5 rgb-lib confirms each anchor only against its derived lock, one
for one; rgb-lib makes no RPC for it. The Bridge holds a record under that id only if a
deposit of exactly the minted units was made for that mint, and the release
checks each cited record, so a mint with no deposit fails on chain. The
burn signer does not check the mint's
beforeBlock. The mint signer checked it before it signed the mint. A mint to a witness seal needs no rule of its own: its tx is one more witness, and its anchor binds the same way. - B2.6 The result is a list of locks: deposit
operationIds. One burn has one list, so onesettlementData;burnIddoes not hash it. Stage 5 uses this list. - B2.7 The contract id must equal the declared
asset_idand the pinnedRGB_ASSET_ID. - B2.8 The settling transition must be
TS_BURN: the burn of the last witness, so one tx also carries rgb-lib's split (or a plain transfer). Two burns there are refused. The amount comes fromburnedAsset, never from the outputs or the host. The flat summary of each transition (the split's outputs included) comes from rgb-lib'sinspect_consignmentover every bundle (BundleFilter::All), run on the scratch file rgb-lib just validated, which is removed after it. rgb-lib's validation returns no amounts or burn facts, so these are read from the same bytes once it passed them. The burn facts themselves (burnedAsset,burnReason, the OpId) come from the validated rgbstd transition, and the summary must name the same settling transition. The bridge reads the same fields with consignment-utils, and the tests compare both readings on every generated shape. - B2.9 The witness resolver resolves every witness, the burn included, on chain: the transaction comes from the consignment, its block from the request's Merkle proof for it. That proof must pass B3.4 and B3.5 first. A witness with no such proof is unmined and fails consensus. A proof the consignment itself carries is never used: the resolver serves no block headers, so rgb-lib asks it instead. Stage 3 runs these proof checks again with coverage and pins.
The parent sends block headers. The burn signer builds its own header chain.
On mainnet and the testnets, it checks linkage, proof of work, and nBits.
On testnet4 it applies the 20-minute rule and BIP94 (retarget from the first
block of the period, the timewarp rule). On every network a header may be at
most 2 hours ahead of the enclave clock, so a host cannot date free blocks
ahead to lower its fork's difficulty. It accepts a replacement chain only
when it has more work and meets the reorganization limits. Signet and regtest
skip proof-of-work and nBits checks.
- B3.1 The chain tip must not be older than 2 hours, nor more than 2 hours in the future. On the testnets a min-difficulty tip counts.
- B3.2 The consignment's network (
chain_net) must equal the enclave's Bitcoin network. - B3.3 The Merkle proofs must be exactly the set of witness transactions in the consignment. None missing. None extra. No duplicates.
- B3.4 Each Merkle path must be at most 32 levels. Each proof must verify against the header that the enclave stores at that height.
- B3.5 Each witness transaction must be at least
SPV_MIN_CONFIRMATIONSblocks deep (default 6). On the testnets min-difficulty blocks count. - B3.6 The enclave records the heights and hashes it used. Just before it uses the key, it checks that they did not change.
Only for an asset whose genesis names a reject list. rgb-lib runs it at the end of its validation (B2.3), once every witness and anchor passed, through the enclave's reject-list resolver. So it refuses, and reads the EVM RPC, before the Stage 3 checks that run after rgb-lib (B3.1, B3.3 and B3.6); B3.4 and B3.5 already passed for every proof the witness resolver uses. The asset bind ran before rgb-lib (B2.2), so a foreign genesis cannot make the signer read a contract of its choice. The resolver reads only the list that genesis names, and a failed read refuses with the reader's own error. The reads trust the pinned EVM RPC for their contents, as the trust table in tee-spec.md states: TLS authenticates the endpoint, not the answers.
- B3.7 The list must be on
EVM_CHAIN_ID, and the node must report it. Another chain is refused: the enclave has no RPC for it. - B3.8 Over the pinned EVM RPC: the latest block and the
finalizedblock (capped at the latest),length()at both, thenentriesin pages of 1000 at the latest block. Any RPC error, no code at the latest block, a list shorter at the latest block, a bad or short page, or more than 20,000 entries refuses. About 5 calls plus one per 1000 entries; the history length does not matter. A call that, at the pace of the slowest call so far, would end within 2 s of the 30 s request deadline is not started: the read refuses with "exceeded the request budget". - B3.9 Decisions: the latest finalized entry for an opout wins. A
pending reject counts at once, also over a finalized allow. A pending allow
does not count.
finalizedis fixed in code, notEVM_FINALITY_TAG, so the signer never releases what an rgb-lib wallet still refuses. - B3.10 rgb-lib's rule: the origins are the inputs and the outputs of every terminal operation of the consignment, the ones nothing in it builds on. Each origin is walked back through rgb-lib's operations DAG. An allowed opout stops its branch. A rejected opout on a branch nothing stopped refuses the release ("Consignment rejected by the reject list"). The burn has no outputs, so its inputs are origins; its input is the split's output, so the walk goes through the split to the burned units. The split itself is not terminal (the burn spends its output), so the change on it is not checked. This rule is stricter than the burn's inputs alone: a companion transfer of the same bundle is terminal too, so a rejected line behind it refuses the release even when the burned units do not descend from it.
- B4.1 Calldata must be at least 4 bytes and at most 96 KiB.
- B4.2 The selector must be
fundsOut(0x340276aa) orlzFundsOut. - B4.3 The burn signer decodes the calldata and encodes it again. The two must be byte-equal. This stops a non-canonical encoding.
- B4.4 The decoded amount must equal the request's
calldata_amount. It must fit in a u64. - B4.5
chain_idmust equal the pinnedEVM_CHAIN_ID.proxy_contractmust equal the pinnedEVM_PROXY_CONTRACT_ADDRESS. - B4.6
destinationChainId:- pools route: must equal
EVM_CHAIN_ID; - LayerZero route: must not be zero, and must not equal
EVM_CHAIN_ID.
- pools route: must equal
- B4.7 The deadline must be in the future, and at most 1 day after now
(
MAX_TEE_DEADLINE_SECS, the same value asMultisigProxy.MAX_TEE_DEADLINE). So every signature expires 1 day after signing. - B4.8 LayerZero route only:
minAmountLDmust be > 0 and at mostamount. The backend sets it from the net quoted at signing. - B4.9 LayerZero route only:
extraOptionsmust be empty or one executorlzReceivegas option (type 3, no value), with gas at mostMAX_LZ_RECEIVE_GAS. Native drop, compose and every other option are refused.
Both routes (fundsOut and lzFundsOut), in this order:
- B5.1 The burned amount must be at least the calldata amount.
- B5.2
sourceChainIdmust equal 827166 (RGB_CHAIN_ID). - B5.3
sourceAddressmust be empty. RGB has no source address. - B5.4 The burn signer calculates
burnIdthe same way asBridge._deriveBurnIdFromFields. It uses the pinnedFUNDS_IN_CONTRACT,EVM_CHAIN_IDandTOKEN_CONTRACT, and the calldata fields. The calldataburnIdmust be equal. - B5.5 BtcRelay proof: the calldata
proofis(sourceHeight, sourceCommit, latestHeight, latestCommit), 128 bytes.sourceHeightmust be the block that holds the last witness transaction.latestHeightmust not be belowsourceHeight.- The enclave must have a header at
latestHeight. That header must be at most 100 blocks below the enclave tip. - With
BTC_RELAY_MODE=required(the production mode), each commit word must equalkeccak256of the 160-byte BtcRelay record that the enclave builds from its own chain. A zero word is refused. To build a record, the checkpoint must carry its chain work. Each proof height must be at least 10 blocks above the checkpoint, and its epoch start must not be below it. - With
BTC_RELAY_MODE=none(local stand only), both words must be zero. A production policy does not boot in this mode.
- B5.6 Amount:
burnedAssetmust equal the calldataamount. - B5.7 Burn id:
sourceBurnTxIdmust not be zero. It must equal the OpId of the burn transition. - B5.8 Destination:
burnReasonmust be present.- High 12 bytes zero (V0): the low 20 bytes are an EVM address. The request
must carry no
burn_destination. Pools route only. The calldatarecipientmust equal the address. - Otherwise (V1): the request must carry
burn_destination, and its hash must equal the 32 bytes (burn-destination.md).destinationChainIdmust equal the record.dstEid == 0means the pools route, with a 20-byterecipientequal to the calldata.dstEid != 0means the LayerZero route: the calldatadstEidmust equal it, and the left-padded recordrecipientmust equal the LayerZerorecipient.
- High 12 bytes zero (V0): the low 20 bytes are an EVM address. The request
must carry no
- B5.9 Settlement:
settlementDataisabi.encode(bytes32[] operationIds). It must be canonical. The ids must be in strictly ascending order, so no id repeats. They must equal, as a set, the locks consensus confirmed in Stage 2. An empty list is refused.
Note - LayerZero route. Only a V1 burn can leave via LayerZero, because only V1 names a chain.
min_amount_ldis checked against the request only.
- B6.1 The domain is
("MultisigProxy", "1", chainId, verifyingContract). - B6.2 The digest uses the decoded fields, never the raw bytes:
- pools route:
TeeFundsOut(recipient, amount, burnId, sourceChainId, destinationChainId, sourceAddress, proof, settlementData, sourceBurnTxId, nonce, deadline); - LayerZero route:
TeeLzFundsOut(...), 14 fields.
- pools route:
- B6.3 The burn signer signs with the EVM bridge key
m/44'/60'/0'/0/0. The signature is 65 bytes (r || s || v).vis the raw recovery id, 0 to 3, with no +27. - B6.4 The response has the signature and the calldata. The calldata is not changed.
This path creates only TeeFundsOut and TeeLzFundsOut digests. It does not
create a batch digest. The receiving contract must interpret these signatures
according to the same ABI.
The burn signer also signs the EVM transaction that sends the release. It
uses the gas key m/44'/60'/0'/0/1.
- G1 The request must have the unsigned transaction. The enclave calculates the digest itself.
- G2 RLP decode must be strict and canonical. EIP-1559 and legacy EIP-155 are accepted.
- G3
chain_idmust equalEVM_CHAIN_ID.tomust equalGAS_TX_ALLOWED_TO. Contract creation is refused. - G4
gasLimitmust be at mostGAS_TX_MAX_GAS_LIMIT. Each fee field must be at mostGAS_TX_MAX_FEE_PER_GAS. - G5 Calldata must start with a selector in
GAS_TX_ALLOWED_SELECTORS. Empty calldata is refused. - G6
valuemust be zero. One exception: the payablelzFundsOutCalltoEVM_PROXY_CONTRACT_ADDRESS, withvalueat mostGAS_TX_MAX_VALUE_WEI. - G7 The destination, gas ceiling, fee ceiling, and selector allowlist
must be configured. An unset value ceiling prevents non-zero
value. It does not prevent a transaction with zerovalue.
The gas rule is part of the attested policy. The gas transaction is not linked to one checked release. The limits apply to one transaction, not to a sum of transactions.
The burn signer keeps no replay state for releases. Replay protection is on chain:
- the
MultisigProxynonce is in the signed digest; - the Bridge refuses a
burnIdthat it used before.
On both routes, burnId is bound to the burn through B5.3, B5.6 and B5.7.
| Request | Result |
|---|---|
Sign with an EVM source or an RGB destination (mint) |
Refused: wrong signer role. |
SignBtc |
Refused: wrong signer role. |
SignRightsRefill |
Refused: wrong signer role. The burn signer holds no mint rights. |
SignCcd |
Refused: not in this build. |
SignRawMessage |
Refused in every build. |
The amount and the payee of a burn are its burnedAsset and burnReason
globals. So the burn signer needs three proofs that agree:
- RGB consensus: the burn and its whole history are valid, and every mint in it is confirmed against the deposit it derives.
- SPV: every witness transaction of the history is buried, and consensus saw each one as mined (B2.9).
- Globals: the amount and the payee in the burn equal those in the calldata.
The contract proves that the cited deposits exist. The burn signer proves that they are the deposits behind this burn (B5.9). Each half needs the other.
The request deadline is 30 s (TOTAL_REQUEST_TIMEOUT), and a worker is not
interrupted when it passes. So before rgb-lib runs, the burn signer bounds the
three parts of rgb-lib's work that grow faster than the file (B2.2,
validation/cost.rs), and then their sum with the file, its opouts, its
operations and the genesis hashes, for rgb-lib's validation and for its
inspection, which reads the summary from the same file once validation
passed (B2.8):
| Cap | Cost of a consignment | Value | Honest burn of 2810 mints |
|---|---|---|---|
MAX_WITNESS_CHECK_COST |
per bundle: transitions x (tx bytes + Tapret partner leaf bytes) + seals x tx inputs | 134,217,728 (128 Mi) | 8,539,773 |
MAX_BUNDLE_ID_COST |
per bundle: (transitions + 1) x input map entries | 262,144 (256 Ki) | 11,242 |
MAX_REJECT_WALK_COST |
listed asset only: the steps of rgb-lib's walk from each origin | 67,108,864 (64 Mi) | 14,050 |
MAX_RGB_LIB_NS |
modeled rgb-lib time. Validation: 1100 ns per file byte, 55,000 per opout (each one the genesis or a transition creates, and each one a transition spends), 15,000 per genesis leaf hash (each genesis global and assignment, once for the genesis and once per transition), 14 per witness step, 4500 per bundle id step, 75 per walk step. Inspection: 650 per file byte, 65,000 per opout, 90,000 per operation (the genesis and each transition), 8 per witness step | 18,000,000,000 (18 s) | 6.1 s on the pinned genesis of 10 mint rights, listed: 3.7 s of validation and 2.4 s of inspection (5.0 to 6.3 s measured end to end) |
Each cap alone holds its part to a few seconds, but the parts add up with the file's own cost: a listed merge of 1400 lines after 23,900 transfers is 12.1 MB, within the size cap and each cap (its walk at 66.98M), and took rgb-lib 19.5 to 19.9 s to validate, 15.0 to 15.2 s of it without the walk. So the gate also models rgb-lib's whole time, from release timings rounded up, and refuses past 18 s. A file alone models at 1.75 us per byte (1.1 to validate, 0.65 to inspect), so the budget holds a file to about 10.3 MB, below the 12 MiB default, whatever it holds. On the pinned genesis of 10 mint rights the 2810-mint burn's opouts, operations, genesis hashes and costs model at 2.5 s besides its file. On a genesis of 255 its genesis hashes alone model at 11.0 s (below). The budget is rgb-lib's alone: the burn signer's own parse, gate and reads of the validated transfer run outside it, in the rest of the 30 s. End to end, the 2810-mint burn with 3 transfers a line took 1.1 to 1.6 s more than rgb-lib's validation and inspection timed alone (2.3 s at a load of 5.6).
The opout term closes a gap the per-byte model had before the rgb-lib
1ae1c160 move. rgb-consensus merklizes every opout an operation creates or
spends into the operation id, and it computes that id three times or more per
operation (validate_transition, validate_state and the state it evolves;
in a profile of a fan-out to 65,535 outputs these hashes took most of
rgb-lib's time). So an opout costs rgb-lib about 35 to 55 us beyond its
bytes, however few they are: a right of a propagate is 14 bytes. A forged
chain of 13 propagates of 65,535 rights each is 11.9 MB with 852k opouts,
within the size cap and every cap, and the old model (1300 ns per byte) let it
through at 15.5 s. It took rgb-lib 42.8 to 44.3 s, and its inspection 51.3 s
more. The model counts 123.1 s now and refuses it.
The genesis term counts the work that grows with the pinned asset's genesis.
For every operation it validates, rgb-consensus validate_state takes the
contract id from genesis.contract_id(), which merklizes every genesis global
and assignment again (logic.rs, unchanged since 4a614b72). A BFA genesis has
5 globals (up to 8 with details, a reject list and the contract it is linked
from) and one assignment per mint right (one more with a link right).
Chains of 2000 one-right propagates, nothing mined, took rgb-lib 0.94 to
0.97 s from a genesis of 1 right, 2.68 to 2.73 s from 64 and 7.83 to 7.86 s
from 255: 13.6 to 13.7 us per genesis leaf and operation, linear in both. The
model counts 15,000 ns and puts the 255-right chain's validation at 9.1 s. On
the pinned genesis of 10 rights (15 leaves, 16 with a reject list) this adds
about 0.23 ms an operation, part of which the per-byte and per-opout rates
already held. On a genesis of 255 rights an operation costs rgb-lib 3.6 ms and
the model 3.9 ms, so the 18 s budget admits about 4,000 operations, whatever
their size: a chain of 5000 one-right propagates (2.3 MB) is refused
(tests/caps.rs). Before this term the gate let through about 27,000 such
propagates under the size cap, which would have taken rgb-lib about 110 s.
Rights a propagate or a refill makes later do not widen the genesis. The
inspection hashes the genesis a fixed few times per call, not per operation.
rgb-consensus af0b751d ("restructure dag for performance") builds the
operations DAG with one edge per spent opout and no scan of the edges already
there, so the DAG has no cap of its own any more (it had MAX_OPOUTS_DAG_COST,
the sum of k^2 over transitions of k inputs, at 8192^2). A forged burn of n
rights now costs rgb-lib the same per byte at every n (1.16 to 1.24 us per byte
at 4096, 16,384 and 65,535 rights), and the 65,535-right burn that took 17.7 s
on rgb-consensus 4a614b72 takes 9.0 to 9.3 s to validate and 9.7 to 9.8 s to
inspect. Its validation models at 16.3 s, so it passed the gate before the
inspection counted; with it, it models at 29.9 s and is refused before rgb-lib.
The inspection term. rgb-lib's validation returns no amounts, so the summary
is read by its inspect_consignment over every bundle, on the scratch file
validation just read (B2.8). No cap bounds it, and it costs about as much per
opout as validation: it strict-decodes the file again through an unbuffered
File (about 0.46 us per byte of read calls), computes each transition id
three times more (about 15 us per opout and pass at opt-level z), serializes
every bundle again for the consignment id and formats every output. Two of its
steps are superlinear, and the witness cap holds both: it hashes the bundle's
witness tx once per transition (about 6.4 ns per byte) and scans its inputs
once per input (about 2 ns a step), the two terms of the witness cost, so
under 1.1 s at the cap. A third, its scan of the bundle's transitions once per
input, is at most the bundle id cost at a few ns a step, as long as no bundle
lists a transition twice. A bundle may (its transitions are a list, and every
copy passes the inspection's commitment check), but rgb-lib refuses the copy:
its inputs are already spent by the first ("references previous state"), and
the inspection runs only on what validation passed (tests/caps.rs). Measured
on the shapes below, the inspection takes 0.46 to 0.5 us per byte, about 46 to
54 us per right and 60 us per asset output beyond the bytes, and up to about
130 us more per operation of one small transition per bundle. The model's
rates (650 ns per byte, 65 us per opout, 90 us per operation, 8 ns per witness
step) hold every shape measured at a load under 4 by 8 percent or more: the
honest 2810-mint burn closest (2.2 s at a load of 4 against 2.4 s), then the
fan-outs (11 to 13 percent). On the burn path every witness of what the
inspection reads is mined. In the swap flow (allow_unbroadcast_last in
networks/rgb/route.rs) the last bundle need not be, so a host can forge one
within the caps at no chain cost; the gate counts its inspection the same way.
The honest burn here is 2810 mints, each on its own right, all burned by one transition (2810 allocations in one tx), the widest the bridge expects, on the pinned asset's genesis of 10 mint rights; the timings below also run it with 3 transfers a line. rgb-lib's own burn (a split, then the burn of the split's output) has one origin.
rgb-consensus checks each transition against its bundle: it clones, hashes
and compares the whole witness tx and the anchor's Tapret partner leaf (a
script of any length), scans the tx inputs for each seal, and hashes the
bundle id, a commitment to the whole input map (validate_transition, and
the bundle id once more per bundle). An honest bundle carries one or two
transitions, so these stay linear; a forged one of thousands of transitions does not. The
walk cost is counted the way rgb-lib walks (rejected_opouts): from each
origin on its own, with no list entry to cut it short, stopping just past the
cap. So a burn of many separate lines costs a few steps per line, however
long each line's history; only origins that share a long history cost much.
Measured in release mode on an Apple M1 Pro (cargo test --release -p utexo-bridge-enclave --no-default-features --features rgb,burn-signer --lib caps -- --ignored --nocapture --test-threads=1, validation/tests/caps.rs),
on rgb-lib 1ae1c160, 2026-10-30, at a load under 4 unless a cell names more,
two runs each; a row marked "one run" ran once. The enclave's CPU may be
slower; the times scale with it. The honest burns are on the pinned genesis of
10 mint rights (GENESIS_RIGHTS=10) unless their row names more; each forged
history is on a genesis of 1 right (GENESIS_RIGHTS unset) unless its row
names more. Each model is validation plus inspection, the genesis hashes
included, and the honest rows give the listed asset's. The end-to-end times
include our parse of the transfer and the gate (0.1 to 0.5 s), the inspection
and our read of the last bundle, 3 to 21 ms on these burns.
| Consignment | Bytes | Opouts | Cost against its cap, model (validation + inspection) | rgb-lib validation (of which its file load) | rgb-lib inspection |
|---|---|---|---|---|---|
| Honest burn, 1404 mints | 1.0 MB | 5.6k | witness 2.3M, walk 7k, model 3.0 s (1.8 + 1.2) | 1.37 to 1.45 s (0.4 s) | 0.94 to 0.98 s; 2.5 to 2.6 s end to end |
| Honest burn, 1404 mints, 3 transfers a line | 3.0 MB | 14k | witness 2.8M, walk 15k, model 8.9 s (5.5 + 3.4) | 4.5 to 4.7 s (1.3 s) | 2.8 to 2.9 s; 7.9 to 8.3 s end to end |
| Honest burn, 2810 mints | 2.0 MB | 11k | witness 8.5M, walk 14k, model 6.1 s (3.7 + 2.4) | 2.7 to 2.8 s (0.9 to 1.1 s), one run 3.5 s | 1.9 to 2.2 s; 5.0 to 6.3 s end to end |
| Honest burn, 2810 mints, 3 transfers a line | 6.0 MB | 28k | witness 9.6M, walk 31k, model 17.9 s (11.1 + 6.8) | 8.8 to 9.2 s (2.5 to 2.6 s) | 5.7 to 5.8 s; 15.9 to 16.2 s end to end |
| Honest burn, 2810 mints, genesis of 255 rights (one run) | 2.1 MB | 11k | model 16.5 s (14.1 + 2.4) | 12.5 to 13.4 s (0.9 s) | 2.0 s (3.7 s at a load of 11); 15.3 to 16.2 s end to end |
| Forged chain of 2000 one-right propagates | 0.9 MB | 4k | model 2.5 s (1.4 + 1.0) | 0.94 to 0.97 s (0.4 s) | 0.87 to 0.89 s |
| Forged chain of 2000 one-right propagates, genesis of 64 rights | 0.9 MB | 4k | model 4.4 s (3.3 + 1.0) | 2.68 to 2.73 s (0.4 s) | 0.88 to 0.90 s |
| Forged chain of 2000 one-right propagates, genesis of 255 rights | 0.9 MB | 4k | model 10.1 s (9.1 + 1.1) | 7.83 to 7.86 s (0.4 s) | 0.88 to 0.89 s |
| Forged burn of 4096 rights | 0.5 MB | 8k | ids 8k, model 1.9 s (1.0 + 0.8) | 0.59 to 0.60 s (0.2 s) | 0.60 to 0.61 s |
| Forged burn of 16,384 rights | 1.9 MB | 33k | ids 33k, model 7.5 s (4.1 + 3.4) | 2.28 to 2.34 s (0.9 s) | 2.42 to 2.44 s (2.9 s at a load of 4.5) |
| Forged burn of 65,535 rights (refused since the inspection counts) | 7.7 MB | 131k | ids 131k, model 29.9 s (16.3 + 13.5) | 9.0 to 9.3 s (3.5 s) | 9.7 to 9.8 s (14.2 s at a load of 4.5) |
| Forged fan-out to 16,384 outputs | 0.4 MB | 16k | model 2.6 s (1.3 + 1.3) | 1.18 to 1.22 s (0.1 s) | 1.15 to 1.16 s |
| Forged fan-out to 65,535 outputs | 1.5 MB | 66k | model 10.5 s (5.3 + 5.2) | 4.69 to 4.70 s (0.5 s) | 4.65 to 4.67 s |
| Forged chain of 13 propagates of 65,535 rights (refused, one run) | 11.9 MB | 852k | model 123.1 s (60.0 + 63.1) | 44.3 s (5.5 s), plus 4.0 s of our pre-checks | 51.3 s |
| Forged chain of 217 propagates of 4096 rights (refused) | 12.5 MB | 889k | model 128.7 s (62.7 + 66.0) | 44.9 to 46.1 s (6.0 to 7.2 s), plus 4.2 s of our pre-checks | 54.3 s (one run) |
| Forged file at the size cap: 150 burns of 700 rights, genesis of 150 rights (refused) | 12.5 MB | 210k | model 48.8 s (27.0 + 21.8) | 16.1 to 16.5 s (5.7 to 5.8 s) | 16.8 s (one run, at a load of 5) |
| Forged file at the size cap: 13 burns of 8192 rights, genesis of 13 rights (refused) | 12.6 MB | 213k | model 48.5 s (26.5 + 22.0) | 14.6 to 14.9 s (5.8 to 6.0 s) | 15.9 s (one run, at a load of 5) |
| Forged bundle of 64 burns, tx of 49,900 dummy inputs | 2.4 MB | 130 | witness 134.1M (at the cap), model 7.2 s (4.6 + 2.7) | 1.94 to 1.95 s (0.9 s) | 1.96 to 1.97 s |
| Forged bundle of 256 burns, tx of 12,480 dummy inputs (refused) | 0.7 MB | 514 | witness 134.2M (126 over the cap) | 1.58 to 1.59 s (0.2 s) | 1.16 s |
| Forged bundle of 64 burns, Tapret partner leaf of 2.09 MB | 2.1 MB | 130 | witness 133.8M (at the cap), model 6.7 s (4.2 + 2.5) | 2.26 to 2.27 s (0.9 s) | 1.00 to 1.03 s |
| Forged bundle of 256 burns, Tapret partner leaf of 523 KB | 0.6 MB | 514 | witness 133.9M (at the cap), model 4.4 s (2.9 + 1.5) | 1.89 s (0.3 s) | 0.31 s |
| Forged bundle of 511 burns, Tapret partner leaf of 262 KB | 0.4 MB | 1k | witness 133.9M (at the cap), ids 261,634, model 5.0 s (3.6 + 1.4) | 2.57 to 2.58 s (0.2 s) | 0.26 s |
| Forged burn of 8192 rights, tx of 16,000 dummy inputs | 1.7 MB | 16k | witness 131.7M (seal scans), model 8.0 s (4.7 + 3.2) | 1.54 s (0.7 s) | 1.82 s |
| Forged bundle of 512 burns (one over the bundle id cap) | 0.1 MB | 1k | ids 262,658 | 1.16 s (0.05 s) | 0.14 s |
| Forged bundle of 2048 burns (refused) | 0.4 MB | 4k | ids 4.2M | 15.5 to 15.8 s (0.2 s) | 0.55 to 0.56 s |
| Forged fan-out of 65,535 after 500 transfers, listed | 1.7 MB | 67k | walk 65.9M, model 16.1 s (10.6 + 5.5) | 9.5 to 9.6 s, of which the walk 4.4 to 4.7 s | 4.86 s |
| Forged merge of 4096 lines after 8180 transfers, listed (refused since the inspection counts) | 6.6 MB | 33k | walk 67.1M (at the cap), model 23.5 s (15.8 + 7.7) | 12.8 to 12.9 s, of which the walk 4.8 s | 6.29 to 6.32 s |
| Forged merge of 1400 lines after 23,900 transfers, listed (refused) | 12.1 MB | 53k | walk 67.0M, model 38.0 s (24.3 + 13.7) | 19.5 to 19.9 s, of which the walk 4.5 to 4.6 s (5.2 s) | 11.6 s (14.0 s at a 5-minute load of 10) |
| Forged merge of 600 lines after 23,900 transfers, listed (refused) | 11.6 MB | 50k | walk 28.7M, model 33.5 s (20.5 + 13.0) | 16.6 to 16.8 s, of which the walk 2.1 to 2.3 s (4.9 to 5.0 s) | 11.0 s |
The forged burns are schema-valid, with no witness mined, so rgb-lib refuses them only after phase 1 ran in full. The inspection runs only once validation passed, so it is timed on the forged files directly; it checks no witness. The walk runs only once every witness passed, so a forged walk needs mined transactions. One rgb-lib walk step takes 45 to 73 ns, so the walk cap holds a walk under about 5 s; the enclave's own count of it took 0.2 s at the cap. One bundle id entry takes about 3.5 to 4 us, one byte of a forged tx or partner leaf about 7 to 11 ns, one seal scan about 2 ns. Every shape measured at a load under 4 stays within its model, on the genesis of its row. The validation rates' margin is about the spread between runs: one of nine runs of the 4096-line merge, while the machine was still settling from other load, took 16.2 s against the 15.8 s its validation models. A busy or slower host runs past the model by that much: at a load of 4.5 the 65,535-right burn's inspection took 14.2 s, 5 percent over its 13.5 s.
What the caps do not bound: rgb-lib's work that is linear in the file, its
opouts and its operations. Validation takes about 0.8 to 1.1 us per byte
(strict-decoding the file through an unbuffered File takes about 0.45 us per
byte of read calls), about 35 to 55 us per opout beyond its bytes, and
13.7 us per genesis leaf for each operation; the inspection about 0.5 us per
byte, 46 to 60 us per opout and up to 130 us per operation. That part is
bounded by MAX_CONSIGNMENT_BYTES (12 MiB by default) and by
MAX_RGB_LIB_NS, which holds the modeled sum of file, opouts, operations,
genesis hashes and capped parts to 18 s: at most about 2.5 s of witness checks
in validation and 1.1 s in the inspection, 1.2 s of bundle ids and 5 s of walk
on top of the file. It is what dominates an honest burn with a long history:
on the pinned genesis the 2810-mint burn with 3 transfers a line takes 15.9 to
16.2 s end to end. The validation rates are rounded up from the shapes above,
5 to 12 percent above the costliest per opout (fan-outs) and per byte (merges
of transfers and honest burns), and 9 percent above the genesis chains; the
inspection rates 8 percent above the costliest, the honest 2810-mint burn. A
shape that costs rgb-lib more per byte, per opout, per operation or per genesis
leaf than those measured would run past it by that much. The genesis rate
double counts on a small genesis: its leaves an operation were already in the
measured shapes the other rates come from.
What the caps can refuse in an honest history. The walk cap (listed asset
only) can refuse an exact-amount burn: with no change there is no split, so
each of its k inputs is an origin and walks on its own. If those
k allocations were paid off one payer chain after D earlier links, the walk
costs 2kD + k^2 + 4k steps: k = 3478 (2 allocations per input) and D = 8000
cost 67.8M. At one allocation per input (k <= 1739) it needs D of at least
18.4k, more witnesses than MAX_MERKLE_PROOFS. A burn with change has one
origin, the split's output, which walks its ancestry once. The time budget
refuses an honest history past about 18 s of modeled work, validation and
inspection together. On the pinned genesis of 10 rights a history of
transfers alone is about 230 bytes per opout, and each opout models at about
0.7 ms, so it reaches 18 s at about 6 MB: the 2810-mint burn with 3 transfers
a line (6.0 MB) passes at 17.9 s, and a longer history is refused. On a
genesis of 255 rights each operation adds 3.9 ms, so the budget refuses a
history past about 4,000 operations, whatever its size. The 2810-mint burn
passes there at 16.5 s, 14.1 s of it validation. 1404 or 2810 mints with 3
transfers a line are refused there; they would take rgb-lib 22.7 and 45.9 s
to validate, the second past the 30 s deadline.