Ingot Protocol Specifications

Complete technical specifications for Ingot Protocol - Pay2Ingot oUTXO model for inscriptions, tokens, and assets on Tondi

Overview

Pay2Ingot is a blockchain protocol design based on the oUTXO (object-oriented UTXO) model, designed to provide enhanced payment and inscription ecosystem support for the Tondi chain.

Standard Schema Library

Tondi provides a complete standard Schema template library (ingot_schemata/), covering common application scenarios:

Category Schemas Purpose
Token 2 BRC-20 compatible tokens, TRC-721 advanced tokens
Inscription 2 Text inscriptions, image inscriptions
NFT 2 ERC-721 compatible NFTs, music NFTs
DAO 2 DAO proposals, DAO voting
Application 2 Social media posts, decentralized identity (DID)

📚 Documentation:

💡 Quick Start:

# Create token using BRC-20 compatible template
tondi-cli ingot create \
  --schema-template token/brc20_compatible.json \
  --params name="MyToken" ticker="MTK" supply=21000000

# Use standard NFT template
tondi-cli ingot create \
  --schema-template nft/erc721_compatible.json \
  --params name="CryptoArt #1" description="..."

Implementation Files

Module File Core Functionality
MAST Tree consensus/core/src/tx/ingot/witness.rs MastTree::build(), create_proof(), verify()
MAST Validation consensus/core/src/tx/ingot/validation.rs MAST proof validation, leaf_lock deserialization
ScriptHash Validation consensus/core/src/tx/ingot/validation.rs HASH160 validation, script hash matching
ScriptHash Execution consensus/src/processes/transaction_validator/tx_validation_in_utxo_context.rs TxScriptEngine integration, script execution
Lock Types consensus/core/src/tx/ingot/lock.rs None, PubKey, ScriptHash, MastOnly
Signature Validation consensus/core/src/tx/ingot/validation.rs Schnorr signature validation, MuSig2 support
Multi-input Validation consensus/core/src/tx/ingot/consensus_integration.rs validate_ingot_inputs()
Asset Merging ingot-client/src/payload.rs Asset List parsing, conservation validation

Core Design Principles

  • Single output type: Pay2Ingot
  • oUTXO soft constraints (unique active tip, spend-old-create-new, lexicographic ordering)
  • Minimal design:
    • MAST privacy enhancement (optional MAST_ROOT, max depth 8)
    • 2 signature types: CopperootSchnorr (BLAKE3), StandardSchnorr (BIP340)
    • 4 Lock types: None, PubKey, ScriptHash, MastOnly
    • Full MuSig2 support: Aggregated signatures indistinguishable from single signatures on-chain
    • ScriptHash support (via Tondi VM execution)
  • Fee system: Default "total byte billing"; layered billing as governance switch, initially equivalent to default
  • MAST: Supports MAST_ROOT and path validation; leaf_lock as fixed enum; max depth 8
  • Time locks: Implemented via ScriptHash + VM scripts (OP_CHECKLOCKTIMEVERIFY/OP_CHECKSEQUENCEVERIFY)
  • Resources: L2 recommended value (84.5 KiB payload) + soft policy (target 20% ratio, non-consensus)
  • Upgrades: Future feature extensions via hard fork; parameter adjustments via governance
  • Full interaction with traditional payments
  • On-chain verifiable + off-chain replayable supporting inscription ecosystem

oUTXO Role

Soft consensus constraints. This avoids L1 state inflation, keeping consensus minimal. Off-chain arbitration rules are part of soft consensus, ensuring indexer behavior consistency.

Solution: Add oUTXO test vectors to Registry (mandatory indexer passing) as mandatory validation standards for soft consensus constraints.

Schnorr Signature Unification

Only Schnorr signatures (based on secp256k1 curve):

  • CopperootSchnorr (0x01): BLAKE3 sighash + SHA256 challenge + BIP340 Schnorr (Tondi native optimization, 4-5x faster)
  • StandardSchnorr (0x04): SHA256 sighash + SHA256 challenge + BIP340 Schnorr (fully Bitcoin Taproot compatible)

Full MuSig2 support: All Schnorr signatures support MuSig2 multi-signature aggregation. Aggregated signatures are completely indistinguishable from single-key signatures on-chain, providing optimal privacy.

Time Lock Mechanism (via VM)

ScriptHash time lock: Time locks implemented via ScriptHash lock type + VM scripts, supporting OP_CHECKLOCKTIMEVERIFY (absolute height) and OP_CHECKSEQUENCEVERIFY (relative block count). Users convert blocks = seconds × 10 BPS to implement time constraints.

Instance Identification (based on Outpoint)

Core concept: Each Ingot instance corresponds to exactly one UTXO. outpoint (txid||vout) naturally guarantees uniqueness, no SALT required.

instance_id formula:

outpoint_bytes = txid[32] || u32le(vout)
instance_id = blake3("Ingot/instance" || artifact_id || outpoint_bytes)

Field description:

  • txid: Transaction ID (32 bytes)
  • vout: Output index (u32, little-endian)
  • artifact_id: Calculated as blake3("Ingot/artifact" || SCHEMA_ID || HASH_PAYLOAD)

Uniqueness guarantee:

  • L1 level: UTXO model guarantees each (txid, vout) is unique on-chain
  • L2 level: oUTXO arbitration rules guarantee single tip strong consistency
  • Indexer level: Instance key is (artifact_id, outpoint)

Advantages:

  • No need for user-provided nonce or salt
  • Deterministic: Completely determined by on-chain data
  • Better performance: Reduced hash computation
  • Simple implementation: No ambiguity, easy to understand

Extension Mechanism

Design simplification:

  • No independent Policy structure
  • No PolicyExt/TLV extension mechanism
  • All authentication logic unified in Lock enum
  • Time locks implemented via ScriptHash + VM scripts
  • Fee policies determined by off-chain miners/relays, not in consensus

1. Output Type Identification

Current implementation: Identified via ScriptPublicKey.version field

// TransactionOutput structure unchanged
pub struct TransactionOutput {
    pub value: u64,
    pub script_public_key: ScriptPublicKey, // version field used for identification
}

// Pay2Ingot uses specific version value
const INGOT_VERSION: u16 = 2; // Pay2Ingot v1.0 (full features)

// Identification method
pub fn is_ingot_output(output: &TransactionOutput) -> bool {
    output.script_public_key.version() == INGOT_VERSION
}

Rules:

  • Transaction outputs can be Value UTXO (traditional version 0, 1) or Pay2Ingot (version 2)
  • Version space allocation:
    • 0: Classic ScriptPublicKey (PubKey, ScriptHash, etc.)
    • 1: Taproot
    • 2: Pay2Ingot v1.0 (full feature version, contains all functionality)
    • 192-255: Reserved for future extensions
  • Pay2Ingot uses ScriptPublicKey.version = 2
  • IngotOutput data serialized and stored in script_public_key.script
  • IngotOutput internal version field fixed at 0x01
  • Consensus rejects any unknown version value (fail-closed)
  • Version consistency check: ScriptPublicKey.version must be 2 and IngotOutput.version must be 0x01

2. Pay2Ingot Output Field Validation

Basic Fields

IngotOutput structure (6 fields total, L1 consensus layer):

pub struct IngotOutput {
    pub version: u8,              // Version (0x01, fixed)
    pub schema_id: Hash,          // Schema ID (32B blake3 hash)
    pub hash_payload: Hash,       // Payload hash (32B blake3 hash)
    pub flags: IngotFlags,        // Flags (u16, bit 0=REVEAL_REQUIRED, bits 1-15 reserved)
    pub lock: Lock,               // Locking mechanism (None/PubKey/ScriptHash/MastOnly)
    pub mast_root: Option<Hash>,  // MAST root (32B, v1.0+, optional)
}

Serialization constraints:

  • Maximum serialized size: MAX_INGOT_OUTPUT_SIZE = 1024 bytes
  • Serialization format: Borsh (Binary Object Representation Serialization for Hashing)
  • Stored in: ScriptPublicKey.script field
  • Outer version identifier: ScriptPublicKey.version = INGOT_VERSION = 2

L1 consensus layer fields (directly affect transaction validity):

  • version: Must be 0x01; determines how to parse output type
  • schema_id: 32B hash; used for routing to state machine implementation
  • hash_payload: 32B hash; commit-reveal integrity check (L1 enforces verification blake3(payload) == hash_payload)
  • flags: u16; currently only bit 0 (REVEAL_REQUIRED), bits 1-15 reserved for future use; directly affects transaction validity
  • lock: Lock enum; basic authentication mechanism (None/PubKey/ScriptHash/MastOnly)
  • mast_root: Optional 32B hash (v1.0+); Merkle root for hiding multiple optional locking conditions

Version Evolution vs Flags Switching

Adding new features = New SPK Version (hard fork)

v1.0 (2) → v2.0 (3) → v3.0 (4) → ...
Basic Pay2Ingot    Add MAST       Add SIGHASH

Flags purpose = Optional switches within same version (no fork required)

// Within same version v1.0 (2):
let flags = IngotFlags::new();                        // Optional reveal
let flags = IngotFlags::new().set_reveal_required(true);  // Mandatory reveal
// Both are v1.0, no hard fork required

L1 vs L2 Flags Criteria

