mirror of
https://github.com/luxfi/node.git
synced 2026-07-27 03:39:39 +00:00
feat(proposervm): configurable window duration (fast local cadence) + strict-PQ classical-proposer refusal
Two changes, both node-local: 1. CADENCE — proposer.WindowDuration (the proposer-slot spacing, a validator's min build delay = slot index x WindowDuration) was a hardcoded 5s const tuned for mainnet-scale validator sets, flooring small/local block cadence at 5s per slot. It is now a startup-configurable var (default 5s, unchanged for mainnet) set via the new --proposervm-window-duration flag → node.Config → ManagerConfig → proposervm.Config → proposer.SetWindowDuration at VM init. Read by both the windower delay math and TimeToSlot, so they stay consistent. Measured on a 5-node strict-PQ net: 5s→1s took sustained C-Chain from 44 TPS / 14s-per-block to 104 TPS / 3.8s-per-block, still 5/5 byte-identical finality. 2. SECURITY (cryptographer MEDIUM-1) — postForkCommonComponents.Verify now refuses a block carrying a CLASSICAL secp256k1 proposer identity when the chain is strict-PQ (StakingMLDSASigner set), UNCONDITIONALLY (before the consensusState==Ready gate, so it also holds during bootstrap/state-sync). Fills the documented-but-unwired 'proposer' SchemeGate site: the downgrade defense is now an explicit fail-closed in-perimeter gate, not merely emergent from 20-byte NodeID collision-resistance + upstream enforcement. Adds SignedBlock.HasClassicalProposer(). Mirrors contract.RefuseUnderStrictPQ. Pins consensus v1.36.7 (consume-on-error build loop — kills the non-leader BuildBlock spin). Block + proposervm VM tests green. Co-authored-by: Hanzo Dev <dev@hanzo.ai>
This commit is contained in:
+14
-11
@@ -369,7 +369,10 @@ type ManagerConfig struct {
|
||||
// Proposer() matches the ML-DSA-keyed validator set. Nil ⇒ classical TLS-leaf.
|
||||
StakingMLDSASigner *mldsa.PrivateKey
|
||||
StakingMLDSAPub []byte
|
||||
StakingBLSKey bls.Signer
|
||||
// ProposerWindowDuration overrides the proposervm proposer-slot spacing (0 ⇒
|
||||
// the 5s mainnet default). Small local/dev nets set it low for fast cadence.
|
||||
ProposerWindowDuration time.Duration
|
||||
StakingBLSKey bls.Signer
|
||||
TracingEnabled bool
|
||||
// Must not be used unless [TracingEnabled] is true as this may be nil.
|
||||
Tracer trace.Tracer
|
||||
@@ -1287,15 +1290,16 @@ func (m *manager) buildChain(chainParams ChainParameters, sb nets.Net) (*chainIn
|
||||
return nil, fmt.Errorf("failed to register proposervm metrics for chain %s: %w", chainParams.ID, regErr)
|
||||
}
|
||||
engineVM = proposervm.New(vmTyped, proposervm.Config{
|
||||
Upgrades: m.Upgrades,
|
||||
NetworkID: networkID, // CRITICAL-3: windower uses the SAME set ID as the cert side
|
||||
MinBlkDelay: proposervm.DefaultMinBlockDelay,
|
||||
NumHistoricalBlocks: proposervm.DefaultNumHistoricalBlocks,
|
||||
StakingLeafSigner: m.StakingTLSSigner,
|
||||
StakingCertLeaf: m.StakingTLSCert,
|
||||
StakingMLDSASigner: m.StakingMLDSASigner,
|
||||
StakingMLDSAPub: m.StakingMLDSAPub,
|
||||
Registerer: proposervmReg,
|
||||
Upgrades: m.Upgrades,
|
||||
NetworkID: networkID, // CRITICAL-3: windower uses the SAME set ID as the cert side
|
||||
MinBlkDelay: proposervm.DefaultMinBlockDelay,
|
||||
NumHistoricalBlocks: proposervm.DefaultNumHistoricalBlocks,
|
||||
StakingLeafSigner: m.StakingTLSSigner,
|
||||
StakingCertLeaf: m.StakingTLSCert,
|
||||
StakingMLDSASigner: m.StakingMLDSASigner,
|
||||
StakingMLDSAPub: m.StakingMLDSAPub,
|
||||
ProposerWindowDuration: m.ProposerWindowDuration,
|
||||
Registerer: proposervmReg,
|
||||
})
|
||||
m.Log.Info("wrapping chain VM in proposervm for single-proposer-per-height block production",
|
||||
log.Stringer("chainID", chainParams.ID),
|
||||
@@ -4333,7 +4337,6 @@ func (g *networkGossiper) BroadcastVote(chainID ids.ID, networkID ids.ID, blockI
|
||||
// it fed no longer exists in the consensus engine (174af3c31). Nova sampling decides; the ⅔
|
||||
// Quasar attestation (a plain accept-vote, gossiped via BroadcastVote) trails it. Keep the braid dead.
|
||||
|
||||
|
||||
// GossipCert broadcasts an assembled α-of-K finality cert to ALL validators so
|
||||
// followers finalize blockID on a verifiable proof (HandleIncomingCert), not a
|
||||
// fast-follow guess. Best effort: the gossiping node's own finality is already
|
||||
|
||||
@@ -1762,6 +1762,10 @@ func GetNodeConfig(v *viper.Viper) (node.Config, error) {
|
||||
if nodeConfig.HealthCheckFreq < 0 {
|
||||
return node.Config{}, fmt.Errorf("%s must be positive", HealthCheckFreqKey)
|
||||
}
|
||||
nodeConfig.ProposerWindowDuration = v.GetDuration(ProposerVMWindowDurationKey)
|
||||
if nodeConfig.ProposerWindowDuration < 0 {
|
||||
return node.Config{}, fmt.Errorf("%s must not be negative", ProposerVMWindowDurationKey)
|
||||
}
|
||||
// Halflife of continuous averager used in health checks
|
||||
healthCheckAveragerHalflife := v.GetDuration(HealthCheckAveragerHalflifeKey)
|
||||
if healthCheckAveragerHalflife <= 0 {
|
||||
|
||||
@@ -369,6 +369,7 @@ func addNodeFlags(fs *pflag.FlagSet) {
|
||||
// ProposerVM
|
||||
fs.Bool(ProposerVMUseCurrentHeightKey, false, "Have the ProposerVM always report the last accepted P-chain block height")
|
||||
fs.Duration(ProposerVMMinBlockDelayKey, proposervm.DefaultMinBlockDelay, "Minimum delay to enforce when building a chain++ block for the primary network chains and the default minimum delay for chains")
|
||||
fs.Duration(ProposerVMWindowDurationKey, 0, "Proposer-slot spacing for block production (0 uses the 5s mainnet default); shrink for fast cadence on small/local networks")
|
||||
|
||||
// Metrics
|
||||
fs.Bool(MeterVMsEnabledKey, true, "Enable Meter VMs to track VM performance with more granularity")
|
||||
|
||||
@@ -187,6 +187,7 @@ const (
|
||||
ConsensusFrontierPollFrequencyKey = "consensus-frontier-poll-frequency"
|
||||
ProposerVMUseCurrentHeightKey = "proposervm-use-current-height"
|
||||
ProposerVMMinBlockDelayKey = "proposervm-min-block-delay"
|
||||
ProposerVMWindowDurationKey = "proposervm-window-duration"
|
||||
FdLimitKey = "fd-limit"
|
||||
IndexEnabledKey = "index-enabled"
|
||||
IndexAllowIncompleteKey = "index-allow-incomplete"
|
||||
|
||||
@@ -177,6 +177,11 @@ type Config struct {
|
||||
// Health
|
||||
HealthCheckFreq time.Duration `json:"healthCheckFreq"`
|
||||
|
||||
// ProposerWindowDuration overrides the proposervm proposer-slot spacing.
|
||||
// Zero keeps the 5s mainnet default; small local/dev nets set it low (e.g.
|
||||
// 1s) so block cadence is not floored at 5s per proposer slot.
|
||||
ProposerWindowDuration time.Duration `json:"proposerWindowDuration"`
|
||||
|
||||
// Network configuration
|
||||
NetworkConfig network.Config `json:"networkConfig"`
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ require (
|
||||
github.com/huin/goupnp v1.3.0
|
||||
github.com/jackpal/gateway v1.1.1
|
||||
github.com/jackpal/go-nat-pmp v1.0.2
|
||||
github.com/luxfi/consensus v1.36.6
|
||||
github.com/luxfi/consensus v1.36.7
|
||||
github.com/luxfi/crypto v1.20.2
|
||||
github.com/luxfi/database v1.21.1
|
||||
github.com/luxfi/ids v1.3.2
|
||||
|
||||
@@ -315,16 +315,8 @@ github.com/luxfi/compress v0.1.1 h1:cQjRYQFRrw8HinjW1T8FmKgqdRwrFYDCdecc1t0i+uQ=
|
||||
github.com/luxfi/compress v0.1.1/go.mod h1:d4CkRRmwPzafIp57Jxra3RmAmNwNwU8Oc0QBBRlTqGo=
|
||||
github.com/luxfi/concurrent v0.1.1 h1:LkAmNXybOsAm97nFILqMDBp/YLgleuLmyGomFT7YNxU=
|
||||
github.com/luxfi/concurrent v0.1.1/go.mod h1:GlkfiJtPBpatNxvROf7hkPi4gsnmdHGmqhDnVWFrkZE=
|
||||
github.com/luxfi/consensus v1.36.2 h1:IbeWQF1wEcA/MNxe4fKniSICPndAGEysV/A3DZGMXFM=
|
||||
github.com/luxfi/consensus v1.36.2/go.mod h1:m4aMFbKYxwM2X4oPYRjq4LmDSShfCgAHnhmxIGKDyFU=
|
||||
github.com/luxfi/consensus v1.36.3 h1:pEfHf7asnIKQ2Dr2tWe5nKv/0lRRa/iAQ9IyevcRU3s=
|
||||
github.com/luxfi/consensus v1.36.3/go.mod h1:m4aMFbKYxwM2X4oPYRjq4LmDSShfCgAHnhmxIGKDyFU=
|
||||
github.com/luxfi/consensus v1.36.4 h1:Pe6srq5vD+ObRQRR8L1F/QVvsW+F86BJMKEiofz7qeQ=
|
||||
github.com/luxfi/consensus v1.36.4/go.mod h1:m4aMFbKYxwM2X4oPYRjq4LmDSShfCgAHnhmxIGKDyFU=
|
||||
github.com/luxfi/consensus v1.36.5 h1:kVJp8celtf2RJmMYkkELnrqNXu7HGLmFC+aR6LsBZ34=
|
||||
github.com/luxfi/consensus v1.36.5/go.mod h1:m4aMFbKYxwM2X4oPYRjq4LmDSShfCgAHnhmxIGKDyFU=
|
||||
github.com/luxfi/consensus v1.36.6 h1:iXmYQ20A7j8spZldfU/1cbabqolniec/WTseBDH18WQ=
|
||||
github.com/luxfi/consensus v1.36.6/go.mod h1:m4aMFbKYxwM2X4oPYRjq4LmDSShfCgAHnhmxIGKDyFU=
|
||||
github.com/luxfi/consensus v1.36.7 h1:ijJh/4sl1Y65ziUFsQJa2DLwnAhb1h/c72FHybEXKps=
|
||||
github.com/luxfi/consensus v1.36.7/go.mod h1:m4aMFbKYxwM2X4oPYRjq4LmDSShfCgAHnhmxIGKDyFU=
|
||||
github.com/luxfi/constants v1.6.2 h1:pXHdKIFbfE9qX4xOjq2LxYvagNhhNvspUVEbPcIEKfA=
|
||||
github.com/luxfi/constants v1.6.2/go.mod h1:r0oH8C/+r/XFYBq1AJxt6zWRKKRKgDzrEMop/CCs9rI=
|
||||
github.com/luxfi/container v0.2.1 h1:MTnfKXzS5+oxV5jKZerdOxSA6iMPaQI9/FWGufizzaw=
|
||||
|
||||
@@ -1356,6 +1356,7 @@ func (n *Node) initChainManager(utxoAssetID ids.ID) error {
|
||||
StakingTLSCert: n.StakingTLSCert,
|
||||
StakingMLDSASigner: n.Config.StakingConfig.StakingMLDSA,
|
||||
StakingMLDSAPub: n.Config.StakingConfig.StakingMLDSAPub,
|
||||
ProposerWindowDuration: n.Config.ProposerWindowDuration,
|
||||
StakingBLSKey: n.Config.StakingSigningKey,
|
||||
Log: n.Log,
|
||||
LogFactory: n.LogFactory,
|
||||
|
||||
+28
-14
@@ -25,20 +25,21 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
errUnsignedChild = errors.New("expected child to be signed")
|
||||
errUnexpectedBlockType = errors.New("unexpected proposer block type")
|
||||
errInnerParentMismatch = errors.New("inner parentID didn't match expected parent")
|
||||
errTimeNotMonotonic = errors.New("time must monotonically increase")
|
||||
errPChainHeightNotMonotonic = errors.New("non monotonically increasing P-chain height")
|
||||
errPChainHeightNotReached = errors.New("block P-chain height larger than current P-chain height")
|
||||
errTimeTooAdvanced = errors.New("time is too far advanced")
|
||||
errEpochMismatch = errors.New("epoch mismatch")
|
||||
errProposerWindowNotStarted = errors.New("proposer window hasn't started")
|
||||
errUnexpectedProposer = errors.New("unexpected proposer for current window")
|
||||
errProposerMismatch = errors.New("proposer mismatch")
|
||||
errProposersNotActivated = errors.New("proposers haven't been activated yet")
|
||||
errPChainHeightTooLow = errors.New("block P-chain height is too low")
|
||||
errNotOracle = errors.New("block is not an oracle block")
|
||||
errUnsignedChild = errors.New("expected child to be signed")
|
||||
errUnexpectedBlockType = errors.New("unexpected proposer block type")
|
||||
errInnerParentMismatch = errors.New("inner parentID didn't match expected parent")
|
||||
errClassicalProposerUnderStrictPQ = errors.New("classical (secp256k1) proposer identity refused on a strict-PQ chain")
|
||||
errTimeNotMonotonic = errors.New("time must monotonically increase")
|
||||
errPChainHeightNotMonotonic = errors.New("non monotonically increasing P-chain height")
|
||||
errPChainHeightNotReached = errors.New("block P-chain height larger than current P-chain height")
|
||||
errTimeTooAdvanced = errors.New("time is too far advanced")
|
||||
errEpochMismatch = errors.New("epoch mismatch")
|
||||
errProposerWindowNotStarted = errors.New("proposer window hasn't started")
|
||||
errUnexpectedProposer = errors.New("unexpected proposer for current window")
|
||||
errProposerMismatch = errors.New("proposer mismatch")
|
||||
errProposersNotActivated = errors.New("proposers haven't been activated yet")
|
||||
errPChainHeightTooLow = errors.New("block P-chain height is too low")
|
||||
errNotOracle = errors.New("block is not an oracle block")
|
||||
)
|
||||
|
||||
// OracleBlock is a block that can return multiple child options
|
||||
@@ -124,6 +125,18 @@ func (p *postForkCommonComponents) Verify(
|
||||
parentEpoch chain.Epoch,
|
||||
child *postForkBlock,
|
||||
) error {
|
||||
// STRICT-PQ PROFILE GATE (fail-closed, UNCONDITIONAL — before the Ready gate,
|
||||
// so it also holds during bootstrap/state-sync when the proposer-window check
|
||||
// below is skipped). A chain whose own proposer signs with ML-DSA-65
|
||||
// (StakingMLDSASigner set) is strict-PQ: its validator set is ML-DSA-keyed, so
|
||||
// a block carrying a CLASSICAL secp256k1 proposer identity can never be a
|
||||
// legitimate proposer and must be refused outright — never relying on the
|
||||
// 20-byte NodeID collision bound or upstream validator-set enforcement to catch
|
||||
// it. Mirrors contract.RefuseUnderStrictPQ: one gate, one place.
|
||||
if p.vm.StakingMLDSASigner != nil && child.SignedBlock.HasClassicalProposer() {
|
||||
return errClassicalProposerUnderStrictPQ
|
||||
}
|
||||
|
||||
if err := verifyIsNotOracleBlock(ctx, p.innerBlk); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -233,6 +246,7 @@ func (p *postForkCommonComponents) Verify(
|
||||
// sub-window timestamp — zero behaviour change on a live chain;
|
||||
// - a snapped time is strictly > parent (monotonic, no errTimeNotMonotonic) and ≤ now (never
|
||||
// errTimeTooAdvanced).
|
||||
//
|
||||
// It is a pure function of (parentTimestamp, now) so it is idempotent for any two calls in the
|
||||
// same slot — the property the liveness fix relies on.
|
||||
func slotSnappedChildTimestamp(parentTimestamp, now time.Time) time.Time {
|
||||
|
||||
@@ -18,10 +18,10 @@ import (
|
||||
var (
|
||||
_ SignedBlock = (*statelessBlock)(nil)
|
||||
|
||||
errUnexpectedSignature = errors.New("signature provided when none was expected")
|
||||
errInvalidCertificate = errors.New("invalid certificate")
|
||||
errUnknownProposerScheme = errors.New("proposervm block: unknown proposer identity scheme")
|
||||
errMLDSAProposerSigInvalid = errors.New("proposervm block: ML-DSA proposer signature invalid")
|
||||
errUnexpectedSignature = errors.New("signature provided when none was expected")
|
||||
errInvalidCertificate = errors.New("invalid certificate")
|
||||
errUnknownProposerScheme = errors.New("proposervm block: unknown proposer identity scheme")
|
||||
errMLDSAProposerSigInvalid = errors.New("proposervm block: ML-DSA proposer signature invalid")
|
||||
)
|
||||
|
||||
// Proposer-identity scheme tags stored as the FIRST byte of the offCert slot,
|
||||
@@ -68,6 +68,12 @@ type SignedBlock interface {
|
||||
// signed this block, [ids.EmptyNodeID] will be returned.
|
||||
Proposer() ids.NodeID
|
||||
|
||||
// HasClassicalProposer reports whether this block carries a CLASSICAL
|
||||
// (secp256k1/ECDSA) proposer identity. A strict-PQ chain refuses such a block
|
||||
// — its proposer must be ML-DSA-65 to match the ML-DSA-keyed validator set.
|
||||
// Unsigned blocks (transition / single-validator) return false.
|
||||
HasClassicalProposer() bool
|
||||
|
||||
// Data Availability fields (v1.1 spec)
|
||||
DARoot() [32]byte // Root of DA commitments
|
||||
WitnessRoot() [32]byte // Root of witnesses/proofs
|
||||
@@ -233,6 +239,10 @@ func (b *statelessBlock) Proposer() ids.NodeID {
|
||||
return b.proposer
|
||||
}
|
||||
|
||||
func (b *statelessBlock) HasClassicalProposer() bool {
|
||||
return b.scheme == schemeSecp256k1
|
||||
}
|
||||
|
||||
func (b *statelessBlock) DARoot() [32]byte {
|
||||
return read32(b.msg.Root(), offDARoot)
|
||||
}
|
||||
|
||||
@@ -35,6 +35,12 @@ type Config struct {
|
||||
// Configurable minimal delay among blocks issued consecutively
|
||||
MinBlkDelay time.Duration
|
||||
|
||||
// ProposerWindowDuration overrides the proposer-slot spacing (proposer.
|
||||
// WindowDuration). Zero keeps the 5s mainnet default; small local/dev nets set
|
||||
// it low (e.g. 1s or less) so block cadence is not floored at 5s per proposer
|
||||
// slot. Applied once at VM init via proposer.SetWindowDuration.
|
||||
ProposerWindowDuration time.Duration
|
||||
|
||||
// Maximal number of block indexed.
|
||||
// Zero signals all blocks are indexed.
|
||||
NumHistoricalBlocks uint64
|
||||
|
||||
@@ -12,27 +12,51 @@ import (
|
||||
|
||||
"gonum.org/v1/gonum/mathext/prng"
|
||||
|
||||
validators "github.com/luxfi/validators"
|
||||
"github.com/luxfi/container/sampler"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/math"
|
||||
"github.com/luxfi/utils"
|
||||
validators "github.com/luxfi/validators"
|
||||
)
|
||||
|
||||
// Proposer list constants
|
||||
// Proposer list slot-COUNT constants (fixed; independent of the wall-clock
|
||||
// window length).
|
||||
const (
|
||||
MaxVerifyWindows = 6
|
||||
MaxBuildWindows = 60
|
||||
MaxLookAheadSlots = 720
|
||||
)
|
||||
|
||||
// WindowDuration is the proposer-slot spacing: a validator's minimum build delay
|
||||
// is its slot index times WindowDuration. Default 5s (the avalanchego mainnet
|
||||
// value, tuned for large validator sets). It is a startup-configurable VAR — small
|
||||
// local/dev networks shrink it via SetWindowDuration (proposervm Config →
|
||||
// --proposer-window-duration) so block cadence is not floored at 5s per proposer
|
||||
// slot. It is read by BOTH the windower's delay math AND TimeToSlot, so the two
|
||||
// stay consistent by construction.
|
||||
var (
|
||||
WindowDuration = 5 * time.Second
|
||||
|
||||
MaxVerifyWindows = 6
|
||||
MaxVerifyDelay = MaxVerifyWindows * WindowDuration // 30 seconds
|
||||
|
||||
MaxBuildWindows = 60
|
||||
MaxBuildDelay = MaxBuildWindows * WindowDuration // 5 minutes
|
||||
|
||||
MaxLookAheadSlots = 720
|
||||
MaxLookAheadWindow = MaxLookAheadSlots * WindowDuration // 1 hour
|
||||
MaxVerifyDelay = MaxVerifyWindows * WindowDuration // 30s at the default
|
||||
MaxBuildDelay = MaxBuildWindows * WindowDuration // 5m at the default
|
||||
MaxLookAheadWindow = MaxLookAheadSlots * WindowDuration // 1h at the default
|
||||
)
|
||||
|
||||
// SetWindowDuration overrides the proposer-slot spacing. It MUST be called at
|
||||
// startup, before any chain begins block production: WindowDuration is read by
|
||||
// both the windower delay math and TimeToSlot, so changing it mid-flight would
|
||||
// desynchronize the slot mapping across the fleet (a consensus fault). Values
|
||||
// <= 0 are ignored, so an unset config leaves the safe 5s default in place.
|
||||
func SetWindowDuration(d time.Duration) {
|
||||
if d <= 0 {
|
||||
return
|
||||
}
|
||||
WindowDuration = d
|
||||
MaxVerifyDelay = MaxVerifyWindows * WindowDuration
|
||||
MaxBuildDelay = MaxBuildWindows * WindowDuration
|
||||
MaxLookAheadWindow = MaxLookAheadSlots * WindowDuration
|
||||
}
|
||||
|
||||
var (
|
||||
_ Windower = (*windower)(nil)
|
||||
|
||||
|
||||
@@ -289,6 +289,10 @@ func (vm *VM) Initialize(
|
||||
// constants.PrimaryNetworkID, so a native chain matches the original
|
||||
// proposer.New(..., PrimaryNetworkID, ...) byte-for-byte.
|
||||
func (vm *VM) newWindower() proposer.Windower {
|
||||
// Apply the configured proposer-slot spacing before the first schedule is
|
||||
// resolved (no-op at the default; shrinks cadence on local/dev nets). Startup
|
||||
// only — see proposer.SetWindowDuration.
|
||||
proposer.SetWindowDuration(vm.Config.ProposerWindowDuration)
|
||||
netID := vm.Config.NetworkID
|
||||
if netID == ids.Empty {
|
||||
netID = constants.PrimaryNetworkID
|
||||
|
||||
Reference in New Issue
Block a user