MENU
IDENTITY
Identity ▼
ACCOUNT
Account ▼
STATE & PROOFS
State & Proofs ▼
CRYPTOGRAPHY
Cryptography ▼
RESOURCES

Account Model

A persistent, dust-free state layer powering Sahyadri's nonce-less Flash Transaction architecture.

What is the Account Model?

An Account Model is a blockchain state management approach where each address maintains persistent state including balance, transaction metadata, and replay protection records. Unlike UTXO models that track discrete unspent outputs, accounts track unified global state per address.

┌─────────────────────────────────────┐
│           ACCOUNT MODEL             │
├─────────────────────────────────────┤
│                                     │
│   csm1sabc123...  ─────►  Balance:   1000 CSM
│                         Nonce:     42       (legacy path)
│                         Flash IDs: 128      (flash path)
│                                     │
│   csm1sxyz789...  ─────►  Balance:   500 CSM
│                         Nonce:     15
│                         Flash IDs: 42
│                                     │
└─────────────────────────────────────┘

Every Sahyadri address is an account. When you create a wallet, you get an address with zero balance. As you receive CSM, your balance increases. As you send transactions, replay protection metadata updates. The network globally tracks every account's current state.

Key Insight: Sahyadri maintains one unified balance per address, but supports two distinct transaction paths — a legacy nonce-based path and a modern nonce-less Flash Transaction path. Both mutate the same balance; they differ only in replay protection strategy.

Why Sahyadri Uses an Account Model

Sahyadri chose the Account Model over UTXO for three practical reasons that align with its identity-focused architecture.

Native Compatibility with Identity

The Crest Model binds DIDs to controller addresses. If addresses were ephemeral UTXO-style pointers without persistent state, identity controllership would require complex tracking layers. Accounts give every address permanent existence, making them natural anchors for Crest documents.

Simpler Developer Experience

Developers send transactions from one address to another without managing UTXO selection, change outputs, or dust thresholds. This lowers the barrier for wallet builders, SDK integrations, and application developers.

Efficient Storage

An account needs one database entry regardless of how many incoming payments it received. A UTXO model grows with transaction count, requiring users to consolidate inputs periodically. For a network targeting mainstream adoption, accounts reduce operational complexity.

Account Structure

Each account in Sahyadri's state contains fields that together define its complete state. The structure has evolved to support both transaction paths simultaneously.

pub struct AccountState {
    // ─── Balance (SHARED by both paths) ───
    // CSM token amount in smallest unit (micro-CSM)
    // 1 CSM = 100,000,000 kana
    pub balance: u64,

    // ─── Legacy Nonce Path ───
    // Sequential counter, starts at 0
    // Increments on every legacy tx from this account
    pub nonce: u64,

    // Hash of the last legacy tx applied — used for reorg refund safety
    pub last_applied_tx_id: [u8; 32],

    // ─── Flash Transaction Path ───
    // Bounded replay-protection window. Each entry records:
    //   (flash_id, expiry_daa_score, source_block_hash)
    // Auto-pruned after FLASH_PRUNE_WINDOW blocks
    pub recent_flashes: Vec<FlashEntry>,
}

pub struct FlashEntry {
    pub flash_id: Hash,          // Unique tx identifier
    pub expiry_daa_score: u64,   // Expiry window endpoint
    pub block_hash: Hash,        // Source block (per-block reorg unwind)
}

Address Format

Sahyadri addresses use bech32 encoding with human-readable prefix csm1s. The address derives from the Dilithium3 public key through SHA3-256 hashing, taking the first 20 bytes, followed by bech32 encoding with an error-checking checksum.

// Rust Reference
pub fn address_from_pubkey(pubkey: &DilithiumPublicKey) -> Address {
    let hash = sha3_256(&pubkey.to_bytes());
    let hash20 = &hash[..20];
    bech32_encode("csm1s", hash20)
}