L1 Flags (in header):

  • Switches that change transaction validity
  • Optional features within same version
  • Unknown bit → fail-closed (E_RESERVED_BITS_SET)

L2 Flags (in payload TLV):

  • Application layer configuration that doesn't affect L1 consensus
  • Display policies, URI policies, asset views, etc.
  • Unknown TLV → indexer can ignore

v1.0 Flags Definition

Bit Name Description Impact
0 REVEAL_REQUIRED Must reveal payload when spending Changes transaction validity
1-15 Reserved Reserved (undefined in current version) Setting bits = reject

Validation logic:

// v1.0 only supports bit 0
if self.flags.0 & !IngotFlags::REVEAL_REQUIRED != 0 {
    return Err(IngotError::ReservedBitsSet(self.flags.0));
}

// REVEAL_REQUIRED check
if output.flags.reveal_required() && !witness.reveals_payload() {
    return Err(IngotError::RevealRequiredNotSatisfied);
}

Lock Design (v1.0 - Only 4 Types)

Lock enum definition (v1.0 actual code implementation):

pub enum Lock {
    /// No authentication (anyone can spend)
    None,
    
    /// Single signature (supports MuSig2 aggregation)
    PubKey { 
        pubkey: Vec<u8>,           // 32B x-only public key (may be MuSig2 aggregated)
        sig_type: SignatureType    // CopperootSchnorr/StandardSchnorr
    },
    
    /// Script hash (P2SH semantics, executed via VM)
    ScriptHash {
        script_hash: [u8; 20]      // HASH160(script)
    },
    
    /// Pure MAST (enforces MAST privacy)
    MastOnly,
}

Removed Lock types in v1.0:

  • Multisig: Use PubKey + MuSig2 aggregation instead (indistinguishable on-chain, saves space, improves privacy)
  • PubKeyHash: Simplified design (use PubKey or ScriptHash instead)
  • Ed25519: Unified Schnorr signatures (secp256k1 curve)

Lock type details (v1.0):

  1. None: No authentication (anyone can spend)

    • For public inscriptions, public data, etc.
  2. PubKey { pubkey, sig_type }: Single signature or MuSig2 aggregated signature

    • pubkey: 32B x-only public key (may be MuSig2 aggregated key)
    • sig_type: CopperootSchnorr or StandardSchnorr
    • Privacy advantage: MuSig2 aggregated keys completely indistinguishable from single keys on-chain
    • Performance advantage: MuSig2 requires only 1 signature, saves 57% space (vs old Multisig)
  3. ScriptHash { script_hash }: P2SH semantics (executed via Tondi VM)

    • script_hash: 20B hash value
    • HASH160 definition (protocol frozen):
      script_hash = RIPEMD160(SHA256(script_reveal_bytes))
      
      Where script_reveal_bytes is the raw byte sequence of the script (not Borsh wrapped)
    • Supports time locks (OP_CHECKLOCKTIMEVERIFY/OP_CHECKSEQUENCEVERIFY)
    • Supports arbitrary custom script logic
  4. MastOnly: Pure MAST lock (maximum privacy)

    • Requires mast_root to exist
    • Must provide MAST proof when spending
    • All locking conditions hidden in Merkle tree

Signature type matrix (v1.0):

Lock Type CopperootSchnorr StandardSchnorr MuSig2 Support Notes
None N/A N/A N/A No signature needed
PubKey ✅ Full support Single or aggregated signature
ScriptHash ✅ (via script) ✅ (via script) ✅ (via script) Defined in script
MastOnly ✅ (in leaves) ✅ (in leaves) ✅ (in leaves) Hidden in MAST tree

MuSig2 full support:

  • ✅ CopperootSchnorr: Supports MuSig2 (BLAKE3 sighash + BIP340 Schnorr)
  • ✅ StandardSchnorr: Supports MuSig2 (SHA256 sighash + BIP340 Schnorr, Taproot compatible)
  • ✅ Aggregated signatures completely indistinguishable from single signatures on-chain
  • ✅ N-of-N multisig: All parties aggregate off-chain, only 1 signature needed on-chain
  • ✅ M-of-N multisig: Use MAST tree containing C(N,M) combinations, reveal used combination when spending

Signature format requirements (v1.0):

  • All signatures enforced 64B BIP340 Schnorr format
  • Format: (r || s) each 32 bytes, where r is x-coordinate, s is scalar
  • IngotWitness.auth_sigs: Vec<Vec<u8>>, each signature must be exactly 64 bytes
  • Public key format: 32B x-only (BIP340 standard, secp256k1 curve)
  • Curve: secp256k1 (same as Bitcoin)
  • Challenge: SHA256 (BIP340 standard: e = SHA256("BIP0340/challenge", r || P || m))

Constraint mechanism (v1.0 unified design):

  • No extension fields: Don't use lock_ext or TLV extension mechanism
  • Implementation: All constraints via ScriptHash lock type
    • Time locks:
      • Absolute height → ScriptHash + OP_CHECKLOCKTIMEVERIFY script
      • Relative block count → ScriptHash + OP_CHECKSEQUENCEVERIFY script
    • Custom constraintsScriptHash + arbitrary Tondi VM scripts
  • Design rationale:
    • ✅ Unified verification mechanism (all via VM)
    • ✅ Avoid L1 special logic
    • ✅ Stronger expressiveness (Turing-complete scripts)
    • ✅ Simplified consensus code

Multi-signature Semantics (v1.0 - Using MuSig2)

v1.0 removes on-chain Multisig:

  • ❌ No longer supports on-chain Multisig lock type
  • ✅ Use PubKey + MuSig2 off-chain aggregation instead

MuSig2 multisig scheme:

  1. N-of-N multisig (all participants):

    // Step 1: Aggregate public keys off-chain
    let agg_key = musig2::key_agg([pk1, pk2, pk3]);
    
    // Step 2: Create Ingot (only aggregated public key on-chain)
    Lock::PubKey {
        pubkey: agg_key.to_bytes(),
        sig_type: CopperootSchnorr,
    }
    
    // Step 3: Aggregate signatures off-chain when spending
    let agg_sig = musig2::sign([signer1, signer2, signer3], msg);
    
    // Step 4: On-chain verification (identical to single signature)
    witness.auth_sigs = vec![agg_sig.to_bytes()];
    

    Advantages:

    • ✅ On-chain only needs 1 public key (32B) + 1 signature (64B) = 96B
    • ✅ Completely indistinguishable from single signature (best privacy)
    • ✅ vs old Multisig: saves 57% space
  2. M-of-N multisig (partial participants):

    // Scheme A: MAST tree (recommended)
    // Create MAST tree with C(N,M) combinations
    let combinations = generate_m_of_n_combinations(pubkeys, M, N);
    let mast_leaves: Vec<Lock> = combinations.map(|combo| {
        let agg_key = musig2::key_agg(combo);
        Lock::PubKey { pubkey: agg_key, sig_type: CopperootSchnorr }
    });
    let mast_root = compute_mast_root(mast_leaves);
    
    // Reveal only used combination when spending
    

    Advantages:

    • ✅ Only reveals actually used signer combination
    • ✅ Unused combinations remain hidden
    • ✅ Suitable for small M, N scenarios

Verification flow (v1.0):

  1. Check signature count = 1 (single signature or aggregated signature)
  2. Use pubkey (single key or aggregated key) to verify signature
  3. Signature verification failure → transaction invalid

Error code mapping (v1.0):

  • Signature length error → E_INVALID_SIGNATURE
  • Public key format error → E_INVALID_SIGNATURE
  • Signature verification failure → E_INVALID_SIGNATURE
  • Any verification failure → E_SIGNATURE_INVALID

MAST Field

  • MAST_ROOT: Option: Optional field (32B). Can exist or not exist; if exists, witness must provide MAST_PROOF containing path and leaf_lock. leaf_lock is Lock enum (Borsh serialized). Consensus verifies Merkle path correctness (depth ≤8) and leaf_lock structure validity

Constraint conditions: All fields fixed-length/bounded; total output size ≤ system limit; parse failure → tx invalid

Version Consistency Check

Dual-layer version mechanism:

  • Outer version: ScriptPublicKey.version (u16) - Used for output type identification, fixed at 2
  • Inner version: IngotOutput.version (u8) - Currently fixed at 0x01

Consistency rules:

// Enforced verification in extract_ingot_output
if spk_version != INGOT_VERSION {
    return Err(IngotError::UnknownVersion(spk_version));
}

if ingot.version != 0x01 {
    return Err(IngotError::UnknownVersion(ingot.version as u16));
}

// MAST optional, no special check needed
Ok(ingot)

Verification targets:

  • ✅ Ensure outer version = 2
  • ✅ Ensure inner version = 0x01
  • ✅ MAST features fully available in v1.0 (optional)

Field Layered Architecture

Field L1 Consensus Validation Off-chain/Indexer Policy/Network
VER Parse/version gate
SCHEMA_ID Exists/length/format Route to state machine, calculate artifact_id
HASH_PAYLOAD commit-reveal integrity (blake3(payload) == hash_payload) State machine input, light verification sampling
FLAGS Hard conditions like reveal (currently only REVEAL_REQUIRED)
lock Signature types, public keys, threshold, etc. auth parameters
mast_root Merkle path validation (optional) Hidden branches

