Files
zeekay 17734274d2 keyvm: one predicate decides a transaction, so no layer can wedge another
Admission accepted any nonce above the committed one while Verify demanded
exactly committed+1. That split was a free, permanent halt: one gapped
transaction from any funded account entered the mempool, was drained into every
block alongside honest work, failed Verify, was requeued by Reject, and the next
build produced the same doomed block forever. Admission, assembly and consensus
now decide through one checkTx, and assembly drops what it cannot build, so a
block this node builds is a block this node verifies.

Block position is now checked before block content. The parent must exist (a
block claiming height 0 skipped the check outright), the height must be
parent+1, and the timestamp must not rewind or run past a skew ceiling floored
at the parent. Block time is the clock every authorization decision is made
against, so an unbounded timestamp let a proposer revive a permit the chain had
already retired.

Also: payload bytes are priced (a megabyte of commitments rode on chain for the
flat fee of an empty one); blocks are canonical on the wire, as transactions
already were; the processing-block index lives under one mutex instead of two,
which the race detector answers with a fatal throw; abandoned blocks are
released rather than retained forever; in-memory state advances after the commit
rather than before it; the height index is real, so GetBlockIDAtHeight answers;
and checkAuth resolves the record Apply works from instead of Apply resolving
and re-authorizing it a second time.

Deleted: keyvm/config, 25 knobs of which one was ever read, including a
ShareCacheSize for a VM whose defining invariant is that it never holds a share;
the duplicate networkID the factory set and Initialize silently discarded; the
duplicate state handle beside versdb; and a path-normalizing branch for a path
that cannot occur.

Coverage 58.9% -> 97.9%, with 30 guards mutation-checked.
2026-08-28 08:37:11 -07:00

131 lines
5.4 KiB
Go

// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package keyvm
import (
"github.com/luxfi/chains/fee"
"github.com/luxfi/ids"
)
// ZERO-SECRET INVARIANT (the K-Chain's reason to exist).
//
// K-Chain is an AUTHORIZATION and COORDINATION plane for distributed keys. It
// can NEVER hold, store, reconstruct, or transmit secret key material or
// threshold shares — even a fully compromised K validator must yield NOTHING
// usable to reconstruct a key. The shares live OFF-K, on the MPC/threshold
// committee. K records only PUBLIC artifacts and policy, and it triggers
// (authorizes + coordinates) ceremonies the committee actually performs.
//
// This invariant is enforced STRUCTURALLY, not by discipline: the only types K
// persists are the three below, and every field of them is public by
// construction (public keys, public VSS commitments, public committee node
// IDs, public policy, public addresses, public timestamps). There is no field
// whose type or name can carry a private key or a share. authonly_test.go
// proves this two ways: (1) a reflection walk of these types rejects any
// secret-typed or secret-named field, and (2) a scan of the package source
// rejects any call that could generate, parse, reconstruct, or sign with a
// secret. Together they make holding-a-secret and reconstructing-a-secret
// structurally impossible in this package, not merely absent from it.
// Key lifecycle states.
const (
StatusActive = "active"
StatusRevoked = "revoked"
)
// Ceremony types K may authorize on the committee. K authorizes and records the
// PUBLIC result; it performs none of the secret-bearing computation.
const (
CeremonyDKG = "dkg" // distributed key generation (result: public key + commitments)
CeremonySign = "sign" // threshold signing (result: aggregate signature)
CeremonyReshare = "reshare" // proactive resharing (result: new commitments/committee)
)
// Ceremony lifecycle states.
const (
CeremonyAuthorized = "authorized" // K authorized it; committee may proceed
CeremonyFulfilled = "fulfilled" // committee returned a public result
CeremonyRejected = "rejected" // policy denied or committee failed
)
// AuthPolicy is the access-control rule set for a key: who may administer it and
// who may invoke ceremonies on it, under what temporal/rate conditions. It is
// pure PUBLIC policy — addresses and limits, never secrets.
type AuthPolicy struct {
// Admins may change this policy and revoke the key.
Admins []fee.Account `json:"admins"`
// Authorized may request ceremonies (sign/dkg/reshare) on this key.
Authorized []fee.Account `json:"authorized"`
// MaxOpsPerEpoch bounds authorized ceremonies per epoch (0 = unlimited).
MaxOpsPerEpoch uint32 `json:"maxOpsPerEpoch"`
// RequireQuorum is the minimum committee acknowledgement K records before a
// ceremony is considered fulfillable (informational on K; enforced by the
// committee). 0 means "use the key's threshold".
RequireQuorum uint32 `json:"requireQuorum"`
// ExpiresAt is a unix timestamp after which no ceremony is authorized; 0 = no expiry.
ExpiresAt int64 `json:"expiresAt"`
}
// MayAdmin reports whether a may administer (set policy / revoke) the key.
func (p AuthPolicy) MayAdmin(a fee.Account) bool {
for _, adm := range p.Admins {
if adm == a {
return true
}
}
return false
}
// MayInvoke reports whether a may request a ceremony at time now. An expired
// policy authorizes no one (fail closed); admins may always invoke.
func (p AuthPolicy) MayInvoke(a fee.Account, now int64) bool {
if p.ExpiresAt != 0 && now >= p.ExpiresAt {
return false
}
if p.MayAdmin(a) {
return true
}
for _, auth := range p.Authorized {
if auth == a {
return true
}
}
return false
}
// KeyRecord is the ONLY per-key state K stores. Every field is PUBLIC. There is
// deliberately no PrivateKey, no Share, no Seed — K holds the public key and the
// VSS COMMITMENTS (public curve points that let anyone verify a share without
// learning it), the committee that holds the shares off-K, and the policy.
type KeyRecord struct {
ID ids.ID `json:"id"`
Name string `json:"name"`
Algorithm string `json:"algorithm"`
PublicKey []byte `json:"publicKey"` // PUBLIC key bytes
Threshold uint32 `json:"threshold"` // t in t-of-n
TotalShares uint32 `json:"totalShares"` // n in t-of-n
Commitments [][]byte `json:"commitments"` // PUBLIC VSS commitments, NOT shares
Committee []ids.NodeID `json:"committee"` // off-K share holders (public IDs)
Policy AuthPolicy `json:"policy"`
Owner fee.Account `json:"owner"`
Status string `json:"status"`
CreatedAt int64 `json:"createdAt"`
UpdatedAt int64 `json:"updatedAt"`
}
// CeremonyRecord is K's record of an authorized DKG/sign/reshare ceremony. K
// stores the authorization decision and the ceremony's PUBLIC inputs/outputs
// (the digest to be signed; the resulting aggregate signature or public key).
// It never stores shares or partial secrets.
type CeremonyRecord struct {
ID ids.ID `json:"id"`
KeyID ids.ID `json:"keyId"`
Type string `json:"type"`
Requester fee.Account `json:"requester"`
Message []byte `json:"message"` // PUBLIC digest to sign (sign ceremonies)
Result []byte `json:"result"` // PUBLIC result (signature / new public key)
Status string `json:"status"`
CreatedAt int64 `json:"createdAt"`
}