mirror of
https://github.com/luxfi/chains.git
synced 2026-07-27 03:39:41 +00:00
707 lines
19 KiB
Go
707 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
|
|
notify chan struct{}
|
|
|
|
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
|
|
vm.notify = make(chan struct{}, 1)
|
|
|
|
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()
|
|
|
|
select {
|
|
case vm.notify <- struct{}{}:
|
|
default:
|
|
}
|
|
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) {
|
|
select {
|
|
case <-ctx.Done():
|
|
return vmcore.Message{}, ctx.Err()
|
|
case <-vm.notify:
|
|
return vmcore.Message{Type: vmcore.PendingTxs}, nil
|
|
}
|
|
}
|
|
|
|
// 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()
|
|
select {
|
|
case vm.notify <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|