Architecture notes:

  • L1 consensus: Relies on HASH/FLAGS/Lock to guarantee verifiability and security
  • Off-chain ecosystem: Relies on SCHEMA_ID + HASH_PAYLOAD (→artifact_id) and oUTXO discipline to drive state machines
  • Instance uniqueness: Guaranteed by outpoint (txid||vout), no SALT needed
  • Network health: Relies on byte-based billing for miner incentives and traffic control, rate limiting implemented by off-chain indexers

Activation Mechanism

Activation check flow:

impl TransactionValidator {
    fn check_ingot_transaction(&self, tx: ..., current_daa_score: u64) -> TxResult<()> {
        // Before activation: reject all Ingot outputs
        if !self.ingot_activation.is_active(current_daa_score) {
            for output in &tx.outputs {
                if is_ingot_output(output) {
                    return Err(TxRuleError::IngotNotActivated(...));
                }
            }
            return Ok(());
        }
        
        // After activation: allow all Ingot outputs (including MAST)
        // All features fully available in v1.0
        // ...
    }
}

Activation timeline (Mainnet):

Genesis            v1.0 Activation
|                  |
0 ---------------> 51,840,000 --------->

Phase 1            Phase 2
❌ No Ingot        ✅ v1.0 Full Features
                    (All features available, including MAST)

Network activation parameters:

  • Mainnet: v1.0 @ 51,840,000 DAA (activates ~2 months later, 10 BPS)
  • Testnet: v1.0 @ 100,000 DAA (activates ~2.8 hours later, 10 BPS)
  • Devnet: v1.0 @ 0 (activates immediately from genesis)
  • Simnet: v1.0 @ 0 (activates immediately from genesis, all features available)

3. Input Witness Validation (Pay2Ingot Spending)

Witness Structure (v1.0 - Minimal Design)

IngotWitness structure (v1.0 actual code implementation):

pub struct IngotWitness {
    pub payload: Option<Vec<u8>>,         // Optional payload reveal (L2 handles chunking)
    pub auth_sigs: Vec<Vec<u8>>,          // Authentication signatures (64B each, Schnorr only)
    pub script_reveal: Option<Vec<u8>>,   // Script reveal (for ScriptHash)
    pub mast_proof: Option<MastProof>,    // MAST proof
}

Removed fields in v1.0 (compared to earlier design):

  • sig_indices: Deleted (no longer needed after removing Multisig)
  • pubkey_reveal: Deleted (no longer needed after removing PubKeyHash)

Field description (v1.0):

  • payload: Optional full payload data
    • L1 verification: blake3(payload) == hash_payload
    • L2 processing: Chunking, reassembly, schema parsing, semantic validation
    • Size: L1 doesn't limit, L2 recommends ≤ 84.5 KiB, actually constrained by Tondi mass ≤ 100k
  • auth_sigs: Signature list, each signature must be 64 bytes BIP340 Schnorr format
    • PubKey lock: Exactly 1 signature (single or MuSig2 aggregated, indistinguishable on-chain)
    • ScriptHash lock: Number of signatures required by script (defined by script)
    • MastOnly lock: Number of signatures required by MAST leaf node (defined by leaf lock)
    • None lock: 0 signatures
  • script_reveal: Must provide for ScriptHash lock type, L1 verifies RIPEMD160(SHA256(script_reveal)) == script_hash
  • mast_proof: Optional MAST proof (contains Merkle path and leaf_lock, depth ≤ 8)

Measurement Domain

L1/L2 responsibility separation:

Component L2 Recommended / Physical Constraint Description
auth_witness Natural constraint Signatures (64B Schnorr), MAST proofs, etc. authentication data
payload_chunks ≤ 85 KiB (physical constraint) REVEAL_FLAG, TOTAL_CHUNKS, CHUNK[i] chunk data
Transaction total ≤ 100 KiB (physical constraint) Tondi mass ≤ 100,000 automatic limit

Tondi Mass constraint (single transaction ≤ 100,000 mass, physical limit):

  • IngotOutput in script_public_key.script → bytes × 10 counted in mass
  • payload_chunks + auth_witness → bytes × 1 counted in mass
  • Actual Mass budget: 1 KiB output header (10k mass) + ~85 KiB payload (~85k mass) + others (≈4k mass) ≈ ~99k mass

Key principles:

  • Both billed, but don't share same upper limit
  • L1 doesn't enforce any size limit, only verifies structure and hash
  • L2 recommended values (adjustable):
    • payload_chunks ≤ 84.5 KiB
    • auth_witness natural constraint (limited by public key count)
  • Physical constraint (Tondi mass ≤ 100k):
    • Actual auth_witness ≈ 2-3 KiB (30 ECDSA signature scenario)
    • Actual payload ≈ 85 KiB
  • This design allows large payload reveals while keeping authentication witness compact, naturally constrained by market and mass mechanism

Signature Message Domain Specification (SIGHASH)

Signature message definition (actual implementation):

SigMsg = blake3("Ingot/SigMsg/v1" || network_id_byte || wtxid || input_index || spent_prevout{txid,vout} || lock_bytes || HASH_PAYLOAD)

Hash domain actually implemented: "Ingot/SigMsg/v1" (fixed string prefix)

Field description:

  • network_id_byte: u8 network identifier (type-safe enum)
    • Rust enum definition:
      pub enum NetworkId {
          Mainnet = 0x01,  // tondi_mainnet
          Testnet = 0x02,  // tondi_testnet
          Devnet = 0x03,   // tondi_devnet
          Simnet = 0x04,   // tondi_simnet
      }
      
    • Key principle: Use type-safe enum instead of string, ensuring signature domain implementation consistency
    • Type method: network_id.as_byte() → u8 value
    • Unknown value → reject transaction (fail-closed)
  • wtxid: Tondi transaction ID (complete transaction hash including witness)
    • Note: Actual implementation uses complete transaction hash, including all inputs, outputs, and witness data
  • input_index: Current input index in transaction (usize, converted to u32 in hash calculation)
  • spent_prevout: Spent input information
    • Actual implementation: Only includes txid (32B) and vout (u32, little-endian)
    • Note: Actual implementation doesn't include amount and asset_tag (simplified design)
  • lock_bytes: Borsh serialized lock enum
    • Serialization: Uses Borsh format to serialize entire Lock enum
  • HASH_PAYLOAD: Current Pay2Ingot output payload hash (32B blake3 hash)

Signature algorithm (v1.0 - Schnorr only):

  • CopperootSchnorr (0x01):
    • Sighash: BLAKE3 (4-5x faster)
    • Challenge: SHA256 (BIP340 standard: e = SHA256("BIP0340/challenge", r || P || m))
    • Verification: BIP340 Schnorr on secp256k1
    • Supports MuSig2 aggregation
  • StandardSchnorr (0x04):
    • Sighash: SHA256 (Bitcoin compatible)
    • Challenge: SHA256 (BIP340 standard)
    • Verification: BIP340 Schnorr on secp256k1
    • Supports MuSig2 aggregation
    • Fully compatible with Bitcoin Taproot (BIP341)
  • Signature verification failure → transaction invalid

Network isolation mechanism:

  • Each network uses independent network_id signature domain
  • Prevents cross-network replay attacks (Mainnet signatures cannot be replayed on Testnet)
  • TransactionValidator dynamically configures network ID:
    pub struct TransactionValidator {
        // ...
        ingot_network_id: NetworkId,  // Type-safe enum, selected from params.net.network_type
    }
    
  • Uses configured network_id enum instead of hardcoded string during verification

Anti-replay mechanism: Signature anchors to "specific input + specific payload commitment + specific policy + network domain", ensuring signatures are non-replayable and resistant to transaction malleability

MAST Witness

  • MAST_PROOF (if MAST_ROOT exists): path[] (hash chain ≤8 deep) + leaf_lock (Lock enum); hash path matches MAST_ROOT
    • Validation rules: Verify hash path correctness, leaf_lock is Lock enum (Borsh serialized)
    • Privacy purpose: Hide specific Lock combination used, only reveal used branch
    • Leaf structure: Lock enum (Borsh serialized)
    • Consensus limit: Only verify Merkle path and leaf encoding boundedness
    • v1.0 fully supported: No need to wait for upgrade

MAST with Copperoot Merkle Specification

MAST hash specification (actual implementation): Uses BLAKE3-256 hash and Merkle tree structure

MAST_LEAF = blake3("MAST/leaf" || leaf_lock_bytes)
MAST_NODE = blake3("MAST/node" || min(Left,Right) || max(Left,Right))

Hash domains actually implemented:

  • Leaf node: "MAST/leaf" (fixed string prefix)
  • Internal node: "MAST/node" (fixed string prefix)

Path validation rules:

  • Sorted merge: Avoid left-right sensitivity, use min(Left,Right) || max(Left,Right)
  • Depth limit: depth ≤ 8. Exceeds limit → E_INVALID_STRUCTURE
  • Leaf boundedness: leaf_lock still fixed enum + bounded TLV
  • Recursive calculation: MAST_ROOT is the above recursion

Implementation requirements:

  • All implementations must use same MAST hash rules
  • Path validation failure → transaction invalid
  • Depth exceeded (>8) → transaction invalid
  • Registry test vectors must include MAST path validation cases
  • v1.0 fully implemented: No need to wait for activation

Flags Interaction and Priority

