Skip to content

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:

  1. What language are RGB smart contracts actually written in?
  2. What is the essential difference between v0.11 and v0.12 smart contracts?
  3. What is the future Contractum smart contract language?
  4. 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?

  1. 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
  2. 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 ​

crateversionsource
rgb-core0.11.0-beta.9github.com/RGB-WG/rgb-core
rgb-std0.11.0-beta.9github.com/RGB-WG/rgb-std
rgb-interfaces0.11.0-beta.9github.com/RGB-WG/rgb-interfaces
aluvm0.11.0-beta.9RGB-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:

text
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):

rust
// 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 ​

rust
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

rust
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)

rust
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)

rust
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 ​

crateversion
aluvm0.12.0
ultrasonic0.12.0
sonic-api0.12.0
hypersonic0.12.0
sonic-persist-fs0.12.0

Code location: v0.12-rgb20/ (rgb-smart-contract-demo)

Contract Structure: Codex + Api + Semantics ​

v0.12 cleanly separates “terms” from “interface”:

text
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_id

Core code (GitHub: v0.12-rgb20/src/main.rs):

rust
// 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):

rust
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

rust
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)

rust
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)

rust
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: ScriptUnspecified

Because 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):

rust
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):

rust
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):

contractum
verify {
    inputs.balance.sum() == outputs.balance.sum()
}

State Definition Comparison ​

v0.11 (Schema + Iface + IfaceImpl, three layers):

rust
// 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):

rust
// 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):

contractum
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) ​

rust
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 like put/pcvs/test/ret into Vec<Instr<RgbIsa>> (an AluVM instruction sequence)
  • Lib::assemble compiles that sequence into an AluVM bytecode library
  • At validation time, Schema::validate_state runs 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) ​

rust
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 the ultrasonic crate) that expands cknxi/jif/jmp/add/eq into Vec<Instr<LibId>>
  • The loop/label you see are not Rust control flow—they are assembly labels (goto targets)
  • jif/jmp jump via the indices of those labels

Analogy:

rust
// 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 ​

rust
pcvs OS_ASSET;  // ← Pedersen commitment sum verification (one instruction)

v0.12 Conservation Check: 26-Line Loop ​

rust
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):

  1. Iterate all input UTXOs of type OS_ASSET
  2. Extract each input’s Pedersen commitment
  3. Accumulate all input commitments (elliptic-curve point addition)
  4. Iterate all output UTXOs of type OS_ASSET
  5. Accumulate all output commitments
  6. 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 pcvsv0.12’s 26-line loopCan 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:

Taskv0.11 approachv0.12 approachAnalogy
Matrix multiplymatrix_multiply(A, B)Triple nested for loops by handGPU specialized vs CPU general
DB sumSELECT SUM(balance)for (row in table) { sum += row.balance; }SQL aggregate vs program loop
Asset conservationpcvs OS_ASSET;26-line hand-written loopRGB 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/jif and arithmetic, it lacks iterator-style state access (cknxi/ldi), and rgbasm! does not expose cnp/cns/ldf mnemonics—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 classv0.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:

rust
// 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:

rust
// 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:

rust
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 metadata

Missing 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 ​

Dimensionv0.11v0.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:

  1. Underlying capability: v0.11’s AluVM + RgbIsa includes Turing-complete control flow and arithmetic
  2. Critical gap: Missing the iterator pattern (cknxi/ldi) forces manual index management for state traversal, and rgbasm! does not expose the needed count/load mnemonics
  3. 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:

  1. v0.11 and v0.12 are not a version upgrade relationship—they are two parallel technology stacks
  2. The Contractum compiler emits v0.12-style general-purpose instructions (cknxi/ldi/jif/jmp), not v0.11 specialized ones (pcvs)
  3. v0.11 cannot “upgrade” directly to Contractum, because the underlying instruction sets differ

Why Did Contractum Choose v0.12 over v0.11? ​

NeedCan 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) ​

contractum
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 ​

Dimensionv0.11 (Gen 1)v0.12 (Gen 2)Contractum (Gen 3)
Timeline2023–20242024–2026In development (future)
Core libraryrgb-core 0.11SONIC/UltraSONICContractum compiler
LanguageAluVM 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 completenessLimited (depends on predefined instructions)Yes (hand-written loops + jumps)Yes (compiler-generated loops)
Amount privacyPedersen commitments (blinding)Plaintext (in this validation)Zk64 type (expected)
FitSimple tokens (RGB20/RGB21)Complex contracts (AMM/DAO)All RGB contracts
VMAluVM + RGB ISAAluVM + UltraSONICAluVM (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 ​

  1. Understand the bottom layer before using high-level tools: Contractum is coming, but knowing AluVM assembly helps with debugging and optimization
  2. v0.11 vs v0.12 characteristics:
    • Simple tokens → v0.11’s pcvs is enough (4 lines)
    • Complex logic (AMM/DAO/conditional transfers) → must use v0.12

For Protocol Researchers ​

  1. Where client-side validation gets determinism: AluVM bytecode executes with consistent results across platforms
  2. Empirical Turing completeness: v0.12’s jif/jmp loops are a real Turing-complete implementation
  3. Two routes for confidential transfers:
    • v0.11: Pedersen commitments (protocol-native)
    • v0.12: plaintext in this validation; Contractum’s Zk64 type 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 ​

Repositories ​

Papers ​

Organizations ​

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.

Trade and secure your RGB assets | Unlock the full potential of the RGB protocol