Skip to main content

Encoding Formats

Applies to both RGB lines

Both lines use the same Baid64 and tagged-hash code, but with different identifier prefixes (rgb: in v0.11, contract: in v0.12). See Two RGB lines.

RGB uses three encodings:

  • Strict encoding for all consensus data: operations, schemas, codexes, consignments. It is binary, deterministic and has no type tags.
  • Tagged SHA-256 to turn that data into ids and commitments.
  • Baid64 to print ids for people: contract ids, seals in invoices, library ids.

Bitcoin data (transactions, scripts, addresses) keeps Bitcoin's own encodings.

Baid64​

Baid64 is "Base64 encoding extended with HRP and mnemonic checksum", from the baid64 crate by UBIDECO. Line A pins baid64 0.4.2 and Line B pins 0.4.1. The encoding is the same in both: the only change between them is a debug assertion. Everything below comes from baid64 src/lib.rs.

PartRule
AlphabetA–Z a–z 0–9 _ ~ (Base64 with _ and ~ instead of + and /), no padding
HRI prefixAn optional human-readable identifier and a colon, e.g. rgb:; up to 16 bytes
ChunkingOptional: groups separated by -, the first group 8 characters, the rest 7 (some types use other sizes)
Embedded checksumOptional: 4 checksum bytes appended to the payload before encoding
MnemonicOptional: the same checksum as three words after #, e.g. #remote-digital-pegasus

- is a separator, not part of the alphabet. Parsers drop it before decoding.

The checksum. With h = SHA-256( SHA-256(HRI) ‖ payload ), the checksum is the four bytes h[0] h[1] h[1] h[2]. Byte 1 really is used twice; that's how the code is written (fn check). Because the HRI is mixed in, the same 32 bytes get a different checksum as a contract id than as a schema id.

Lengths. A 32-byte id is 43 Base64 characters. With an embedded checksum it is 36 bytes, so 48 characters. Chunk separators and the prefix come on top.

Parsing (FromBaid64Str) accepts the string with or without the HRI. It checks the HRI if one is present, strips -, and verifies the checksum when either an embedded checksum or a mnemonic is present.

Which ids use which format​

Each type sets its own options through DisplayBaid64 constants:

IdHRIChunkedChecksumExample
Contract idrgbyesnorgb:Ca0d5l5K-Cr4Wm6I-owUkn6t-2NHrgjk-R1KRreF-tVBhAR0
Schema idrgb:schnomnemonicrgb:sch:RWhwUfTMpuP2Zfx1~j4nswCANGeJrYOqDcKelaMV4zU#remote-digital-pegasus
Blinded seal (SecretSeal)utxobyesembeddedutxob:4vm1CX2Z-K8hMo59-e7dgGBS-Jka7mYn-Xe~yP85-yUiHHxr-aVlYa
Witness output (33 bytes)wvoutyesembeddedin invoices as bc:wvout:…
Consignment idrgb:csgyesmnemonic
Kit idrgb:kityesno
AluVM libraryaluyesmnemonicalu:q~CZ0ovt-UN9eBlc-VMn86mz-Kfd3ywu-f7~9jTB-k6A8tiY#japan-nylon-center

Sources: rgb-consensus 0.11.1 src/operation/commit.rs, src/schema/schema.rs, src/seals/secret.rs; rgb-ops 0.11.1 src/containers/consignment.rs, src/containers/kit.rs, invoice/src/parse.rs; rgb-aluvm 0.11.1 src/library/lib.rs.

The contract id, schema id and library id above are real output from rgb-cmd 0.11.1, from importing NonInflatableAsset.rgb from rgb-schemas 0.11.1 and issuing a contract offline on regtest:

rgb -n regtest import NonInflatableAsset.rgb
rgb -n regtest schemata
NonInflatableAsset      	rgb:sch:RWhwUfTMpuP2Zfx1~j4nswCANGeJrYOqDcKelaMV4zU#remote-digital-pegasus

The utxob: example is from the rgb-ops invoice tests.

Strict-types ids use Baid64 too: semid: (a type's semantic id), stl: (type library) and sts: (type system), all chunked with a mnemonic (strict_types 2.9.0 src/ast/id.rs, src/typelib/id.rs, src/typesys/id.rs).

Reference implementation​

This Python sketch follows baid64 0.4.x. It reproduces the real ids above, including the embedded checksums of the utxob: and at: examples:

import base64, hashlib

STD = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
BAID = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_~"

def checksum(hri: str, payload: bytes) -> bytes:
h = hashlib.sha256(hashlib.sha256(hri.encode()).digest() + payload).digest()
return bytes([h[0], h[1], h[1], h[2]])

def baid64(hri, payload, chunking=True, embed=False, first=8, size=7, prefix=True):
data = payload + (checksum(hri, payload) if embed else b"")
s = base64.b64encode(data).decode().rstrip("=").translate(str.maketrans(STD, BAID))
if chunking:
s = "-".join([s[:first]] + [s[i:i + size] for i in range(first, len(s), size)])
return f"{hri}:{s}" if prefix else s

cid = bytes.fromhex("09ad1de65e4a0abe169ba228c14927eadd8d1eb823911d4a46b785b55061011d")
print(baid64("rgb", cid)) # rgb:Ca0d5l5K-Cr4Wm6I-owUkn6t-2NHrgjk-R1KRreF-tVBhAR0

The mnemonic suffix encodes the same 4 checksum bytes with the word list of the Rust mnemonic crate. It is left out here.

Tagged hashes​

RGB ids and commitments are tagged SHA-256 hashes, the same construction as BIP 340:

tH(tag, data) = SHA-256( SHA-256(tag) ‖ SHA-256(tag) ‖ data )

data is the strict encoding of the object (commit_verify src/digest.rs, Sha256::from_tag). The tag is a dated URN, so the same bytes hashed as two different kinds of object can't collide. Some tags in use:

TagUsed forLine
urn:lnp-bp:rgb:operation#2024-02-03Operation id; the contract id is the genesis idB
urn:lnp-bp:rgb:bundle#2024-02-03Transition bundle idB
urn:lnp-bp:rgb:schema#2024-02-03Schema idB
urn:lnp-bp:seals:secret#2024-02-03Blinded seal (SecretSeal)B
urn:ubideco:sonic:contract#2024-11-16Contract idA
urn:ubideco:sonic:codex#2025-05-15Codex idA
urn:ubideco:ultrasonic:operation#2024-11-14Operation idA
urn:lnp-bp:mmb:bundle#2024-11-18Multi-message bundleA
urn:ubideco:mpc:commitment#2024-01-31MPC commitmentboth
urn:ubideco:merkle:node#2024-01-31Merkle nodes and MPC leavesboth

Sources: const TAG in rgb-consensus 0.11.1, ultrasonic 0.12.0, bp-core 0.12.0 and commit_verify 0.12.0. Merkle nodes commit to their branching, depth and width as well as the two children, and children are not sorted (MPC).

RGB never uses Bitcoin's double SHA-256 for its own ids. That appears only in Bitcoin's txids and block hashes.