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:
- Main docs:
/ingot_schemata/README.md - Token guide:
/ingot_schemata/token/README.md - NFT guide:
/ingot_schemata/nft/README.md - DAO guide:
/ingot_schemata/dao/README.md - Application guide:
/ingot_schemata/application/README.md
💡 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)
- MAST privacy enhancement (optional
- Fee system: Default "total byte billing"; layered billing as governance switch, initially equivalent to default
- MAST: Supports
MAST_ROOTand path validation;leaf_lockas 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 asblake3("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
Lockenum - 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: Taproot2: 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
versionfield fixed at0x01 - Consensus rejects any unknown version value (fail-closed)
- Version consistency check:
ScriptPublicKey.versionmust be 2 andIngotOutput.versionmust 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 = 1024bytes - Serialization format: Borsh (Binary Object Representation Serialization for Hashing)
- Stored in:
ScriptPublicKey.scriptfield - 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 + MuSig2aggregation instead (indistinguishable on-chain, saves space, improves privacy) - ❌ PubKeyHash: Simplified design (use
PubKeyorScriptHashinstead) - ❌ Ed25519: Unified Schnorr signatures (secp256k1 curve)
Lock type details (v1.0):
-
None: No authentication (anyone can spend)
- For public inscriptions, public data, etc.
-
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)
-
ScriptHash { script_hash }: P2SH semantics (executed via Tondi VM)
script_hash: 20B hash value- HASH160 definition (protocol frozen):
Wherescript_hash = RIPEMD160(SHA256(script_reveal_bytes))script_reveal_bytesis the raw byte sequence of the script (not Borsh wrapped) - Supports time locks (OP_CHECKLOCKTIMEVERIFY/OP_CHECKSEQUENCEVERIFY)
- Supports arbitrary custom script logic
-
MastOnly: Pure MAST lock (maximum privacy)
- Requires
mast_rootto exist - Must provide MAST proof when spending
- All locking conditions hidden in Merkle tree
- Requires
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_extor TLV extension mechanism - Implementation: All constraints via
ScriptHashlock type- Time locks:
- Absolute height →
ScriptHash+OP_CHECKLOCKTIMEVERIFYscript - Relative block count →
ScriptHash+OP_CHECKSEQUENCEVERIFYscript
- Absolute height →
- Custom constraints →
ScriptHash+ arbitrary Tondi VM scripts
- Time locks:
- 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
Multisiglock type - ✅ Use PubKey + MuSig2 off-chain aggregation instead
MuSig2 multisig scheme:
-
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
-
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 spendingAdvantages:
- ✅ Only reveals actually used signer combination
- ✅ Unused combinations remain hidden
- ✅ Suitable for small M, N scenarios
Verification flow (v1.0):
- Check signature count = 1 (single signature or aggregated signature)
- Use
pubkey(single key or aggregated key) to verify signature - 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_PROOFcontainingpathandleaf_lock.leaf_lockis 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
- L1 verification:
- 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):
IngotOutputinscript_public_key.script→ bytes × 10 counted in masspayload_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 KiBauth_witnessnatural 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)
- Rust enum definition:
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) andvout(u32, little-endian) - Note: Actual implementation doesn't include
amountandasset_tag(simplified design)
- Actual implementation: Only includes
lock_bytes: Borsh serializedlockenum- 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_idsignature domain - Prevents cross-network replay attacks (Mainnet signatures cannot be replayed on Testnet)
TransactionValidatordynamically configures network ID:pub struct TransactionValidator { // ... ingot_network_id: NetworkId, // Type-safe enum, selected from params.net.network_type }- Uses configured
network_idenum 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:
Lockenum (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_lockstill fixed enum + bounded TLV - Recursive calculation:
MAST_ROOTis 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 settingsr_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
ScriptHashlock type + VM scripts- Absolute time lock:
OP_CHECKLOCKTIMEVERIFYchecks block height - Relative time lock:
OP_CHECKSEQUENCEVERIFYchecks 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
- Absolute time lock:
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_CHUNKSand 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/u64uniformly use LE (little-endian)txid32B unchanged (byte order unchanged)voutfixedu32(little-endian)LENu32(little-endian)FLAGSu16(little-endian)amountu64(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)
2) Value ↔ Ingot Mixed: Transfer Ownership (Spend-to-Reveal + Payment)
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)
- Where
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)
- Unique active tip: Each
artifact_idcan only have one active tip - Spend-old-create-new: New tip must spend old active tip
- 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
txiduses wtxid (including witness) or explicit Tondi standard - Set
FINALITY_K(recommend 6~10): Indexers allow arbitration flipping withinKconfirmations, exceedingKconsidered 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
Kconsidered final, indexers must not modify arbitration results - Reorg scenarios: Indexers must perform rollback and re-arbitration for reorgs
Arbitration consistency:
- Using
blue_scoreinstead ofheightmore 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)
- Structure/type/limits: Version, field format, size limits
- Hash integrity: commit-reveal (blake3(payload) == hash_payload)
- Lock validation: Public key count, sorting, deduplication, format
- Signature validation: auth_sigs, MAST proof (if any)
- 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
- Protocol frozen: HASH160 defined as
- 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 listURI_CONTENT_HASH_TLV{alg:u8, digest:var}: Each URI includes target content hash;alg ∈ {1=blake3_256, 2=sha256};digest_lenbound to algorithm (32B)
URI_CONTENT_HASH_TLV alg values fixed; unknown values → E_UNKNOWN_TLV (fail-closed).
Consensus validation:
HASH_PAYLOADmust 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 commitsHASH_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 NFTsmusic.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 postsidentity.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_LISTafter parsing total bytes ≤ 48 KiB (reserved payload budget)
Limits:
- Maximum 256 asset items
- Single
ASSET_LISTTLV 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 byHASH_PAYLOAD - Indexers responsible for parsing and validating asset list validity
- Same
asset_idappearing 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_LISTandTLV_ASSET_MERKLE_ROOTmutually exclusive (if both present, use Merkle, but recommend choosing only one) - If both exist, indexers should use
ASSET_MERKLE_ROOTas 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:
- Indexer calculates leaf hash with
leaf_bytes:leaf_hash = blake3(leaf_bytes) - Reconstruct Merkle root along
merkle_path - Verify reconstructed root equals
ASSET_MERKLE_ROOTin payload - Match success → this leaf considered valid reveal, merged into visible asset view
- 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 creationTransfer (1): Transfer ownership, parent must existMutate (2): Modify state, parent must existAttach (3): Attach data, parent must existBurn (4): Destroy instance, explicit destruction semantics
Validation:
- Non-consensus field, but indexers should verify
op_kindmatches 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-
Mintoperations need - Points to spent previous tip (easy to verify "spend-old-create-new" intent consistency off-chain)
Validation:
- Indexers need to verify
parent_refmatches 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 byHASH_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
- Where
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_Kconfirmations (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_idappearing 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_tlvunknown types: Same principle- If both
ASSET_LISTandASSET_MERKLE_ROOTpresent: 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 existGET /artifact/{id}/assets→ Paginated list asset entries (can filterasset_type,asset_id)POST /verify-asset-proof→ Verify leaf matches root (for light wallets)
4. Wallet/SDK Tasks
Construction
- Encode
ASSET_LISTper agreed TLV (or generate asset leaves and Merkleize, writeASSET_MERKLE_ROOT) - Write
OP_KIND,PARENT_REFinto 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_PROOFto reveal only involved assets if needed) - Support RBF/CPFP package relay
- After confirmation, solidify
instance_idwithtxid||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)
- Where
- 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)
- Output type: Pay2Ingot is new output type, mixed use with Value UTXO
- Two-phase: commit (commitment) → reveal (reveal), supports large payloads
- oUTXO three disciplines: Unique tip, spend-old-create-new, lexicographic ordering
- Resource limits: Single Pay2Ingot output, actual size constrained by Tondi mass (≤ 100k), L2 recommends payload ≤ 84.5 KiB
- 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_iddirectly 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.scriptvia Borsh serialization - Borsh Vec
format: u32le(len) || T[0] || T[1] || ... - Witness divided into
payload_chunks(≤64 KiB) andauth_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