mirror of
https://github.com/luxfi/consensus.git
synced 2026-09-24 00:26:58 +00:00
Reading both floors over the signers let a set shrink to its signers, and a rung whose floors are read over n is satisfiable at every n. TwoThirdsCount(1) is 1, so one signature over a one-signer set clears the count floor and the stake floor together and mints export finality. Byzantine tolerance is f = ⌊(n−1)/3⌋, which is ZERO for n of one, two and three: such a certificate absorbs no fault at all, and one compromised key among its signers forges it outright. So the export rung refuses a signing set below minBFTCommittee — the same constant bftCommittee floors committee SELECTION at, read at the certificate instead of at the sampler, and reused rather than restated. It is a floor on the SET, stated beside the n<1 refusal because neither quorum floor implies it: both are read over n and both shrink with it. Nova has no such clause and must not grow one — it authorizes local execution the chain can still reorg away, so a small or partitioned chain has to be able to make progress. A three-validator chain now PRODUCES and never CERTIFIES; operators need n ≥ 4 to export with any fault tolerance. Rust mirrors it as CertError::MinCommittee, which the corpus vocabulary folds into Go's one belowThreshold class. The transcription guard was reading the wrong denominator. verdict_test computed n as len(c.Set), the membership ROLL, so a floor moved onto the roll would agree with the guard and be re-blessed by -update — the second line of defence agreeing with the regression it exists to catch. It now counts the seats that hold a key, on the Quasar branch and the Nova one, and transcribes the committee floor as a search for the least n whose fault budget reaches one rather than as the constant. Proven by construction: with the corpus source reporting the roll as its signer count and the golden re-blessed, the old guard passes and the new one fails. The corpus could not have caught it either. quasar_keyless_third had three signers on a roll of four, and ⌊2·3/3⌋+1 and ⌊2·4/3⌋+1 are both three, so its count half was vacuous. Two rows now separate the denominators one at a time: quasar_keyless_stake (six signers, a seventh member holding a third of the roll — the count floor is five either way, so only stake decides) and quasar_keyless_count (four signers, two members holding one unit each — the stake clears under either reading, so only the count decides). TestKeylessRowsBind- BothDenominators fails if either is lost or flattened. quasar_keyless_third itself FLIPS to a refusal, and it is the row that states the new clause: the recorded stake floor is 200 over the three hundred that can sign and not 333 over the five hundred the chain carries, so the denominator IS the signers — and the certificate is refused anyway, on neither quorum floor. Reading the floors over the signers stops a spectator stranding a chain; it is not a way for a chain with three signers to export. Every other frozen verdict is byte-identical, and the admission section and the signed vectors are untouched. StakeSource gains CarriedStake, and no floor may read it. SignerStake alone decides a certificate correctly and cannot show the set shrinking underneath one: a chain whose signable stake has fallen to a sliver of what it carries still certifies at ⅔ of the sliver, and ResponsiveStakePct — voted over signer — stays at one the whole way down. FinalityStatus.SignerStakePct is the number that falls, and the one to alarm on. Rust has carried(), C++ has total_stake(); Go had lost the projection when the denominator moved. Co-authored-by: Hanzo Dev <[email protected]>
462 lines
17 KiB
Go
462 lines
17 KiB
Go
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
|
||
// See the file LICENSE for licensing terms.
|
||
|
||
package chain
|
||
|
||
import (
|
||
"context"
|
||
"crypto/ed25519"
|
||
"crypto/hmac"
|
||
"crypto/sha256"
|
||
"sync"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/luxfi/consensus/config"
|
||
"github.com/luxfi/consensus/engine/chain/block"
|
||
"github.com/luxfi/ids"
|
||
)
|
||
|
||
// -----------------------------------------------------------------------------
|
||
// nodeAuth — a deterministic per-node HMAC authenticator for tests.
|
||
//
|
||
// It models a real vote authenticator without per-test key registration: each
|
||
// validator's secret key is derived from its NodeID (key = SHA256("lux-test-vote-key"||nodeID)),
|
||
// a vote signature is HMAC-SHA256(key, message), and verification recomputes it.
|
||
// This is a genuine MAC (forging a vote requires the node's derived key), so the
|
||
// security properties under test (forged/unknown/wrong-position votes fail) hold
|
||
// — while letting existing tests keep using ids.GenerateTestNodeID() and only
|
||
// attach a signature. It is NOT used in production (the node injects a real
|
||
// signature scheme); it exists purely so the engine's quorum-cert path can be
|
||
// exercised with arbitrary node ids.
|
||
// -----------------------------------------------------------------------------
|
||
|
||
type nodeAuth struct{}
|
||
|
||
var testAuth = nodeAuth{}
|
||
|
||
func (nodeAuth) keyFor(nodeID ids.NodeID) []byte {
|
||
h := sha256.Sum256(append([]byte("lux-test-vote-key"), nodeID[:]...))
|
||
return h[:]
|
||
}
|
||
|
||
func (a nodeAuth) sign(nodeID ids.NodeID, message []byte) []byte {
|
||
mac := hmac.New(sha256.New, a.keyFor(nodeID))
|
||
mac.Write(message)
|
||
return mac.Sum(nil)
|
||
}
|
||
|
||
// VerifyVote implements VoteVerifier: recompute the node's MAC over message.
|
||
// The MAC is keyed by nodeID, not by epoch, so epochHeight is not consulted here
|
||
// (the deterministic harness has one fixed key per node across all epochs).
|
||
func (a nodeAuth) VerifyVote(nodeID ids.NodeID, message []byte, sig []byte, _ uint64) bool {
|
||
want := a.sign(nodeID, message)
|
||
return hmac.Equal(want, sig)
|
||
}
|
||
|
||
// signerFor returns a VoteSigner bound to nodeID's derived key.
|
||
func (a nodeAuth) signerFor(nodeID ids.NodeID) VoteSigner {
|
||
return voteSignerFunc(func(message []byte) ([]byte, error) { return a.sign(nodeID, message), nil })
|
||
}
|
||
|
||
// signedVoteForEngine builds a signed accept Vote for a block the engine is
|
||
// already tracking, signing the engine's OWN reconstructed position so the
|
||
// engine's handleVote verification accepts it. This is the minimal-edit helper
|
||
// for migrating legacy tests: replace an unsigned ReceiveVote(Vote{...,Accept:true})
|
||
// with ReceiveVote(signedVoteForEngine(e, blockID, nodeID)).
|
||
func signedVoteForEngine(e *Transitive, blockID ids.ID, nodeID ids.NodeID) Vote {
|
||
e.mu.RLock()
|
||
pending, ok := e.pendingBlocks[blockID]
|
||
var pos VotePosition
|
||
if ok {
|
||
pos = e.blockPositionLocked(pending, blockID)
|
||
} else {
|
||
pos = VotePosition{ChainID: e.chainID, BlockID: blockID}
|
||
}
|
||
e.mu.RUnlock()
|
||
return Vote{
|
||
BlockID: blockID,
|
||
NodeID: nodeID,
|
||
Accept: true,
|
||
SignedAt: time.Now(),
|
||
Signature: testAuth.sign(nodeID, CanonicalVoteMessage(pos)),
|
||
ParentID: pos.ParentID,
|
||
Round: pos.Round,
|
||
}
|
||
}
|
||
|
||
// newTestEngine builds an UNSTARTED engine wired with the deterministic
|
||
// nodeAuth verifier+signer (so the K>1 Start guard is satisfied) plus any extra
|
||
// options. The drop-in replacement for `New(opts...)` in legacy lifecycle tests
|
||
// that call Start/Stop themselves. The self nodeID is a fresh test id.
|
||
func newTestEngine(opts ...Option) *Transitive {
|
||
self := ids.GenerateTestNodeID()
|
||
all := append([]Option{WithQuorumCert(ids.Empty, self, testAuth, nil, testAuth.signerFor(self))}, opts...)
|
||
return New(all...)
|
||
}
|
||
|
||
// newTestEngineParams is newTestEngine with explicit params.
|
||
func newTestEngineParams(params config.Parameters, opts ...Option) *Transitive {
|
||
self := ids.GenerateTestNodeID()
|
||
all := append([]Option{WithParams(params), WithQuorumCert(ids.Empty, self, testAuth, nil, testAuth.signerFor(self))}, opts...)
|
||
return New(all...)
|
||
}
|
||
|
||
// signedRejectForEngine builds a signed REJECT vote for a tracked block (signs
|
||
// the reject-bound canonical message). The reject counterpart of
|
||
// signedVoteForEngine.
|
||
func signedRejectForEngine(e *Transitive, blockID ids.ID, nodeID ids.NodeID) Vote {
|
||
e.mu.RLock()
|
||
pending, ok := e.pendingBlocks[blockID]
|
||
var pos VotePosition
|
||
if ok {
|
||
pos = e.blockPositionLocked(pending, blockID)
|
||
} else {
|
||
pos = VotePosition{ChainID: e.chainID, BlockID: blockID}
|
||
}
|
||
e.mu.RUnlock()
|
||
return Vote{
|
||
BlockID: blockID,
|
||
NodeID: nodeID,
|
||
Accept: false,
|
||
SignedAt: time.Now(),
|
||
Signature: testAuth.sign(nodeID, canonicalVoteMessageFor(pos, false)),
|
||
ParentID: pos.ParentID,
|
||
Round: pos.Round,
|
||
}
|
||
}
|
||
|
||
// newAuthEngine builds a started multi-validator engine wired with the
|
||
// deterministic nodeAuth verifier+signer for `self`. Used by migrated legacy
|
||
// tests that need Start to succeed under the K>1 guard.
|
||
func newAuthEngine(t *testing.T, params config.Parameters, self ids.NodeID, opts ...Option) *Transitive {
|
||
t.Helper()
|
||
all := append([]Option{WithQuorumCert(ids.Empty, self, testAuth, nil, testAuth.signerFor(self))}, opts...)
|
||
e := NewWithConfig(Config{Params: params}, all...)
|
||
if err := e.Start(context.Background(), true); err != nil {
|
||
t.Fatalf("Start: %v", err)
|
||
}
|
||
t.Cleanup(func() { _ = e.Stop(context.Background()) })
|
||
return e
|
||
}
|
||
|
||
// testValidatorSet is a deterministic Ed25519-backed validator key set used to
|
||
// exercise the real quorum-cert path in tests. It is a faithful stand-in for a
|
||
// node-layer VoteVerifier/VoteSigner: each validator has an Ed25519 keypair,
|
||
// signatures are over the canonical vote message, and verification rejects
|
||
// unknown validators and bad signatures. Ed25519 is a proven primitive — the
|
||
// tests do NOT roll custom crypto.
|
||
type testValidatorSet struct {
|
||
mu sync.RWMutex
|
||
keys map[ids.NodeID]ed25519.PrivateKey
|
||
pub map[ids.NodeID]ed25519.PublicKey
|
||
ids []ids.NodeID
|
||
|
||
// committed models each honest validator running the fixed engine's per-height
|
||
// vote-once discipline (reserveSlotForSign): keyed (validatorIndex,height) →
|
||
// the canonical it first signed. Asking a validator to sign a DIFFERENT block at
|
||
// a height it already signed returns an unsigned (refused) vote — an honest
|
||
// validator never equivocates. So a real cross-node fork needs ≥ intersection
|
||
// (≥3 of 5 here) ACTUAL Byzantine equivocators — f ≥ n/3, beyond the guarantee.
|
||
committed map[[2]uint64]ids.ID
|
||
|
||
// simValidatorCount, when simValidatorCountSet, OVERRIDES what SignerCount reports (the live
|
||
// committee size the round-scoped view-change sizes its POL/precommit quorum to). Lets a test
|
||
// model the mainnet condition where the current-map Manager read 0 validators at construction:
|
||
// with a forced 0 the view-change cannot shrink the sample preset (K=21/α=15) and FREEZES (the
|
||
// pre-fix stall); unset, SignerCount reports the real len(ids).
|
||
simValidatorCount int
|
||
simValidatorCountSet bool
|
||
}
|
||
|
||
func newTestValidatorSet(n int) *testValidatorSet {
|
||
s := &testValidatorSet{
|
||
keys: make(map[ids.NodeID]ed25519.PrivateKey, n),
|
||
pub: make(map[ids.NodeID]ed25519.PublicKey, n),
|
||
committed: make(map[[2]uint64]ids.ID),
|
||
}
|
||
for i := 0; i < n; i++ {
|
||
nodeID := ids.GenerateTestNodeID()
|
||
pub, priv, err := ed25519.GenerateKey(nil)
|
||
if err != nil {
|
||
panic(err)
|
||
}
|
||
s.keys[nodeID] = priv
|
||
s.pub[nodeID] = pub
|
||
s.ids = append(s.ids, nodeID)
|
||
}
|
||
return s
|
||
}
|
||
|
||
func (s *testValidatorSet) nodeID(i int) ids.NodeID { return s.ids[i] }
|
||
|
||
// VerifyVote implements VoteVerifier. Unknown node → false (never an error).
|
||
// The fixed test set is the same at every epoch, so epochHeight is not consulted
|
||
// here; the height-pinned-resolution behavior (RESIDUAL-B) is exercised by the
|
||
// dedicated epochValidatorSet in quorum_finality_test.go.
|
||
func (s *testValidatorSet) VerifyVote(nodeID ids.NodeID, message []byte, sig []byte, _ uint64) bool {
|
||
s.mu.RLock()
|
||
pub, ok := s.pub[nodeID]
|
||
s.mu.RUnlock()
|
||
if !ok {
|
||
return false
|
||
}
|
||
return ed25519.Verify(pub, message, sig)
|
||
}
|
||
|
||
// Weight implements StakeSource: every validator in the test set carries EQUAL unit
|
||
// weight (1), so a count-α quorum is also a ⅔-stake supermajority — the test set is a
|
||
// valid stake source for the value-DEX quorum-finality gate (Mode() requires one). An
|
||
// unknown node has weight 0 (it contributes no stake).
|
||
func (s *testValidatorSet) Weight(nodeID ids.NodeID, _ uint64) uint64 {
|
||
s.mu.RLock()
|
||
defer s.mu.RUnlock()
|
||
if _, ok := s.pub[nodeID]; ok {
|
||
return 1
|
||
}
|
||
return 0
|
||
}
|
||
|
||
// SignerStake implements StakeSource: the total active stake is the validator count
|
||
// (equal unit weights). Non-zero for a non-empty set, so the stake-weighted check is
|
||
// usable (a zero total would be treated as unusable / fail-closed by VerifyWeighted).
|
||
func (s *testValidatorSet) SignerStake(_ uint64) uint64 {
|
||
s.mu.RLock()
|
||
defer s.mu.RUnlock()
|
||
return uint64(len(s.ids))
|
||
}
|
||
|
||
// SignerCount implements StakeSource: the DISTINCT validator count (the live committee size the
|
||
// view-change sizes n/α to), unless a test forces a value via setSimulatedValidatorCount.
|
||
func (s *testValidatorSet) SignerCount(_ uint64) int {
|
||
s.mu.RLock()
|
||
defer s.mu.RUnlock()
|
||
if s.simValidatorCountSet {
|
||
return s.simValidatorCount
|
||
}
|
||
return len(s.ids)
|
||
}
|
||
|
||
// CarriedStake implements StakeSource: every member of the test set holds a key, so what
|
||
// the set carries is what its signers hold. The two numbers part company only on a set with
|
||
// a keyless member — see keyless_denominator_test.go for the one that has them.
|
||
func (s *testValidatorSet) CarriedStake(_ uint64) uint64 {
|
||
s.mu.RLock()
|
||
defer s.mu.RUnlock()
|
||
return uint64(len(s.ids))
|
||
}
|
||
|
||
// setSimulatedValidatorCount forces SignerCount to report c (0 models the current-map Manager
|
||
// unresolved at construction — the mainnet freeze). Call before the fleet builds.
|
||
func (s *testValidatorSet) setSimulatedValidatorCount(c int) {
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
s.simValidatorCount, s.simValidatorCountSet = c, true
|
||
}
|
||
|
||
// signerFor returns a VoteSigner bound to validator i's key.
|
||
func (s *testValidatorSet) signerFor(i int) VoteSigner {
|
||
nodeID := s.ids[i]
|
||
return voteSignerFunc(func(message []byte) ([]byte, error) {
|
||
s.mu.RLock()
|
||
priv := s.keys[nodeID]
|
||
s.mu.RUnlock()
|
||
return ed25519.Sign(priv, message), nil
|
||
})
|
||
}
|
||
|
||
// sign produces validator i's signature over the position's canonical message.
|
||
func (s *testValidatorSet) sign(i int, pos VotePosition) []byte {
|
||
s.mu.RLock()
|
||
priv := s.keys[s.ids[i]]
|
||
s.mu.RUnlock()
|
||
return ed25519.Sign(priv, CanonicalVoteMessage(pos))
|
||
}
|
||
|
||
// signedVote builds validator i's signed accept Vote for a position, HONORING the
|
||
// per-validator vote-once discipline: an honest validator signs at most one canonical
|
||
// per height. If validator i was already asked to sign a DIFFERENT canonical at this
|
||
// height, it REFUSES — returns an unsigned vote (empty signature) that fails
|
||
// verification and is never counted, exactly as a fixed-engine honest validator would.
|
||
// The same canonical is idempotent. This makes the test set model reality post-fix, so
|
||
// a cross-node fork can only be forced by ACTUAL Byzantine equivocators (f ≥ n/3).
|
||
func (s *testValidatorSet) signedVote(i int, pos VotePosition) Vote {
|
||
key := [2]uint64{uint64(i), pos.Height}
|
||
canonical := slotCanonical(pos)
|
||
s.mu.Lock()
|
||
if bound, ok := s.committed[key]; ok && bound != canonical {
|
||
s.mu.Unlock()
|
||
// honest validator refuses to equivocate: unsigned vote (won't verify).
|
||
return Vote{BlockID: pos.BlockID, NodeID: s.ids[i], Accept: true, SignedAt: time.Now(), ParentID: pos.ParentID, Round: pos.Round}
|
||
}
|
||
s.committed[key] = canonical
|
||
s.mu.Unlock()
|
||
return Vote{
|
||
BlockID: pos.BlockID,
|
||
NodeID: s.ids[i],
|
||
Accept: true,
|
||
SignedAt: time.Now(),
|
||
Signature: s.sign(i, pos),
|
||
ParentID: pos.ParentID,
|
||
Round: pos.Round,
|
||
}
|
||
}
|
||
|
||
type voteSignerFunc func(message []byte) ([]byte, error)
|
||
|
||
func (f voteSignerFunc) SignVote(message []byte) ([]byte, error) { return f(message) }
|
||
|
||
// recordingGossiper captures gossiped certs/votes for assertions and can relay
|
||
// them to a set of follower runtimes (to drive an end-to-end multi-node test in
|
||
// a single process).
|
||
type recordingGossiper struct {
|
||
mu sync.Mutex
|
||
certs [][]byte
|
||
votes [][]byte
|
||
certBlock []ids.ID
|
||
}
|
||
|
||
func (g *recordingGossiper) GossipPut(ids.ID, ids.ID, []byte) int { return 0 }
|
||
func (g *recordingGossiper) SendPullQuery(ids.ID, ids.ID, ids.ID, []ids.NodeID) int {
|
||
return 0
|
||
}
|
||
func (g *recordingGossiper) SendPushQuery(ids.ID, ids.ID, []byte, []ids.NodeID) int { return 0 }
|
||
func (g *recordingGossiper) SendVote(ids.ID, ids.NodeID, ids.ID) error { return nil }
|
||
|
||
func (g *recordingGossiper) BroadcastVote(_ ids.ID, _ ids.ID, _ ids.ID, voteBytes []byte) int {
|
||
g.mu.Lock()
|
||
g.votes = append(g.votes, append([]byte(nil), voteBytes...))
|
||
g.mu.Unlock()
|
||
return 1
|
||
}
|
||
|
||
func (g *recordingGossiper) GossipCert(_ ids.ID, blockID ids.ID, certBytes []byte) error {
|
||
g.mu.Lock()
|
||
g.certs = append(g.certs, append([]byte(nil), certBytes...))
|
||
g.certBlock = append(g.certBlock, blockID)
|
||
g.mu.Unlock()
|
||
return nil
|
||
}
|
||
|
||
// also satisfy the engine-level CertGossiper used by tryFinalizeBlock.
|
||
var _ CertGossiper = (*recordingGossiper)(nil)
|
||
var _ QuorumGossiper = (*certQuorumGossiper)(nil)
|
||
|
||
// certQuorumGossiper adapts recordingGossiper to QuorumGossiper (network-level)
|
||
// signature (chainID, networkID, blockID, bytes).
|
||
type certQuorumGossiper struct{ rec *recordingGossiper }
|
||
|
||
func (g *certQuorumGossiper) GossipPut(c, n ids.ID, b []byte) int { return g.rec.GossipPut(c, n, b) }
|
||
func (g *certQuorumGossiper) SendPullQuery(c, n, b ids.ID, v []ids.NodeID) int {
|
||
return g.rec.SendPullQuery(c, n, b, v)
|
||
}
|
||
func (g *certQuorumGossiper) SendPushQuery(c, n ids.ID, b []byte, v []ids.NodeID) int {
|
||
return g.rec.SendPushQuery(c, n, b, v)
|
||
}
|
||
func (g *certQuorumGossiper) SendVote(c ids.ID, to ids.NodeID, b ids.ID) error {
|
||
return g.rec.SendVote(c, to, b)
|
||
}
|
||
func (g *certQuorumGossiper) BroadcastVote(c, n, b ids.ID, vb []byte) int {
|
||
return g.rec.BroadcastVote(c, n, b, vb)
|
||
}
|
||
func (g *certQuorumGossiper) GossipCert(c, n, b ids.ID, cb []byte) int {
|
||
_ = g.rec.GossipCert(c, b, cb)
|
||
return 1
|
||
}
|
||
|
||
// newQuorumEngine builds a started multi-validator engine for validator index
|
||
// `self`, wired with the test validator set as verifier+signer and the given
|
||
// cert gossiper. K/alpha come from params.
|
||
func newQuorumEngine(t *testing.T, params config.Parameters, vs *testValidatorSet, self int, gossiper CertGossiper) (*Transitive, ids.ID) {
|
||
t.Helper()
|
||
return newQuorumEngineOpts(t, params, vs, self, gossiper)
|
||
}
|
||
|
||
// newQuorumEngineOpts builds the engine and applies any EXTRA options on top of the
|
||
// base quorum-cert wiring. The base harness deliberately does NOT wire a stake source —
|
||
// the finality tests rely on COUNT-α finality (cert.Verify), and stake-weighting
|
||
// (cert.VerifyWeighted) would change the threshold (⅔-of-stake) and break those tests.
|
||
// A test that exercises the value-DEX MODE gate (which requires a stake source) passes
|
||
// WithStakeWeighting explicitly, scoped to that test, so finality math stays count-α for
|
||
// everyone else.
|
||
func newQuorumEngineOpts(t *testing.T, params config.Parameters, vs *testValidatorSet, self int, gossiper CertGossiper, extra ...Option) (*Transitive, ids.ID) {
|
||
t.Helper()
|
||
chainID := ids.GenerateTestID()
|
||
opts := append([]Option{
|
||
WithQuorumCert(chainID, vs.nodeID(self), vs, gossiper, vs.signerFor(self)),
|
||
// Every K>1 chain runs with a stake source — the node refuses to start one
|
||
// without live validator state — so a quorum engine built without one is not
|
||
// a configuration these tests should be reasoning about.
|
||
WithStakeWeighting(vs),
|
||
}, extra...)
|
||
e := NewWithConfig(Config{Params: params}, opts...)
|
||
if err := e.Start(context.Background(), true); err != nil {
|
||
t.Fatalf("Start: %v", err)
|
||
}
|
||
t.Cleanup(func() { _ = e.Stop(context.Background()) })
|
||
return e, chainID
|
||
}
|
||
|
||
// trackProposal inserts a verified own-proposal pending block into the engine
|
||
// (as buildBlocksLocked would) so a test can drive votes at it. Returns the
|
||
// position votes must bind to.
|
||
func trackProposal(e *Transitive, chainID ids.ID, blk *verifyOnceBlock, round uint32) VotePosition {
|
||
cb := &Block{
|
||
id: blk.id,
|
||
parentID: blk.parentID,
|
||
height: blk.height,
|
||
timestamp: blk.timestamp.Unix(),
|
||
data: blk.bytes,
|
||
}
|
||
_ = e.consensus.AddBlock(context.Background(), cb)
|
||
_ = e.consensus.ProcessVote(context.Background(), blk.id, ids.GenerateTestNodeID(), true)
|
||
e.mu.Lock()
|
||
pb := &PendingBlock{
|
||
ConsensusBlock: cb,
|
||
VMBlock: blk,
|
||
ProposedAt: time.Now(),
|
||
VoteCount: 1,
|
||
Round: round,
|
||
Decided: false,
|
||
IsOwnProposal: true,
|
||
}
|
||
e.pendingBlocks[blk.id] = pb
|
||
// Mirror production buildBlocksLocked: record the proposer's OWN signed
|
||
// accept vote so the assembled cert includes it.
|
||
e.recordOwnVoteLocked(pb, blk.id)
|
||
e.mu.Unlock()
|
||
return VotePosition{
|
||
ChainID: chainID,
|
||
Height: blk.height,
|
||
Round: round,
|
||
BlockID: blk.id,
|
||
ParentID: blk.parentID,
|
||
}
|
||
}
|
||
|
||
// waitFor polls cond until true or the deadline elapses. Returns cond's final value.
|
||
func waitFor(d time.Duration, cond func() bool) bool {
|
||
deadline := time.Now().Add(d)
|
||
for time.Now().Before(deadline) {
|
||
if cond() {
|
||
return true
|
||
}
|
||
time.Sleep(5 * time.Millisecond)
|
||
}
|
||
return cond()
|
||
}
|
||
|
||
// newTestBlock is a small constructor for verifyOnceBlock used across tests.
|
||
func newTestBlock(height uint64, parent ids.ID, tag string) *verifyOnceBlock {
|
||
return &verifyOnceBlock{
|
||
id: ids.GenerateTestID(),
|
||
parentID: parent,
|
||
height: height,
|
||
timestamp: time.Now(),
|
||
bytes: []byte(tag),
|
||
}
|
||
}
|
||
|
||
// ensure block.Block is referenced (harness builds VM-less engines in some tests).
|
||
var _ = block.Block(nil)
|