Currently valid Flags (v2.0.1):

Bit Name Description Status
0 REVEAL_REQUIRED Must include payload reveal when spending ✅ Implemented
1-15 Reserved Reserved for future use (requires hard fork activation) 🔒 Reserved

Flags combination rules (simplified):

Bit 0 Value Hex Meaning Validity
0 0x0000 Standard mode - optional reveal ✅ Valid
1 0x0001 Mandatory reveal mode ✅ Valid
≥2 ≥0x0002 Reserved bits set Invalid (E_RESERVED_BITS_SET)

Special rules:

  • When REVEAL_REQUIRED=1: Output when spent must include reveal witness (witness.reveal_flag=true); not enforced during creation
  • Reserved bits (bits 1-15) must be 0, otherwise transaction invalid
  • Total witness bytes counted in tx fee calculation (follows existing fee system, byte-based billing)

4. Transaction Overall Validation

Basic Validation

  • Single transaction paradigm (Inputs[] + Outputs[] + Meta); allows Value/Pay2Ingot mixing
  • UTXO double-spend prevention: All inputs must be unspent; Pay2Ingot inputs destroyed after spending

Rule-P2O-Single (L1 consensus rule):

  • Each transaction contains at most 1 Pay2Ingot output
  • Violation → CONSENSUS_INVALID_STRUCTURE
  • Rationale: Simplifies validation logic, avoids complexity of multiple Ingot states within single transaction

Fee Calculation (default unified billing, reserved layered billing as governance switch)

  • Fee Rule (default): fee = base_fee + rate * total_bytes(tx) (includes header, witness, and Pay2Ingot payload all bytes)
  • Governance switch (when disabled equivalent to default): fee = base_fee + r_overhead*overhead + r_witness*witness + r_payload*payload, initial settings r_overhead = r_witness = r_payload = rate. Switch enable/parameter modification requires governance process and announcement of implementation height
  • Hard limit priority: Exceed limit directly reject; high fees cannot break MAX_*. Guardrails at strategy layer (soft targets/quotas) rather than consensus approval
  • Hard limits cannot be bypassed by any fee rate; nodes/Miners must directly reject when discovering exceeded limits

Time Lock Validation

  • ScriptHash time lock: Implemented via ScriptHash lock type + VM scripts
    • Absolute time lock: OP_CHECKLOCKTIMEVERIFY checks block height
    • Relative time lock: OP_CHECKSEQUENCEVERIFY checks confirmation count
    • Implementation: Lock = ScriptHash { script_hash }, witness provides script
    • Validation logic: Tondi VM executes script, checks time conditions
    • oUTXO visibility: Unlocked instances not counted as active tip

Other Constraints

  • Instance uniqueness guaranteed by outpoint (txid||vout), no additional SALT field needed
  • Consensus doesn't execute Schema/oUTXO (soft consensus constraints); only structure/hash/signature/fees

5. Resource and Boundary Rules

Resource Limits and Parameter Set

Parameter Layer Value Description Implementation Location
MAX_INGOT_OUTPUT_SIZE L1 Physical Limit 1024 bytes IngotOutput serialization limit (SPK script length) mod.rs:95
MAX_TX_MASS Tondi Physical Constraint 100,000 mass Tondi base chain transaction mass limit (not Ingot parameter) Tondi protocol layer ✅
MAST_MAX_DEPTH L1 Structure Limit 8 MAST tree maximum depth witness.rs:79
RECOMMENDED_MAX_PAYLOAD L2 Policy Recommendation 86,560 bytes (84.5 KiB) Recommended payload total limit (adjustable) Documentation only
RECOMMENDED_MAX_CHUNKS L2 Policy Recommendation 8 Recommended maximum chunks (adjustable) Documentation only
RECOMMENDED_MAX_CHUNK_DATA L2 Policy Recommendation 10,820 bytes (10.57 KiB) Recommended single chunk maximum data (adjustable) Documentation only

Key constraints (based on Tondi 100k mass physical limit):

Physical limit (Tondi mass mechanism, cannot bypass):
  tx_total_mass ≤ 100,000 mass (Tondi base chain consensus, not Ingot parameter)

L1 consensus validation (only structure, hash, signature):
  ✅ IngotOutput ≤ 1 KiB (SPK script physical limit)
  ✅ Maximum 1 Pay2Ingot output per transaction (structural rule, see Rule-P2O-Single below)
  ✅ MAST depth ≤ 8 (structure limit)
  ✅ commit-reveal integrity (blake3(payload) == hash_payload)
  ✅ Signature validation
  ❌ **Does not understand chunk** (chunking is entirely L2 behavior, L1 only verifies overall payload hash)
  ❌ **Does not parse payload content** (L1 only verifies blake3(payload)==hash_payload)
  ❌ **Does not enforce payload/witness size** (naturally constrained by Tondi mass)
  
L2 indexer validation (chunk parsing and reassembly):
  ✅ Chunk index strictly递增 (0, 1, 2, ...)
  ✅ Chunk hash matching (per chunk + overall package)
  ✅ Payload reassembly and semantic validation
  ✅ Recommended values: payload ≤ 84.5 KiB, chunks ≤ 8, chunk_data ≤ 10.57 KiB

Actual Mass calculation example:
  IngotOutput (1 KiB) × 10 = 10k mass (in script_public_key)
  + payload_chunks (~85 KiB) × 1 = ~85k mass
  + auth_witness (~1 KiB) × 1 = ~1k mass
  + sigops (1) × 1k = 1k mass
  + Transaction framework overhead ≈ 0.5k mass
  ≈ 97.5k mass (naturally constrained within 100k) ✅

Implementation notes:

  • L1 consensus layer defined constants in code: mod.rs:91-96, lock.rs:20, witness.rs:106
  • L2 recommended values only defined in this document, not enforced by L1 code
  • Actual sizes naturally constrained by Tondi mass mechanism

Notes:

  • L1 layer: Does not enforce any size limits, only verifies structure and hash
  • L2 layer: Controls actual size through fee market and mempool policies
  • Physical layer: Tondi mass mechanism automatically limits (payload ≈ 85 KiB, witness ≈ 2-3 KiB)

Multi Pay2Ingot/Multi-input Resource Limits

Transaction-level resource limits:

  • Multi Pay2Ingot output policy: L1 consensus rule prohibits more than 1 Pay2Ingot output per transaction
  • L1 consensus constraints (structural validity):
    • Hash must match (blake3(payload) == hash_payload)
    • Signatures must be valid
    • Structural field integrity
  • L2 policy recommendations (dynamically adjustable):
    • TOTAL_CHUNKS ≤ 8
    • Single chunk data.len() ≤ 10.57 KiB
    • Total payload ≤ 84.5 KiB
  • Physical constraints (Tondi mass):
    • Total transaction mass ≤ 100k (automatically limits actual size)

Zero-copy ordering and validation:

  • A priori read: TOTAL_CHUNKS and each chunk's data length
  • Pre-check: Basic reasonableness check (total length doesn't overflow u32)
  • Chunk indexing: Must be 0..N-1 strictly increasing, no duplicates/skips (L1 enforced)
  • Non-strictly increasing chunk index or duplicates/skips → E_CHUNK_INDEX_INVALID
  • Deduplication check: Prevents "thousand-fragment" memory amplification and cross-input stacking attacks

Design principle: Single Pay2Ingot output simplifies validation logic

Design principle: Use standard types but clearly define upper limits, simple implementation with margin Hard limit priority: Exceed limit directly reject; high fees cannot break MAX_*. Guardrails at strategy layer (soft targets/quotas) rather than consensus approval

Governance and Upgrades

  • Parameter governance: PROTOCOL_MAX_* parameters via soft fork governance (initial values fixed, subsequent DAO proposal adjustments)
  • Upgrade path: When Tondi increases transaction limits in future, orderly increase Pay2Ingot parameters via soft fork
  • Backward compatibility: Parameter upgrades maintain backward compatibility, old nodes can continue processing smaller payloads

Soft Policy Targets (Non-consensus)

  • L2 recommended values: payload ≤ 84.5 KiB (adjustable), tx_total ≤ 100 KiB (Tondi physical limit)
  • Physical constraint: Tondi mass ≤ 100k automatically limits actual size
  • Soft policy (non-consensus): Recommend Pay2Ingot byte ratio target 20% within block (maintain "non-consensus soft target", rely on pool/package policies and fee curve adjustment)
  • EMA smoothing mechanism: Use exponential moving average to smooth ratio fluctuations, avoid peak impact
  • Excess condition handling: When exceeded, enable budget accumulator, delay processing rather than reject
  • Market friendly: Soft policy prevents peak flooding without suppressing natural market demand
  • Circuit breaker backup: Retain consensus-level circuit breaker option, only activate through governance proposal in extreme cases ("consensus-level circuit breaker" only as extreme case governance backup, disabled by default)

6. Serialization and Endianness Specification

Numeric Field Endianness Specification

Unified endianness rules:

  • u8/u16/u32/u64 uniformly use LE (little-endian)
  • txid 32B unchanged (byte order unchanged)
  • vout fixed u32 (little-endian)
  • LEN u32 (little-endian)
  • FLAGS u16 (little-endian)
  • amount u64 (little-endian)

Encoding specification table:

Field Type Size Endianness Description
u8 1B LE Single byte no endianness
u16 2B LE Little-endian
u32 4B LE Little-endian
u64 8B LE Little-endian
txid 32B As-is Byte order unchanged
vout 4B LE u32 little-endian
LEN 4B LE u32 little-endian
FLAGS 2B LE u16 little-endian
amount 8B LE u64 little-endian

Implementation requirements:

  • All implementations must use same endianness specification
  • Registry test vectors must include serialization cases
  • Endianness inconsistency → transaction invalid

Design principle: Unified endianness specification eliminates 80% of implementation differences

Activation Mechanism

  • v1.0 activation: Via hard fork activation (signal block threshold + height lock)
  • One-time activation: All features (MAST, multiple signature types, ScriptHash, etc.) activate simultaneously
  • Upgrade type discrimination:
    • Hard fork (HF): New rules accept some blocks/transactions old rules reject
    • Soft fork (SF): New rules reject some blocks/transactions old rules accept (stricter)
    • Non-fork: Only change mempool/packaging/relay policies, don't change consensus validation
  • Core discrimination: Any change making old nodes see invalid while new nodes see valid is a hard fork
  • Future upgrades: If new features needed, via new hard fork; parameter adjustments via governance mechanism

Compatibility

  • Interaction with Value UTXO: No interference (type isolation); mixed tx naturally supports fees/change/transfer
  • On-chain verifiable + off-chain replayable: L1 verifies structure/hash/signature, off-chain replays Schema/oUTXO; prevents war (Registry test vectors enforce consistency)
  • fail-closed principle: Insist on fail-closed design, functional extensions = hard fork, parameter tightening = soft fork
  • Security boundary: Risk of old nodes accepting blocks violating new constraints eliminated by fail-closed principle
  • Design choice: Choose fail-closed over "known but content-expandable" containers, ensuring security boundary

Core Interaction Scenario Sequence Diagrams

The following sequence diagrams demonstrate Pay2Ingot core interaction scenarios, covering L1 consensus layer and off-chain indexer collaboration:

1) Value + Ingot Mixed: Create Inscription (Mint / Initial Release)

