mirror of
https://github.com/luxfi/threshold.git
synced 2026-10-30 15:15:53 +00:00
Every round in this package sat at zero coverage: the tests built a session and asserted it was non-nil, so no share had ever been produced and the key check inside round 3 had never executed. Run for the first time it refused, and each thing it refused for was a separate defect. The resharing polynomial's constant term was the party's bare share xⱼ, so the new shares summed to Σxⱼ instead of the secret Σλⱼxⱼ — a different key. It is now the share weighted by its Lagrange coefficient over the old committee. The round-1 broadcast carried commitments as curve.Point, an interface CBOR has no concrete type to decode into, so it failed on every receiver. Points and scalars travel as bytes now, as keygen's messages already do. The broadcast declares round 1, so the handler routed it to round1, whose StoreBroadcastMessage discarded it with a note that round 2 would store it — and round 2 declared no broadcast. The maps live on round 1, where the message lands; round 2 reads them through the embed. A party's own commitments were never in that map, since a broadcast is not delivered back to its sender, while round 3 added the party's own share separately — public shares one contribution short of private ones. Seeded. Round 2 sent a share to itself. Whether that arrives depends on the transport, so the party's contribution counted once or twice. It is stored directly now and never sent, and round 3 sums the map once with no special case. The test network delivered self-addressed messages; production refuses them (Message.IsFor is false when From is the recipient). It refuses them now, which is what makes the last fix testable: send-to-self passed under the old network and fails under this one. The test runs the reshare, checks every new share against its own public share, then signs with the new shares and verifies under the ORIGINAL key. Each fix, reverted alone, fails it. Co-authored-by: Hanzo Dev <[email protected]>
143 lines
3.5 KiB
Go
143 lines
3.5 KiB
Go
package test
|
|
|
|
import (
|
|
"sync"
|
|
|
|
"github.com/luxfi/threshold/pkg/party"
|
|
"github.com/luxfi/threshold/pkg/protocol"
|
|
)
|
|
|
|
// Network is the in-memory message bus used by every protocol test.
|
|
//
|
|
// Close discipline: Send takes inFlight.RLock for the duration of the
|
|
// publish; Close takes inFlight.Lock so all in-flight publishes drain
|
|
// before closing the underlying channels. This is the only way to make
|
|
// "go network.Send(msg)" + deferred network.Close() race-free without
|
|
// either widening Send's lock (which would serialize the harness) or
|
|
// dropping messages.
|
|
type Network struct {
|
|
messages map[party.ID]chan *protocol.Message
|
|
done map[party.ID]chan struct{}
|
|
mu sync.RWMutex
|
|
|
|
inFlight sync.RWMutex
|
|
closed bool
|
|
}
|
|
|
|
// NewNetwork creates an in-memory test network with buffered per-party queues.
|
|
func NewNetwork(parties []party.ID) *Network {
|
|
n := &Network{
|
|
messages: make(map[party.ID]chan *protocol.Message),
|
|
done: make(map[party.ID]chan struct{}),
|
|
}
|
|
|
|
for _, id := range parties {
|
|
n.messages[id] = make(chan *protocol.Message, 1000)
|
|
n.done[id] = make(chan struct{})
|
|
}
|
|
|
|
return n
|
|
}
|
|
|
|
// Send routes a message to the appropriate party.
|
|
//
|
|
// Holds inFlight.RLock for the duration of the publish so Close (which
|
|
// takes inFlight.Lock) waits for every in-flight send to complete before
|
|
// closing the channels. If Close already ran, Send returns immediately
|
|
// without panicking.
|
|
func (n *Network) Send(msg *protocol.Message) {
|
|
if msg == nil {
|
|
return
|
|
}
|
|
|
|
n.inFlight.RLock()
|
|
defer n.inFlight.RUnlock()
|
|
if n.closed {
|
|
return
|
|
}
|
|
|
|
n.mu.RLock()
|
|
targets := make([]chan *protocol.Message, 0)
|
|
|
|
if msg.To == "" {
|
|
// Broadcast to all parties except sender
|
|
for id, ch := range n.messages {
|
|
if id != msg.From {
|
|
targets = append(targets, ch)
|
|
}
|
|
}
|
|
} else if msg.To != msg.From {
|
|
// Send to specific party. A message addressed to its own sender is
|
|
// dropped here because that is what happens in production —
|
|
// protocol.Message.IsFor is false when From is the recipient — and a
|
|
// network that delivered it would let a round count its own
|
|
// contribution twice, or depend on a delivery that never occurs.
|
|
if ch, ok := n.messages[msg.To]; ok {
|
|
targets = append(targets, ch)
|
|
}
|
|
}
|
|
n.mu.RUnlock()
|
|
|
|
// Send without holding n.mu (lock-order: inFlight.RLock is still held).
|
|
for _, ch := range targets {
|
|
ch <- msg
|
|
}
|
|
}
|
|
|
|
// Next returns the message channel for a party
|
|
func (n *Network) Next(id party.ID) <-chan *protocol.Message {
|
|
n.mu.RLock()
|
|
defer n.mu.RUnlock()
|
|
|
|
if ch, ok := n.messages[id]; ok {
|
|
return ch
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Done returns the done channel for a party
|
|
func (n *Network) Done(id party.ID) <-chan struct{} {
|
|
n.mu.RLock()
|
|
defer n.mu.RUnlock()
|
|
|
|
if ch, ok := n.done[id]; ok {
|
|
return ch
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SetSession is a no-op for the in-memory network.
|
|
func (n *Network) SetSession([]byte) {}
|
|
|
|
// Close closes all channels.
|
|
//
|
|
// Blocks until every in-flight Send has returned, so callers may safely
|
|
// defer Close from the test goroutine even when fire-and-forget
|
|
// "go network.Send(msg)" calls are still running in HandlerLoop.
|
|
func (n *Network) Close() {
|
|
// Acquire write lock — waits for every Send (held with RLock) to drain
|
|
// and prevents new ones from entering the publish path.
|
|
n.inFlight.Lock()
|
|
defer n.inFlight.Unlock()
|
|
if n.closed {
|
|
return
|
|
}
|
|
n.closed = true
|
|
|
|
n.mu.Lock()
|
|
defer n.mu.Unlock()
|
|
|
|
for _, ch := range n.done {
|
|
select {
|
|
case <-ch:
|
|
// Already closed
|
|
default:
|
|
close(ch)
|
|
}
|
|
}
|
|
|
|
for _, ch := range n.messages {
|
|
close(ch)
|
|
}
|
|
}
|