// Output format:
// csm1s9vd6l8yu6cegjs922qm8klrn7jj3d7xgv6jdmp7v9

The Evolution: From Nonce to Flash

Traditional account-model blockchains enforce a sequential nonce for every transaction. Each transaction must reference the exact counter of the previous one, forcing transactions from the same account to be processed strictly in order. This introduces bottlenecks, stuck transactions, and race conditions in multi-device wallets.

Sahyadri removes this dependency entirely. Every Flash Transaction carries its own unique identifier, an expiry window, and a cryptographically random salt — allowing multiple transactions from the same account to be processed in parallel, in the same block, without any ordering constraint.

AspectNonce Path (Legacy)Flash Path (Sahyadri)
Replay ProtectionSequential counterExpiry + flash_id
Parallel Txs (same sender)No — strictly sequentialYes — order-independent
Multi-Device WalletsRace conditionsNo race
Offline SigningNeeds chain query100-block window
Stuck Tx RecoveryNonce gap blocks allIndependent
Reorg HandlingNonce rollbackPer-block-hash unwind

Flash Transaction Structure

A Flash Transaction is a compact, self-contained data structure. Every field is fixed-width or length-prefixed to allow zero-copy decoding in Rust.

pub struct FlashTransaction {
    pub version: u16,             // Protocol version
    pub pubkey: Vec<u8>,          // 1952 bytes (ML-DSA-65)
    pub recipient: Vec<u8>,       // 20-byte address hash
    pub amount: u64,              // kana (10^8 kana = 1 CSM)
    pub fee: u64,                 // kana (min 1000)
    pub expiry_daa_score: u64,    // current + 90 buffer
    pub salt: [u8; 16],           // CSPRNG random
    pub signature: Vec<u8>,       // 3309 bytes (ML-DSA-65)
}
Expiry Buffer: The 90-block buffer (not exactly 100) accounts for network round-trip and block-time between RPC fetch and block inclusion. This prevents borderline transactions from expiring before they are mined.

One State, Two Paths

A critical architectural principle: Flash Transactions do not create a parallel state. They are a distinct mutation path on the same AccountState, coexisting with the nonce path on the same balance.

┌─────────────────────────────────────────────────────┐
│              SINGLE ACCOUNT STATE                   │
│                                                     │
│  csm1sXXX:  balance: 1000 CSM                       │
│             nonce: 42                                │
│             recent_flashes: [...]                    │
└─────────────────────────────────────────────────────┘
              ▲                          ▲
              │                          │
      ┌───────┴────────┐         ┌───────┴────────┐
      │  Nonce Path    │         │  Flash Path    │
      │                │         │                │
      │  sender.balance -= amount                        │
      │  recipient.balance += amount                     │
      │  sender.nonce += 1                               │
      └────────────────┘         └────────────────┘
                                  │
                                  │  sender.balance -= amount
                                  │  recipient.balance += amount
                                  │  sender.recent_flashes.push(...)
                                  └────────────────
Why not a separate Flash state? A parallel state would enable double-spending (same address, two balances), break interop between paths, and make reorg unwinding exponentially complex. The unified balance preserves the fundamental blockchain invariant: one address, one balance.

Transaction Flow

Legacy Nonce Path

┌────────┐   ┌──────────┐   ┌────────┐   ┌────────┐
│ Wallet │──▶│ Mempool  │──▶│ Block  │──▶│ State  │
│ / SDK  │   │          │   │Builder │   │ Update │
└────────┘   └──────────┘   └────────┘   └────────┘
     │              │              │             │
     ▼              ▼              ▼             ▼
 Build Tx       Validate       Order by      Apply
 Sign with      Signature      Priority      Balance
 Private Key    Nonce Match    Fees          Change
                Balance OK                   Nonce += 1
                                             last_applied_tx_id = tx.id

Flash Transaction Path

