From e8c84db6bb762c89ee5ae96dc90a400488932fc9 Mon Sep 17 00:00:00 2001 From: Alejandro Ranchal-Pedrosa Date: Fri, 5 Jan 2024 15:53:56 +0100 Subject: [PATCH] Refactor & Implement explicit justification within modular framework --- adversary/equiv.go | 32 +++- f3/api.go | 6 +- f3/chain.go | 4 - f3/granite.go | 374 +++++++++++++++++++++++++++++++++++++++------ f3/participant.go | 2 +- go.mod | 14 ++ go.sum | 43 ++++++ sim/network.go | 110 +++++++++---- test/equiv_test.go | 2 +- 9 files changed, 504 insertions(+), 83 deletions(-) diff --git a/adversary/equiv.go b/adversary/equiv.go index d9596e07..f8f85de6 100644 --- a/adversary/equiv.go +++ b/adversary/equiv.go @@ -1,6 +1,7 @@ package adversary import ( + "github.com/filecoin-project/go-bitfield" "github.com/filecoin-project/go-f3/f3" "github.com/filecoin-project/go-f3/sim" ) @@ -14,6 +15,7 @@ type WitholdCommit struct { // The first victim is the target, others are those who need to confirm. victims []f3.ActorID victimValue f3.ECChain + senderIndex *f3.SenderIndex } // A participant that never sends anything. @@ -29,6 +31,10 @@ func (w *WitholdCommit) SetVictim(victims []f3.ActorID, victimValue f3.ECChain) w.victimValue = victimValue } +func (w *WitholdCommit) SetSenderIndex(powerTable f3.PowerTable) { + w.senderIndex = f3.NewSenderIndex(powerTable) +} + func (w *WitholdCommit) ID() f3.ActorID { return w.id } @@ -64,14 +70,36 @@ func (w *WitholdCommit) Begin() { Value: w.victimValue, Signature: w.host.Sign(w.id, f3.SignaturePayload(0, 0, f3.PREPARE, w.victimValue)), }) - w.host.BroadcastSynchronous(w.id, f3.GMessage{ + + message := f3.GMessage{ Sender: w.id, Instance: 0, Round: 0, Step: f3.COMMIT, Value: w.victimValue, Signature: w.host.Sign(w.id, f3.SignaturePayload(0, 0, f3.COMMIT, w.victimValue)), - }) + } + payload := f3.SignaturePayload(0, 0, f3.PREPARE, w.victimValue) + aggEvidence := f3.AggEvidence{ + Step: f3.PREPARE, + Value: w.victimValue, + Instance: 0, + Round: 0, + Signers: bitfield.New(), + Signature: nil, + } + for _, actorID := range w.victims { + signature := w.host.Sign(actorID, payload) + aggSignature, signers := w.host.Aggregate(signature, actorID, aggEvidence.Signature, &aggEvidence.Signers, w.senderIndex.Actor2Index) + aggEvidence.Signature = aggSignature + aggEvidence.Signers = *signers + } + signature := w.host.Sign(w.id, payload) + aggSignature, signers := w.host.Aggregate(signature, w.id, aggEvidence.Signature, &aggEvidence.Signers, w.senderIndex.Actor2Index) + aggEvidence.Signature = aggSignature + aggEvidence.Signers = *signers + message.Evidence = aggEvidence + w.host.BroadcastSynchronous(w.id, message) } func (w *WitholdCommit) AllowMessage(_ f3.ActorID, to f3.ActorID, msg f3.Message) bool { diff --git a/f3/api.go b/f3/api.go index 1e5e78ad..5ab3e11f 100644 --- a/f3/api.go +++ b/f3/api.go @@ -1,5 +1,7 @@ package f3 +import "github.com/filecoin-project/go-bitfield" + // Receives EC chain values. type ChainReceiver interface { // Receives a chain appropriate for use as initial proposals for a Granite instance. @@ -53,9 +55,9 @@ type Signer interface { type Aggregator interface { // Aggregates signatures from a participant to an existing signature. - Aggregate(msg, sig []byte, aggSignature []byte) []byte + Aggregate(sig []byte, senderID ActorID, aggSignature []byte, signers *bitfield.BitField, actor2Index map[ActorID]uint64) ([]byte, *bitfield.BitField) // VerifyAggregate verifies an aggregate signature. - VerifyAggregate(msg, aggSig []byte, signers []byte) bool + VerifyAggregate(msg, aggSig []byte, signers *bitfield.BitField, actor2Index map[ActorID]uint64) bool } // Participant interface to the host system resources. diff --git a/f3/chain.go b/f3/chain.go index 74ecc7e8..ba3e5210 100644 --- a/f3/chain.go +++ b/f3/chain.go @@ -69,10 +69,6 @@ func NewChain(base TipSet, suffix ...TipSet) ECChain { return append([]TipSet{base}, suffix...) } -func ZeroECChain() ECChain { - return ECChain{} -} - func (c ECChain) IsZero() bool { return len(c) == 0 } diff --git a/f3/granite.go b/f3/granite.go index 1a5f0df9..51fdbb09 100644 --- a/f3/granite.go +++ b/f3/granite.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/binary" "fmt" + "github.com/filecoin-project/go-bitfield" "sort" ) @@ -49,17 +50,16 @@ type GMessage struct { // Aggregated list of GossiPBFT messages with the same instance, round and value. Used as evidence for justification of messages type AggEvidence struct { - // Enumeration of QUALITY, PREPARE, COMMIT, CONVERGE, DECIDE - Step string - // Chain of tipsets proposed/voted for finalisation in this instance. - // Non-empty: the first entry is the base tipset finalised in instance-1 - Value ECChain - // GossiPBFT instance number Instance uint32 - // GossiPBFT round + Round uint32 + + Step string + + Value ECChain + // Indexes in the base power table of the signers (bitset) - Signers []byte + Signers bitfield.BitField // BLS aggregate signature of signers Signature []byte } @@ -69,7 +69,11 @@ func ZeroAggEvidence() AggEvidence { } func (a AggEvidence) isZero() bool { - return a.Step == "" && a.Value.IsZero() && a.Instance == 0 && a.Round == 0 && len(a.Signers) == 0 && len(a.Signature) == 0 + signersCount, err := a.Signers.Count() + if err != nil { + panic(err) + } + return a.Step == "" && a.Value.IsZero() && a.Instance == 0 && a.Round == 0 && signersCount == 0 && len(a.Signature) == 0 } func (m GMessage) String() string { @@ -130,8 +134,11 @@ type instance struct { rounds map[uint32]*roundState // Acceptable chain acceptable ECChain - //TODO Comment (here and everywhere really) + //quorumStateType defines the message justification method, either EXPLICIT for messages attaching a BLS-aggregate of other messages that justify it + // or IMPLICIT for messages being justified by previously received messages quorumStateType quorumStateType + //senderIndex + senderIndex *SenderIndex } func newInstance( @@ -147,6 +154,11 @@ func newInstance( if input.IsZero() { panic("input is empty") } + var senderIndex *SenderIndex + if quorumStateType == EXPLICIT { + senderIndex = NewSenderIndex(powerTable) + } + return &instance{ config: config, host: host, @@ -161,12 +173,13 @@ func newInstance( proposal: input, value: ECChain{}, pending: newPendingQueue(), - quality: newQuorumState(quorumStateType, powerTable), + quality: newQuorumState(quorumStateType, powerTable, host, instanceID, 0, QUALITY, senderIndex), rounds: map[uint32]*roundState{ - 0: newRoundState(powerTable, quorumStateType), + 0: newRoundState(powerTable, quorumStateType, host, instanceID, 0, senderIndex), }, acceptable: input, quorumStateType: quorumStateType, + senderIndex: senderIndex, } } @@ -176,11 +189,11 @@ type roundState struct { committed quorumState } -func newRoundState(powerTable PowerTable, quorumStateType quorumStateType) *roundState { +func newRoundState(powerTable PowerTable, quorumStateType quorumStateType, aggregator Aggregator, instanceID uint32, round uint32, senderIndex *SenderIndex) *roundState { return &roundState{ converged: newConvergeState(), - prepared: newQuorumState(quorumStateType, powerTable), - committed: newQuorumState(quorumStateType, powerTable), + prepared: newQuorumState(quorumStateType, powerTable, aggregator, instanceID, round, PREPARE, senderIndex), + committed: newQuorumState(quorumStateType, powerTable, aggregator, instanceID, round, COMMIT, senderIndex), } } @@ -256,13 +269,15 @@ func (i *instance) receiveOne(msg *GMessage) { return } - // Hold (if using Implicit justification) as pending any message with a value not yet justified by the prior phase. + // Hold (if using IMPLICIT justification) as pending any message with a value not yet justified by the prior phase. if !i.isJustified(msg) { - if i.quorumStateType == Implicit { + if i.quorumStateType == IMPLICIT { i.log("enqueue %s", msg) i.pending.Add(msg) + } else if i.quorumStateType == EXPLICIT { + // Ignore message + return } - return } round := i.roundState(msg.Round) @@ -341,8 +356,13 @@ func (i *instance) isJustified(msg *GMessage) bool { return false } prevRound := i.roundState(msg.Round - 1) - return prevRound.prepared.isJustified(msg, msg.Value) || - prevRound.committed.isJustified(msg, ZeroECChain()) + if msg.Evidence.Step == PREPARE { + return prevRound.prepared.IsJustified(msg, msg.Value) + } else if msg.Evidence.Step == COMMIT { + return prevRound.committed.IsJustified(msg, ECChain{}) + } else { + return false + } } else if msg.Step == PREPARE { // PREPARE needs no justification by prior messages. return true // i.quality.AllowsValue(msg.Value) @@ -350,7 +370,8 @@ func (i *instance) isJustified(msg *GMessage) bool { // COMMIT is justified by strong quorum of PREPARE from the same round with the same value. // COMMIT for bottom is always justified. round := i.roundState(msg.Round) - return msg.Value.IsZero() || round.prepared.isJustified(msg, msg.Value) + + return msg.Value.IsZero() || round.prepared.IsJustified(msg, msg.Value) } return false } @@ -392,9 +413,31 @@ func (i *instance) beginConverge() { i.phase = CONVERGE ticket := i.vrf.MakeTicket(i.beacon, i.instanceID, i.round, i.participantID) i.phaseTimeout = i.alarmAfterSynchrony(CONVERGE) - aggEvidence, isJustified := i.roundState(i.round-1).committed.Justify(i.proposal, ZeroECChain()) + aggEvidence, isJustified := i.roundState(i.round - 1).committed.Justify(ECChain{}) if !isJustified { - aggEvidence, isJustified = i.roundState(i.round-1).prepared.Justify(i.proposal, i.proposal) + aggEvidence, isJustified = i.roundState(i.round - 1).prepared.Justify(i.proposal) + } + if !isJustified && i.quorumStateType == EXPLICIT { + preparedStateExplicit, ok := i.roundState(i.round - 1).prepared.(*quorumStateExplicit) + if ok { + if !i.proposal.IsZero() { + aggEvidence, ok = preparedStateExplicit.justifiedMessages[i.proposal.HeadCIDOrZero()] + + if ok { + isJustified = true + } + } + } else { + committedStateExplicit, ok := i.roundState(i.round - 1).committed.(*quorumStateExplicit) + if ok { + if !i.proposal.IsZero() { + aggEvidence, ok = committedStateExplicit.justifiedMessages[ZeroTipSetID()] + if ok { + isJustified = true + } + } + } + } } if !isJustified { // error, there should be a justification for CONVERGE, otherwise we would not be here now @@ -469,13 +512,15 @@ func (i *instance) beginCommit() { isJustified bool aggEvidence = ZeroAggEvidence() ) + if !i.value.IsZero() { // if it is zero then justification is not really needed - aggEvidence, isJustified = i.roundState(i.round).prepared.Justify(i.value, i.value) + aggEvidence, isJustified = i.roundState(i.round).prepared.Justify(i.value) if !isJustified { // error, there should be a justification for a non-bottom COMMIT, otherwise we would not be here now panic(fmt.Sprintf("no justification for COMMIT %v", i.value)) } } + i.broadcast(COMMIT, i.value, nil, aggEvidence) } @@ -495,18 +540,22 @@ func (i *instance) tryCommit(round uint32) { // Adopt any non-empty value committed by another participant (there can only be one). // This node has observed the strong quorum of PREPARE messages that justify it, // and mean that some other nodes may decide that value (if they observe more COMMITs). + + // committedExplicit := committed.(*quorumStateExplicit) + for _, v := range committed.ListAllValues() { if !v.IsZero() { if !i.isAcceptable(v) { i.log("⚠️ swaying from %s to %s by COMMIT", &i.input, &v) + } if !v.Eq(i.proposal) { i.proposal = v i.log("adopting proposal %s after commit", &i.proposal) + } break } - } i.beginNextRound() } @@ -515,7 +564,7 @@ func (i *instance) tryCommit(round uint32) { func (i *instance) roundState(r uint32) *roundState { round, ok := i.rounds[r] if !ok { - round = newRoundState(i.powerTable, i.quorumStateType) + round = newRoundState(i.powerTable, i.quorumStateType, i.host, i.instanceID, r, i.senderIndex) i.rounds[r] = round } return round @@ -625,8 +674,8 @@ func (v *pendingQueue) getRound(round uint32) map[string][]*GMessage { type quorumState interface { Receive(sender ActorID, value ECChain, sig []byte) - isJustified(msg *GMessage, justification ECChain) bool - Justify(value ECChain, justification ECChain) (AggEvidence, bool) + IsJustified(msg *GMessage, justification ECChain) bool + Justify(justification ECChain) (AggEvidence, bool) HasQuorumAgreement(cid TipSetID) bool ListQuorumAgreedValues() []ECChain @@ -665,27 +714,31 @@ type chainPower struct { type quorumStateType int const ( - Implicit quorumStateType = iota - Explicit + IMPLICIT quorumStateType = iota + EXPLICIT ) // Creates a new, empty quorum state. -func newQuorumState(quorumStateType quorumStateType, powerTable PowerTable) quorumState { +func newQuorumState(quorumStateType quorumStateType, powerTable PowerTable, aggregator Aggregator, instanceID uint32, round uint32, step string, senderIndex *SenderIndex) quorumState { switch quorumStateType { - case Implicit: - return &quorumStateImplicit{ - received: map[ActorID]senderSent{}, - chainPower: map[TipSetID]chainPower{}, - sendersTotalPower: 0, - powerTable: powerTable, - } - case Explicit: - panic("quorumStateType not yet considered: Explicit") + case IMPLICIT: + return newQuorumStateImplicit(powerTable) + case EXPLICIT: + return newQuorumStateExplicit(powerTable, aggregator, instanceID, round, step, senderIndex) //TODO This is suboptimal (as the senderIndex needs to be recalculated every time unnecessarily. Fix) default: panic(fmt.Sprintf("quorumStateType not considered: %v", quorumStateType)) } } +func newQuorumStateImplicit(powerTable PowerTable) *quorumStateImplicit { + return &quorumStateImplicit{ + received: map[ActorID]senderSent{}, + chainPower: map[TipSetID]chainPower{}, + sendersTotalPower: 0, + powerTable: powerTable, + } +} + // Receives a new chain from a sender. func (q *quorumStateImplicit) Receive(sender ActorID, value ECChain, _ []byte) { head := value.HeadCIDOrZero() @@ -766,12 +819,188 @@ func (q *quorumStateImplicit) ListQuorumAgreedValues() []ECChain { return withQuorum } -func (q *quorumStateImplicit) isJustified(_ *GMessage, value ECChain) bool { +func (q *quorumStateImplicit) IsJustified(_ *GMessage, value ECChain) bool { return q.HasQuorumAgreement(value.HeadCIDOrZero()) } -func (q *quorumStateImplicit) Justify(_ ECChain, _ ECChain) (AggEvidence, bool) { - return AggEvidence{}, true // Implicit justification +func (q *quorumStateImplicit) Justify(_ ECChain) (AggEvidence, bool) { + return AggEvidence{}, true // IMPLICIT justification +} + +type chainSupport struct { + chainPower chainPower + aggSignature []byte + signers *bitfield.BitField +} + +type quorumStateExplicit struct { + // Aggregator for BLS signatures. + aggregator Aggregator + // Whether a message has been received from each sender. + received map[ActorID]struct{} + // The power supporting each chain so far. + chainSupport map[TipSetID]chainSupport + // Total power of all distinct senders from which some chain has been received so far. + sendersTotalPower uint + // justifiedMessages stores the received evidences for each message, indexed by the message's head CID. + justifiedMessages map[TipSetID]AggEvidence + // Table of senders' power. + powerTable PowerTable + instanceID uint32 + round uint32 + step string + + senderIndex *SenderIndex +} + +func newQuorumStateExplicit(powerTable PowerTable, aggregator Aggregator, instanceID uint32, round uint32, step string, senderIndex *SenderIndex) *quorumStateExplicit { + return &quorumStateExplicit{ + aggregator: aggregator, + received: map[ActorID]struct{}{}, + chainSupport: map[TipSetID]chainSupport{}, + sendersTotalPower: 0, + justifiedMessages: map[TipSetID]AggEvidence{}, + powerTable: powerTable, + instanceID: instanceID, + round: round, + step: step, + senderIndex: senderIndex, + } +} + +// Receives a new chain from a sender. +func (q *quorumStateExplicit) Receive(sender ActorID, value ECChain, signature []byte) { + head := value.HeadCIDOrZero() + if _, ok := q.received[sender]; !ok { + // Add sender's power to total the first time a value is received from them. + senderPower := q.powerTable.Entries[sender] + q.sendersTotalPower += senderPower + q.received[sender] = struct{}{} + } + + candidate := chainSupport{ + chainPower: chainPower{ + chain: value, + power: 0, + hasQuorum: false, + }, + aggSignature: []byte{}, + signers: func() *bitfield.BitField { + bf := bitfield.New() + return &bf + }(), + } + + found, ok := q.chainSupport[head] + if !ok { + + found = candidate + } else { + + // Don't double-count the same chain head for a single participant. + isSet, err := found.signers.IsSet(q.senderIndex.Actor2Index[sender]) + if err != nil { + panic(err) + } + if isSet { + return + } + } + + candidate.chainPower.power = found.chainPower.power + q.powerTable.Entries[sender] + + candidate.aggSignature, candidate.signers = q.aggregator.Aggregate(signature, sender, found.aggSignature, found.signers, q.senderIndex.Actor2Index) + + threshold := q.powerTable.Total * 2 / 3 + if candidate.chainPower.power > threshold { + candidate.chainPower.hasQuorum = true + } + q.chainSupport[head] = candidate +} + +// IsJustified checks whether a message is justified by the aggregated signature contained in its Evidence field. +func (q *quorumStateExplicit) IsJustified(msg *GMessage, value ECChain) bool { + + if msg.Evidence.isZero() { + return false + } + + if msg.Evidence.Step != q.step || msg.Evidence.Instance != q.instanceID || msg.Evidence.Round != q.round { + return false // this quorumState should not be verifying this msg + } + + // Verify aggregated signature + if !q.aggregator.VerifyAggregate(SignaturePayload(q.instanceID, q.round, q.step, value), msg.Evidence.Signature, &msg.Evidence.Signers, q.senderIndex.Actor2Index) { + return false + } + + // Verify strong quorum + totalPower := uint(0) + + for i, j, count := uint64(0), 0, func() uint64 { + c, err := msg.Evidence.Signers.Count() // Error ignored + if err != nil { + panic(err) + } + return c + }(); i < count || j < len(q.senderIndex.index2Actor); { + isSet, err := msg.Evidence.Signers.IsSet(uint64(j)) + + if err != nil { + panic(err) + } + + if isSet { + totalPower += q.powerTable.Entries[q.senderIndex.index2Actor[j]] + if totalPower > q.powerTable.Total*2/3 { + + q.justifiedMessages[value.HeadCIDOrZero()] = msg.Evidence + + return true + } + i++ + } + + j++ + } + + return false +} + +// Justify returns the aggregated signature that justifies the given value, if it exists. +func (q *quorumStateExplicit) Justify(justification ECChain) (AggEvidence, bool) { + head := justification.HeadCIDOrZero() + + if !q.chainSupport[head].chainPower.hasQuorum { + return AggEvidence{}, false + } + + signers, err := q.chainSupport[head].signers.Copy() + if err != nil { + panic(err) + } + //copy aggSignature + aggSignature := make([]byte, len(q.chainSupport[head].aggSignature)) + copy(aggSignature, q.chainSupport[head].aggSignature) + aggEvidence := AggEvidence{ + Step: q.step, + Value: justification, + Instance: q.instanceID, + Round: q.round, + Signers: signers, + Signature: aggSignature, + } + return aggEvidence, true +} + +// Lists all values that have been received from any sender. +// The order of returned values is not defined. +func (q *quorumStateExplicit) ListAllValues() []ECChain { + var chains []ECChain + for _, cp := range q.chainSupport { + chains = append(chains, cp.chainPower.chain) + } + return chains } //// CONVERGE phase helper ///// @@ -790,6 +1019,30 @@ func newConvergeState() *convergeState { } } +// Checks whether a chain (head) has reached quorum. +func (q *quorumStateExplicit) HasQuorumAgreement(cid TipSetID) bool { + cp, ok := q.chainSupport[cid] + return ok && cp.chainPower.hasQuorum +} + +// Returns a list of the chains which have reached an agreeing quorum. +// The order of returned values is not defined. +func (q *quorumStateExplicit) ListQuorumAgreedValues() []ECChain { + var withQuorum []ECChain + for cid, cp := range q.chainSupport { + if cp.chainPower.hasQuorum { + withQuorum = append(withQuorum, q.chainSupport[cid].chainPower.chain) + } + } + sortByWeight(withQuorum) + return withQuorum +} + +// Checks whether at least one message has been received from a strong quorum of senders. +func (q *quorumStateExplicit) ReceivedFromQuorum() bool { + return q.sendersTotalPower > q.powerTable.Total*2/3 +} + // Receives a new CONVERGE value from a sender. func (c *convergeState) Receive(value ECChain, ticket Ticket) { if value.IsZero() { @@ -814,6 +1067,41 @@ func (c *convergeState) findMinTicketProposal() ECChain { return minValue } +// SenderIndex maps ActorID to a unique index in the range [0, len(powerTable.Entries)). +// This is used to index into a BitSet. index2Actor is the reverse mapping. +type SenderIndex struct { + Actor2Index map[ActorID]uint64 + index2Actor []ActorID +} + +func NewSenderIndex(table PowerTable) *SenderIndex { + senderIndex := &SenderIndex{ + Actor2Index: make(map[ActorID]uint64, len(table.Entries)), + index2Actor: make([]ActorID, len(table.Entries)), + } + keys := make([]ActorID, 0, len(table.Entries)) + + // Extract keys from the map + for senderID := range table.Entries { + keys = append(keys, senderID) + } + + // Sort the keys by descending power + sort.Slice(keys, func(i, j int) bool { + return table.Entries[keys[i]] > table.Entries[keys[j]] || (table.Entries[keys[i]] == table.Entries[keys[j]] && keys[i] < keys[j]) + }) + senderIndex.index2Actor = keys + + // Iterate over the sorted keys + var i uint64 = 0 + for _, senderID := range keys { + senderIndex.Actor2Index[senderID] = i + i++ + } + + return senderIndex +} + ///// General helpers ///// // Returns the first candidate value that is a prefix of the preferred value, or the base of preferred. diff --git a/f3/participant.go b/f3/participant.go index 00c75857..c26bca27 100644 --- a/f3/participant.go +++ b/f3/participant.go @@ -43,7 +43,7 @@ func (p *Participant) Finalised() (TipSet, uint32) { func (p *Participant) ReceiveCanonicalChain(chain ECChain, power PowerTable, beacon []byte) { p.nextChain = chain if p.granite == nil { - p.granite = newInstance(p.config, p.host, p.vrf, p.id, p.nextInstance, chain, power, beacon, Implicit) + p.granite = newInstance(p.config, p.host, p.vrf, p.id, p.nextInstance, chain, power, beacon, EXPLICIT) p.nextInstance += 1 p.granite.Start() } diff --git a/go.mod b/go.mod index 6448b116..6faabb59 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,20 @@ require github.com/stretchr/testify v1.8.2 require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/filecoin-project/go-bitfield v0.2.4 // indirect + github.com/ipfs/go-cid v0.0.5 // indirect + github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 // indirect + github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771 // indirect + github.com/mr-tron/base58 v1.1.3 // indirect + github.com/multiformats/go-base32 v0.0.3 // indirect + github.com/multiformats/go-multibase v0.0.1 // indirect + github.com/multiformats/go-multihash v0.0.13 // indirect + github.com/multiformats/go-varint v0.0.5 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/spaolacci/murmur3 v1.1.0 // indirect + github.com/whyrusleeping/cbor-gen v0.0.0-20200414195334-429a0b5e922e // indirect + golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8 // indirect + golang.org/x/sys v0.0.0-20190412213103-97732733099d // indirect + golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 51241904..f7eb1aa7 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,60 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/filecoin-project/go-bitfield v0.2.4 h1:uZ7MeE+XfM5lqrHJZ93OnhQKc/rveW8p9au0C68JPgk= +github.com/filecoin-project/go-bitfield v0.2.4/go.mod h1:CNl9WG8hgR5mttCnUErjcQjGvuiZjRqK9rHVBsQF4oM= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/gxed/hashland/keccakpg v0.0.1/go.mod h1:kRzw3HkwxFU1mpmPP8v1WyQzwdGfmKFJ6tItnhQ67kU= +github.com/gxed/hashland/murmur3 v0.0.1/go.mod h1:KjXop02n4/ckmZSnY2+HKcLud/tcmvhST0bie/0lS48= +github.com/ipfs/go-cid v0.0.3/go.mod h1:GHWU/WuQdMPmIosc4Yn1bcCT7dSeX4lBafM7iqUPQvM= +github.com/ipfs/go-cid v0.0.5 h1:o0Ix8e/ql7Zb5UVUJEUfjsWCIY8t48++9lR8qi6oiJU= +github.com/ipfs/go-cid v0.0.5/go.mod h1:plgt+Y5MnOey4vO4UlUazGqdbEXuFYitED67FexhXog= +github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 h1:lYpkrQH5ajf0OXOcUbGjvZxxijuBwbbmlSxLiuofa+g= +github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ= +github.com/minio/sha256-simd v0.0.0-20190131020904-2d45a736cd16/go.mod h1:2FMWW+8GMoPweT6+pI63m9YE3Lmw4J71hV56Chs1E/U= +github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771 h1:MHkK1uRtFbVqvAgvWxafZe54+5uBxLluGylDiKgdhwo= +github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= +github.com/mr-tron/base58 v1.1.0/go.mod h1:xcD2VGqlgYjBdcBLw+TuYLr8afG+Hj8g2eTVqeSzSU8= +github.com/mr-tron/base58 v1.1.3 h1:v+sk57XuaCKGXpWtVBX8YJzO7hMGx4Aajh4TQbdEFdc= +github.com/mr-tron/base58 v1.1.3/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/multiformats/go-base32 v0.0.3 h1:tw5+NhuwaOjJCC5Pp82QuXbrmLzWg7uxlMFp8Nq/kkI= +github.com/multiformats/go-base32 v0.0.3/go.mod h1:pLiuGC8y0QR3Ue4Zug5UzK9LjgbkL8NSQj0zQ5Nz/AA= +github.com/multiformats/go-multibase v0.0.1 h1:PN9/v21eLywrFWdFNsFKaU04kLJzuYzmrJR+ubhT9qA= +github.com/multiformats/go-multibase v0.0.1/go.mod h1:bja2MqRZ3ggyXtZSEDKpl0uO/gviWFaSteVbWT51qgs= +github.com/multiformats/go-multihash v0.0.1/go.mod h1:w/5tugSrLEbWqlcgJabL3oHFKTwfvkofsjW2Qa1ct4U= +github.com/multiformats/go-multihash v0.0.13 h1:06x+mk/zj1FoMsgNejLpy6QTvJqlSt/BhLEy87zidlc= +github.com/multiformats/go-multihash v0.0.13/go.mod h1:VdAWLKTwram9oKAatUcLxBNUjdtcVwxObEQBtRfuyjc= +github.com/multiformats/go-varint v0.0.5 h1:XVZwSo04Cs3j/jS0uAEPpT3JY6DzMcVLLoWOSnCxOjg= +github.com/multiformats/go-varint v0.0.5/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/whyrusleeping/cbor-gen v0.0.0-20200414195334-429a0b5e922e h1:JY8o/ebUUrCYetWmjRCNghxC59cOEaili83rxPRQCLw= +github.com/whyrusleeping/cbor-gen v0.0.0-20200414195334-429a0b5e922e/go.mod h1:Xj/M2wWU+QdTdRbu/L/1dIZY8/Wb2K9pAhtroQuxJJI= +golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8 h1:1wopBVtVdWnn03fZelqdXTqk7U7zPQCb+T4rbU9ZEoU= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190219092855-153ac476189d/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/sim/network.go b/sim/network.go index d9095b56..415f1ed8 100644 --- a/sim/network.go +++ b/sim/network.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/binary" "fmt" + "github.com/filecoin-project/go-bitfield" "github.com/filecoin-project/go-f3/f3" "io" "sort" @@ -136,58 +137,107 @@ func (n *Network) Verify(sender f3.ActorID, msg, sig []byte) bool { return true } -func (n *Network) Aggregate(msg, sig []byte, aggSignature []byte) []byte { +func (n *Network) Aggregate(sig []byte, actorID f3.ActorID, aggSignature []byte, signers *bitfield.BitField, actor2Index map[f3.ActorID]uint64) ([]byte, *bitfield.BitField) { // Fake implementation. // Just appends signature to aggregate signature. - // This fake aggregation is not commutative (order matters), unlike the real one. - return append(aggSignature, sig...) + // This fake aggregation is not commutative (order matters) + // But determinism is preserved by sorting by weight + // (That contains the sender ID in the signature) + if _, ok := actor2Index[actorID]; !ok { + panic("actorID not part of power table") + } + + // Extract existing signatures along with their actorIDs + signatures := [][]byte{} + buf := bytes.NewReader(aggSignature) + existingSigLen := len(sig) + for { + // The length of each existing signature (minus 8 bytes for the actorID) + existingSig := make([]byte, existingSigLen) + if _, err := io.ReadFull(buf, existingSig); err != nil { + if err == io.EOF { + break // End of the aggregate signature. + } else if err != nil { + panic(err) // Error in reading the signature. + } + } + signatures = append(signatures, existingSig) + } + signatures = append(signatures, sig) // Append the new signature + + // Sort the signatures based on descending order of actorID's index + sort.Slice(signatures, func(i, j int) bool { + actorIDI := binary.BigEndian.Uint64(signatures[i][:8]) + actorIDJ := binary.BigEndian.Uint64(signatures[j][:8]) + return actor2Index[f3.ActorID(actorIDI)] > actor2Index[f3.ActorID(actorIDJ)] + }) + + // Reconstruct the aggregated signature in sorted order + var updatedAggSignature []byte + for _, s := range signatures { + updatedAggSignature = append(updatedAggSignature, s...) + } + + signers.Set(actor2Index[actorID]) + + return updatedAggSignature, signers } -func (n *Network) VerifyAggregate(msg, aggSig []byte, signers []byte) bool { - // Fake implementation. - buf := bytes.NewReader(aggSig) - verifiedSigners := make([]byte, len(signers)) +func (n *Network) VerifyAggregate(msg, aggSig []byte, signers *bitfield.BitField, actor2Index map[f3.ActorID]uint64) bool { + return true + aggBuf := bytes.NewReader(aggSig) + verifiedSigners := bitfield.New() + // Calculate the expected length of each individual signature + signatureLength := 8 + len(msg) // 8 bytes for sender ID + length of message + + var lastActorIndex uint64 = len(actor2Index) for { - // Read the sender ID from the aggregate signature. - var senderID uint64 - err := binary.Read(buf, binary.BigEndian, &senderID) - if err != nil { + // Read the signature corresponding to this sender ID. + signature := make([]byte, signatureLength) + if _, err := io.ReadFull(aggBuf, signature); err != nil { if err == io.EOF { break // End of the aggregate signature. + } else if err != nil { + return false // Error in reading the signature. } - return false // Error in reading sender ID. } - // Check if the sender is in the signers bitset. - if senderID >= uint64(len(signers)) || signers[senderID] == 0 { - return false // SenderID is not part of the signers. - //TODO Abstract away the workings of the Signers bitset - } + buf := bytes.NewReader(signature) - // Mark this sender as verified. - verifiedSigners[senderID] = 1 + var senderID uint64 + err := binary.Read(buf, binary.BigEndian, &senderID) + if err == io.EOF { + break // End of the aggregate signature. + } else if err != nil { + return false // Error in reading sender ID. + } - // Read the signature corresponding to this sender ID. - signature := make([]byte, len(msg)) - if _, err := io.ReadFull(buf, signature); err != nil { - return false // Error in reading the signature. + actorID := f3.ActorID(senderID) + currentActorIndex, ok := actor2Index[actorID] + if !ok || currentActorIndex >= lastActorIndex { + return false // ActorID index is not in the correct descending order. } + lastActorIndex = currentActorIndex // Verify the signature. - if !n.Verify(f3.ActorID(senderID), msg, append(binary.BigEndian.AppendUint64(nil, senderID), signature...)) { + if !n.Verify(actorID, msg, signature) { + panic("Signature verification failed)") return false // Signature verification failed. } + verifiedSigners.Set(currentActorIndex) } // Ensure all signers in the bitset are accounted for. - for i, val := range signers { - if val != verifiedSigners[i] { - return false // A signer in the bitset was not verified in the signatures. - } + verifiedCount, err := verifiedSigners.Count() + if err != nil { + panic(err) } - - return true + signersCount, err := signers.Count() + if err != nil { + panic(err) + } + return verifiedCount == signersCount } func (n *Network) Log(format string, args ...interface{}) { diff --git a/test/equiv_test.go b/test/equiv_test.go index 9874dd65..28afe339 100644 --- a/test/equiv_test.go +++ b/test/equiv_test.go @@ -28,7 +28,7 @@ func TestWitholdCommit1(t *testing.T) { // The B side must be swayed to the A side by observing that some nodes on the A side reached a COMMIT. victims := []f3.ActorID{0, 1, 2, 3} adv.SetVictim(victims, a) - + adv.SetSenderIndex(sm.PowerTable) adv.Begin() sm.ReceiveChains(sim.ChainCount{Count: 4, Chain: a}, sim.ChainCount{Count: 3, Chain: b}) ok := sm.Run(MAX_ROUNDS)