Indexer(Off-chain oUTXO)ChainMiner/ProposerMempoolFull Node(L1)Wallet(Author)Indexer(Off-chain oUTXO)ChainMiner/ProposerMempoolFull Node(L1)Wallet(Author)Goal: Pay with Value input, produce Pay2Ingot output (commitment header)alt[Same-tx reveal][Commit-only]Unknown schema marked raw_data, don't enter state machineConstruct TX: Inputs[Value] + Outputs[Pay2IngotOutput{VER,SCHEMA_ID,HASH,FLAGS,LOCK}]1Same-tx reveal(REVEAL_FLAG=true, CHUNKS)2Only commit header(REVEAL_FLAG=false)3Broadcast TX4Validate structure/signature/fees/hash/chunk order/length5Enter mempool6Select package(fee priority, Pay2Ingot ratio policy only at strategy layer)7Package on-chain8New block event9Extract Pay2Ingot output, validate HASH_PAYLOAD and revealed payload10oUTXO rules: new artifact established, generate tip11

2) Value ↔ Ingot Mixed: Transfer Ownership (Spend-to-Reveal + Payment)

Indexer(Off-chain oUTXO)ChainMinerMempoolFull Node(L1)Wallet(Recipient)Wallet(Current Holder)Indexer(Off-chain oUTXO)ChainMinerMempoolFull Node(L1)Wallet(Recipient)Wallet(Current Holder)Goal: Spend old Ingot UTXO, produce new Ingot UTXO (owner change), pay fees with Value inputSelect Ingot input + Value input (payment)1Assemble witness: payload (optional), AUTH_SIGS (satisfy Lock requirements)2Broadcast transfer TX3Validate(structure/hash/signature/timelock/fees)4Enter pool5Fee priority package selection6On-chain(old Ingot spent, new Ingot produced)7Block event8oUTXO: identify same artifact, old tip invalid, new becomes tip9Notify new ownership(indexer API / wallet subscription)10

Additional Consensus Clauses

1. Naming and Identification Specifications

artifact_id Definition

  • artifact_id formula: artifact_id = blake3("Ingot/artifact" || SCHEMA_ID || HASH_PAYLOAD)
  • Domain separation constant: Written into specification (fail-closed, ensures everyone calculates consistently)
  • Sequence allocation (ordinal_seq): Clearly determined by (first_seen_txid:vout), as non-consensus but unique algorithm (written in test vectors)

Hash Algorithm and Domain Separation (Unified BLAKE3/32B)

  • HASH_PAYLOAD = blake3("Ingot/payload" || canonical_bytes)
  • artifact_id = blake3("Ingot/artifact" || SCHEMA_ID || HASH_PAYLOAD)
  • instance_id = blake3("Ingot/instance" || artifact_id || outpoint_bytes)
    • Where outpoint_bytes = txid[32] || u32le(vout)

Design principle: Unified algorithm is cleanest, reduces implementation divergence and pitfall surface

canonical_bytes Encoding Specification

Encoding rules: Adopt TLV with deterministic ordering

TLV normalization rules:

  • Type ascending: TLV types sorted by numerical value ascending
  • Same Type multiple values: Sorted by value lexicographically
  • Integer encoding: Uniformly use uLE (little-endian)
  • Prohibit zero prefix: Integers cannot have leading zeros
  • String encoding: UTF-8 encoding
  • Byte order fixed: All numeric fields use fixed endianness

HASH_PAYLOAD calculation:

HASH_PAYLOAD = blake3("Ingot/payload" || tlv_canonical_bytes)

Implementation requirements:

  • All implementations must use same canonical_bytes encoding rules
  • Registry test vectors must include encoding cases
  • Encoding inconsistency → transaction invalid

Consensus Requirements

  • All indexers must use same artifact_id calculation rules
  • Registry test vectors must include identifier calculation cases, enforce validation implementation
  • Registry test vector "enforcement" mechanism: Not in consensus; through Registry access, ecosystem whitelist, client default source/ranking, wallet integration threshold and other governance/operational means to "enforce" consistency

2. Transaction Arbitration and Concurrency Rules

Off-chain Arbitration Rules (Soft Consensus Constraints)

  1. Unique active tip: Each artifact_id can only have one active tip
  2. Spend-old-create-new: New tip must spend old active tip
  3. Concurrent conflict arbitration: Arbitrate by (blue_score, wtxid) lexicographic order (DAG environment uses blue score not height)

oUTXO arbitration unit: Unique active tip, spend-old-create-new, concurrent arbitration - these three rules apply to unit artifact_id. Same artifact_id has only one active tip at any moment; different artifact_id don't interfere with each other.

Tondi wtxid Specification

Tondi wtxid: wtxid = blake3("tondi/wtxid" || canonical_tx_bytes_with_witness); canonical_tx_bytes_with_witness explicitly includes: transaction header, inputs, outputs, all witnesses (including Pay2Ingot chunk indices and data), lock time, etc.; endianness follows §6.

oUTXO conflict arbitration key: (blue_score, block_id, wtxid); if blue_score same, compare block_id (block hash byte string lexicographic order), then compare wtxid; wtxid comparison uses byte string lexicographic order (big-endian).

Transaction Malleability and Arbitration Stability Window

Transaction identifier specification:

  • oUTXO conflict arbitration txid uses wtxid (including witness) or explicit Tondi standard
  • Set FINALITY_K (recommend 6~10): Indexers allow arbitration flipping within K confirmations, exceeding K considered stable
  • DAG/parallel block scenarios, use (blue_score, wtxid) (blue score or topological order), and specify priority field in specification

Stability window mechanism:

  • Document states "recommended notification semantics": within K "state may flip"
  • Exceeding K considered final, indexers must not modify arbitration results
  • Reorg scenarios: Indexers must perform rollback and re-arbitration for reorgs

Arbitration consistency:

  • Using blue_score instead of height more stable in DAG environment
  • Must be consistent with underlying chain consensus mechanism
  • Registry test vectors must include reorg and concurrent conflict cases

