// Package blockchain — native (non-WASM) contract infrastructure. // // System contracts like `username_registry` are latency-sensitive and must // never hang the chain. Running them through the WASM VM means: // // - 100× the CPU cost of equivalent Go code; // - a bug in gas metering / opcode instrumentation can freeze AddBlock // indefinitely (see the hangs that motivated this rewrite); // - every node needs an identical wazero build — extra supply-chain risk. // // Native contracts are written as plain Go code against a narrow interface // (NativeContract below). They share the same contract_id space, ABI, and // explorer views as WASM contracts, so clients can't tell them apart — the // dispatcher in applyTx just routes the call to Go instead of wazero when // it sees a native contract_id. // // Authorship notes: // - A native contract has full, direct access to the current BadgerDB txn // and chain helpers via NativeContext. It MUST only read/write keys // prefixed with `cstate::` or `clog::…` — same // as WASM contracts see. This keeps on-chain state cleanly segregated // so one day we can migrate a native contract back to WASM (or vice // versa) without a storage migration. // - A native contract MUST return deterministic errors. The dispatcher // treats any returned error as `ErrTxFailed`-wrapped — fees stay // debited, but state changes roll back with the enclosing Badger txn. package blockchain import ( "encoding/json" "errors" "fmt" badger "github.com/dgraph-io/badger/v4" ) // NativeContract is the Go-side counterpart of a WASM smart contract. // // Implementors are expected to be stateless (all state lives in BadgerDB // under cstate::…). An instance is created once per chain and // reused across all calls. type NativeContract interface { // ID returns the deterministic contract ID used in CALL_CONTRACT txs. // Must be stable across node restarts and identical on every node. ID() string // ABI returns a JSON document describing the contract's methods. // Identical shape to the WASM contracts' *_abi.json files so the // well-known endpoint and explorer can discover it uniformly. ABI() string // Call dispatches a method invocation. Returns the gas it wants to // charge (will be multiplied by the current gas price). Returning an // error aborts the tx; returning (0, nil) means free success. Call(ctx *NativeContext, method string, argsJSON []byte) (gasUsed uint64, err error) } // NativeContext hands a native contract the minimum it needs to run, // without exposing the full Chain type (which would tempt contracts to // touch state they shouldn't). type NativeContext struct { Txn *badger.Txn ContractID string Caller string // hex Ed25519 pubkey of the tx sender TxID string BlockHeight uint64 // TxAmount is tx.Amount — the payment the caller attached to this call // in µT. It is NOT auto-debited from the caller; the contract decides // whether to collect it (via ctx.Debit), refund, or ignore. Exposing // payment via tx.Amount (instead of an implicit debit inside the // contract) makes contract costs visible in the explorer — a user can // see exactly what a call charges by reading the tx envelope. TxAmount uint64 // chain is kept unexported; contract code uses the helper methods below // rather than reaching into Chain directly. chain *Chain } // Balance returns the balance of the given pubkey in µT. func (ctx *NativeContext) Balance(pubHex string) uint64 { var bal uint64 item, err := ctx.Txn.Get([]byte(prefixBalance + pubHex)) if errors.Is(err, badger.ErrKeyNotFound) { return 0 } if err != nil { return 0 } _ = item.Value(func(val []byte) error { return unmarshalUint64(val, &bal) }) return bal } // Debit removes amt µT from pub's balance, or returns an error if insufficient. func (ctx *NativeContext) Debit(pub string, amt uint64) error { return ctx.chain.debitBalance(ctx.Txn, pub, amt) } // Credit adds amt µT to pub's balance. func (ctx *NativeContext) Credit(pub string, amt uint64) error { return ctx.chain.creditBalance(ctx.Txn, pub, amt) } // Get reads a contract-scoped state value. Returns nil if not set. func (ctx *NativeContext) Get(key string) ([]byte, error) { item, err := ctx.Txn.Get([]byte(prefixContractState + ctx.ContractID + ":" + key)) if errors.Is(err, badger.ErrKeyNotFound) { return nil, nil } if err != nil { return nil, err } var out []byte err = item.Value(func(v []byte) error { out = append([]byte(nil), v...) return nil }) return out, err } // Set writes a contract-scoped state value. func (ctx *NativeContext) Set(key string, value []byte) error { return ctx.Txn.Set([]byte(prefixContractState+ctx.ContractID+":"+key), value) } // Delete removes a contract-scoped state value. func (ctx *NativeContext) Delete(key string) error { return ctx.Txn.Delete([]byte(prefixContractState + ctx.ContractID + ":" + key)) } // Log emits a contract log line for the explorer. Uses the same storage as // WASM contracts' env.log() so the explorer renders them identically. func (ctx *NativeContext) Log(msg string) error { return ctx.chain.writeContractLog(ctx.Txn, ctx.ContractID, ctx.BlockHeight, ctx.TxID, msg) } // ─── Native contract registry ──────────────────────────────────────────────── // Native contracts are registered into the chain once during chain setup // (typically right after `NewChain`). Lookups happen on every CALL_CONTRACT // and DEPLOY_CONTRACT — they're hot path, so the registry is a plain map // guarded by a RW mutex. // RegisterNative associates a NativeContract with its ID on this chain. // Panics if two contracts share an ID (clear programmer error). // Must be called before AddBlock begins processing user transactions. // // Uses a DEDICATED mutex (c.nativeMu) rather than c.mu, because // lookupNative is called from inside applyTx which runs under c.mu.Lock(). // sync.RWMutex is non-reentrant — reusing c.mu would deadlock. func (c *Chain) RegisterNative(nc NativeContract) { c.nativeMu.Lock() defer c.nativeMu.Unlock() if c.native == nil { c.native = make(map[string]NativeContract) } if _, exists := c.native[nc.ID()]; exists { panic(fmt.Sprintf("native contract %s registered twice", nc.ID())) } c.native[nc.ID()] = nc } // lookupNative returns the registered native contract for id, or nil. // Hot path — called on every CALL_CONTRACT from applyTx. Safe to call while // c.mu is held because we use a separate RWMutex here. func (c *Chain) lookupNative(id string) NativeContract { c.nativeMu.RLock() defer c.nativeMu.RUnlock() return c.native[id] } // NativeContracts returns a snapshot of every native contract registered on // this chain. Used by the well-known endpoint so clients auto-discover // system services without the user having to paste contract IDs. func (c *Chain) NativeContracts() []NativeContract { c.nativeMu.RLock() defer c.nativeMu.RUnlock() out := make([]NativeContract, 0, len(c.native)) for _, nc := range c.native { out = append(out, nc) } return out } // writeContractLog is the shared log emitter for both WASM and native // contracts. Keeping it here (on Chain) means we can change the log key // layout in one place. func (c *Chain) writeContractLog(txn *badger.Txn, contractID string, blockHeight uint64, txID, msg string) error { // Best-effort: match the existing WASM log format so the explorer's // renderer doesn't need to branch. seq := c.nextContractLogSeq(txn, contractID, blockHeight) entry := ContractLogEntry{ ContractID: contractID, BlockHeight: blockHeight, TxID: txID, Seq: int(seq), Message: msg, } val, err := json.Marshal(entry) if err != nil { return err } key := fmt.Sprintf("%s%s:%020d:%05d", prefixContractLog, contractID, blockHeight, seq) return txn.Set([]byte(key), val) } // nextContractLogSeq returns the next sequence number for a (contract,block) // pair by counting existing entries under the prefix. func (c *Chain) nextContractLogSeq(txn *badger.Txn, contractID string, blockHeight uint64) uint32 { prefix := []byte(fmt.Sprintf("%s%s:%020d:", prefixContractLog, contractID, blockHeight)) opts := badger.DefaultIteratorOptions opts.PrefetchValues = false opts.Prefix = prefix it := txn.NewIterator(opts) defer it.Close() var count uint32 for it.Rewind(); it.Valid(); it.Next() { count++ } return count } // ─── Small helpers used by native contracts ────────────────────────────────── // Uint64 is a tiny helper for reading a uint64 stored as 8 big-endian bytes. // (We deliberately don't use JSON for hot state keys.) func unmarshalUint64(b []byte, dst *uint64) error { if len(b) != 8 { return fmt.Errorf("not a uint64") } *dst = uint64(b[0])<<56 | uint64(b[1])<<48 | uint64(b[2])<<40 | uint64(b[3])<<32 | uint64(b[4])<<24 | uint64(b[5])<<16 | uint64(b[6])<<8 | uint64(b[7]) return nil }