deps: pulsar v1.9.0 pkg/ layout + threshold v1.12.0 dealerless daemon

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-06-30 16:27:27 -07:00
co-authored by Hanzo Dev
parent 9245f17f90
commit f09d8a1b3e
9 changed files with 362 additions and 30 deletions
+1 -1
View File
@@ -138,6 +138,6 @@ For k=32 sampling with β=15 rounds, expected unique signers ≈ 50-80.
## References
- Existing Quasar family: `~/work/lux/consensus/protocol/{photon,wave,flare,focus,horizon,ray,nova,nebula,prism,quasar}`
- Avalanche/Snowman metastable family (academic prior art; superseded by Quasar/Nova per LP-020, LP-134)
- the classical metastable consensus family (academic prior art; superseded by Quasar/Nova per LP-020, LP-134)
- LP-045 hierarchical quorum certs
- `~/work/lux/proofs/pq-finality-no-bls.tex`
+43 -17
View File
@@ -162,16 +162,20 @@ type PendingBlock struct {
rePollBackoff time.Duration
// rePollAttempts counts how many times the re-poll loop has re-solicited this
// block. Once it reaches maxRePollAttempts the block is ABANDONED for re-poll
// purposes (rePollAbandoned) — re-soliciting the SAME block to peers who
// cannot vote (they are behind its parent) is pure spam and never recovers it;
// the catch-up path (requestCatchup) is what recovers a behind frontier.
// block. For a NON-OWN (gossiped) block, once it reaches maxRePollAttempts the
// block is ABANDONED for re-poll purposes (rePollAbandoned) — re-soliciting a
// gossiped block to peers who cannot vote (they are behind its parent) is pure
// spam and never recovers it; the catch-up path (requestCatchup) is what recovers
// a behind frontier. An OWN proposal is never abandoned (the proposer drives it to
// finality), so this counter only paces its bounded-backoff re-solicitation.
rePollAttempts int
// rePollAbandoned is set once rePollAttempts hits the cap. An abandoned block
// is NEVER re-polled again (no infinite spam), but is NOT deleted: it remains
// pending and recoverable — a late cert (HandleIncomingCert) or a catch-up
// that fills its parent can still finalize it.
// rePollAbandoned is set once a NON-OWN block's rePollAttempts hits the cap. An
// abandoned block is NEVER re-polled again (no infinite spam), but is NOT deleted:
// it remains pending and recoverable — a late cert (HandleIncomingCert) or a
// catch-up that fills its parent can still finalize it. An OWN proposal never sets
// this flag: it is re-solicited until it decides, so a down/wedged/forked
// designated proposer cannot halt the chain by starving the substitute's votes.
rePollAbandoned bool
// IsOwnProposal is true when this node built and proposed the block. It now
@@ -1106,11 +1110,15 @@ const (
// every maxRePollBackoff, turning a 250ms storm into a trickle.
maxRePollBackoff = 16 * time.Second
// maxRePollAttempts is the hard cap on re-poll attempts per block. After this
// many re-solicitations the block is abandoned for re-poll (rePollAbandoned)
// — it is never re-polled again (but stays pending, recoverable via cert or
// catch-up). With doubling from a 250ms base, 8 attempts span
// 0.25+0.5+1+2+4+8+16+16 ≈ 48s of backstop before giving up on re-poll.
// maxRePollAttempts is the hard cap on re-poll attempts for a NON-OWN (gossiped)
// block. After this many re-solicitations such a block is abandoned for re-poll
// (rePollAbandoned) — it is never re-polled again (but stays pending, recoverable
// via cert or catch-up): re-soliciting a gossiped block whose voters are behind
// its parent is spam that re-poll cannot fix. An UNDECIDED OWN proposal is NEVER
// abandoned (see rePollAllPending): the proposer keeps re-soliciting it until it
// finalizes, matching avalanchego's "re-poll a processing block until decided".
// With doubling from a 250ms base, 8 attempts span
// 0.25+0.5+1+2+4+8+16+16 ≈ 48s of backstop before giving up on a gossiped block.
maxRePollAttempts = 8
// catchupCooldown rate-limits ancestor fetches per missing parent: at most one
@@ -1261,8 +1269,9 @@ func (t *Transitive) rePollAllPending(ctx context.Context, base time.Duration) {
continue
}
// This block is due. Record the attempt, advance the backoff (double, cap),
// and abandon once the cap is hit so it is NEVER re-polled again.
// This block is due. Record the attempt and advance the backoff (double,
// cap) so re-solicitation is a bounded trickle (≤ maxRePollBackoff), never a
// storm.
pending.lastRePoll = now
pending.rePollAttempts++
next := window * 2
@@ -1270,9 +1279,26 @@ func (t *Transitive) rePollAllPending(ctx context.Context, base time.Duration) {
next = maxRePollBackoff
}
pending.rePollBackoff = next
if pending.rePollAttempts >= maxRePollAttempts {
// LIVENESS (the down/wedged/forked-proposer halt): an UNDECIDED OWN proposal
// is NEVER abandoned. This node BUILT it on the finalized tip (its voters
// therefore HAVE the parent and CAN vote), and as the proposer it owns driving
// it to an α-of-K cert — so it must keep re-soliciting until the block decides
// (then it leaves pendingBlocks and the re-poll quiesces). This is exactly
// avalanchego's contract: re-poll a PROCESSING block until it is decided, and
// only quiesce at NumProcessing()==0 (snow/engine/snowman voter.go +
// Engine.Gossip's 100ms repoll). Abandoning an own proposal after a fixed
// attempt cap was the Lux-only divergence that froze mainnet C-Chain: the
// substitute's canonical block stopped being re-solicited and the chain halted
// even though the honest majority was ready to vote (zero-margin 4-of-5 once a
// 5th validator forks/wedges). The bounded backoff above keeps this storm-safe.
//
// A NON-own (gossiped) block keeps the attempt cap: re-soliciting a block whose
// voters are BEHIND its parent (the gossip-from-an-ahead-peer case) is pure spam
// and never recovers it — that block recovers via cert-gossip or the catch-up
// fetch, not by re-poll. So the cap still bounds the follower path.
if !pending.IsOwnProposal && pending.rePollAttempts >= maxRePollAttempts {
pending.rePollAbandoned = true
t.log.Warn("re-poll: block abandoned after attempt cap — not re-soliciting further (recoverable via cert/catch-up)",
t.log.Warn("re-poll: gossiped block abandoned after attempt cap — not re-soliciting further (recoverable via cert/catch-up)",
"blockID", blockID, "attempts", pending.rePollAttempts)
}
+292
View File
@@ -0,0 +1,292 @@
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// proposer_liveness_test.go — the DURABLE regression suite for the
// down/wedged/forked-designated-proposer LIVENESS halt (Lux mainnet C-Chain,
// stuck producing 1082825 above a finalized 1082824).
//
// ROOT CAUSE (in the engine's terms): a height's designated proposer is
// down/wedged/FORKED (present, sampled, gossiping, but it never produces a
// canonical-extending block that finalizes). A SUBSTITUTE (the next slot's
// designated proposer) builds the canonical block, self-votes, and solicits peer
// votes ONCE. If the α-of-K signed votes do not assemble on that first
// solicitation — the common case when a follower is briefly behind, or the first
// PushQuery is dropped, or (the mainnet reality) one of the five validators is a
// permanent non-voter so the OTHER four must ALL vote with ZERO margin — nothing
// re-solicits the missing votes once `rePollAllPending` ABANDONS the block after
// `maxRePollAttempts` (the Lux-only divergence from avalanchego, which re-polls a
// processing block until it decides and only quiesces at NumProcessing()==0). The
// block then sits in pendingBlocks, never re-solicited, and the chain HALTS.
//
// THE FIX (proven here): an UNDECIDED OWN proposal is NEVER abandoned for re-poll.
// It is re-solicited forever on the bounded (capped) backoff schedule until it
// decides (and is removed from pendingBlocks, so the re-poll naturally quiesces) —
// matching avalanchego's "re-poll while processing, quiesce when decided". This is
// a pure LIVENESS retry: it changes NOTHING about the finality predicate (a block
// still finalizes ONLY on a verified α-of-K cert that clears the stake gate), so
// the safety tests below (sub-quorum, forged, out-of-set, tampered) still reject.
//
// fails-before / passes-after is shown for every behavior: the pre-fix abandon is
// asserted RED, the post-fix persistence GREEN.
package chain
import (
"context"
"sync"
"testing"
"time"
"github.com/luxfi/consensus/config"
"github.com/luxfi/ids"
)
// params5Prod returns the PRODUCTION 5-validator quorum params: K=5, α=4
// (⌊2·5/3⌋+1 — what bftCommittee derives for a 5-node value network, see
// dynamic_test.go which asserts K5/α4). The finality_test.go params5() uses α=3
// for legacy count tests; the liveness/safety properties here must hold at the
// REAL production threshold where the 4 healthy nodes are the EXACT quorum (zero
// margin — the mainnet condition after a 5th validator forks/wedges).
func params5Prod() config.Parameters {
p := params5()
p.AlphaPreference = 4
p.AlphaConfidence = 4
return p
}
// reSolicitProbe is a BlockProposer that records how many times the engine
// re-solicits votes for each block (RequestVotes), and fires an optional hook on
// each solicitation. It is the instrument that makes the re-poll behavior
// observable: pre-fix the count plateaus at the abandon cap; post-fix it grows
// without bound while the block is undecided. The hook models a follower that
// only votes once it is (re-)solicited — the production reality, where a node that
// missed the first gossip casts its vote only when the proposer re-sends it.
type reSolicitProbe struct {
mu sync.Mutex
requests map[ids.ID]int
proposed map[ids.ID]int
onRequest func(blockID ids.ID, cumulative int)
}
func newReSolicitProbe() *reSolicitProbe {
return &reSolicitProbe{requests: map[ids.ID]int{}, proposed: map[ids.ID]int{}}
}
func (p *reSolicitProbe) Propose(_ context.Context, proposal BlockProposal) error {
p.mu.Lock()
p.proposed[proposal.BlockID]++
p.mu.Unlock()
return nil
}
func (p *reSolicitProbe) RequestVotes(_ context.Context, req VoteRequest) error {
p.mu.Lock()
p.requests[req.BlockID]++
n := p.requests[req.BlockID]
hook := p.onRequest
p.mu.Unlock()
if hook != nil {
hook(req.BlockID, n)
}
return nil
}
func (p *reSolicitProbe) requestCount(id ids.ID) int {
p.mu.Lock()
defer p.mu.Unlock()
return p.requests[id]
}
// driveRePoll forces `cycles` re-poll passes over the engine's pending blocks
// synchronously, each pass made "due" by resetting every undecided block's backoff
// window to the distant past. This drives the re-poll attempt counter deterministically
// WITHOUT waiting on the exponential backoff (so the test is fast and not timing-flaky),
// and is isolated from the background ticker by the caller using a long RoundTO so the
// background loop never fires during the synchronous drive.
func driveRePoll(e *Transitive, cycles int) {
for i := 0; i < cycles; i++ {
e.mu.Lock()
for _, pb := range e.pendingBlocks {
if pb.Decided {
continue
}
pb.lastRePoll = time.Now().Add(-time.Hour)
pb.rePollBackoff = time.Nanosecond
}
e.mu.Unlock()
e.rePollAllPending(context.Background(), time.Millisecond)
}
}
// -----------------------------------------------------------------------------
// LIVENESS — the core bug. An undecided own proposal must be re-solicited
// forever, never abandoned, so the substitute's canonical block finalizes once
// the honest majority's votes arrive.
// -----------------------------------------------------------------------------
// TestProposerLiveness_UndecidedOwnProposal_NeverAbandoned is the DIRECT
// fails-before/passes-after proof of the fix. A substitute proposer (node 0)
// builds the canonical next block with only a sub-quorum of votes present (the
// wedged/forked 5th validator never votes, and a 4th honest vote has not yet
// arrived). The engine must keep re-soliciting it forever.
//
// PRE-FIX: rePollAllPending sets rePollAbandoned after maxRePollAttempts (8), then
// SKIPS the block — RequestVotes plateaus near 8 and the block is never re-solicited
// again → permanent halt.
// POST-FIX: the undecided own proposal is never abandoned; RequestVotes keeps firing
// every pass.
func TestProposerLiveness_UndecidedOwnProposal_NeverAbandoned(t *testing.T) {
vs := newTestValidatorSet(5)
params := params5Prod()
params.RoundTO = 10 * time.Second // park the background ticker; we drive re-poll manually
rec := &recordingGossiper{}
e, chainID := newQuorumEngine(t, params, vs, 0, rec)
cp := newReSolicitProbe()
e.SetProposer(cp)
// Node 0 is the substitute: it builds the canonical block at the stuck height.
blk := newTestBlock(1, ids.Empty, "substitute-canonical")
pos := trackProposal(e, chainID, blk, 0)
// Only one peer (node 1) has voted so far → 2 of 5, below α=4. The block is a
// genuine, healthy, undecided own proposal awaiting the rest of the honest quorum.
e.ReceiveVote(vs.signedVote(1, pos))
const cycles = 40 // >> maxRePollAttempts (8)
driveRePoll(e, cycles)
e.mu.RLock()
pb := e.pendingBlocks[blk.id]
abandoned := pb != nil && pb.rePollAbandoned
e.mu.RUnlock()
reqs := cp.requestCount(blk.id)
if abandoned {
t.Fatalf("LIVENESS HALT: undecided own proposal was ABANDONED for re-poll after %d attempts "+
"(rePollAbandoned=true) — the substitute's canonical block stops being re-solicited and the "+
"chain halts forever. avalanchego re-polls a processing block until it decides.", reqs)
}
if reqs <= maxRePollAttempts {
t.Fatalf("re-solicitation stopped early: RequestVotes fired only %d times over %d passes "+
"(expected continuous re-solicitation > maxRePollAttempts=%d). A block awaiting the honest "+
"quorum must keep being solicited.", reqs, cycles, maxRePollAttempts)
}
}
// TestProposerLiveness_LateHonestVote_FinalizesViaPersistentRePoll is the
// EMERGENT proof: a 4th honest voter (node 3) only casts its vote in RESPONSE to a
// re-solicitation that arrives AFTER the pre-fix abandon point (it was briefly
// behind). The block finalizes iff the proposer keeps re-soliciting past that
// point — i.e. only post-fix. The wedged/forked 5th validator (node 4) never votes.
func TestProposerLiveness_LateHonestVote_FinalizesViaPersistentRePoll(t *testing.T) {
vs := newTestValidatorSet(5)
params := params5Prod()
params.RoundTO = 10 * time.Second
rec := &recordingGossiper{}
e, chainID := newQuorumEngine(t, params, vs, 0, rec)
cp := newReSolicitProbe()
e.SetProposer(cp)
blk := newTestBlock(7, ids.Empty, "late-quorum")
pos := trackProposal(e, chainID, blk, 0) // node 0 self-vote = 1
e.ReceiveVote(vs.signedVote(1, pos)) // node 1
e.ReceiveVote(vs.signedVote(2, pos)) // node 2 → 3 of 5, still < α=4
// Node 3 is briefly behind: it casts its (4th, quorum-completing) vote ONLY once
// the proposer has re-solicited past the abandon point. Node 4 (wedged/forked)
// never votes.
const lateSolicit = maxRePollAttempts + 4 // 12: only reachable if re-poll never abandons
var once sync.Once
cp.onRequest = func(id ids.ID, n int) {
if id == blk.id && n >= lateSolicit {
once.Do(func() { e.ReceiveVote(vs.signedVote(3, pos)) })
}
}
// Drive re-poll well past both the abandon cap and the late-solicit threshold.
driveRePoll(e, 60)
finalized := waitFor(3*time.Second, func() bool { return e.IsAccepted(blk.id) })
reqs := cp.requestCount(blk.id)
if !finalized {
t.Fatalf("LIVENESS HALT: block never finalized. RequestVotes fired %d times; the late honest "+
"vote is only solicited at attempt %d, but the pre-fix abandon caps re-poll at "+
"maxRePollAttempts=%d, so node 3 is never re-solicited and the 4-of-5 quorum never assembles.",
reqs, lateSolicit, maxRePollAttempts)
}
}
// -----------------------------------------------------------------------------
// SAFETY — the fix must NOT lower the BFT threshold. Persistent re-solicitation
// re-sends a block; it can NEVER manufacture a vote, so a sub-quorum, a forged
// vote, or an out-of-set signer still cannot finalize.
// -----------------------------------------------------------------------------
// TestProposerSafety_SubQuorumNeverFinalizes_EvenWithPersistentRePoll proves the
// liveness retry does not weaken safety: 3 of 5 (< α=4) is re-solicited forever and
// STILL never finalizes. A genuine minority cannot finalize no matter how long it
// re-polls.
func TestProposerSafety_SubQuorumNeverFinalizes_EvenWithPersistentRePoll(t *testing.T) {
vs := newTestValidatorSet(5)
params := params5Prod()
params.RoundTO = 10 * time.Second
rec := &recordingGossiper{}
e, chainID := newQuorumEngine(t, params, vs, 0, rec)
e.SetProposer(newReSolicitProbe())
blk := newTestBlock(3, ids.Empty, "minority")
pos := trackProposal(e, chainID, blk, 0) // self = 1
e.ReceiveVote(vs.signedVote(1, pos)) // 2
e.ReceiveVote(vs.signedVote(2, pos)) // 3 of 5 < α=4
driveRePoll(e, 60) // re-solicit far past the old abandon cap
if waitFor(500*time.Millisecond, func() bool { return e.IsAccepted(blk.id) }) {
t.Fatal("SAFETY VIOLATION: a sub-quorum (3 of 5, below α=4) finalized — persistent re-poll " +
"must re-SOLICIT only; it must never lower the α-of-K threshold.")
}
rec.mu.Lock()
certs := len(rec.certs)
rec.mu.Unlock()
if certs != 0 {
t.Fatalf("no cert may be produced below α; got %d", certs)
}
}
// TestProposerSafety_ForgedAndOutOfSetVotes_NeverCount proves re-solicitation
// cannot be exploited: an out-of-set signer and a real validator's signature lifted
// to the WRONG position are both dropped, so even re-solicited forever the block
// stays at its 3 genuine votes (< α) and never finalizes.
func TestProposerSafety_ForgedAndOutOfSetVotes_NeverCount(t *testing.T) {
vs := newTestValidatorSet(5)
params := params5Prod()
params.RoundTO = 10 * time.Second
rec := &recordingGossiper{}
e, chainID := newQuorumEngine(t, params, vs, 0, rec)
e.SetProposer(newReSolicitProbe())
blk := newTestBlock(9, ids.Empty, "forged-attack")
pos := trackProposal(e, chainID, blk, 0) // self = 1
e.ReceiveVote(vs.signedVote(1, pos)) // 2
e.ReceiveVote(vs.signedVote(2, pos)) // 3 (genuine)
// (a) out-of-set signer: keys unknown to vs.
outsider := newTestValidatorSet(1)
e.ReceiveVote(Vote{
BlockID: blk.id, NodeID: outsider.nodeID(0), Accept: true, SignedAt: time.Now(),
Signature: outsider.sign(0, pos), ParentID: pos.ParentID, Round: pos.Round,
})
// (b) real validator (node 3) signature over a DIFFERENT (wrong) position.
wrong := pos
wrong.Height++
e.ReceiveVote(Vote{
BlockID: blk.id, NodeID: vs.nodeID(3), Accept: true, SignedAt: time.Now(),
Signature: vs.sign(3, wrong), ParentID: pos.ParentID, Round: pos.Round,
})
driveRePoll(e, 60)
if waitFor(500*time.Millisecond, func() bool { return e.IsAccepted(blk.id) }) {
t.Fatal("SAFETY VIOLATION: forged/out-of-set/wrong-position votes were counted toward the quorum.")
}
}
+11 -5
View File
@@ -200,11 +200,17 @@ func TestRecovery_RePollBacksOffExponentially(t *testing.T) {
time.Sleep(window)
got := prop.count()
// Linear would be ~ window/RoundTO = 50. Backoff (10,20,40,80,160,...) capped
// at maxRePollAttempts (8) yields at most maxRePollAttempts re-polls EVER.
if got > int64(maxRePollAttempts) {
t.Fatalf("re-poll must be bounded by the attempt cap (%d), got %d over %v — poll storm not fixed",
maxRePollAttempts, got, window)
// Linear (a fixed-cadence storm) would be ~ window/RoundTO = 50. EXPONENTIAL
// backoff (10,20,40,80,160,…) makes the count GEOMETRIC in the window —
// log2(window/RoundTO) ≈ log2(50) ≈ 6 — so it is bounded far below linear. This
// is the storm-safety invariant, and it does NOT depend on the attempt cap: an
// own proposal is re-solicited until it decides (never abandoned), but the
// backoff keeps that a slow trickle, not a 250ms storm. Ceiling 12 is a safe
// geometric bound for this window, still ≪ the linear 50.
const geometricCeiling = 12
if got > geometricCeiling {
t.Fatalf("re-poll must be bounded by exponential backoff (geometric ≈%d), got %d over %v — poll storm not fixed",
geometricCeiling, got, window)
}
if got == 0 {
t.Fatalf("liveness: a stuck own block must be re-polled at least once, got 0")
+4 -4
View File
@@ -13,19 +13,19 @@ require (
github.com/luxfi/crypto v1.19.26
github.com/luxfi/database v1.19.3
github.com/luxfi/dkg v0.3.5
github.com/luxfi/ids v1.2.15
github.com/luxfi/ids v1.3.0
github.com/luxfi/lattice/v7 v7.1.4
github.com/luxfi/log v1.4.3
github.com/luxfi/magnetar v1.2.3
github.com/luxfi/math v1.4.1
github.com/luxfi/metric v1.5.8
github.com/luxfi/p2p v1.21.1
github.com/luxfi/pulsar v1.8.0
github.com/luxfi/pulsar v1.9.0
github.com/luxfi/runtime v1.1.1
github.com/luxfi/threshold v1.10.2
github.com/luxfi/threshold v1.12.0
github.com/luxfi/validators v1.2.0
github.com/luxfi/version v1.0.1
github.com/luxfi/warp v1.19.3
github.com/luxfi/warp v1.24.0
github.com/luxfi/zap v0.8.1
github.com/prometheus/client_golang v1.23.2
github.com/prometheus/client_model v0.6.2
+8
View File
@@ -216,6 +216,8 @@ github.com/luxfi/geth v1.17.11 h1:opyVqYiH3t4qglAdMymudxKIO09O1LWB9tKGc57o9sA=
github.com/luxfi/geth v1.17.11/go.mod h1:3vQfQJd9JC+AVBjxNXa9PYQOqpbE2dKu8E3jqhPZ3LU=
github.com/luxfi/ids v1.2.15 h1:omE+E4+0Poj9DzM11ejSFgteaSQ3KDHi5g54iH6jcxI=
github.com/luxfi/ids v1.2.15/go.mod h1:Fj73K5xcblvdE0SxU/ip+jE8VqNdu+80548su5KJ7xI=
github.com/luxfi/ids v1.3.0 h1:11xnwRDm6zQzbqcRnkFujOYkvhK4Fs/+g+sKRlRUNsU=
github.com/luxfi/ids v1.3.0/go.mod h1:6vpdcdZW0qxeade+3xby8aLTutbcJ7O0r8+fNQrksGI=
github.com/luxfi/lattice/v7 v7.1.4 h1:hQR02M6cHTAV5+joOPi9gb9Gm+z/hKJnhJF4IlciIJs=
github.com/luxfi/lattice/v7 v7.1.4/go.mod h1:DmIQFi3mJiehVsR235l1NKYEU0JhU649OX5p7gMEW2c=
github.com/luxfi/lens v0.1.4 h1:goGjGDXx2BNdjzXDunL5QT8elK2ZyCcc0z8TAbtWYrg=
@@ -246,12 +248,16 @@ github.com/luxfi/pulsar v1.7.1 h1:q/TVn0TWeMImm67EJZ3CMmBAGQCW+bhIX7YQGL3aYK0=
github.com/luxfi/pulsar v1.7.1/go.mod h1:wW+V2mUVOqppN68+n5u9Qtlfcscd0vOz4acvubcSOKI=
github.com/luxfi/pulsar v1.8.0 h1:ZWOwVQ8HcEJzWljj2blkNIRKwktngPJc05knkZtRjVM=
github.com/luxfi/pulsar v1.8.0/go.mod h1:wW+V2mUVOqppN68+n5u9Qtlfcscd0vOz4acvubcSOKI=
github.com/luxfi/pulsar v1.9.0 h1:c0JnatYF79aN87aof9VlYjIoCzmixxrgNPeUUuh8ScU=
github.com/luxfi/pulsar v1.9.0/go.mod h1:1+/atAiiiOm9RnXM3c66eHF3garjAa3C+sn4rAU7JUU=
github.com/luxfi/runtime v1.1.1 h1:vOMe82PL3bpSbslS7p69dKRpbr2qW2vOOAwej3UkkmU=
github.com/luxfi/runtime v1.1.1/go.mod h1:fmG6+Zxj4wSNlXwiUfDthQDY+SfxVu6S0fX2lL6VbrE=
github.com/luxfi/sampler v1.1.0 h1:u3iRDl7V06ARh0e85h3HT+aZ1saCFo2yMMsh+dCJbqk=
github.com/luxfi/sampler v1.1.0/go.mod h1:kJa53S3tC9+VSbuV3RFu68MmbCCBlr2UM39LOClQ/Hs=
github.com/luxfi/threshold v1.10.2 h1:FFj6H+wd3DI/Y5J1Jb3bClCAtFi/ueItWtBoNBIdIo4=
github.com/luxfi/threshold v1.10.2/go.mod h1:GETt6Px9vrQ19J9Mnpf5LgMXAdvJu8ijyByOp+QtD48=
github.com/luxfi/threshold v1.12.0 h1:JJ369xC/YyDvrqXj+xFoK98nP2rUM099qFs03hBvq/M=
github.com/luxfi/threshold v1.12.0/go.mod h1:iuRQGDAy8ZKjQhZjkSKg7NtbP75/8Up9zj52y7IuyZo=
github.com/luxfi/utils v1.2.0 h1:gtEiI7/NM6PQ/OasEpH0PvB+e5hIS/tpum9r64pYjMc=
github.com/luxfi/utils v1.2.0/go.mod h1:T2OCKT1xG9jtKR/gyJQoSkticzrE9WFQ8eohJHGu9Fg=
github.com/luxfi/validators v1.2.0 h1:VygpiBqBAdGrfkb7xzE2yrVmnXaqE+hm8FLWdGXO7G8=
@@ -262,6 +268,8 @@ github.com/luxfi/vm v1.2.3 h1:IdfB02OtDmdy/YNxaIR8DquPp5Aq0fi5QXBqGHoRkug=
github.com/luxfi/vm v1.2.3/go.mod h1:ZXmw1sKA6GL3Ma3BfYUv5Fzvx3aWu2QMtkxF3lL/3R4=
github.com/luxfi/warp v1.19.3 h1:tU6aAniYrPiRGrht7oFSwsUmk5KBb9RDFSyZq3lJ4HY=
github.com/luxfi/warp v1.19.3/go.mod h1:kqyk7Fa5mgw1zzMLqjrZWVLuTef35CCW0gOEv10UlZk=
github.com/luxfi/warp v1.24.0 h1:jrcJNlbOiZsAEopJMy9bSaCwI5NDZ8qgp/6sNoXqepg=
github.com/luxfi/warp v1.24.0/go.mod h1:bKvTi24JHlANsl7qkWZAVr/DsMfvwy42f+Cc9x4+Sq8=
github.com/luxfi/zap v0.8.1 h1:IzYWf4T35QjsUJhoNbuhew8MDDRYmlZUVx4gKBNLjMA=
github.com/luxfi/zap v0.8.1/go.mod h1:gUoQ/V9nTLLIb4/n0bY/L2zQDYK6pDBG121rK4t8bpg=
github.com/luxfi/zapdb v1.10.0 h1:1lLHEmkyC0BucnA/zjQYsMkUVxuEo2vQkEaQGjYfuuc=
@@ -46,7 +46,7 @@ import (
"errors"
"testing"
pulsarwire "github.com/luxfi/pulsar/ref/go/pkg/pulsar"
pulsarwire "github.com/luxfi/pulsar/pkg/pulsar"
coronaThreshold "github.com/luxfi/threshold/protocols/corona"
)
+1 -1
View File
@@ -32,7 +32,7 @@ import (
"github.com/luxfi/crypto/bls"
magnetar "github.com/luxfi/magnetar/ref/go/pkg/magnetar"
pulsarwire "github.com/luxfi/pulsar/ref/go/pkg/pulsar"
pulsarwire "github.com/luxfi/pulsar/pkg/pulsar"
coronaThreshold "github.com/luxfi/threshold/protocols/corona"
pulsar "github.com/luxfi/threshold/protocols/pulsar"
)
@@ -9,7 +9,7 @@ import (
"testing"
"github.com/luxfi/ids"
pulsarwire "github.com/luxfi/pulsar/ref/go/pkg/pulsar"
pulsarwire "github.com/luxfi/pulsar/pkg/pulsar"
)
// mapResolver is a test CommitteeKeyResolver backed by a map. A committee with