Files
chains/keyvm/vm.go
T
zeekayandHanzo Dev 6732e966d0 keyvm, mpcvm: one latch, not one per chain
Both had grown the same thing: a depth-one channel, a non-blocking send guarded
by a select with an empty default, and a WaitForEvent selecting over it and the
context. That is what vm.Latch is, so they use it. Four copies of the pattern
become none, and the comment explaining why the send is non-blocking lives with
the primitive rather than beside each caller.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-25 13:06:58 -07:00

695 lines
19 KiB
Go

// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package keyvm implements the K-Chain: an AUTH-ONLY service VM for distributed
// key management. K authorizes and coordinates key ceremonies; it never holds,
// stores, reconstructs, or transmits secret key material or threshold shares.
// See state.go for the structurally-enforced zero-secret invariant. Mutating
// operations take effect only through fee-settled consensus blocks (block.go),
// priced by a per-algorithm gas schedule (gas.go) and burned from the payer's
// on-chain balance via the native fee settlement primitive (github.com/luxfi/
// chains/fee).
package keyvm
import (
"context"
"encoding/binary"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"sync"
"time"
"github.com/gorilla/rpc/v2"
grjson "github.com/gorilla/rpc/v2/json"
"github.com/luxfi/chains/fee"
"github.com/luxfi/chains/keyvm/config"
"github.com/luxfi/database"
"github.com/luxfi/database/versiondb"
"github.com/luxfi/ids"
"github.com/luxfi/log"
nodefee "github.com/luxfi/node/vms/types/fee"
"github.com/luxfi/timer/mockable"
vmcore "github.com/luxfi/vm"
"github.com/luxfi/vm/chain"
)
const (
// Version of the K-Chain VM.
Version = "2.0.0"
// VMName is the human-readable name of the K-Chain VM.
VMName = "keyvm"
// Database namespaces. Key/ceremony records are JSON; balances live under
// the fee ledger's own namespace (github.com/luxfi/chains/fee).
KeyPrefix = "key:"
CeremonyPrefix = "ceremony:"
BlockPrefix = "block:"
)
var (
lastAcceptedKey = []byte("keyvm/last-accepted")
genesisMarker = []byte("keyvm/genesis-applied")
)
// Verify VM implements the consensus ChainVM interface.
var _ chain.ChainVM = (*VM)(nil)
// Sentinel errors. Every one denies an operation — the package fails secure.
var (
errVMShutdown = errors.New("keyvm: shutting down")
errNoPendingTxs = errors.New("keyvm: no pending transactions")
errNoParentBlock = errors.New("keyvm: no parent block")
ErrInvalidTxType = errors.New("keyvm: invalid transaction type")
ErrInvalidPayload = errors.New("keyvm: invalid transaction payload")
ErrUnknownAlgorithm = errors.New("keyvm: unsupported algorithm")
ErrInvalidThreshold = errors.New("keyvm: invalid threshold (need 0 < t <= n)")
ErrInvalidCeremony = errors.New("keyvm: invalid ceremony type")
ErrUnsignedTx = errors.New("keyvm: transaction missing payer auth/signature")
ErrPayerMismatch = errors.New("keyvm: payer does not match auth public key")
ErrBadSignature = errors.New("keyvm: invalid payer signature")
ErrKeyNotFound = errors.New("keyvm: key not found")
ErrKeyExists = errors.New("keyvm: key already exists")
ErrKeyRevoked = errors.New("keyvm: key is revoked")
ErrUnauthorized = errors.New("keyvm: payer not authorized for operation")
// ErrBadNonce rejects a replayed or out-of-order transaction. A payer's
// transactions MUST carry strictly increasing nonces starting at 1; this is
// what stops a captured signed transaction from being resubmitted to drain
// the payer's balance through repeated fee burns.
ErrBadNonce = errors.New("keyvm: bad or replayed nonce")
)
var noncePrefix = []byte("nonce:")
// VM implements the K-Chain auth-only Virtual Machine.
//
// STRUCTURAL ZERO-SECRET NOTE: every field below is either runtime plumbing or
// a cache of PUBLIC records (KeyRecord / CeremonyRecord). There is deliberately
// no key cache of private keys, no share store, and no GPU crypto session — the
// previous design's *mlkem.PrivateKey cache, KeyShare store, and accel session
// are gone. authonly_test.go proves no reachable field can hold a secret.
type VM struct {
config.Config
cancel context.CancelFunc
log log.Logger
db database.Database
versdb *versiondb.Database
state database.Database // == versdb; buffered writes commit per block
toEngine chan<- vmcore.Message
work vmcore.Latch
networkID uint32
clock mockable.Clock
// PUBLIC state caches (authoritative copy lives in the DB).
stateLock sync.RWMutex
keys map[ids.ID]*KeyRecord
keysByName map[string]ids.ID
ceremonies map[ids.ID]*CeremonyRecord
// Native fee balance ledger (debit + burn), backed by the VM's versiondb so
// settlement commits atomically with the operations it pays for.
ledger *fee.Ledger
// Admission policy (node/vms/types/fee). Orthogonal to settlement: this is
// the boot-time floor declaration Manager validates; the per-op burn is done
// through `ledger`. Kept so the chain still satisfies the zero-fee refusal.
feePolicy nodefee.Policy
// Consensus mempool + block bookkeeping.
mempoolLock sync.Mutex
mempool []*Transaction
pendingBlocks map[ids.ID]*Block
lastAccepted ids.ID
lastBlock *Block
height uint64
rpcServer *rpc.Server
shutdownLock sync.RWMutex
shuttingDown bool
}
// Genesis is the K-Chain genesis: a funding allocation (address hex -> nLUX) and
// metadata. Initial keys are registered via consensus transactions, not genesis,
// so genesis carries no key material.
type Genesis struct {
Version int `json:"version"`
Message string `json:"message"`
Timestamp int64 `json:"timestamp"`
Alloc map[string]uint64 `json:"alloc"`
}
// Initialize wires the VM: database, ledger, fee policy, caches, genesis seeding,
// and the JSON-RPC service.
func (vm *VM) Initialize(ctx context.Context, init vmcore.Init) error {
_, vm.cancel = context.WithCancel(ctx)
vm.db = init.DB
vm.versdb = versiondb.New(init.DB)
vm.state = vm.versdb
vm.toEngine = init.ToEngine
if init.Runtime != nil {
if logger, ok := init.Runtime.Log.(log.Logger); ok {
vm.log = logger
}
}
if vm.log == nil {
if init.Log != nil {
vm.log = init.Log
} else {
vm.log = log.NewNoOpLogger()
}
}
cfg, err := config.ParseConfig(init.Config)
if err != nil {
return fmt.Errorf("keyvm: parse config: %w", err)
}
vm.Config = cfg
if err := vm.Config.Validate(); err != nil {
return fmt.Errorf("keyvm: invalid config: %w", err)
}
vm.stateLock.Lock()
vm.keys = make(map[ids.ID]*KeyRecord)
vm.keysByName = make(map[string]ids.ID)
vm.ceremonies = make(map[ids.ID]*CeremonyRecord)
vm.stateLock.Unlock()
vm.pendingBlocks = make(map[ids.ID]*Block)
if init.Runtime != nil {
vm.networkID = init.Runtime.NetworkID
}
if vm.Config.NetworkID != 0 {
vm.networkID = vm.Config.NetworkID
}
vm.ledger = fee.NewLedger(vm.versdb)
vm.feePolicy = newFeePolicy(vm.networkID)
if err := nodefee.Validate(vm.feePolicy); err != nil {
return fmt.Errorf("keyvm: fee policy: %w", err)
}
genesis := &Genesis{}
if len(init.Genesis) > 0 {
if err := json.Unmarshal(init.Genesis, genesis); err != nil {
return fmt.Errorf("keyvm: parse genesis: %w", err)
}
}
// Genesis block at height 0.
genesisBlock := &Block{
id: ids.Empty,
parentID: ids.Empty,
height: 0,
timestamp: time.Unix(genesis.Timestamp, 0),
vm: vm,
}
genesisBlock.id = genesisBlock.computeID()
vm.lastAccepted = genesisBlock.id
vm.lastBlock = genesisBlock
if err := vm.seedGenesis(genesis); err != nil {
return fmt.Errorf("keyvm: seed genesis: %w", err)
}
if err := vm.loadState(); err != nil {
return fmt.Errorf("keyvm: load state: %w", err)
}
if err := vm.initHTTP(); err != nil {
return fmt.Errorf("keyvm: init http: %w", err)
}
vm.log.Info("K-Chain (auth-only) initialized",
log.String("version", Version),
log.Uint32("networkID", vm.networkID),
log.Uint64("height", vm.height),
)
return nil
}
// seedGenesis credits the funding allocation once (idempotent via a marker key).
// It is the only trusted state mutation; all later mutations go through blocks.
func (vm *VM) seedGenesis(g *Genesis) error {
applied, err := vm.versdb.Has(genesisMarker)
if err != nil {
return err
}
if applied {
return nil
}
for addrHex, amount := range g.Alloc {
acct, err := accountFromHex(addrHex)
if err != nil {
return fmt.Errorf("alloc %q: %w", addrHex, err)
}
if err := vm.ledger.Credit(acct, amount); err != nil {
return fmt.Errorf("alloc %q: %w", addrHex, err)
}
}
if err := vm.versdb.Put(genesisMarker, []byte{1}); err != nil {
return err
}
return vm.versdb.Commit()
}
// loadState rebuilds the PUBLIC caches and lastAccepted pointer from the DB.
func (vm *VM) loadState() error {
vm.stateLock.Lock()
defer vm.stateLock.Unlock()
return vm.loadStateLocked()
}
// loadStateLocked is loadState's body; callers must hold stateLock. The block
// Accept error path uses it to reload caches after a versiondb Abort while it
// still holds the lock.
func (vm *VM) loadStateLocked() error {
vm.keys = make(map[ids.ID]*KeyRecord)
vm.keysByName = make(map[string]ids.ID)
vm.ceremonies = make(map[ids.ID]*CeremonyRecord)
kit := vm.state.NewIteratorWithPrefix([]byte(KeyPrefix))
defer kit.Release()
for kit.Next() {
var rec KeyRecord
if err := json.Unmarshal(kit.Value(), &rec); err != nil {
vm.log.Warn("keyvm: skip corrupt key record", log.String("error", err.Error()))
continue
}
r := rec
vm.keys[r.ID] = &r
vm.keysByName[r.Name] = r.ID
}
if err := kit.Error(); err != nil {
return err
}
cit := vm.state.NewIteratorWithPrefix([]byte(CeremonyPrefix))
defer cit.Release()
for cit.Next() {
var c CeremonyRecord
if err := json.Unmarshal(cit.Value(), &c); err != nil {
vm.log.Warn("keyvm: skip corrupt ceremony record", log.String("error", err.Error()))
continue
}
cc := c
vm.ceremonies[cc.ID] = &cc
}
if err := cit.Error(); err != nil {
return err
}
if b, err := vm.state.Get(lastAcceptedKey); err == nil && len(b) == 32 {
copy(vm.lastAccepted[:], b)
if blk, err := vm.getBlockLocked(vm.lastAccepted); err == nil {
vm.lastBlock = blk
vm.height = blk.height
}
}
return nil
}
// ---- PUBLIC state accessors (used by tx Apply, under stateLock held by Accept) ----
func (vm *VM) getKey(id ids.ID) (*KeyRecord, bool) {
r, ok := vm.keys[id]
return r, ok
}
func (vm *VM) putKey(rec *KeyRecord) error {
data, err := json.Marshal(rec)
if err != nil {
return err
}
if err := vm.state.Put([]byte(KeyPrefix+rec.ID.String()), data); err != nil {
return err
}
vm.keys[rec.ID] = rec
vm.keysByName[rec.Name] = rec.ID
return nil
}
func (vm *VM) putCeremony(c *CeremonyRecord) error {
data, err := json.Marshal(c)
if err != nil {
return err
}
if err := vm.state.Put([]byte(CeremonyPrefix+c.ID.String()), data); err != nil {
return err
}
vm.ceremonies[c.ID] = c
return nil
}
// nonceOf returns the payer's last-used nonce (0 if the account has never
// transacted). The next valid nonce is nonceOf(payer)+1. Caller holds a lock.
func (vm *VM) nonceOf(payer fee.Account) uint64 {
key := append(append([]byte{}, noncePrefix...), payer[:]...)
b, err := vm.state.Get(key)
if err != nil || len(b) != 8 {
return 0
}
return binary.BigEndian.Uint64(b)
}
// setNonce records the payer's last-used nonce (writes to the versiondb, so it
// commits atomically with the block). Caller holds stateLock.
func (vm *VM) setNonce(payer fee.Account, n uint64) error {
key := append(append([]byte{}, noncePrefix...), payer[:]...)
var u [8]byte
binary.BigEndian.PutUint64(u[:], n)
return vm.state.Put(key, u[:])
}
// ---- Read-only public queries (RPC) ----
// KeyByID returns a copy of a key record.
func (vm *VM) KeyByID(id ids.ID) (*KeyRecord, bool) {
vm.stateLock.RLock()
defer vm.stateLock.RUnlock()
r, ok := vm.keys[id]
if !ok {
return nil, false
}
c := *r
return &c, true
}
// KeyByName returns a copy of a key record by name.
func (vm *VM) KeyByName(name string) (*KeyRecord, bool) {
vm.stateLock.RLock()
defer vm.stateLock.RUnlock()
id, ok := vm.keysByName[name]
if !ok {
return nil, false
}
r := *vm.keys[id]
return &r, true
}
// Keys returns copies of all key records.
func (vm *VM) Keys() []*KeyRecord {
vm.stateLock.RLock()
defer vm.stateLock.RUnlock()
out := make([]*KeyRecord, 0, len(vm.keys))
for _, r := range vm.keys {
c := *r
out = append(out, &c)
}
return out
}
// Ceremony returns a copy of a ceremony record.
func (vm *VM) Ceremony(id ids.ID) (*CeremonyRecord, bool) {
vm.stateLock.RLock()
defer vm.stateLock.RUnlock()
c, ok := vm.ceremonies[id]
if !ok {
return nil, false
}
cc := *c
return &cc, true
}
// Balance returns an account's spendable nLUX.
func (vm *VM) Balance(acct fee.Account) (uint64, error) {
vm.stateLock.RLock()
defer vm.stateLock.RUnlock()
return vm.ledger.Balance(acct)
}
// Burned returns cumulative burned supply in nLUX.
func (vm *VM) Burned() (uint64, error) {
vm.stateLock.RLock()
defer vm.stateLock.RUnlock()
return vm.ledger.Burned()
}
// ---- Mempool / consensus driver ----
// SubmitTx validates, authenticates, and admission-checks a transaction, then
// enqueues it and signals the engine to build a block. The fee is SETTLED later,
// in block Accept — never here. Returns the transaction ID.
func (vm *VM) SubmitTx(tx *Transaction) (ids.ID, error) {
if err := tx.SyntacticVerify(); err != nil {
return ids.Empty, err
}
if err := tx.authenticate(); err != nil {
return ids.Empty, err
}
feeAmt, err := FeeFor(tx)
if err != nil {
return ids.Empty, err
}
vm.stateLock.RLock()
if tx.Nonce <= vm.nonceOf(tx.Payer) {
vm.stateLock.RUnlock()
return ids.Empty, ErrBadNonce
}
if err := tx.checkAuth(vm, vm.clock.Time().Unix()); err != nil {
vm.stateLock.RUnlock()
return ids.Empty, err
}
err = fee.CanPay(vm.ledger, tx.Payer, feeAmt)
vm.stateLock.RUnlock()
if err != nil {
return ids.Empty, err
}
vm.mempoolLock.Lock()
vm.mempool = append(vm.mempool, tx)
vm.mempoolLock.Unlock()
vm.work.Signal()
return tx.ID(), nil
}
// WaitForEvent blocks until there are pending transactions or the VM stops.
func (vm *VM) WaitForEvent(ctx context.Context) (vmcore.Message, error) {
return vm.work.WaitForEvent(ctx)
}
// BuildBlock drains the mempool into a new block extending the last accepted
// block. The block is not yet verified or accepted — settlement happens in
// Verify/Accept.
func (vm *VM) BuildBlock(ctx context.Context) (chain.Block, error) {
vm.shutdownLock.RLock()
down := vm.shuttingDown
vm.shutdownLock.RUnlock()
if down {
return nil, errVMShutdown
}
vm.mempoolLock.Lock()
txs := vm.mempool
vm.mempool = nil
vm.mempoolLock.Unlock()
if len(txs) == 0 {
return nil, errNoPendingTxs
}
vm.stateLock.RLock()
parent := vm.lastBlock
parentID := vm.lastAccepted
vm.stateLock.RUnlock()
if parent == nil {
vm.requeue(txs)
return nil, errNoParentBlock
}
blk := &Block{
parentID: parentID,
height: parent.height + 1,
timestamp: vm.clock.Time(),
transactions: txs,
vm: vm,
}
blk.id = blk.computeID()
vm.shutdownLock.Lock()
vm.pendingBlocks[blk.id] = blk
vm.shutdownLock.Unlock()
return blk, nil
}
// requeue returns transactions to the front of the mempool (on build/reject).
func (vm *VM) requeue(txs []*Transaction) {
if len(txs) == 0 {
return
}
vm.mempoolLock.Lock()
vm.mempool = append(txs, vm.mempool...)
vm.mempoolLock.Unlock()
vm.work.Signal()
}
// dropFromMempool removes accepted transactions from the mempool by ID.
func (vm *VM) dropFromMempool(txs []*Transaction) {
if len(txs) == 0 {
return
}
accepted := make(map[ids.ID]struct{}, len(txs))
for _, tx := range txs {
accepted[tx.ID()] = struct{}{}
}
vm.mempoolLock.Lock()
kept := vm.mempool[:0]
for _, tx := range vm.mempool {
if _, ok := accepted[tx.ID()]; !ok {
kept = append(kept, tx)
}
}
vm.mempool = kept
vm.mempoolLock.Unlock()
}
// ---- Block storage ----
// ParseBlock decodes a block from bytes.
func (vm *VM) ParseBlock(ctx context.Context, blockBytes []byte) (chain.Block, error) {
return parseBlock(vm, blockBytes)
}
// GetBlock returns a block by ID.
func (vm *VM) GetBlock(ctx context.Context, blockID ids.ID) (chain.Block, error) {
vm.shutdownLock.RLock()
defer vm.shutdownLock.RUnlock()
return vm.getBlockLocked(blockID)
}
func (vm *VM) getBlockLocked(blockID ids.ID) (*Block, error) {
if vm.pendingBlocks != nil {
if blk, ok := vm.pendingBlocks[blockID]; ok {
return blk, nil
}
}
if vm.lastBlock != nil && vm.lastBlock.id == blockID {
return vm.lastBlock, nil
}
b, err := vm.state.Get(append([]byte(BlockPrefix), blockID[:]...))
if err != nil {
return nil, fmt.Errorf("keyvm: block %s: %w", blockID, err)
}
return parseBlock(vm, b)
}
// ---- ChainVM lifecycle / misc ----
func (vm *VM) SetState(ctx context.Context, state uint32) error { return nil }
func (vm *VM) SetPreference(ctx context.Context, id ids.ID) error { return nil }
func (vm *VM) LastAccepted(ctx context.Context) (ids.ID, error) {
vm.stateLock.RLock()
defer vm.stateLock.RUnlock()
return vm.lastAccepted, nil
}
func (vm *VM) GetBlockIDAtHeight(ctx context.Context, height uint64) (ids.ID, error) {
return ids.Empty, errors.New("keyvm: height index not implemented")
}
func (vm *VM) NewHTTPHandler(ctx context.Context) (http.Handler, error) {
handlers, err := vm.CreateHandlers(ctx)
if err != nil {
return nil, err
}
mux := http.NewServeMux()
for path, h := range handlers {
if path == "" {
path = "/"
}
mux.Handle(path, h)
}
return mux, nil
}
func (vm *VM) CreateHandlers(ctx context.Context) (map[string]http.Handler, error) {
return map[string]http.Handler{"/rpc": vm.rpcServer}, nil
}
func (vm *VM) CreateStaticHandlers(ctx context.Context) (map[string]http.Handler, error) {
return nil, nil
}
func (vm *VM) Version(ctx context.Context) (string, error) { return Version, nil }
func (vm *VM) Connected(ctx context.Context, nodeID ids.NodeID, ver *chain.VersionInfo) error {
return nil
}
func (vm *VM) Disconnected(ctx context.Context, nodeID ids.NodeID) error { return nil }
func (vm *VM) HealthCheck(ctx context.Context) (chain.HealthResult, error) {
vm.shutdownLock.RLock()
down := vm.shuttingDown
vm.shutdownLock.RUnlock()
vm.stateLock.RLock()
keyCount := len(vm.keys)
ceremonyCount := len(vm.ceremonies)
vm.stateLock.RUnlock()
burned, _ := vm.Burned()
return chain.HealthResult{
Healthy: !down,
Details: map[string]string{
"version": Version,
"authOnly": "true",
"keys": fmt.Sprintf("%d", keyCount),
"ceremonies": fmt.Sprintf("%d", ceremonyCount),
"height": fmt.Sprintf("%d", vm.height),
"burnedNLUX": fmt.Sprintf("%d", burned),
},
}, nil
}
// Shutdown stops the VM. There is no secret material to zero — by construction
// the VM never held any.
func (vm *VM) Shutdown(ctx context.Context) error {
vm.shutdownLock.Lock()
vm.shuttingDown = true
vm.shutdownLock.Unlock()
if vm.cancel != nil {
vm.cancel()
}
if vm.versdb != nil {
if err := vm.versdb.Close(); err != nil {
vm.log.Error("keyvm: close db", log.String("error", err.Error()))
}
}
vm.log.Info("K-Chain shut down")
return nil
}
func (vm *VM) initHTTP() error {
vm.rpcServer = rpc.NewServer()
vm.rpcServer.RegisterCodec(grjson.NewCodec(), "application/json")
vm.rpcServer.RegisterCodec(grjson.NewCodec(), "application/json;charset=UTF-8")
return vm.rpcServer.RegisterService(&Service{vm: vm}, "kchain")
}
// accountFromHex parses a 20-byte hex address into a fee.Account.
func accountFromHex(s string) (fee.Account, error) {
var a fee.Account
b, err := hex.DecodeString(strings.TrimPrefix(s, "0x"))
if err != nil {
return a, err
}
if len(b) != ids.ShortIDLen {
return a, fmt.Errorf("address must be %d bytes, got %d", ids.ShortIDLen, len(b))
}
copy(a[:], b)
return a, nil
}