oUTXO Rules and Schema Coordination

  • oUTXO = mandatory soft constraints: Unique active tip / spend-old-create-new / concurrent lexicographic arbitration - these three rules apply to all schemas (including unknown). Indexers must pass Registry published test vectors; implementations not passing cannot claim executable state, can only output raw_data
  • Unknown schema handling: Unknown schema ⇒ raw_data (don't run FSM), but still participate in oUTXO arbitration
  • Unlocked instances: Unlocked instances (L1 TIMELOCK not satisfied) must not be counted as active tip
  • Tip uniquification: Tip uniquification/arbitration unrelated to "whether schema recognized"

Soft Consensus Requirements

  • Off-chain arbitration rules as soft consensus constraints, ensure indexer behavior consistency
  • Registry test vectors must include conflict cases, enforce validation implementation
  • oUTXO as soft consensus, enforced by test vectors, object layer order/uniqueness arbitrated off-chain

3. Resource Limits

Mandatory Validation Order

  • Pre-validation: First validate TOTAL_CHUNKS * MAX_CHUNK_LEN ≤ PROTOCOL_MAX_PAYLOAD (8 * 12 KB ≤ 96 KB), then receive chunks
  • Zero-copy strategy: Consensus required, prevents memory amplification attacks

Validation Order and Rejection Conditions (L1 Consensus Layer)

  1. Structure/type/limits: Version, field format, size limits
  2. Hash integrity: commit-reveal (blake3(payload) == hash_payload)
  3. Lock validation: Public key count, sorting, deduplication, format
  4. Signature validation: auth_sigs, MAST proof (if any)
  5. Fee compliance: Byte-based billing

Rejection principle: Any step failure ⇒ E_* reject; exceed limit directly reject (not allow with high fees)

Notes:

  • L1 doesn't validate chunk indices (chunks entirely L2 concept)
  • L1 doesn't handle TLV extensions (no PolicyExt mechanism)
  • Time locks via ScriptHash + VM scripts

4. Time Lock Semantics

ScriptHash Time Lock Validation (v1.0)

Time locks via ScriptHash lock type + VM scripts:

  • Script committed in Lock = ScriptHash { script_hash }
  • Witness provides script_reveal, L1 validates RIPEMD160(SHA256(script_reveal)) == script_hash
    • Protocol frozen: HASH160 defined as RIPEMD160(SHA256(bytes)), where bytes is raw byte sequence
  • Tondi VM executes script, checks time conditions (OP_CHECKLOCKTIMEVERIFY/OP_CHECKSEQUENCEVERIFY)
  • Script execution failure → transaction invalid
  • Off-chain (oUTXO) reads L1 validation results, determines active tip visibility

Consensus Requirements

  • ✅ Clearly define time lock validation L1 execution
  • ✅ Off-chain indexers must follow unlocked Pay2Ingot processing rules

5. URI and External Data Security

URI Pattern Security Requirements

Enhanced security specification: If ALLOW_EXTERNAL_URIS=1, payload must include:

  • URI_LIST_TLV: External URI list
  • URI_CONTENT_HASH_TLV{alg:u8, digest:var}: Each URI includes target content hash; alg ∈ {1=blake3_256, 2=sha256}; digest_len bound to algorithm (32B)

URI_CONTENT_HASH_TLV alg values fixed; unknown values → E_UNKNOWN_TLV (fail-closed).

Consensus validation:

  • HASH_PAYLOAD must cover URI list + content hash (not just URI text)
  • Light nodes can only verify "URI and content hash" matches HASH_PAYLOAD
  • When external data disappears/tampered, content hash mismatch → transaction invalid

Security principle:

  • Only committing URL easily points to mutable content, loses verifiability
  • Must simultaneously commit content hash, ensure external data immutability
  • Light nodes can verify external data integrity via content hash

6. Rate Limiting Implementation (Off-chain)

Rate Limiting Policy Suggestions

  • Implementation location: Rate limiting should be implemented by off-chain indexers/mining pools, not L1 consensus layer
  • Tracking method: Identify creator via transaction input address, or extract public key from Lock
  • Rate limit key: Recommend using (schema_id, creator_address) combination to implement per-user rate limits
  • Flexibility: Different indexers can implement different rate limiting policies based on needs

Implementation Points

  • L1 doesn't enforce any rate limiting mechanism
  • Indexers can freely choose rate limiting algorithms (token bucket, leaky bucket, etc.)
  • Mining pools can adjust transaction priority based on fee rate and creation frequency

7. Error Code Namespace and Numbering

CONSENSUS_0x0001–0x03FF (L1):

  • CONSENSUS_INVALID_STRUCTURE = 0x0001
  • CONSENSUS_UNKNOWN_VERSION = 0x0002 ✅ u16 type
  • CONSENSUS_INVALID_LENGTH = 0x0003
  • CONSENSUS_HASH_MISMATCH = 0x0010
  • CONSENSUS_CANONICAL_ENCODING_ERROR = 0x0012
  • CONSENSUS_SIGNATURE_INVALID = 0x0020
  • CONSENSUS_SIGNATURE_COUNT_MISMATCH = 0x0022
  • CONSENSUS_LOCK_VIOLATION = 0x0030
  • CONSENSUS_TIMELOCK_NOT_SATISFIED = 0x0031
  • CONSENSUS_TIMELOCK_INVALID = 0x0032
  • CONSENSUS_FLAGS_COMBINATION_INVALID = 0x0040
  • CONSENSUS_RESERVED_BITS_SET = 0x0041
  • CONSENSUS_REVEAL_REQUIRED_NOT_SATISFIED = 0x0042
  • CONSENSUS_PAYLOAD_TOO_LARGE = 0x0050
  • CONSENSUS_WITNESS_TOO_LARGE = 0x0052
  • CONSENSUS_INGOT_NOT_ACTIVATED = 0x0060 ✅ (using Ingot before activation)
  • CONSENSUS_VERSION_MISMATCH = 0x0061 ✅ (outer and inner version inconsistent)
  • CONSENSUS_MAST_ROOT_REQUIRED = 0x0062 ✅ (MastOnly requires mast_root to exist)

INDEXER_0x1001–0x13FF (Off-chain):

  • INDEXER_MUTATION_FORBIDDEN = 0x1001
  • INDEXER_UNKNOWN_TLV = 0x1002
  • INDEXER_URI_CONTENT_MISMATCH = 0x1004

Processing Flow (Minimal):

  • L1: Reject tx/block on error, log code, txid/wtxid, pointer(field)
  • Indexer: On soft consensus conflict, re-arbitrate by oUTXO and log decision_key, from→to
  • Wallet: Transparent CONSENSUS_* to user-readable prompts, use INDEXER_* as prompts/retry strategy

TLV Type Space and Reserved Prefixes

TLV Type space division:

  • 0x0001–0x3FFF: Standard TLV (ASSET_LIST, PARENT_REF, MEMO, TIMESTAMP, etc., no Policy-related TLV)
  • 0x4000–0x7FFF: Reserved space (future hard fork extensions)
  • 0x8000–0xBFFF: Governance temporary TLV (community proposals, experimental features)
  • 0xC000–0xFFFF: Private TLV (vendor/application private extensions)

Processing rules:

  • L1 doesn't parse payload TLV: Payload content committed by blake3(payload)==hash_payload, L1 doesn't care about internal structure
  • Unknown fields/enums in output header (IngotOutput) → fail-closed (E_UNKNOWN_TLV)
    • For example: Unknown Lock enum, future new output header fields
  • Unknown enum in MAST leaf lock encoding → fail-closed (E_UNKNOWN_TLV)
  • Unknown TLV in Payload: Handled by L2 indexers per soft consensus (can ignore or reject, strategy layer decides)

Unknown Field Handling

  • fail-closed principle: Any unknown field or unknown enum → fail-closed
  • Field layout: MAST_ROOT placed at end field, v1.0 nodes fully support
  • Activation method: Tondi-style signal block activation: signal block threshold + height lock

Schema Layer Specification (L2 / Inside Payload)

Design positioning: These are L2 / Schema layer concepts, placed in payload, interpreted by indexers/wallets.
L1 commitment: L1 only commits HASH_PAYLOAD, doesn't understand semantics; payload bytes cannot be tampered with.
No consensus change: No need to add L1 fields or hard fork; clients/indexers validate based on payload commitments.

Standard Schema Template Library

Tondi provides production-ready standard Schema templates (located at /ingot_schemata):

Token:

  • brc20_compatible.json - BRC-20 compatible tokens (simple, community-friendly)
  • trc721_advanced.json - TRC-721 advanced tokens (TLV format, governance, asset conservation)

Inscription:

  • text.json - Text inscriptions (plain text, Markdown, code)
  • image.json - Image inscriptions (SVG, PNG, JPEG, GIF, WebP)

NFT:

  • erc721_compatible.json - ERC-721 compatible NFTs
  • music.json - Music NFTs (audio files, lyrics, copyright)

DAO:

  • proposal.json - DAO proposals (parameter modifications, fund allocation, code upgrades)
  • vote.json - DAO voting (weighted voting, delegation, rationale)

Application:

  • social_post.json - Decentralized social media posts
  • identity.json - Decentralized identity (W3C DID standard)

📚 Usage Guide: Each category has detailed README and usage examples, see /ingot_schemata/ directory.

🔗 Compatible Standards: BRC-20 (Bitcoin), ERC-20/ERC-721 (Ethereum), W3C DID, Verifiable Credentials, OpenSea Metadata

1. Asset List Specification

TLV Type Definition

TLV_ASSET_LIST (type = 0x0101)

Placed in payload, used to carry asset list (repeatable, max 256 items).

Structure (per item):

asset_type  : u8        // 0=fungible, 1=semi-fungible, 2=nft, 3=custom
asset_id    : [32]      // BLAKE3 identifier or upstream asset ID
amount      : u128le    // For NFT use 1; semi use shares; fungible use quantity
meta_tlv    : TLV[]     // Bounded extensions (e.g., display/URI, etc.); unknown→indexer can ignore

Encoding rules:

  • Follow TLV deterministic encoding specification (see §C.2):
    • Type ascending
    • Same Type multiple values sorted by value lexicographically
    • Integers uniformly use LE (little-endian)
    • No leading zeros
  • Entire ASSET_LIST after parsing total bytes ≤ 48 KiB (reserved payload budget)

Limits:

  • Maximum 256 asset items
  • Single ASSET_LIST TLV total size ≤ 48 KiB
  • Exceeding limits → indexer rejects parsing (marked as invalid_asset_list)

Semantics:

  • L1 doesn't understand ASSET_LIST, only guarantees it's committed by HASH_PAYLOAD
  • Indexers responsible for parsing and validating asset list validity
  • Same asset_id appearing multiple times in same container, aggregate per TLV spec "same Type multiple values value lexicographic order"; conflicts decided by application layer (default latter overrides or error)

TLV_ASSET_MERKLE_ROOT (type = 0x0102) (optional)

Used when asset count is large or privacy reveal needed.

Structure:

root : [32]     // BLAKE3-256 Merkle root

Purpose:

  • When asset count large, only place Merkle root in payload
  • Specific asset entries can be selectively revealed as leaves in witness (validated by indexers, not in consensus)
  • Provides "partial visibility" capability

Mutual exclusivity:

  • Recommend TLV_ASSET_LIST and TLV_ASSET_MERKLE_ROOT mutually exclusive (if both present, use Merkle, but recommend choosing only one)
  • If both exist, indexers should use ASSET_MERKLE_ROOT as standard

Witness: Asset Merkle Proof

WITNESS_ASSET_PROOF (can appear multiple times; total witness still subject to 4 KiB limit)

Structure:

leaf_bytes   : TLV[]     // TLV encoding identical to ASSET_LIST unit entry
merkle_path  : [depth]   // Depth ≤ 8, each step 32B hash

Validation flow:

  1. Indexer calculates leaf hash with leaf_bytes: leaf_hash = blake3(leaf_bytes)
  2. Reconstruct Merkle root along merkle_path
  3. Verify reconstructed root equals ASSET_MERKLE_ROOT in payload
  4. Match success → this leaf considered valid reveal, merged into visible asset view
  5. Match failure → indexer rejects this leaf, records alert log

Constraints:

  • Depth ≤ 8 (max 256 leaves)
  • When payload doesn't have ASSET_MERKLE_ROOT, don't use this structure
  • L1 doesn't validate Merkle path, only length limit (4 KiB total witness)

2. Operation Semantics (Suggested Commitment in Payload)

For readability and auditing, recommend carrying following TLV in payload:

TLV_OP_KIND (type = 0x0110)

Structure:

op_kind : u8    // 0=Mint, 1=Transfer, 2=Mutate, 3=Attach, 4=Burn

Semantics:

  • Mint (0): No parent, first creation
  • Transfer (1): Transfer ownership, parent must exist
  • Mutate (2): Modify state, parent must exist
  • Attach (3): Attach data, parent must exist
  • Burn (4): Destroy instance, explicit destruction semantics

Validation:

  • Non-consensus field, but indexers should verify op_kind matches actual operation
  • When inconsistent, record alert log, but doesn't affect L1 transaction validity

TLV_PARENT_REF (type = 0x0111)

Structure:

parent_ref : outpoint_bytes    // txid[32] || u32le(vout)
or
parent_ref : instance_id        // [32] parent instance ID

Semantics:

  • Only non-Mint operations need
  • Points to spent previous tip (easy to verify "spend-old-create-new" intent consistency off-chain)

Validation:

  • Indexers need to verify parent_ref matches actually spent tip
  • When inconsistent, record high-priority audit log, optionally reject this change
  • L1 doesn't validate parent_ref, only guarantees it's committed by HASH_PAYLOAD

3. Indexer Implementation Points (Strong Consistency Landing)

Object Identification and Keys

  • artifact_id = blake3("Ingot/artifact" || SCHEMA_ID || HASH_PAYLOAD)
  • instance_id = blake3("Ingot/instance" || artifact_id || outpoint_bytes)
    • Where outpoint_bytes = txid[32] || u32le(vout)
    • Instance = that UTXO, uniqueness naturally guaranteed by outpoint

Unique Active Tip

  • Read each candidate successor whether spent current tip's outpoint
  • Successors claiming but not spending old directly marked as illegal change/ignore
  • For concurrent successors, use decision key: (blue_score, block_id, wtxid) lexicographic order to pick unique winner
  • Rest marked as stale
  • Lock after FINALITY_K confirmations (recommend K=6)

Asset View Construction

If payload contains TLV_ASSET_MERKLE_ROOT:

  • Maintain "revealed leaves" cache
  • When transaction witness contains WITNESS_ASSET_PROOF, verify path=root, merge into visible asset view
  • Unrevealed leaves remain "hidden/unknown" state
  • Provide "partial visibility" API

If payload directly contains TLV_ASSET_LIST:

  • Directly parse full list
  • Build asset account view

Consistency Rules

  • Same asset_id appearing multiple times in same container, aggregate per TLV spec "same Type multiple values value lexicographic order"
  • Conflicts decided by application layer (default latter overrides or error, write into Registry cases)
  • v1.0 doesn't use TLV extension mechanism
  • meta_tlv unknown types: Same principle
  • If both ASSET_LIST and ASSET_MERKLE_ROOT present: Use Merkle as standard (or directly specify as mutually exclusive, recommend mutual exclusion simpler)

API Recommendations

  • GET /artifact/{id} → Returns tip, visible asset aggregation, whether unrevealed leaves exist
  • GET /artifact/{id}/assets → Paginated list asset entries (can filter asset_type, asset_id)
  • POST /verify-asset-proof → Verify leaf matches root (for light wallets)

4. Wallet/SDK Tasks

Construction

  • Encode ASSET_LIST per agreed TLV (or generate asset leaves and Merkleize, write ASSET_MERKLE_ROOT)
  • Write OP_KIND, PARENT_REF into payload, bind intent consistency
  • Still observe "max 1 Pay2Ingot output per transaction" L1 consensus constraint

Spending/Transfer

  • Transfer/change: Must spend current tip
  • Reassemble witness (attach WITNESS_ASSET_PROOF to reveal only involved assets if needed)
  • Support RBF/CPFP package relay
  • After confirmation, solidify instance_id with txid||vout

Validation

  • Parse/check TLV boundedness, integer LE, no leading zeros (consistent with current specification)
  • If using Merkle: Locally recalculate leaf hash and compare ASSET_MERKLE_ROOT (pure client validation, doesn't depend on L1)

Glossary

Core Concepts

  • Pay2Ingot (Pay2Ingot): New output type supporting inscriptions and off-chain state machines
  • oUTXO: object-oriented UTXO (object-oriented UTXO), mandatory soft constraint off-chain arbitration mechanism
  • artifact_id: blake3("Ingot/artifact" || SCHEMA_ID || HASH_PAYLOAD), uniquely identifies inscription object
  • instance_id: blake3("Ingot/instance" || artifact_id || outpoint_bytes), uniquely identifies inscription instance
    • Where outpoint_bytes = txid[32] || u32le(vout)
  • tip: Each schema's unique active instance
  • blue_score: Tondi DAG blue score, used for arbitration ordering
  • wtxid: Transaction hash including witness, suppresses malleability

Technical Terms

  • canonical_bytes: TLV deterministically encoded payload bytes
  • MAST: Merkle Abstract Syntax Tree, v1.0 privacy enhancement
  • fail-closed: Security principle where unknown fields/enums cause transaction invalid
  • Registry: Authoritative source for test vectors and ecosystem governance
  • FINALITY_K: Confirmation count for arbitration stability window

Field Descriptions

  • asset_tag: 32B asset identifier, zero for TONDI native assets
  • outpoint: Transaction output location (txid||vout), used to generate unique instance_id

Quick Start Guide

Core Rules (5-minute mastery)

  1. Output type: Pay2Ingot is new output type, mixed use with Value UTXO
  2. Two-phase: commit (commitment) → reveal (reveal), supports large payloads
  3. oUTXO three disciplines: Unique tip, spend-old-create-new, lexicographic ordering
  4. Resource limits: Single Pay2Ingot output, actual size constrained by Tondi mass (≤ 100k), L2 recommends payload ≤ 84.5 KiB
  5. fail-closed: Unknown fields → transaction invalid

Implementation Checklist

  • Endianness unified: All numeric fields use little-endian (LE)
  • Signature message: Use wtxid and complete SigMsg formula
  • Chunk validation: Per-chunk hash + overall HASH_PAYLOAD
  • Flags combination: Reference combination matrix table
  • DAG support: Use blue_score not height
  • Test vectors: Pass Registry mandatory validation

Common Pitfalls

  • ❌ Mixed endianness (vout using big-endian)
  • ❌ Multiple Pay2Ingot outputs (L1 consensus prohibits)
  • ❌ Using deprecated fields (should use outpoint)
  • ❌ Ignoring FINALITY_K stability window
  • ❌ Unknown fields not fail-closed

Implementation Boundary Strict Separation

Goal: Ensure implementation boundaries clear, responsibilities verifiable, avoid accidentally writing "policy/soft consensus" into L1, or missing "L1 mandatory checks" in off-chain.

Terminology

  • L1 client consensus: Rules full nodes (validators/miners/complete verification clients) must execute; violation means reject transaction/reject block.
  • Off-chain client consensus (soft): Indexers/wallets/SDKs must be consistent in arbitration and semantics; doesn't trigger L1 block rejection, but affects object uniqueness/visible state.
  • Off-chain client consensus ("hard"): From user/ecosystem perspective "like hard constraint" off-chain rules: Through Registry access, whitelist, wallet default source, market/operational thresholds to "enforce" consistency. Violations rejected by ecosystem, but will not cause L1 fork.

Separation Table

Domain Rule/Field Belongs To Description/Validation Action
Output type identification (OP_PAY2Ingot) Type code identification, unknown type fail-closed L1 Don't recognize → reject tx
Version/field limits version==0x01, IngotOutput≤1KiB, MAST depth≤8 L1 Exceed limit directly E_INVALID_STRUCTURE
Hash commitment HASH_PAYLOAD commit-reveal integrity (blake3(payload)==hash_payload) L1 Mismatch → E_HASH_MISMATCH/E_PAYLOAD_HASH_MISMATCH
Chunk structure Index strictly increasing, no duplicates/skips (L2 indexer validation, L1 doesn't parse chunk) L2 Violation → INDEXER_CHUNK_INDEX_INVALID
Signatures and Lock All Lock types (None, PubKey, ScriptHash, MastOnly) L1 Signature verification failure → E_SIGNATURE_INVALID, script execution failure → E_SCRIPT_FAILED
MAST (v1.0) MAST_ROOT existence and path validation (depth≤8, bounded leaf) L1 (from v1.0) Fully supports MAST validation
Fee system "Total byte billing" and (if enabled) layered billing formula L1 Only affects block entry threshold and rejection reasons
Endianness specification All numerics LE, txid as-is L1 Non-compliance → E_INVALID_STRUCTURE
Tondi wtxid definition blake3("tondi/wtxid" || tx_with_witness) L1 (definition) / Off-chain use (arbitration key) L1 needs unified digest standard; off-chain uses it as oUTXO arbitration key
oUTXO three disciplines Unique tip / spend-old-create-new / concurrent arbitration (blue_score, block_id, wtxid) Off-chain soft consensus Doesn't trigger block rejection; affects object visible "final tip"
Unknown schema handling Unknown → raw_data mode but still participates in oUTXO arbitration Off-chain soft consensus Unified display and API semantics
artifact_id / instance_id Formula and domain separation constants (including network_id); use outpoint to guarantee uniqueness Off-chain "hard" consensus Mandatory pass Registry test vectors; cannot claim executable state without passing
FINALITY_K Allow flip within K, lock after K Off-chain soft consensus Wallet/indexer consistent UX/notification and rollback strategies
URI security URI_LIST_TLV + URI_CONTENT_HASH_TLV{alg,digest} L1 (commitment coverage & algorithm enum) / Off-chain (fetch validation) L1 validates commitment structure; off-chain responsible for fetching and comparison
Rate limiting Track creator via transaction input address or Policy public key Off-chain soft consensus Indexers/mining pools implement rate limiting policies themselves
TLV type space Standard/reserved/governance/private behavior L1 (unknown enum in output header/MAST leaf→fail-closed, payload TLV not parsed) / L2 (private interpretation in payload) L1 doesn't parse payload; unknown output header fields/MAST leaf enum → fail-closed
Error code mapping CONSENSUS_* vs INDEXER_* Implementation specification Facilitates observation and cross-implementation consistency
Registry access Test vector CI/whitelist Off-chain "hard" consensus No pass→only raw_data

Shorthand: Structure/boundary/hash/signature/timelock/MAST/fee system/endianness in L1; Object identity/ordering/visible state/concurrent arbitration/reorg handling off-chain; Identity calculation and encoding specifications (artifact_id/instance_id/canonical TLV) enforced by Registry as "off-chain hard consensus" for uniformity.


Summary

Core Design Principles

  • On-chain (L1) only manages "can enter block": Structure, limits, hash, signature, time locks, MAST, fee system, endianness—error means reject block/reject tx.
  • Off-chain (client) only manages "calculate consistently": Object identity, concurrent arbitration, visible state, reorg window, URI validation, account policies—error means ecosystem rejection (Registry "off-chain hard consensus" bottom line).

Design Principles

  • 🎯 Fail-Closed: Unknown types/bits/indices all rejected
  • 🎯 One-Shot: Single version contains all features, future upgrades use new version number
  • 🎯 Privacy-First: MuSig2 makes multisig and single sig indistinguishable on-chain
  • 🎯 Bitcoin-Compatible: StandardSchnorr fully compatible with Taproot

Appendix A: Reference Algorithms and Wire Formats

A.1 Identification and Digests (All BLAKE3/32B)

Current formula (v1.0, using outpoint):

HASH_PAYLOAD = blake3("Ingot/payload" || tlv_canonical_bytes)
artifact_id  = blake3("Ingot/artifact" || SCHEMA_ID || HASH_PAYLOAD)
instance_id  = blake3("Ingot/instance" || artifact_id || txid[32] || u32le(vout))
wtxid        = blake3("tondi/wtxid" || canonical_tx_bytes_with_witness)

Key changes:

  • instance_id directly uses outpoint (txid||vout) to anchor uniqueness
  • ✅ Domain separation prefixes ensure hashes don't conflict
  • ✅ Instance uniqueness naturally guaranteed by UTXO model, no need for user-provided additional data

wtxid packaging specification (ensures cross-implementation consistency):

canonical_tx_bytes_with_witness serialization order (all integers little-endian):

Field Type Length Prefix Description
version u16 No Transaction version
inputs.len() u32 Yes Input count
For each input:
├─ previous_outpoint.txid [u8; 32] No Previous transaction ID
├─ previous_outpoint.vout u32 No Output index
├─ sequence u64 No Sequence number
└─ sig_op_count u8 No Signature operation count
outputs.len() u32 Yes Output count
For each output:
├─ value u64 No Amount
├─ script_public_key.version u16 No SPK version
└─ script_public_key.script Vec Yes (u32) SPK script (includes IngotOutput Borsh)
lock_time u64 No Lock time
subnetwork_id [u8; 20] No Subnetwork ID
gas u64 No Gas fee
payload Vec Yes (u32) Transaction payload
witnesses (critical)
witnesses.len() u32 Yes Witness count
For each witness:
├─ payload_chunks ... See A.4 REVEAL_FLAG + CHUNKS (if any)
└─ auth_witness ... See A.4 AUTH_SIGS + MAST_PROOF

Notes:

  • IngotOutput embedded in script_public_key.script via Borsh serialization
  • Borsh Vec format: u32le(len) || T[0] || T[1] || ...
  • Witness divided into payload_chunks (≤64 KiB) and auth_witness (natural constraint) two measurement domains

A.2 TLV Normalization Rules (Canonical Bytes Participating in Hash)

  • Type ascending; same Type multiple values sorted by value lexicographically
  • Integers uniformly little-endian, no leading zeros
  • Strings UTF-8; byte strings as-is
  • Unknown/out-of-bounds TLV → fail-closed

A.3 Signature Message SigMsg

SigMsg = blake3(
  "Ingot/SigMsg/v1" ||
  network_id ||
  wtxid ||
  u32le(input_index) ||
  spent_prevout{ txid || u32le(vout) } ||
  lock_bytes ||
  HASH_PAYLOAD
)
  • v1.0: CopperootSchnorr + StandardSchnorr (BIP340-compatible)
  • MuSig2: Off-chain aggregation, indistinguishable on-chain (via PubKey lock type)

A.4 Witness Wire Format (Reference Structure, Fields Little-Endian)

IngotWitness Borsh serialization format:

// Borsh serialization order (all integers little-endian)
pub struct IngotWitness {
    pub payload: Option<Vec<u8>>,         // Option + Vec (u32le len prefix)
    pub auth_sigs: Vec<Vec<u8>>,          // Vec of Vec (double u32le len prefix)
    pub script_reveal: Option<Vec<u8>>,   // Option + Vec
    pub mast_proof: Option<MastProof>,    // Option + struct
}

Borsh encoding rules:

[payload]
u8  tag                    (0=None, 1=Some)
if tag==1:
  u32le len                (payload length)
  u8[len] data             (payload data)

[auth_sigs]
u32le count                (signature count)
repeat count times:
  u32le sig_len            (signature length, must = 64)
  u8[64] signature         (signature data, compact format)

[script_reveal]
u8  tag                    (0=None, 1=Some)
if tag==1:
  u32le len                (script length)
  u8[len] script           (script data)

[mast_proof]
u8  tag                    (0=None, 1=Some)
if tag==1:
  u32le path_count         (Merkle path length, ≤8)
  [32][path_count] hashes  (path hashes)
  u32le leaf_len           (leaf_lock length)
  u8[leaf_len] leaf_lock   (leaf lock data)

Important changes:

  • L1 removes chunk processing: No longer REVEAL_FLAG, TOTAL_CHUNKS, CHUNK[i] fields
  • payload complete storage: L1 only verifies blake3(payload) == hash_payload
  • L2 responsible for chunking: ingot-client handles payload chunking/reassembly, chunk validation
  • Signature format: All signatures must be 64 byte compact format
  • MuSig2: Implemented via PubKey lock type, no additional fields needed

Actual size constraints:

  • payload: ≤ 85 KiB (L2 recommended, naturally constrained by Tondi mass ≤ 100k)
  • auth_sigs: ≤ 1 × 64B = 64B (PubKey lock type, MuSig2 aggregated needs only 1 signature)
  • mast_proof: ≤ 8 × 32B + leaf_lock ≈ 0.3 KiB
  • script_reveal: Variable length (constrained by mass)

Document Complete