┌────────┐   ┌──────────┐   ┌────────┐   ┌────────┐
│ Wallet │──▶│ Mempool  │──▶│ Block  │──▶│ State  │
│ / SDK  │   │          │   │Builder │   │ Update │
└────────┘   └──────────┘   └────────┘   └────────┘
     │              │              │             │
     ▼              ▼              ▼             ▼
 Build Tx       Validate       Batch by      Apply
 Sign with      Expiry         fee           Balance
 Private Key    Replay Check   density       Change
                Signature                    recent_flashes.push(
                Balance OK                     flash_id, expiry,
                                               block_hash)

The nonce path requires the wallet to fetch the current nonce from chain state before building a transaction. The flash pathrequires the wallet to fetch only the current DAA score for expiry calculation — no sequential dependency.

Validation Order

Each Flash Transaction is validated in a strict, cheapest-first order to minimize wasted computation on invalid inputs.

  1. Expiry Check. Reject if current_daa > expiry_daa_score. Single integer comparison — cheapest possible fail-fast.
  2. Fee Minimum. Reject if fee < 1000 kana. Prevents spam without competing with legitimate traffic.
  3. Replay Detection. Check derived flash_id against sender's bounded recent_flashes set. O(n) lookup where n is bounded by the pruning window.
  4. Signature Verification. Verify ML-DSA-65 signature against computed sighash. Most CPU-intensive step — runs on dedicated parallel thread pool.
  5. Balance Check. Ensure sender has sufficient balance to cover amount + fee. In a batch, balances are aggregated per-sender before this check to prevent double-spend within a block.

Replay Protection: Nonce vs Flash

Nonce Path

Nonce Timeline:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━▶ Time

Nonce=0  Nonce=1  Nonce=2  Nonce=3  Nonce=4
  │        │        │        │        │
  ▼        ▼        ▼        ▼        ▼
