Deep Dive into RGB Smart Contracts: From Handwritten AluVM Assembly to Understanding Contractum
About this post: A learning-oriented validation record. By writing RGB contract code for both v0.11 and v0.12 by hand, we dig into client-side validation, the AluVM virtual machine, and the evolutionary path toward Contractum. Intended for RGB developers, ecosystem partners, and protocol researchers.
Disclaimer and Stance
Guided by a willingness to share, we document our exploration and practice around RGB smart contracts for the community.
Important disclaimer:
The RGB v0.12 and the earlier v0.11 discussed here are developed by the RGB Consortium and the LNP/BP Standards Association.
On the v0.11 side, this article uniformly locks to the official RGB-WG v0.11.0-beta.9. Regarding other tags / release lines:
- The community should not treat this article as a source of controversy across versions, and we have no intent to stir dispute
- We compare the two versions so learners can better understand how RGB iterates through concrete changes in code implementation
Why This Exploration?
As developers and researchers in the RGB ecosystem, we keep seeing the same questions in the community:
- What language are RGB smart contracts actually written in?
- What is the essential difference between v0.11 and v0.12 smart contracts?
- What is the future Contractum smart contract language?
- How does client-side validation work at the bottom?
Rather than stop at reading docs and theorizing, we decided to hand-write complete RGB contract code for both versions, run the full issue-to-transfer flow, and answer these questions with practice.
Scope of Validation
We use RGB20 fungible tokens as the control case, with a minimal hand-written implementation on both v0.11 and v0.12.
Why RGB20?
- RGB20 (asset conservation: input amount equals output amount) has official counterparts in both versions (NIA on v0.11, RGB20-FNA on v0.12), so the comparison is direct
- NIA (Non-Inflatable Asset): v0.11 official reference implementation, non-inflatable asset
- RGB20-FNA (Fungible Non-inflatable Asset): v0.12 official reference implementation, fungible non-inflatable asset
- The “transfer conservation” invariant is simple enough to implement for real on two completely different validation mechanisms
Part I: Hands-On Validation
We document the complete process of hand-writing contracts for both generations—this is where the article's credibility comes from.
v0.11 Practice: RGB-WG Official v0.11.0-beta.9
Environment
| crate | version | source |
|---|---|---|
rgb-core | 0.11.0-beta.9 | github.com/RGB-WG/rgb-core |
rgb-std | 0.11.0-beta.9 | github.com/RGB-WG/rgb-std |
rgb-interfaces | 0.11.0-beta.9 | github.com/RGB-WG/rgb-interfaces |
aluvm | 0.11.0-beta.9 | RGB-WG ecosystem release of the same period |
Code location: v0.11-rgb20/ (rgb-smart-contract-demo)
Contract Structure: Schema + Iface + IfaceImpl
A v0.11 contract is defined by three objects together:
Schema "SimpleRGB20" Iface "SimpleRGB20Iface"
├─ global_types ├─ global_state: spec/terms/issuedSupply
│ ├─ spec (AssetSpec, once) ├─ assignments: assetOwner (Amount)
│ ├─ terms (ContractTerms) ├─ genesis (globals/assignments occurrences)
│ └─ issuedSupply (Amount, once) └─ transitions: transfer
├─ owned_types
│ └─ assetOwner (Fungible<u64>) IfaceImpl (binds Schema <-> Iface)
├─ genesis ├─ GS_NOMINAL <-> "spec"
│ ├─ globals/assignments occurrences ├─ GS_TERMS <-> "terms"
│ └─ validator -> LibSite(FN_GENESIS_OFFSET) ├─ GS_ISSUED_SUPPLY <-> "issuedSupply"
└─ transitions ├─ OS_ASSET <-> "assetOwner"
└─ transfer (TS_TRANSFER=10000) └─ TS_TRANSFER <-> "transfer"
├─ inputs/assignments occurrences
└─ validator -> LibSite(FN_TRANSFER_OFFSET=0)Core code (GitHub: v0.11-rgb20/src/main.rs):
// Schema: consensus terms
fn schema() -> Schema {
let types = types();
let lib = rgb20_lib();
let lib_id = lib.id();
Schema {
name: tn!("SimpleRGB20"),
global_types: tiny_bmap! {
GS_NOMINAL => GlobalStateSchema::once(types.get("RGBContract.AssetSpec")),
GS_TERMS => GlobalStateSchema::once(types.get("RGBContract.ContractTerms")),
GS_ISSUED_SUPPLY => GlobalStateSchema::once(types.get("RGBContract.Amount")),
},
owned_types: tiny_bmap! {
OS_ASSET => OwnedStateSchema::Fungible(FungibleType::Unsigned64Bit),
},
genesis: GenesisSchema {
validator: Some(LibSite::with(FN_GENESIS_OFFSET, lib_id)),
..
},
transitions: tiny_bmap! {
TS_TRANSFER => TransitionSchema {
validator: Some(LibSite::with(FN_TRANSFER_OFFSET, lib_id)),
..
}
},
..
}
}
// Iface: human-readable interface
fn iface() -> Iface {
Iface {
name: tn!("SimpleRGB20Iface"),
global_state: tiny_bmap! {
fname!("spec") => GlobalIface::required(types.get("RGBContract.AssetSpec")),
fname!("issuedSupply") => GlobalIface::required(types.get("RGBContract.Amount")),
},
assignments: tiny_bmap! {
fname!("assetOwner") => AssignIface::private(OwnedIface::Amount, Req::OneOrMore),
},
transitions: tiny_bmap! {
fname!("transfer") => TransitionIface { .. },
},
..
}
}
// IfaceImpl: bind Schema and Iface
fn iimpl(schema: &Schema, iface: &Iface) -> IfaceImpl {
IfaceImpl {
schema_id: schema.schema_id(),
iface_id: iface.iface_id(),
global_state: tiny_bset! { NamedField::with(GS_NOMINAL, fname!("spec")) },
assignments: tiny_bset! { NamedField::with(OS_ASSET, fname!("assetOwner")) },
transitions: tiny_bset! { NamedField::with(TS_TRANSFER, fname!("transfer")) },
..
}
}AluVM Validation Script: Specialized pcvs/pcas
fn rgb20_lib() -> Lib {
let code = rgbasm! {
// SUBROUTINE Transfer validation
put a8[0],ERRNO_NON_EQUAL_IN_OUT;
// Checking that the sum of Pedersen commitments in inputs equals the sum in outputs.
pcvs OS_ASSET;
test;
ret;
// SUBROUTINE Genesis validation
put a8[0],ERRNO_ISSUED_MISMATCH;
put a8[1],0;
put a16[0],0;
ldg GS_ISSUED_SUPPLY,a8[1],s16[0];
extr s16[0],a64[0],a16[0];
pcas OS_ASSET;
test;
ret;
};
Lib::assemble::<Instr<RgbIsa<MemContract>>>(&code)
}Technical note: pcvs/pcas verify the sum of blinded Pedersen commitments, not plaintext amounts. This is part of RGB’s confidential-transfer mechanism: every RevealedValue carries a random BlindingFactor, and on-chain comparison only checks whether the algebraic sum of commitments is equal.
Validation Flow
Step 1: Construct a contract instance
let contract = ContractBuilder::with(
Identity::default(),
iface.clone(),
schema.clone(),
iimpl.clone(),
types.clone(),
scripts.clone(),
)
.add_global_state("spec", AssetSpec::new("DEMO", "Demo RGB20 v0.11 Token", Precision::CentiMicro))
.add_global_state("terms", ContractTerms { text: Default::default(), media: None })
.add_global_state("issuedSupply", Amount::from(100_000u64))
.add_fungible_state("assetOwner", alice_seal, 70_000u64)
.add_fungible_state("assetOwner", bob_seal, 30_000u64)
.issue_contract()
.expect("contract doesn't fit schema requirements");Step 2: Valid transfer (Alice 70000 → Bob +10000, Alice-change 60000)
let valid = run_transfer(
&schema, &iface, &iimpl, &types, &scripts,
70_000u64, // input
&[10_000u64, 60_000u64], // outputs
);Result: ✅ Validation passed (pcvs confirmed equal commitment sums)
Step 3: Invalid transfer (spend 10000 but output 999999)
let invalid = run_transfer(
&schema, &iface, &iimpl, &types, &scripts,
10_000u64, // input
&[999_999u64], // output (intentionally non-conserving)
);Result: ❌ Correctly rejected
ScriptFailure(OpId(...), Some(0), None)Error code 0 corresponds to our defined ERRNO_NON_EQUAL_IN_OUT, meaning pcvs detected unequal commitment sums.
v0.11 Validation Takeaways
✅ Schema is the smart contract: By hand-writing Schema + Iface + IfaceImpl, we built a usable RGB20 token contract from scratch
✅ pcvs correctly rejects non-conserving transfers: Client-side validation correctness confirmed
✅ Pedersen commitments hide amounts: Protocol-native support for blinded transfers
v0.12 Practice: SONIC/UltraSONIC
Environment
| crate | version |
|---|---|
aluvm | 0.12.0 |
ultrasonic | 0.12.0 |
sonic-api | 0.12.0 |
hypersonic | 0.12.0 |
sonic-persist-fs | 0.12.0 |
Code location: v0.12-rgb20/ (rgb-smart-contract-demo)
Contract Structure: Codex + Api + Semantics
v0.12 cleanly separates “terms” from “interface”:
Codex (terms) Api (interface)
├─ verifiers: call_id → LibSite ├─ global: _ticker/_name/_precision/_issuedSupply
└─ no state definitions ├─ owned: balance (Fungible)
├─ aggregators: ticker/issuedSupply
└─ verifiers: issue/transfer → call_idCore code (GitHub: v0.12-rgb20/src/main.rs):
// Codex: only call_id -> LibSite mapping
fn codex() -> Codex {
let compiled = libs::build();
let issue_site = compiled.routine(libs::ROUTINE_ISSUE);
let transfer_site = compiled.routine(libs::ROUTINE_TRANSFER);
Codex {
name: tiny_s!("SimpleRGB20"),
verifiers: tiny_bmap! {
CALL_ISSUE => issue_site,
CALL_TRANSFER => transfer_site,
},
..
}
}
// Api: state definitions live here
fn api() -> Api {
let types = stl::Rgb20Types::new();
let codex = codex();
Api {
codex_id: codex.codex_id(),
global: tiny_bmap! {
vname!("_ticker") => GlobalApi {
sem_id: types.get("RGB20.Ticker"),
..
},
vname!("_issuedSupply") => GlobalApi {
sem_id: types.get("RGB20.Amount"),
..
},
},
owned: tiny_bmap! {
vname!("balance") => OwnedApi {
sem_id: types.get("RGB20.Amount"),
arithmetics: StateArithm::Fungible,
..
}
},
verifiers: tiny_bmap! {
vname!("issue") => CALL_ISSUE,
vname!("transfer") => CALL_TRANSFER,
},
..
}
}AluVM Validation Script: Hand-Written General-Purpose Loop
Full code (GitHub: v0.12-rgb20/src/libs.rs):
pub fn build() -> CompiledLib {
let code: Vec<Instr<LibId>> = uasm! {
// ---- routine TRANSFER: transfer verifier ------------------------------------------
// Invariant: sum(destructible_in) == sum(destructible_out)
routine TRANSFER:
put E2, 0; // 1. init input accumulator
put E3, 0; // 2. init output accumulator
rsti destructible; // 3. reset input iterator
rsto destructible; // 4. reset output iterator
loop LOOP_IN: // 5. start iterating inputs
cknxi destructible; // 6. check for next input
jif CO, EXIT_IN; // 7. if none, jump to EXIT_IN
ldi destructible; // 8. load current input value into EB
add E2, EB; // 9. accumulate into E2 (input sum)
jmp LOOP_IN; // 10. jump back to loop head
label EXIT_IN: // 11. input iteration done
loop LOOP_OUT: // 12. start iterating outputs
cknxo destructible; // 13. check for next output
jif CO, EXIT_OUT; // 14. if none, jump to EXIT_OUT
ldo destructible; // 15. load current output value into EB
add E3, EB; // 16. accumulate into E3 (output sum)
jmp LOOP_OUT; // 17. jump back to loop head
label EXIT_OUT: // 18. output iteration done
eq E2, E3; // 19. compare E2 (in) == E3 (out)
chk CO; // 20. check result; abort if unequal
stop; // 21. done
};
CompiledLib::compile(code, &[])
}Reading the code: These 26 lines of assembly do exactly the same thing as v0.11’s pcvs (iterate inputs, sum, iterate outputs, sum, compare), except:
- v0.11: the protocol encapsulates the steps inside
pcvs - v0.12: the developer writes them with general-purpose instructions
Validation Flow
Step 1: Construct a contract instance
let issuer = Issuer::new(codex, semantics).expect("invalid codex/semantics pair");
issuer.save(&issuer_path).expect("unable to save issuer");
let articles = issuer
.start_issue_testnet("issue", Consensus::None)
.append("_ticker", svstr("DEMO"), None)
.append("_name", svstr("Demo RGB20 v0.12 Token"), None)
.append("_precision", svnum!(2u8), None)
.append("_issuedSupply", svnum!(100_000u64), None)
.assign("balance", alice_auth, svnum!(70_000u64), None)
.assign("balance", bob_auth, svnum!(30_000u64), None)
.finish("SimpleRGB20Demo", 1732529307);
let mut ledger = LedgerDir::new(articles, contract_path.to_path_buf());Step 2: Valid transfer (Alice 70000 → Bob +10000, Alice-change 60000)
let alice_cell = ledger.state().addr(alice_auth);
let transfer_opid = ledger
.start_deed("transfer")
.using(alice_cell)
.assign("balance", bob_auth, svnum!(10_000u64), None)
.assign("balance", carol_auth, svnum!(60_000u64), None)
.commit()
.expect("valid transfer must succeed");Result: ✅ Validation passed (hand-written loop confirmed input sum == output sum)
Step 3: Invalid transfer (spend 10000 but output 999999)
let bob_received_cell = CellAddr::new(transfer_opid, 0);
let result = ledger
.start_deed("transfer")
.using(bob_received_cell)
.assign("balance", mallory_auth, svnum!(999_999u64), None)
.commit();Result: ❌ Correctly rejected
Error: ScriptUnspecifiedBecause this validation did not set an error-code branch in the script (no write to register E1), the return is ScriptUnspecified without an error code—but validation did fail.
v0.12 Validation Takeaways
✅ Turing complete: Real loops via jif/jmp (iteration count = number of cells, not fixed unrolling)
✅ 26 lines of assembly for conservation: Functionally equivalent to v0.11’s pcvs
✅ Correctly rejects non-conserving transfers: Client-side validation works
Side-by-Side Core Code Comparison
This is the most important comparison in the article—kept in the blog for readers.
Conservation Validation
v0.11 (4 lines):
let code = rgbasm! {
put a8[0],ERRNO_NON_EQUAL_IN_OUT;
pcvs OS_ASSET; // ← protocol built-in specialized instruction
test;
ret;
};v0.12 (26 lines):
let code = uasm! {
routine TRANSFER:
put E2, 0; put E3, 0;
rsti destructible; rsto destructible;
loop LOOP_IN:
cknxi destructible; jif CO, EXIT_IN;
ldi destructible; add E2, EB; jmp LOOP_IN;
label EXIT_IN:
loop LOOP_OUT:
cknxo destructible; jif CO, EXIT_OUT;
ldo destructible; add E3, EB; jmp LOOP_OUT;
label EXIT_OUT:
eq E2, E3; chk CO; stop;
};Contractum (1 line, future):
verify {
inputs.balance.sum() == outputs.balance.sum()
}State Definition Comparison
v0.11 (Schema + Iface + IfaceImpl, three layers):
// Schema: numeric IDs
global_types: tiny_bmap! {
GS_NOMINAL => GlobalStateSchema::once(types.get("RGBContract.AssetSpec")),
},
// Iface: field names
global_state: tiny_bmap! {
fname!("spec") => GlobalIface::required(types.get("RGBContract.AssetSpec")),
},
// IfaceImpl: binding table
global_state: tiny_bset! {
NamedField::with(GS_NOMINAL, fname!("spec")),
},v0.12 (Codex + Api, two layers):
// Codex: routing only
verifiers: tiny_bmap! {
CALL_TRANSFER => transfer_site,
},
// Api: state definitions
global: tiny_bmap! {
vname!("_ticker") => GlobalApi { sem_id: types.get("RGB20.Ticker"), .. },
},Contractum (one layer, future):
global _ticker: String;
owned balance: Zk64;Part II: Core Findings
Finding 1: RGB Contract Validation Logic Is AluVM Assembly, Not Rust
This was the most important finding in our validation process—and the most common misconception.
Common misconception: Seeing .rs source files and assuming RGB contracts are written in Rust.
Reality: RGB contract validation logic is AluVM assembly bytecode. Rust is only the host language and build tooling.
Reference code: https://github.com/rgbstashlabs/rgb-smart-contract-demo
v0.11 Example (RGB-WG Official v0.11.0-beta.9)
fn rgb20_lib() -> Lib {
let code = rgbasm! {
// SUBROUTINE Transfer validation
put a8[0],ERRNO_NON_EQUAL_IN_OUT;
// Checking that the sum of Pedersen commitments in inputs equals the sum in outputs.
pcvs OS_ASSET; // ← AluVM instruction, not a Rust function call
test;
ret;
// SUBROUTINE Genesis validation
put a8[0],ERRNO_ISSUED_MISMATCH;
put a8[1],0;
put a16[0],0;
ldg GS_ISSUED_SUPPLY,a8[1],s16[0];
extr s16[0],a64[0],a16[0];
pcas OS_ASSET;
test;
ret;
};
Lib::assemble::<Instr<RgbIsa<MemContract>>>(&code)
}How to read this:
rgbasm!is a compile-time macro that expands identifiers likeput/pcvs/test/retintoVec<Instr<RgbIsa>>(an AluVM instruction sequence)Lib::assemblecompiles that sequence into an AluVM bytecode library- At validation time,
Schema::validate_stateruns this bytecode on the AluVM virtual machine—it does not execute Rust functions
Rust’s role here: It only lets you write assembly with the Rust compiler (via macro sugar). The compile artifact is bytecode.
v0.12 Example (SONIC/UltraSONIC)
pub fn build() -> CompiledLib {
let code: Vec<Instr<LibId>> = uasm! {
routine TRANSFER:
put E2, 0;
put E3, 0;
rsti destructible;
rsto destructible;
loop LOOP_IN:
cknxi destructible; // ← check whether there is a next input
jif CO, EXIT_IN; // ← conditional jump (assembly)
ldi destructible; // ← load input value
add E2, EB; // ← accumulate into register E2
jmp LOOP_IN; // ← unconditional jump back to loop head
label EXIT_IN:
loop LOOP_OUT:
cknxo destructible;
jif CO, EXIT_OUT;
ldo destructible;
add E3, EB;
jmp LOOP_OUT;
label EXIT_OUT:
eq E2, E3; // ← compare two registers
chk CO; // ← check comparison result; abort on failure
stop;
};
CompiledLib::compile(code, &[])
}How to read this:
uasm!is likewise a compile-time macro (from theultrasoniccrate) that expandscknxi/jif/jmp/add/eqintoVec<Instr<LibId>>- The
loop/labelyou see are not Rust control flow—they are assembly labels (goto targets) jif/jmpjump via the indices of those labels
Analogy:
// This is a Rust loop (for/while)
for i in 0..count {
sum += items[i];
}
// The loop inside uasm! above is an assembly loop (jmp back to a label)
LOOP_IN:
cknxi destructible;
jif CO, EXIT_IN; // if no next input, jump to EXIT_IN
ldi destructible;
add E2, EB;
jmp LOOP_IN; // jump back to LOOP_IN
EXIT_IN:They look similar on the surface, but the former compiles to native machine code, while the latter compiles to AluVM bytecode instructions.
Key conclusion: RGB contract validation logic must be written as AluVM assembly because client-side validation requires “sender and receiver each independently execute the same deterministic bytecode and get the same result.” If validation were native machine code (x86/ARM), behavior could diverge across platforms.
Finding 2: Why Does Summing Take 1 Line in v0.11 but 26 Lines in v0.12?
This is the most confusing part: on the surface, v0.12 looks like a “regression.”
v0.11 Conservation Check: 1 Instruction
pcvs OS_ASSET; // ← Pedersen commitment sum verification (one instruction)v0.12 Conservation Check: 26-Line Loop
routine TRANSFER:
put E2, 0; put E3, 0;
rsti destructible; rsto destructible;
loop LOOP_IN:
cknxi destructible; jif CO, EXIT_IN;
ldi destructible; add E2, EB; jmp LOOP_IN;
label EXIT_IN:
loop LOOP_OUT:
cknxo destructible; jif CO, EXIT_OUT;
ldo destructible; add E3, EB; jmp LOOP_OUT;
label EXIT_OUT:
eq E2, E3; chk CO; stop;Reality: Specialized Instructions vs General-Purpose Instructions
v0.11’s pcvs is not a “normal assembly instruction”—it is a high-level instruction designed at the RGB protocol layer specifically for asset validation, similar to:
- A GPU’s specialized
matrix_multiply()(one instruction for complex matrix math) - SQL’s
SUM(balance)aggregate (one statement for summing)
What pcvs does internally (protocol-layer implementation):
- Iterate all input UTXOs of type
OS_ASSET - Extract each input’s Pedersen commitment
- Accumulate all input commitments (elliptic-curve point addition)
- Iterate all output UTXOs of type
OS_ASSET - Accumulate all output commitments
- Compare whether the two commitment points are equal
These six steps are encapsulated inside pcvs, so it looks like one line—the protocol does the loop for you.
v0.12’s 26 lines hand-write those six steps with “general-purpose instructions”:
cknxi(check next input)ldi(load input value)add(accumulate)jif(conditional jump)jmp(unconditional jump)
These are like a CPU’s mov/add/cmp/jmp—you write the loop yourself.
Why Go from 1 Line to 26? Flexibility
Important update (validated 2026-08-12): After deeper source analysis, we found that v0.11 can in theory but cannot in practice hand-write loops the way v0.12 does.
v0.11 pcvs | v0.12’s 26-line loop | Can v0.11 hand-write the loop? |
|---|---|---|
| Only “unconditional sum-and-compare” | Any logic can be added inside the loop | ❌ Assembly sugar does not support it |
| Cannot implement “VIP transfer limits” | Can add if (amount > 1000) check_VIP() in the loop | ❌ Missing iterator pattern |
| Cannot implement “3-of-5 multisig authorization” | Can read global state and check signature count | ⚠️ Can read globals but cannot traverse inputs |
| Cannot implement “AMM slippage checks” | Can read quotes, compute slippage, verify outputs | ❌ Cannot customize conservation logic |
Analogy summary:
| Task | v0.11 approach | v0.12 approach | Analogy |
|---|---|---|---|
| Matrix multiply | matrix_multiply(A, B) | Triple nested for loops by hand | GPU specialized vs CPU general |
| DB sum | SELECT SUM(balance) | for (row in table) { sum += row.balance; } | SQL aggregate vs program loop |
| Asset conservation | pcvs OS_ASSET; | 26-line hand-written loop | RGB ISA specialized vs AluVM general |
Core insight:
- v0.11 → v0.12: from specialized hardware to a general-purpose processor (trading concision for flexibility)
- The 26 lines in v0.12 are not a “regression”—they are the necessary cost of supporting arbitrarily complex logic
- v0.11’s fundamental limit: although the underlying AluVM supports
jmp/jifand arithmetic, it lacks iterator-style state access (cknxi/ldi), andrgbasm!does not exposecnp/cns/ldfmnemonics—so you cannot practically write custom conservation logic
Finding 3: Can v0.11 Write Custom Contracts? Technical Validation
Question: Can v0.11 hand-write a conservation loop like v0.12?
Underlying Capability Comparison
| Instruction class | v0.11 (AluVM 0.11 + RgbIsa) | v0.12 (AluVM 0.12 + UltraSONIC) |
|---|---|---|
| Control flow | ✅ jmp/jif/routine/call | ✅ jmp/jif/call |
| Arithmetic | ✅ add/sub/mul/div/eq | ✅ add/sub/mul/div/eq |
| State counting | ⚠️ CnP/CnS (present under the hood, not in the macro) | ✅ Not needed (use iterators) |
| State loading | ⚠️ LdF(type, idx, val) (present under the hood, not in the macro) | ✅ ldi/ldo (auto-iterate) |
| Iterators | ❌ None | ✅ rsti/rsto/cknxi/cknxo |
Critical Difference: State Access Pattern
v0.11 index-access pattern:
// Feasible in theory, but rgbasm! does not support it
cnp OS_ASSET, a16[0]; // get input count → a16[0]
put a16[1], 0; // init index
loop LOOP:
lt.u a16[1], a16[0]; // index < count?
jif EXIT;
ldf OS_ASSET, a16[1], a64[2]; // ❌ rgbasm! lacks ldf mnemonic
add.uw a64[2], a64[0]; // accumulate
inc a16[1]; // index++
jmp LOOP;v0.12 iterator pattern:
// Native uasm! support
rsti destructible; // reset input iterator
loop LOOP:
cknxi destructible; // check for next
jif CO, EXIT;
ldi destructible; // auto-load + advance iterator
add E2, EB; // accumulate
jmp LOOP;Limits of the rgbasm! Macro
v0.11’s rgbasm! only exposes these RGB extension instructions:
pcvs // Pedersen commitment verify sum (specialized conservation)
pcas // Pedersen commitment against sum
pcps // Pedersen commitment previous sum
ldg // Load global state
ldp // Load previous structured state
lds // Load state structured
cng // Count global
cnc // Count contract global
ldm // Load metadataMissing critical instructions:
- ❌
cnp/cns(count inputs/outputs) - ❌
ldf(load fungible value by index)
Although the underlying ContractOp enum includes CnP/CnS/LdF, the isa_instr! macro does not provide mnemonics. Developers would have to manually construct Instr<RgbIsa<_>> enum values—which contradicts the idea of an “assembly language.”
Validation Conclusion
| Dimension | v0.11 | v0.12 |
|---|---|---|
| Control-flow Turing completeness | ✅ jmp/jif/loop/label | ✅ Supported |
| Arithmetic | ✅ Full add/sub/mul/div/compare | ✅ Full |
| State traversal | ❌ No iterators; manual indexing required | ✅ Iterator pattern |
| Assembly macro support | ⚠️ Core mnemonics missing | ✅ Fully exposed |
| Practical writability | ❌ Only specialized pcvs | ✅ Arbitrary hand-written logic |
Conclusion:
- Underlying capability: v0.11’s AluVM + RgbIsa includes Turing-complete control flow and arithmetic
- Critical gap: Missing the iterator pattern (
cknxi/ldi) forces manual index management for state traversal, andrgbasm!does not expose the needed count/load mnemonics - Practical result: v0.11 developers cannot hand-write conservation loops like v0.12; they must rely on specialized instructions such as
pcvs/pcas
That explains why official v0.11 examples all use pcvs instead of hand-written loops—not for lack of desire, but because the toolchain does not support it.
Finding 4: v0.11 and v0.12 Are Parallel Tracks; Contractum Chose v0.12
This is the most important conclusion for ecosystem partners and developers.
Key facts:
- v0.11 and v0.12 are not a version upgrade relationship—they are two parallel technology stacks
- The Contractum compiler emits v0.12-style general-purpose instructions (
cknxi/ldi/jif/jmp), not v0.11 specialized ones (pcvs) - v0.11 cannot “upgrade” directly to Contractum, because the underlying instruction sets differ
Why Did Contractum Choose v0.12 over v0.11?
| Need | Can v0.11 do it? | Can v0.12 do it? | Does Contractum need it? |
|---|---|---|---|
| Simple conservation | ✅ One-line pcvs | ✅ 26-line loop | ✅ Must support |
| Conditional transfer (VIP limits) | ❌ pcvs cannot express conditions | ✅ Add if logic in the loop | ✅ Must support |
| Multisig authorization (3/5) | ❌ pcvs cannot read global state | ✅ Read immutable_in | ✅ Must support |
| AMM slippage | ❌ pcvs cannot read quotes | ✅ Read globals + compute + verify | ✅ Must support |
| DAO governance | ❌ pcvs only sees inputs/outputs | ✅ Read permissions + vote + execute | ✅ Must support |
Conclusion: Contractum aims to “support arbitrarily complex smart contracts,” so it must sit on v0.12’s general-purpose instruction model. v0.11’s specialized model is too constrained.
Contractum Example (Future, In Development)
schema FungibleAsset {
global _issuedSupply: Amount;
owned balance: Zk64;
transition transfer {
owned balance-;
owned balance+;
verify {
inputs.balance.sum() == outputs.balance.sum()
}
}
}This one-line declarative check will be compiled into v0.12’s 26-line loop assembly—developers do not write it by hand.
Three-Generation Comparison
| Dimension | v0.11 (Gen 1) | v0.12 (Gen 2) | Contractum (Gen 3) |
|---|---|---|---|
| Timeline | 2023–2024 | 2024–2026 | In development (future) |
| Core library | rgb-core 0.11 | SONIC/UltraSONIC | Contractum compiler |
| Language | AluVM assembly (rgbasm!) | AluVM assembly (uasm!) | Contractum high-level language |
| Code size (RGB20 conservation) | 4 lines (pcvs specialized) | 26 lines (hand-written loop) | ~5 lines (.sum() aggregate) |
| Turing completeness | Limited (depends on predefined instructions) | Yes (hand-written loops + jumps) | Yes (compiler-generated loops) |
| Amount privacy | Pedersen commitments (blinding) | Plaintext (in this validation) | Zk64 type (expected) |
| Fit | Simple tokens (RGB20/RGB21) | Complex contracts (AMM/DAO) | All RGB contracts |
| VM | AluVM + RGB ISA | AluVM + UltraSONIC | AluVM (compilation target) |
Evolutionary logic:
- v0.11 → v0.12: from specialized instructions to a general register machine—trading concision for flexibility
- v0.12 → Contractum: from hand-written assembly to a high-level language—trading compiler complexity for developer experience
Part III: Notes for Developers and Ecosystem Partners
For RGB Developers
- Understand the bottom layer before using high-level tools: Contractum is coming, but knowing AluVM assembly helps with debugging and optimization
- v0.11 vs v0.12 characteristics:
- Simple tokens → v0.11’s
pcvsis enough (4 lines) - Complex logic (AMM/DAO/conditional transfers) → must use v0.12
- Simple tokens → v0.11’s
For Protocol Researchers
- Where client-side validation gets determinism: AluVM bytecode executes with consistent results across platforms
- Empirical Turing completeness: v0.12’s
jif/jmploops are a real Turing-complete implementation - Two routes for confidential transfers:
- v0.11: Pedersen commitments (protocol-native)
- v0.12: plaintext in this validation; Contractum’s
Zk64type is expected to support privacy
What We Validated
✅ v0.11’s pcvs correctly rejects non-conserving transfers
✅ v0.12’s hand-written loop implements Turing-complete validation logic
✅ Compared real differences across generations (code size, flexibility, privacy)
✅ Clarified the Rust vs assembly layering (validation logic is assembly; Rust is tooling)
References
Official Docs
- RGB website: https://rgb.tech/
- RGB FAQ: https://www.rgbfaq.com/
- Contractum: https://www.contractum.org/
Repositories
- Code for this article: https://github.com/rgbstashlabs/rgb-smart-contract-demo
- RGB-WG/rgb: https://github.com/RGB-WG/rgb
- RGB-WG/contractum-lang: https://github.com/RGB-WG/contractum-lang
- AluVM: https://github.com/AluVM
Papers
Organizations
- LNP/BP Standards Association: https://github.com/LNP-BP
- RGB-WG: https://github.com/RGB-WG
Acknowledgments
Thanks to the RGB Consortium, Bitlight Labs, the LNP/BP Standards Association, and Dr. Maxim Orlovsky for their contributions to the RGB ecosystem. All validations in this article are based on code and documentation open-sourced by these teams.
Honest Note
The Stash Labs team is still in a learning phase in the RGB space, with limited knowledge and hands-on experience. This document may contain errors or omissions; we welcome peers to critique and correct us through any channel.
Closing
Through this deep dive, we not only understood the bottom-layer mechanics of RGB smart contracts, but also saw the distinctive strength of the client-side validation path: a Turing-complete AluVM that can express arbitrarily complex logic.
From v0.11’s specialized instructions, to v0.12’s general register machine, to the future Contractum high-level language, RGB smart contract capability keeps evolving. We look forward to the Contractum compiler shipping so more developers can write RGB contracts declaratively—without hand-writing assembly.