RGB conformance specification
Rules for reading RGB invoices and validating consignments, for both RGB lines, with conformance test vectors.
Version 0.0.1 · 41 rules · 725 vectors · generated from the ruleset · living document
- 1. Abstract
- 2. Status of this document
- 3. Conformance
- 4. Rules
- 5. Test vectors
- 6. Implementations
- 7. References
1. Abstract
This document specifies the rules an implementation of RGB applies when it reads invoices and validates consignments, for both RGB lines: v0.11 (RGB Protocol Association) and v0.12 (RGB-WG). Each rule comes with test vectors. The rules are data (rules/v011.jsonld, rules/v012.jsonld); §4 is generated from them, and the live suite runs every vector in your browser.
2. Status of this document
A living document, version 0.0.1, generated from source on each build. It is an independent community project, not affiliated with or endorsed by the RGB Protocol Association, RGB-WG or LNP/BP. It describes the rules as the reference crates implement them at the versions named in each rule, plus a few checks the rgbjs implementation adds (marked rgbjs). Where this document and the reference code disagree, the reference code is right and this document is wrong. It is early work: not for real funds.
3. Conformance
The key words MUST, MUST NOT, SHOULD and MAY are to be read as in RFC 2119.
Each rule has a level:
- upstream: the reference code enforces it. A conforming implementation MUST reject input that violates it.
- rgbjs: the reference code doesn't enforce it, but accepting such input is unsafe or misleading. A conforming implementation SHOULD reject it.
- informative: such input can be valid, but it is not the same as the original. A conforming implementation MAY accept it but MUST NOT report it as the original: its result MUST show a different contract ID, an untrusted issuer, or different terminals, witnesses or state.
An implementation conforms if, for every vector in §5, its verdict matches: accept, reject, or for informative vectors reject or visibly different. Error messages are shown but not compared, so implementations may word them differently. Implementations MUST NOT crash on any vector: a decode error is a verdict, an abort is not.
4. Rules
41 rules: 31 upstream, 5 rgbjs, 5 informative. Each row links to its vectors in the live suite.
4.1 RGB v0.11
Rules for the RGB v0.11 line (RGB Protocol Association: rgb-protocol and RGB-Tools crates), as implemented by rgb-invoicing, rgb-ops, rgb-consensus and rgb-schemas 0.11.1.
4.1.1 Invoices v0.11
Parsing an rgb: invoice string. Grammar: rgb:<contract|~>/<schema|~>/<state|~>/<chain>:<utxob:…|wvout:…>[?query].
| Rule | Requirement | Level | Error and source | Vectors |
|---|---|---|---|---|
v011-invoice-scheme | An invoice is a URI with the scheme rgb. Anything else, including a missing scheme, is rejected. invoice.scheme == "rgb" | upstream | InvoiceParseError::Invalid, InvoiceParseError::InvalidScheme rgb-invoicing 0.11.1 src/parse.rs | 5 |
v011-invoice-beneficiary | The beneficiary is <chain>:utxob:<blinded seal> or <chain>:wvout:<address payload>[+<internal key>], with a known chain prefix (bc, tb3, tb4, sb, sbc, bcrt, lq, tl). parse(invoice.beneficiary) is BlindedSeal | WitnessVout | upstream | InvoiceParseError::Beneficiary rgb-invoicing 0.11.1 src/parse.rs | 5 |
v011-invoice-expiry | The expiry query parameter, if present, is a Unix timestamp in seconds. invoice.query.expiry is integer | upstream | InvoiceParseError::InvalidExpiration rgb-invoicing 0.11.1 src/parse.rs | 1 |
v011-invoice-query | Query parameters are key=value pairs. endpoints is a comma-separated list of transports with a host: rpc://, rpcs://, http(s)://, ws(s):// or storm://. Unknown keys are kept, not rejected. ∀ p ∈ invoice.query: p has '=' ∧ (p.key ≠ endpoints ∨ all(parse_transport)) | upstream | InvoiceParseError::InvalidQueryParam rgb-invoicing 0.11.1 src/parse.rs | 6 |
v011-invoice-roundtrip | A valid invoice prints back exactly as it was given. to_string(parse(s)) == s | upstream | - rgb-invoicing 0.11.1 src/parse.rs (Display for RgbInvoice) | 22 |
v011-invoice-fields | Parsing an invoice made by rgb-cmd recovers the contract, network, amount and beneficiary that were given to rgb invoice. Amounts are encoded in a digit-free base32 alphabet: BF is 100. parse(rgb_invoice(contract, amount, seal)) == {contract, amount, seal} | upstream | - rgb-invoicing 0.11.1 src/invoice.rs (RgbInvoice) | 2 |
4.1.2 Consignment files v0.11
Reading a binary consignment: RGB\0 followed by TFR (transfer) or CON (contract), then the strict-encoded consignment.
| Rule | Requirement | Level | Error and source | Vectors |
|---|---|---|---|---|
v011-file-header | A consignment file starts with RGB\0 and the magic TFR or CON. bytes[0..4] == "RGB\0" ∧ bytes[4..7] ∈ {TFR, CON} | upstream | LoadError::InvalidMagic rgb-ops 0.11.1 src/containers/file.rs:63 | 48 |
v011-strict-decoding | The body is strict-encoded: maps and sets are sorted with no duplicates, collections stay within their bounds, identifiers aren't empty, reserved version bytes have their defined value, and the data doesn't end early. Any deviation is a decode error, not a different reading of the data. strict_decode(body) succeeds ∧ encoding is canonical | upstream | DecodeError (DataIntegrityError, BrokenMapOrder, BrokenSetOrder, RepeatedMapValue, RepeatedSetValue, Confinement, Io UnexpectedEof) rgb-strict-encoding 1.0.5 src/error.rs | 48 |
v011-consignment-type | The transfer flag inside the consignment matches the file type: TFR files hold transfers, CON files hold contracts, and a contract consignment has no bundles or terminals. consignment.transfer == (magic == TFR) ∧ (¬transfer → bundles = terminals = ∅) | upstream | Failure::Custom("invalid consignment type") rgb-ops 0.11.1 src/containers/consignment.rs:376 | 18 |
v011-terminals-present | Every bundle a transfer names as a terminal is in the consignment. ∀ b ∈ terminals.keys: b ∈ bundle_ids | upstream | Failure::Custom("terminal bundle id … is not present in the consignment") rgb-ops 0.11.1 src/containers/consignment.rs:388 | 24 |
4.1.3 Schema and type trust v0.11
What the validator takes on trust. The consignment carries its schema and types, but they are checked against definitions the validator already has.
| Rule | Requirement | Level | Error and source | Vectors |
|---|---|---|---|---|
v011-known-schema | rgbjs only validates consignments for the standard schemas it can trust: NIA, CFA, UDA, IFA and PFA from rgb-schemas 0.11.1. Wallets do the same with the schemas they have imported. consignment.schema_id ∈ {NIA, CFA, UDA, IFA, PFA} | rgbjs | rgbjs: unknown schema … rgbjs rust/v011/src/lib.rs (known schemas from rgb-schemas 0.11.1) | 48 |
v011-schema-match | The schema shipped in the consignment is the one the contract's genesis commits to. schema_id(consignment.schema) == genesis.schema_id | upstream | Failure::SchemaMismatch rgb-consensus 0.11.1 src/validation/status.rs:136 | 48 |
v011-trusted-types | Every type in the consignment equals the trusted definition with the same semantic ID. Upstream compares with Ty's PartialEq, which treats variants with the same tag as equal even if their names differ (rgb-strict-encoding Variant::eq); rgbjs also requires the names to match. ∀ (id, ty) ∈ consignment.types: trusted[id] == ty (names included) | upstream | Failure::TypeSystemMismatch; rgbjs: type … differs from the trusted … definition rgb-consensus 0.11.1 src/validation/validator.rs:285 (validate_schema) | 48 |
4.1.4 Genesis and operations v0.11
Each operation (genesis and transitions) belongs to the contract, has the ID it claims, follows the schema, and passes its AluVM scripts.
| Rule | Requirement | Level | Error and source | Vectors |
|---|---|---|---|---|
v011-contract-id | The contract ID is a commitment to the genesis. Changing anything the genesis commits to yields a different contract, which a recipient will not mistake for the one in their invoice. contract_id == commit(genesis) | informative | - rgb-consensus 0.11.1 src/operation/operations.rs (Genesis::contract_id) | 18 |
v011-same-contract | Every transition in the consignment names this contract. ∀ op ∈ transitions: op.contract_id == contract_id | upstream | Failure::ContractMismatch rgb-consensus 0.11.1 src/validation/status.rs:209 | 24 |
v011-opid | A transition's claimed ID is the commitment to its contents. ∀ op: op.claimed_id == commit(op) | upstream | Failure::TransitionIdMismatch rgb-consensus 0.11.1 src/validation/status.rs:211 | 24 |
v011-schema-conformance | Operations use only the global and owned state types their schema declares, with values of the declared semantic type and the declared number of assignments. ∀ op: types(op) ⊆ schema ∧ occurrences(op) ∈ schema.bounds ∧ typecheck(op.globals) | upstream | Failure::SchemaUnknownGlobalStateType, SchemaUnknownAssignmentType, SchemaAssignmentOccurrences, SchemaInvalidGlobalValue rgb-consensus 0.11.1 src/validation/status.rs:171-202 | 48 |
v011-scripts-present | Every AluVM library an operation's validation calls is shipped in the consignment. ∀ lib ∈ called(schema): lib ∈ consignment.scripts | upstream | Failure::MissingScript rgb-consensus 0.11.1 src/validation/status.rs:260 | 48 |
v011-aluvm | Each operation passes its schema's AluVM validation scripts, for example that a fungible transfer doesn't create value. ∀ op: aluvm_run(schema.validator(op)) == ok | upstream | Failure::ScriptFailure rgb-consensus 0.11.1 src/validation/status.rs:255 | 49 |
4.1.5 Witnesses and seals v0.11
For transfers: each bundle is anchored in a witness transaction that closes the seals it spends. Whether the witness is mined is a separate, on-chain check.
| Rule | Requirement | Level | Error and source | Vectors |
|---|---|---|---|---|
v011-dbc-output | The witness transaction has the output its commitment method needs: OP_RETURN for opret, taproot for tapret. ∃ out ∈ witness.outputs: out is OP_RETURN ∨ taproot | upstream | Failure::NoDbcOutput rgb-consensus 0.11.1 src/validation/status.rs:232 | 12 |
v011-anchor | The bundle's multi-protocol commitment proof places it at the right position for this contract's protocol ID. mpc_verify(anchor.proof, contract_id, bundle_id) | upstream | Failure::AnchorInvalid rgb-consensus 0.11.1 src/validation/status.rs:229 | 24 |
v011-seal-closing | The witness transaction spends every seal the bundle closes, and its commitment matches the bundle. ∀ seal ∈ closed(bundle): seal.outpoint ∈ witness.inputs ∧ commitment(witness) == bundle | upstream | Failure::SealsInvalid rgb-consensus 0.11.1 src/validation/status.rs:226 | 24 |
v011-terminals-hint | The blinded seals a transfer lists as its terminals are not validated. A changed terminal leaves a valid consignment that no longer names the recipient's seal, so results must show the terminals, and a recipient checks that their own seal is there. result.terminals == consignment.terminals | informative | - rgb-ops 0.11.1 src/containers/consignment.rs:250 (terminals) | 18 |
v011-state-shows-changes | Fields of an unsigned witness transaction that its commitment doesn't cover (such as input sequence numbers) can change without making the transfer invalid; its txid changes instead. Results must show the witness IDs and state a consignment produces, so such a change is visible. result.witnesses and result.state reflect the consignment | informative | - rgb-ops 0.11.1 src/containers/anchors.rs:274 (PubWitness) | 24 |
4.2 RGB v0.12
Rules for the RGB v0.12 line (RGB-WG / LNP/BP), as implemented by rgb-std and rgb-invoice 0.12.0-rc.3, rgb-core, sonic-api, sonic-callreq, hypersonic and ultrasonic 0.12.0.
4.2.1 Invoices v0.12
Parsing a contract: invoice string. Grammar: contract:<chain>@<contract>/[<api>/][<method>/[<state>/]]<value>@<seal>/[?query].
| Rule | Requirement | Level | Error and source | Vectors |
|---|---|---|---|---|
v012-invoice-scheme | An invoice is a URI with the scheme contract. A v0.11 rgb: invoice is rejected, so the two lines' invoices can't be confused. invoice.scheme == "contract" | upstream | ParseInvoiceError::NoScheme; CallReqParseError (invalid contract call request URI scheme) sonic-callreq 0.12.0 src/uri.rs; rgb-invoice 0.12.0-rc.3 src/lib.rs:132 | 2 |
v012-invoice-roundtrip | A valid invoice prints back as it was given. Known deviation: with rgb-invoice's RgbInvoice type, CallScope's Display doesn't pass on the {:-} flag, so the contract ID prints with a second contract: prefix (contract:tb@contract:<id>, AluVM/sonic#27). Both forms parse to the same invoice; conforming implementations accept both. fold(to_string(parse(s))) == s, where fold removes the doubled contract: prefix | upstream | - sonic-callreq 0.12.0 src/uri.rs:65 (Display for CallRequest) | 10 |
v012-invoice-fields | Parsing an invoice made by rgb-wallet recovers the contract, layer 1, value and seal (an at: auth token or a wout: witness output) given to rgb invoice. parse(rgb_invoice(contract, value, seal)) == {contract, value, seal} | upstream | - sonic-callreq 0.12.0 src/data.rs:110 (CallRequest) | 2 |
4.2.2 Consignment files v0.12
Reading a binary consignment: the 8-byte magic RGBCNSGN, a 2-byte file version, a consignment version byte, the contract ID, then the strict-encoded consignment.
| Rule | Requirement | Level | Error and source | Vectors |
|---|---|---|---|---|
v012-file-header | A consignment file starts with RGBCNSGN and file version 0, followed by consignment version 0. Later versions are rejected, not guessed at. bytes[0..8] == "RGBCNSGN" ∧ file_version == 0 ∧ consign_version == 0 | upstream | binfile magic/version error; DecodeError("unsupported future consignment version") rgb-std 0.12.0-rc.3 src/contracts.rs:657 (CONSIGN_MAGIC_NUMBER), src/consignment.rs:124 (parse_consignment) | 28 |
v012-strict-decoding | The body is strict-encoded: maps and sets are sorted with no duplicates, enum and union tags are known, identifiers are valid, strings are UTF-8, reserved bytes have their defined value, and the data doesn't end early. strict_decode(body) succeeds ∧ encoding is canonical | upstream | DecodeError (DataIntegrityError, BrokenMapOrder, BrokenSetOrder, RepeatedMapValue, RepeatedSetValue, EnumTagNotKnown, UnionTagNotKnown, Utf8, Io UnexpectedEof) strict_encoding 2.9.1 src/error.rs | 28 |
v012-bounded-allocation | A decoder must not allocate memory in proportion to a length it read from the data before checking it. Upstream strict_encoding and strict_types 2.9.1 pre-allocate from such lengths, so one corrupted byte can request about 4.4 GB (aborting a wasm or memory-limited process) or overflow capacity and panic. The pinned rgb-wallet 0.12.0-rc.3 crashes on rgb accept of such a file. A corrupted length must give a decode error or a verdict, never a crash. ∀ forged length: decode returns an error, never aborts | rgbjs | DecodeError or a verdict (instead of an out-of-memory abort or capacity-overflow panic) strict_encoding 2.9.1 src/embedded.rs:514 and strict_types 2.9.1 src/value/decode.rs:91 (Vec::with_capacity(len)); fixed in rgbjs rust/patches/ | 4 |
4.2.3 Articles and issuer trust v0.12
A v0.12 consignment carries its articles: the codex (which the contract ID commits to) and the semantics (APIs, types, libraries), which the contract ID does not commit to.
| Rule | Requirement | Level | Error and source | Vectors |
|---|---|---|---|---|
v012-header-contract | The contract ID in the file header equals the ID computed from the articles. Upstream reads the header ID to decide whether the contract is known, then imports the articles under their own ID; rgbjs rejects a mismatch. header.contract_id == articles.contract_id | rgbjs | rgbjs: header names contract … but the articles are for … rgbjs rust/v012/src/lib.rs (validate_consignment) | 28 |
v012-genesis-codex | Genesis carries a codex_id field that neither the contract ID nor validation uses: the contract ID commits to the codex in the issue instead. rgbjs rejects a genesis whose codex_id differs from that codex. genesis.codex_id == issue.codex.codex_id | rgbjs | rgbjs: genesis names codex … but the contract uses … ultrasonic 0.12.0 src/operation.rs:282 (Genesis.codex_id), src/issue.rs:79 (Issue::commit_encode) | 28 |
v012-codex-libs | Every zk-AluVM library the codex verifiers call is in the articles. ∀ lib ∈ codex.verifier_libs: lib ∈ semantics.codex_libs | upstream | SemanticError::MissingCodexLib sonic-api 0.12.0 src/api.rs:579 | 28 |
v012-issuer-trust | The semantics say what the state means but travel outside the contract ID, so they are trusted only when they match a known issuer. The issuer ID's checksum covers type IDs but not type contents (strict_types TypeSystem::id hashes the keys only), so rgbjs requires the full semantics to match, names included. trusted ⇔ ∃ issuer ∈ known: issuer.id == id(semantics) ∧ issuer.semantics == semantics | rgbjs | result.issuer.trusted == false sonic-api 0.12.0 src/issuer.rs:58 (IssuerId), src/api.rs:193 (Semantics commit_encode) | 28 |
4.2.4 Genesis and operations v0.12
Every operation is evaluated into the contract: its codex verifier passes and the seals it defines and spends are consistent.
| Rule | Requirement | Level | Error and source | Vectors |
|---|---|---|---|---|
v012-contract-id | The contract ID commits to the issue: version, metadata, codex ID and genesis. Changing them yields a different contract. contract_id == commit(issue.version, issue.meta, codex_id, genesis_opid) | informative | - ultrasonic 0.12.0 src/issue.rs:76 (Issue::commit_encode) | 21 |
v012-same-contract | Every operation in the consignment names this contract. ∀ op: op.contract_id == contract_id | upstream | CallError::WrongContract ultrasonic 0.12.0 src/codex.rs:296 | 12 |
v012-verify-script | Each operation calls a verifier its codex defines, and passes that zk-AluVM verifier. ∀ op: codex.verify(op) == ok | upstream | CallError::NotFound, CallError::Script ultrasonic 0.12.0 src/codex.rs:308-342 | 16 |
v012-seal-definitions | Every cell an operation spends has a known seal definition, and the seals an operation reveals match the auth tokens in its outputs. ∀ op, cell ∈ op.inputs: seal(cell) known ∧ ∀ (no, seal) ∈ op.seals: auth_token(seal) == op.outputs[no].auth | upstream | VerificationError::NoSealDefinition, VerificationError::SealsDefinitionMismatch rgb-core 0.12.0 src/verify.rs:283-286 | 28 |
4.2.5 Witnesses and seals v0.12
For transfers: each operation that spends seals comes with the witness transaction that closes them. Whether the witness is mined is a separate, on-chain check.
| Rule | Requirement | Level | Error and source | Vectors |
|---|---|---|---|---|
v012-witness-known | An operation that closes seals has a witness. ∀ op ≠ genesis: op.witness ≠ ∅ | upstream | VerificationError::NoWitness rgb-core 0.12.0 src/verify.rs:275 | 6 |
v012-seal-closing | The witness transaction spends every seal the operation closes, and its anchor commits to the operation. ∀ seal ∈ closed(op): seal.primary ∈ witness.inputs ∧ verify(anchor, witness, op) | upstream | VerificationError::SealsNotClosed rgb-core 0.12.0 src/verify.rs:278 | 12 |
v012-state-shows-changes | Some bytes are not committed to: a revealed seal definition that doesn't match its auth token (the allocation becomes unknown rather than moving), or uncommitted fields of an unsigned witness transaction (its txid changes). Such a consignment can still be valid, so results must show the state and witness IDs it produces. result.state and result.witnesses reflect the consignment | informative | - rgb-std 0.12.0-rc.3 src/contract.rs:467 (Contract::state) | 17 |
5. Test vectors
725 vectors (vectors/index.json), all copied into this repository; nothing is fetched at runtime.
- 51 invoice vectors. Every case in the invoice tests of
rgb-invoicing0.11.1 andsonic-callreq0.12.0, real invoices printed byrgb-cmd0.11.1 andrgb-wallet0.12.0-rc.3, and each of those given to the other line's parser. - 20 consignments. Contracts and transfers for every v0.11 schema (NIA, CFA, UDA, IFA, PFA) and for the v0.12 RGB20, RGB21 and RGB25 issuers. They come from the pinned CLIs and from offline generators that drive the reference wallet libraries (v0.11, v0.12). Files named
created-*.rgbwere made by rgbjs's ownissueContractfrom fixed parameters, so creating is held to the same rules. Files named*.invalid.rgbmust be rejected as they are, such as a PFA transfer without its issuer signature. - 654 corruptions. Every single-byte change (one bit flipped) of each consignment was run through the reference code and classified by the rule it breaks; up to three per rule and file are kept as vectors. No corruption is accepted as the same contract with the same result.
6. Implementations
rgbjs (rgbjs 1917069): each line's Rust crates compiled to WebAssembly, run in Node and the browser. A copy is in engine/. Other implementations can run the suite with lib/runner.js, which needs parseInvoice and validateConsignment for each line.
7. References
- RGB v0.11 crates: github.com/rgb-protocol
- RGB v0.12 crates: github.com/RGB-WG, github.com/AluVM
- The two RGB lines: rgb-docs.github.io
- RFC 2119: Key words for use in RFCs to Indicate Requirement Levels
- Inspired by bitcoin-kernel's conformance specification.