[Tx#1]   [Tx#2]   [Tx#3]   [Tx#4]   [Tx#5]

✓ Valid: Tx with nonce=3 when stored nonce=3
✗ Reject: Tx with nonce=2 when stored nonce=3 (REPLAY)
✗ Reject: Tx with nonce=5 when stored nonce=3 (GAP)

Flash Path

Flash ID Timeline (Bounded Window: 100 blocks):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━▶ Time

flash_id=a3f   flash_id=b7c   flash_id=d9e
  │              │              │
  ▼              ▼              ▼
[Tx#1]         [Tx#2]         [Tx#3]
expiry=100     expiry=200     expiry=300
  │              │              │
  ▼              ▼              ▼
Stored in      Stored in      Stored in
recent_flashes recent_flashes recent_flashes

✓ Valid:   flash_id not in recent_flashes AND current_daa <= expiry
✗ Replay:  flash_id already in recent_flashes
✗ Expired: current_daa > expiry_daa_score

The flash path's replay protection is bounded, self-pruning, and order-independent. No global monotonic counter, no chain-wide replay set, no unbounded per-account index.

Reorg Safety

Sahyadri's consensus is a DAG — blocks can be reordered, disconnected, or re-merged as the blue set evolves. Every recent_flashes entry stores the hash of the block that produced it.

Block B reorg hua:
  │
  ▼
For each flash tx in Block B:
  │
  ├── sender.recent_flashes.retain(|e| e.block_hash != B.hash)
  │     ← Only removes entries from THIS block
  │
  ├── sender.balance += (amount + fee)   ← Refund sender
  │
  └── recipient.balance -= amount         ← Reverse credit

✓ Parallel txs from other blocks remain untouched
✓ No blind removal — cross-block parallelism preserved
✓ No double-refund possible
Legacy Path Note: The nonce path additionally checks last_applied_tx_id before refunding. Flash path uses block_hash matching — cleaner because flash entries are per-block tagged from the start.

State Pruning

Replay-tracking state does not grow unbounded. Each flash_id carries the expiry value under which it was accepted. On every block commit, the protocol prunes entries whose expiry has fallen below the current DAA score minus a safety buffer.

pub const FLASH_EXPIRY_BLOCKS: u64 = 100;
pub const FLASH_PRUNE_BUFFER: u64 = 50;
pub const FLASH_PRUNE_WINDOW: u64 = 150;

// Called after each block commit:
pub fn prune_recent_flashes(&mut self, current_daa_score: u64) {
    let cutoff = current_daa_score.saturating_sub(FLASH_PRUNE_BUFFER);
    self.recent_flashes.retain(|e| e.expiry_daa_score > cutoff);
}

With a 100-block expiry and a 50-block safety buffer, the maximum replay window is 150 blocks. After 100 blocks the transaction is invalid anyway; after 150 blocks its replay record is discarded. Storage per account remains bounded by recent activity only.

Batch Processing & Double-Spend Prevention

A single block can contain Flash Transactions from many different senders, processed in parallel. The critical property: same-sender transactions within a block are aggregated before balance validation.

pub fn validate_flash_batch(
    &self,
    txs: &[FlashTransaction],
    current_daa_score: u64,
) -> TxResult<()> {
    // 1. Aggregate sender debits
    let mut sender_debits: HashMap<ScriptPublicKey, u64> = HashMap::new();
    for tx in txs {
        let spk = pubkey_to_p2pkh_spk(&tx.pubkey);
        *sender_debits.entry(spk).or_insert(0) += tx.amount + tx.fee;
    }

    // 2. Cumulative balance check (all-or-nothing)
    for (spk, total_debit) in sender_debits.iter() {
        let account = self.account_store.get(spk)?;
        if account.balance < *total_debit {
            return Err(TxRuleError::SpendTooHigh(*total_debit, account.balance));
        }
    }

    // 3. Duplicate flash_id within batch
    // 4. Per-tx validation (expiry, replay, signature)
    ...
}

Transaction Fees

Fees associated with CSM transactions and applicable protocol operations. Every transaction pays a base fee plus optional priority fee for faster inclusion during congestion periods.

ComponentCostPurpose
Base Fee1000 kana (0.00001 CSM)Network resource compensation
Priority FeeVariableFaster inclusion during congestion
Crest OperationsFREEIdentity is a basic right

Account Model vs UTXO

AspectAccount Model (Sahyadri)UTXO Model (Bitcoin)
StateGlobal per-addressUnspent outputs
AddressPermanent existenceEphemeral (new per tx)
PrivacyLower (balance visible)Higher (change addresses)
ComplexitySimpler for developersUTXO management required
Identity FitNatural anchor for CrestRequires extra layer
StorageConstant per accountGrows with tx count
Parallel TxsYes (Flash path)Limited by UTXO graph

Account ↔ Crest Relationship

┌─────────────────────────────────────────────┐
│              SAHYADRI PROTOCOL              │
│                                             │
│  ┌───────────────┐    ┌───────────────┐    │
│  │ ACCOUNT MODEL │◄──►│  CREST MODEL  │    │
│  │               │    │               │    │
│  │ • Balance     │    │ • DID         │    │
│  │ • Nonce       │    │ • Controller  │    │
│  │ • Transfers   │    │ • Keys        │    │
│  │ • Flash State │    │ • Services    │    │
│  │ • Fees        │    │               │    │
│  └───────┬───────┘    └───────┬───────┘    │
│          │                    │             │
│          │    Controller      │             │
│          │◄═══════════════════┘             │
│          │    Address links them            │
│          ▼                                  │
│  ┌──────────────────────────────────┐       │
│  │         RocksDB Storage          │       │
│  │                                  │       │
│  │  accounts-store/ || addr → State │       │
│  │  dids-store/     || did  → Crest │       │
│  │  dids-addr-index/|| addr → DID   │       │
│  └──────────────────────────────────┘       │
└─────────────────────────────────────────────┘

The two models connect through the controller address field. Every Crest stores its controller as a Sahyadri address. That address exists in the Account Model with its own balance. When a Crest operation requires signing, the private key controlling that account address produces the Dilithium3 signature.

This separation means you can have funds without identity (pure EOA sending CSM), identity without funds (free Crest creation), or both linked through the same address. The reverse index enables looking up which DID belongs to any given address, bridging the two models for applications that need complete entity information.

Implementation / Rust Reference

The following code reflects the actual implementation within the Sahyadri consensus layer. These are the real structures and functions currently deployed.

// ════════════════════════════════════════════════════════
// FILE: consensus/src/model/stores/account_store.rs
// Unified Account State (balance + nonce + flash tracking)
// ════════════════════════════════════════════════════════

/// A single flash-tx entry in an account's recent history.
/// Block_hash tracking enables per-block reorg unwind.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Default)]
pub struct FlashEntry {
    pub flash_id: Hash,
    pub expiry_daa_score: u64,
    pub block_hash: Hash,
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Default)]
pub struct AccountState {
    pub balance: u64,
    pub recent_flashes: Vec<FlashEntry>,
    pub nonce: u64,
    pub last_applied_tx_id: [u8; 32],
}

// ════════════════════════════════════════════════════════
// FILE: consensus/core/src/tx.rs
// FlashTransaction struct + sighash + flash_id
// ════════════════════════════════════════════════════════

#[derive(Clone, Debug, Serialize, Deserialize,
         BorshSerialize, BorshDeserialize, PartialEq, Eq)]
