From 292421624c29a976e3234b193005083eeb90f3d3 Mon Sep 17 00:00:00 2001 From: Jakub Sztandera Date: Thu, 22 Aug 2024 15:22:08 +0200 Subject: [PATCH 1/6] Failing quality deny test keeping GST Signed-off-by: Jakub Sztandera --- sim/adversary/deny_phase.go | 75 +++++++++++++++++++++++++++++++++++++ test/deny_test.go | 32 ++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 sim/adversary/deny_phase.go diff --git a/sim/adversary/deny_phase.go b/sim/adversary/deny_phase.go new file mode 100644 index 00000000..60fc8690 --- /dev/null +++ b/sim/adversary/deny_phase.go @@ -0,0 +1,75 @@ +package adversary + +import ( + "time" + + "github.com/filecoin-project/go-f3/gpbft" +) + +var _ Receiver = (*DenyPhase)(nil) + +// DenyPhase adversary denies messages with given phase to a given set of participants for a +// configured duration of time. +// +// For this adversary to take effect global stabilisation time must be configured +// to be at least as long as the configured deny duration. +// +// See sim.WithGlobalStabilizationTime. +type DenyPhase struct { + id gpbft.ActorID + host Host + targetsByID map[gpbft.ActorID]struct{} + gst time.Time + phase gpbft.Phase +} + +func NewDenyPhase(id gpbft.ActorID, host Host, denialDuration time.Duration, phase gpbft.Phase, targets ...gpbft.ActorID) *DenyPhase { + targetsByID := make(map[gpbft.ActorID]struct{}) + for _, target := range targets { + targetsByID[target] = struct{}{} + } + return &DenyPhase{ + id: id, + host: host, + targetsByID: targetsByID, + gst: time.Time{}.Add(denialDuration), + phase: phase, + } +} + +func NewDenyPhaseGenerator(power gpbft.StoragePower, denialDuration time.Duration, phase gpbft.Phase, targets ...gpbft.ActorID) Generator { + return func(id gpbft.ActorID, host Host) *Adversary { + return &Adversary{ + Receiver: NewDenyPhase(id, host, denialDuration, phase, targets...), + Power: power, + } + } +} + +func (d *DenyPhase) ID() gpbft.ActorID { + return d.id +} + +func (d *DenyPhase) AllowMessage(from gpbft.ActorID, to gpbft.ActorID, msg gpbft.GMessage) bool { + // DenyPhase all messages to or from targets until Global Stabilisation Time has + // elapsed, except messages to self. + switch { + case from == to, d.host.Time().After(d.gst): + return true + default: + isAffected := d.isTargeted(to) && msg.Vote.Step == d.phase + return !isAffected + } +} + +func (d *DenyPhase) isTargeted(id gpbft.ActorID) bool { + _, found := d.targetsByID[id] + return found +} + +func (*DenyPhase) StartInstanceAt(uint64, time.Time) error { return nil } +func (*DenyPhase) ValidateMessage(msg *gpbft.GMessage) (gpbft.ValidatedMessage, error) { + return Validated(msg), nil +} +func (*DenyPhase) ReceiveMessage(_ gpbft.ValidatedMessage) error { return nil } +func (*DenyPhase) ReceiveAlarm() error { return nil } diff --git a/test/deny_test.go b/test/deny_test.go index 9c886fd4..c59e605e 100644 --- a/test/deny_test.go +++ b/test/deny_test.go @@ -3,6 +3,7 @@ package test import ( "math" "testing" + "time" "github.com/filecoin-project/go-f3/gpbft" "github.com/filecoin-project/go-f3/sim" @@ -33,3 +34,34 @@ func TestDeny_SkipsToFuture(t *testing.T) { chain := ecChainGenerator.GenerateECChain(instanceCount-1, gpbft.TipSet{}, math.MaxUint64) requireConsensusAtInstance(t, sm, instanceCount-1, chain...) } + +func TestDenyQuality(t *testing.T) { + t.Parallel() + const ( + instanceCount = 20 + maxRounds = 30 + gst = 1 * time.Second + participants = 30 + ) + + ecGen := sim.NewUniformECChainGenerator(4332432, 1, 5) + + attacked := []gpbft.ActorID{} + for i := gpbft.ActorID(1); i <= participants; i++ { + attacked = append(attacked, i) + } + + sm, err := sim.NewSimulation( + syncOptions( + sim.AddHonestParticipants(1, ecGen, sim.UniformStoragePower(gpbft.NewStoragePower(100*participants))), + sim.AddHonestParticipants(participants, ecGen, sim.UniformStoragePower(gpbft.NewStoragePower(100))), + sim.WithAdversary(adversary.NewDenyPhaseGenerator(gpbft.NewStoragePower(1), gst, gpbft.QUALITY_PHASE, attacked...)), + sim.WithTraceLevel(1), + sim.WithGlobalStabilizationTime(gst), + )..., + ) + require.NoError(t, err) + require.NoErrorf(t, sm.Run(instanceCount, maxRounds), "%s", sm.Describe()) + chain := ecGen.GenerateECChain(instanceCount-1, gpbft.TipSet{}, math.MaxUint64) + requireConsensusAtInstance(t, sm, instanceCount-1, chain...) +} From 5a39890adf0b08d4494b081ef6740c25b29946e6 Mon Sep 17 00:00:00 2001 From: Jakub Sztandera Date: Thu, 22 Aug 2024 15:22:32 +0200 Subject: [PATCH 2/6] Keep all tickets around Signed-off-by: Jakub Sztandera --- gpbft/gpbft.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gpbft/gpbft.go b/gpbft/gpbft.go index a2d61a2a..c5ba70dd 100644 --- a/gpbft/gpbft.go +++ b/gpbft/gpbft.go @@ -1305,8 +1305,8 @@ func (c *convergeState) Receive(sender ActorID, value ECChain, ticket Ticket, ju // Keep only the first justification and ticket received for a value. if _, found := c.values[key]; !found { c.values[key] = ConvergeValue{Chain: value, Justification: justification} - c.tickets[key] = append(c.tickets[key], ConvergeTicket{Sender: sender, Ticket: ticket}) } + c.tickets[key] = append(c.tickets[key], ConvergeTicket{Sender: sender, Ticket: ticket}) return nil } From 7132ed6b5c2e5589518974d8a9c71117448aafeb Mon Sep 17 00:00:00 2001 From: Jakub Sztandera Date: Thu, 22 Aug 2024 15:39:47 +0200 Subject: [PATCH 3/6] Increase participants to 50, broken again Signed-off-by: Jakub Sztandera --- test/deny_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/deny_test.go b/test/deny_test.go index c59e605e..e21e37f6 100644 --- a/test/deny_test.go +++ b/test/deny_test.go @@ -41,7 +41,7 @@ func TestDenyQuality(t *testing.T) { instanceCount = 20 maxRounds = 30 gst = 1 * time.Second - participants = 30 + participants = 50 ) ecGen := sim.NewUniformECChainGenerator(4332432, 1, 5) From 7d7701a0f81407166e23e85da1efa23f06c7c64a Mon Sep 17 00:00:00 2001 From: Jakub Sztandera Date: Thu, 22 Aug 2024 15:59:22 +0200 Subject: [PATCH 4/6] Janky (float based) fix Signed-off-by: Jakub Sztandera --- gpbft/gpbft.go | 15 ++++++++++----- test/deny_test.go | 3 +-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/gpbft/gpbft.go b/gpbft/gpbft.go index c5ba70dd..6dff2e7a 100644 --- a/gpbft/gpbft.go +++ b/gpbft/gpbft.go @@ -1318,23 +1318,28 @@ func (c *convergeState) FindMaxTicketProposal(table PowerTable) ConvergeValue { // If the same ticket is used for two different values then either we get a decision on one of them // only or we go to a new round. Eventually there is a round where the max ticket is held by a // correct participant, who will not double vote. - var maxTicket *big.Int var maxValue ConvergeValue + var minTicket float64 = math.Inf(1) for key, value := range c.values { for _, ticket := range c.tickets[key] { senderPower, _ := table.Get(ticket.Sender) ticketHash := blake2b.Sum256(ticket.Ticket) ticketAsInt := new(big.Int).SetBytes(ticketHash[:]) - weightedTicket := new(big.Int).Mul(ticketAsInt, big.NewInt(int64(senderPower))) - if maxTicket == nil || weightedTicket.Cmp(maxTicket) > 0 { - maxTicket = weightedTicket + + // here comes the jank before I write proper math + ticketF, _ := new(big.Int).Rsh(ticketAsInt, 256-52).Float64() + ticketF = ticketF / float64(1<<52) // create float64 in [0, 1) based on ticket + ticketF = -math.Log(ticketF) / float64(senderPower) // change the ticket from uniform to exponentital + + if math.IsInf(minTicket, 1) || ticketF < minTicket { + minTicket = ticketF maxValue = value } } } - if maxTicket == nil && c.HasSelfValue() { + if math.IsInf(minTicket, 1) && c.HasSelfValue() { return *c.self } return maxValue diff --git a/test/deny_test.go b/test/deny_test.go index e21e37f6..3a4d6bad 100644 --- a/test/deny_test.go +++ b/test/deny_test.go @@ -3,7 +3,6 @@ package test import ( "math" "testing" - "time" "github.com/filecoin-project/go-f3/gpbft" "github.com/filecoin-project/go-f3/sim" @@ -40,7 +39,7 @@ func TestDenyQuality(t *testing.T) { const ( instanceCount = 20 maxRounds = 30 - gst = 1 * time.Second + gst = 10 * EcEpochDuration participants = 50 ) From 1c0c2b5823c068b022c276931a2d8ec50da0af5c Mon Sep 17 00:00:00 2001 From: Jakub Sztandera Date: Fri, 23 Aug 2024 20:58:03 +0200 Subject: [PATCH 5/6] Implement hybrid precision ticket function Signed-off-by: Jakub Sztandera --- gpbft/gpbft.go | 70 ++++++++++++++++++------------------ gpbft/ticket_quality.go | 69 +++++++++++++++++++++++++++++++++++ gpbft/ticket_quality_test.go | 58 ++++++++++++++++++++++++++++++ 3 files changed, 162 insertions(+), 35 deletions(-) create mode 100644 gpbft/ticket_quality.go create mode 100644 gpbft/ticket_quality_test.go diff --git a/gpbft/gpbft.go b/gpbft/gpbft.go index 6dff2e7a..ad2c0414 100644 --- a/gpbft/gpbft.go +++ b/gpbft/gpbft.go @@ -7,7 +7,6 @@ import ( "errors" "fmt" "math" - "math/big" "slices" "sort" "time" @@ -16,7 +15,6 @@ import ( rlepluslazy "github.com/filecoin-project/go-bitfield/rle" "github.com/filecoin-project/go-f3/merkle" "go.opentelemetry.io/otel/metric" - "golang.org/x/crypto/blake2b" ) type Phase uint8 @@ -378,7 +376,7 @@ func (i *instance) receiveOne(msg *GMessage) (bool, error) { // Receive each prefix of the proposal independently. i.quality.ReceiveEachPrefix(msg.Sender, msg.Vote.Value) case CONVERGE_PHASE: - if err := msgRound.converged.Receive(msg.Sender, msg.Vote.Value, msg.Ticket, msg.Justification); err != nil { + if err := msgRound.converged.Receive(msg.Sender, i.powerTable, msg.Vote.Value, msg.Ticket, msg.Justification); err != nil { return false, fmt.Errorf("failed processing CONVERGE message: %w", err) } case PREPARE_PHASE: @@ -1252,25 +1250,22 @@ type convergeState struct { senders map[ActorID]struct{} // Chains indexed by key. values map[ChainKey]ConvergeValue - // Tickets provided by proposers of each chain. - tickets map[ChainKey][]ConvergeTicket } type ConvergeValue struct { Chain ECChain Justification *Justification -} -type ConvergeTicket struct { - Sender ActorID - Ticket Ticket + // these are tracked on per ticket basis + Sender ActorID + Ticket Ticket + Quality float64 } func newConvergeState() *convergeState { return &convergeState{ senders: map[ActorID]struct{}{}, values: map[ChainKey]ConvergeValue{}, - tickets: map[ChainKey][]ConvergeTicket{}, } } @@ -1292,7 +1287,7 @@ func (c *convergeState) HasSelfValue() bool { // Receives a new CONVERGE value from a sender. // Ignores any subsequent value from a sender from which a value has already been received. -func (c *convergeState) Receive(sender ActorID, value ECChain, ticket Ticket, justification *Justification) error { +func (c *convergeState) Receive(sender ActorID, table PowerTable, value ECChain, ticket Ticket, justification *Justification) error { if value.IsZero() { return fmt.Errorf("bottom cannot be justified for CONVERGE") } @@ -1302,11 +1297,28 @@ func (c *convergeState) Receive(sender ActorID, value ECChain, ticket Ticket, ju c.senders[sender] = struct{}{} key := value.Key() - // Keep only the first justification and ticket received for a value. - if _, found := c.values[key]; !found { - c.values[key] = ConvergeValue{Chain: value, Justification: justification} + senderPower, _ := table.Get(sender) + // Keep only the first justification and best ticket + if v, found := c.values[key]; !found { + c.values[key] = ConvergeValue{ + Chain: value, + Justification: justification, + + Sender: sender, + Ticket: ticket, + Quality: ComputeTicketQuality(ticket, senderPower), + } + } else { + newQual := ComputeTicketQuality(ticket, senderPower) + // best ticket is lowest + if newQual < v.Quality { + v.Sender = sender + v.Ticket = ticket + v.Quality = newQual + + c.values[key] = v + } } - c.tickets[key] = append(c.tickets[key], ConvergeTicket{Sender: sender, Ticket: ticket}) return nil } @@ -1318,31 +1330,19 @@ func (c *convergeState) FindMaxTicketProposal(table PowerTable) ConvergeValue { // If the same ticket is used for two different values then either we get a decision on one of them // only or we go to a new round. Eventually there is a round where the max ticket is held by a // correct participant, who will not double vote. - var maxValue ConvergeValue - var minTicket float64 = math.Inf(1) - - for key, value := range c.values { - for _, ticket := range c.tickets[key] { - senderPower, _ := table.Get(ticket.Sender) - ticketHash := blake2b.Sum256(ticket.Ticket) - ticketAsInt := new(big.Int).SetBytes(ticketHash[:]) - - // here comes the jank before I write proper math - ticketF, _ := new(big.Int).Rsh(ticketAsInt, 256-52).Float64() - ticketF = ticketF / float64(1<<52) // create float64 in [0, 1) based on ticket - ticketF = -math.Log(ticketF) / float64(senderPower) // change the ticket from uniform to exponentital - - if math.IsInf(minTicket, 1) || ticketF < minTicket { - minTicket = ticketF - maxValue = value - } + var bestValue ConvergeValue + bestValue.Quality = math.Inf(1) + + for _, value := range c.values { + if value.Quality < bestValue.Quality { + bestValue = value } } - if math.IsInf(minTicket, 1) && c.HasSelfValue() { + if math.IsInf(bestValue.Quality, 1) && c.HasSelfValue() { return *c.self } - return maxValue + return bestValue } // Finds some proposal which matches a specific value. diff --git a/gpbft/ticket_quality.go b/gpbft/ticket_quality.go new file mode 100644 index 00000000..883814c2 --- /dev/null +++ b/gpbft/ticket_quality.go @@ -0,0 +1,69 @@ +package gpbft + +import ( + "fmt" + "math" + "math/big" + + "golang.org/x/crypto/blake2b" +) + +// ComputeTicketQuality computes the quality of the ticket. +// The lower the resulting quality the better. +// We take the ticket, hash it using Blake2b256, take the low 128 bits, interpret them as a Q.128 +// fixed point number in range of [0, 1). Then we convert this uniform distribution into exponential one, +// using -log(x) inverse distribution function. +// The exponential distribution has a property where minimum of two exponentially distributed random +// variables is itself a exponentially distributed. +// This allows us to use the rate parameter to weight across different participants according to there power. +// This ends up being `-log(ticket) / power` where ticket is [0, 1). +// We additionally use log-base-2 instead of natural logarithm as it is easier to implement, +// and it is just a linear factor on all tickets, meaning it does not influence their ordering. +func ComputeTicketQuality(ticket []byte, power uint16) float64 { + // we could use Blake2b-128 but 256 is more common and more widely supported + ticketHash := blake2b.Sum256(ticket) + quality := linearToExpDist(ticketHash[:16]) + return quality / float64(power) +} + +// ticket should be 16 bytes +func linearToExpDist(ticket []byte) float64 { + // we are interpreting the ticket as fixed-point number with 128 fractional bits + // and adjusting using exponential distribution inverse function, -log(x) + // we are computing Log2 of it with the adjustment that Log2(0) == -129 + // we can use Log2 instead of Ln as the difference is linear transform between them which + // has no relative effect + asInt := new(big.Int).SetBytes(ticket) // interpret at Q.128 + log2Int, log2Frac := bigLog2(asInt) + // combine integer and fractional parts, in theory we could operate on them separately + // but the 7bit gain on top of 52bits is minor + log2 := float64(log2Int) + log2Frac + return -log2 +} + +// bigLog2 takes an approximate logarithm of the big integer interpreted as Q.128 +// If the input is zero, the output is [-129, 0.f). +// The result is an integer and fraction, where fraction is in [0, 1) +func bigLog2(asInt *big.Int) (int64, float64) { + bitLen := uint(asInt.BitLen()) + if bitLen == 0 { + return -129, 0. + } + log2Int := -int64(128 - bitLen + 1) //integer part of the Log2 + // now that we saved the integer part, we want to interpret it as [1,2) + // so it will be Q.(bitlen-1) + // to convert to float exactly, we need to bring it down to 53 bits + if bitLen > 53 { + asInt = asInt.Rsh(asInt, bitLen-53) + } else if bitLen < 53 { + asInt = asInt.Lsh(asInt, 53-bitLen) + } + if asInt.BitLen() != 53 { + panic(fmt.Sprintf("wrong bitlen: %v", asInt.BitLen())) + } + asFloat := float64(asInt.Uint64()) / (1 << 52) + if asFloat < 1 || asFloat >= 2 { + panic("wrong range") + } + return log2Int, math.Log2(asFloat) +} diff --git a/gpbft/ticket_quality_test.go b/gpbft/ticket_quality_test.go new file mode 100644 index 00000000..f1952138 --- /dev/null +++ b/gpbft/ticket_quality_test.go @@ -0,0 +1,58 @@ +package gpbft + +import ( + "bytes" + "math/big" + "runtime" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestTQ_BigLog2_Table(t *testing.T) { + tests := []struct { + name string + input string + integer int64 + fract float64 + }{ + {"0.(9)", "ffffffffffffffffffffffffffffffff", -1, 0.9999999999999999}, + {"0.(9)8", "fffffffffffff8000000000000000000", -1, 0.9999999999999999}, + {"0.(9)7", "fffffffffffff7000000000000000000", -1, 0.9999999999999997}, + {"0.5", "80000000000000000000000000000000", -1, 0.0}, + {"2^-128", "1", -128, 0.0}, + {"2^-127", "2", -127, 0.0}, + {"2^-127 + eps", "3", -127, 0.5849625007211563}, + {"zero", "0", -129, 0.0}, + {"medium", "10020000000000000", -64, 0.0007042690112466499}, + {"medium2", "1000000000020000000000000", -32, 1.6409096303959814e-13}, + {"2^(53-128)", "20000000000000", -75, 0.0}, + {"2^(53-128)+eps", "20000000000001", -75, 0.0}, + {"2^(53-128)-eps", "1fffffffffffff", -76, 0.9999999999999999}, + {"2^(53-128)-2eps", "1ffffffffffff3", -76, 0.9999999999999979}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + bigInt, ok := new(big.Int).SetString(test.input, 16) + require.True(t, ok, "parsing int") + integer, fract := bigLog2(bigInt) + assert.EqualValues(t, test.integer, integer, "wrong integer part") + assert.EqualValues(t, test.fract, fract, "wrong fractional part") + }) + } +} + +func FuzzTQ_linearToExp(f *testing.F) { + f.Add(make([]byte, 16)) + f.Add(bytes.Repeat([]byte{0xff}, 16)) + f.Add(bytes.Repeat([]byte{0xa0}, 16)) + f.Fuzz(func(t *testing.T, ticket []byte) { + if len(ticket) != 16 { + return + } + q := linearToExpDist(ticket) + runtime.KeepAlive(q) + }) +} From 72979627f6c7816086a33ebc19285da4e008ffee Mon Sep 17 00:00:00 2001 From: Jakub Sztandera Date: Fri, 23 Aug 2024 22:39:39 +0200 Subject: [PATCH 6/6] Cleanup self-converge value Signed-off-by: Jakub Sztandera --- gpbft/gpbft.go | 85 ++++++++++++++++++++++++-------------------------- 1 file changed, 41 insertions(+), 44 deletions(-) diff --git a/gpbft/gpbft.go b/gpbft/gpbft.go index ad2c0414..a520c68b 100644 --- a/gpbft/gpbft.go +++ b/gpbft/gpbft.go @@ -435,7 +435,7 @@ func (i *instance) shouldSkipToRound(round uint64, state *roundState) (ECChain, return nil, nil, false } proposal := state.converged.FindMaxTicketProposal(i.powerTable) - if proposal.Justification == nil { + if !proposal.IsValid() { // FindMaxTicketProposal returns a zero-valued ConvergeValue if no such ticket is // found. Hence the check for nil. Otherwise, if found such ConvergeValue must // have a non-nil justification. @@ -545,7 +545,7 @@ func (i *instance) tryConverge() error { } winner := i.getRound(i.round).converged.FindMaxTicketProposal(i.powerTable) - if winner.Chain.IsZero() { + if !winner.IsValid() { return fmt.Errorf("no values at CONVERGE") } possibleDecisionLastRound := i.getRound(i.round-1).committed.CouldReachStrongQuorumFor( @@ -564,8 +564,8 @@ func (i *instance) tryConverge() error { // Else preserve own proposal. // This could alternatively loop to next lowest ticket as an optimisation to increase the // chance of proposing the same value as other participants. - fallback, ok := i.getRound(i.round).converged.FindProposalFor(i.proposal) - if !ok { + fallback := i.getRound(i.round).converged.FindProposalFor(i.proposal) + if !fallback.IsValid() { panic("own proposal not found at CONVERGE") } justification = fallback.Justification @@ -1243,25 +1243,32 @@ func (q *quorumState) FindStrongQuorumValue() (quorumValue ECChain, foundQuorum //// CONVERGE phase helper ///// type convergeState struct { - // Stores this participant's value so the participant can use it even if it doesn't receive its own - // CONVERGE message (which carries the ticket) in a timely fashion. - self *ConvergeValue // Participants from which a message has been received. senders map[ActorID]struct{} // Chains indexed by key. values map[ChainKey]ConvergeValue } +// ConvergeValue is valid when the Chain is non-zero and Justification is non-nil type ConvergeValue struct { Chain ECChain Justification *Justification - // these are tracked on per ticket basis - Sender ActorID - Ticket Ticket Quality float64 } +// TakeBetter merges the argument into the ConvergeValue if the ConvergeValue is zero valued or +// if argument is better due to quality. +func (cv *ConvergeValue) TakeBetter(cv2 ConvergeValue) { + if !cv.IsValid() || cv2.Quality < cv.Quality { + *cv = cv2 + } +} + +func (cv *ConvergeValue) IsValid() bool { + return !cv.Chain.IsZero() && cv.Justification != nil +} + func newConvergeState() *convergeState { return &convergeState{ senders: map[ActorID]struct{}{}, @@ -1273,92 +1280,82 @@ func newConvergeState() *convergeState { // This means the participant need not rely on messages broadcast to be received by itself. // See HasSelfValue. func (c *convergeState) SetSelfValue(value ECChain, justification *Justification) { - c.self = &ConvergeValue{ - Chain: value, - Justification: justification, + // any converge for the given value is better than self-reported + // as self-reported has no ticket + key := value.Key() + if _, ok := c.values[key]; !ok { + c.values[key] = ConvergeValue{ + Chain: value, + Justification: justification, + Quality: math.Inf(1), // +Inf because any real ConvergeValue is better than self-value + } } } -// HasSelfValue checks whether the participant recorded a converge value. -// See SetSelfValue. -func (c *convergeState) HasSelfValue() bool { - return c.self != nil -} - // Receives a new CONVERGE value from a sender. // Ignores any subsequent value from a sender from which a value has already been received. func (c *convergeState) Receive(sender ActorID, table PowerTable, value ECChain, ticket Ticket, justification *Justification) error { if value.IsZero() { return fmt.Errorf("bottom cannot be justified for CONVERGE") } + if justification == nil { + return fmt.Errorf("CONVERGE message cannot carry nil-justification") + } + if _, ok := c.senders[sender]; ok { return nil } c.senders[sender] = struct{}{} - key := value.Key() - senderPower, _ := table.Get(sender) + + key := value.Key() // Keep only the first justification and best ticket if v, found := c.values[key]; !found { c.values[key] = ConvergeValue{ Chain: value, Justification: justification, - - Sender: sender, - Ticket: ticket, - Quality: ComputeTicketQuality(ticket, senderPower), + Quality: ComputeTicketQuality(ticket, senderPower), } } else { newQual := ComputeTicketQuality(ticket, senderPower) // best ticket is lowest if newQual < v.Quality { - v.Sender = sender - v.Ticket = ticket v.Quality = newQual - c.values[key] = v } } return nil } -// FindMaxTicketProposal finds the value with the highest ticket, weighted by -// sender power. Returns the self value (which may be zero) if and only if no -// other value is found. +// FindMaxTicketProposal finds the value with the best ticket, weighted by +// sender power. Returns an invalid (zero-value) ConvergeValue if no converge is found. func (c *convergeState) FindMaxTicketProposal(table PowerTable) ConvergeValue { // Non-determinism in case of matching tickets from an equivocation is ok. // If the same ticket is used for two different values then either we get a decision on one of them // only or we go to a new round. Eventually there is a round where the max ticket is held by a // correct participant, who will not double vote. + var bestValue ConvergeValue - bestValue.Quality = math.Inf(1) for _, value := range c.values { - if value.Quality < bestValue.Quality { - bestValue = value - } + bestValue.TakeBetter(value) } - if math.IsInf(bestValue.Quality, 1) && c.HasSelfValue() { - return *c.self - } return bestValue } // Finds some proposal which matches a specific value. // This searches values received in messages first, falling back to the participant's self value // only if necessary. -func (c *convergeState) FindProposalFor(chain ECChain) (ConvergeValue, bool) { +func (c *convergeState) FindProposalFor(chain ECChain) ConvergeValue { for _, value := range c.values { if value.Chain.Eq(chain) { - return value, true + return value } } - if c.HasSelfValue() && c.self.Chain.Eq(chain) { - return *c.self, true - } - return ConvergeValue{}, false + // Default converge value is not valid + return ConvergeValue{} } type broadcastState struct {