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:
+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