pub struct FlashTransaction {
    pub version: u16,
    pub pubkey: Vec<u8>,
    pub recipient: Vec<u8>,
    pub amount: u64,
    pub fee: u64,
    pub expiry_daa_score: u64,
    pub salt: [u8; 16],
    pub signature: Vec<u8>,
}

impl FlashTransaction {
    /// Deterministic unique ID — used for replay detection
    pub fn flash_id(&self) -> Hash {
        use sha3::{Digest, Sha3_256};
        let mut h = Sha3_256::new();
        h.update(b"SAHYADRI_FLASH_ID_V1");
        h.update(self.version.to_le_bytes());
        h.update(&self.pubkey);
        h.update(&self.recipient);
        h.update(self.amount.to_le_bytes());
        h.update(self.fee.to_le_bytes());
        h.update(self.expiry_daa_score.to_le_bytes());
        h.update(&self.salt);
        Hash::from_bytes(h.finalize().into())
    }
}

Design Principles

One State, Two Paths

A single AccountState per address. Both nonce-based and Flash transactions mutate the same balance. Flash transactions do not create a parallel state — they are a distinct mutation path with their own replay protection, coexisting with the nonce path on the same balance.

Bounded State

Replay protection is local, temporary, and self-pruning. No global monotonically-growing counter, no chain-wide replay set, no unbounded per-account index. The cost of accepting a transaction is bounded and predictable.

Reorg-Correct

Every applied flash transaction is tagged with its source block. Reorgs unwind only the affected entries. Cross-block parallelism is preserved through reorgs without any replay or state corruption.

Quantum-Resistant

Every transaction is signed with ML-DSA-65 (CRYSTALS-Dilithium3), a NIST-standardized post-quantum signature scheme. The security of the transaction layer does not depend on the eventual hardness of elliptic-curve problems.

Backward Compatible

The legacy nonce path remains fully functional. CEXes, hot wallets, and existing integrations continue to work without modification. Migration to Flash is opt-in via feature flag.

Summary

Sahyadri's Account Model is not a copy of Ethereum's. It is a unified state layer with dual transaction paths — a legacy nonce path for backward compatibility, and a nonce-less Flash Transaction path for parallel execution, multi-device ownership, and quantum-resistant finality.

Combined with Sahyadri's DAG consensus, post-quantum signature scheme, and native identity layer, it forms the state substrate on which Web5 applications can be built without inherited bottlenecks from the previous generation of blockchains.