diff --git a/gen/main.go b/gen/main.go index 49045860..3c6f03f8 100644 --- a/gen/main.go +++ b/gen/main.go @@ -12,6 +12,7 @@ import ( func main() { err := gen.WriteTupleEncodersToFile("../gpbft/gen.go", "gpbft", + gpbft.TipSet{}, gpbft.GMessage{}, gpbft.Payload{}, gpbft.Justification{}, diff --git a/gpbft/chain.go b/gpbft/chain.go index dc67b03e..5a1f69c7 100644 --- a/gpbft/chain.go +++ b/gpbft/chain.go @@ -6,15 +6,66 @@ import ( "encoding/hex" "errors" "strings" + + cbg "github.com/whyrusleeping/cbor-gen" + "golang.org/x/crypto/blake2b" ) +// TipSetKey is a +type TipSetKey = []byte + +type CID = []byte + +var cidPrefix = []byte{0x01, 0x71, 0xA0, 0xE4, 0x02, 0x20} + +// Hashes the given data and returns a CBOR + blake2b-256 CID. +func MakeCid(data []byte) []byte { + // TODO: Consider just using go-cid? We implicitly depend on it through cbor-gen anyways. + digest := blake2b.Sum256(data) + + out := make([]byte, 0, 38) + out = append(out, cidPrefix...) + out = append(out, digest[:]...) + return out +} + // Opaque type representing a tipset. // This is expected to be: // - a canonical sequence of CIDs of block headers identifying a tipset, // - a commitment to the resulting power table, // - a commitment to additional derived values. // However, GossipPBFT doesn't need to know anything about that structure. -type TipSet = []byte +type TipSet struct { + Epoch int64 + TipSet TipSetKey + PowerTable CID + Commitments [32]byte +} + +func (ts *TipSet) IsZero() bool { + return len(ts.TipSet) == 0 +} + +func (ts *TipSet) Equal(b *TipSet) bool { + return ts.Epoch == b.Epoch && + bytes.Equal(ts.TipSet, b.TipSet) && + bytes.Equal(ts.PowerTable, b.PowerTable) && + ts.Commitments == b.Commitments +} + +func (ts *TipSet) MarshalForSigning() []byte { + var buf bytes.Buffer + cbg.WriteByteArray(&buf, ts.TipSet) + tsCid := MakeCid(buf.Bytes()) + buf.Reset() + buf.Grow(len(tsCid) + len(ts.PowerTable) + 32 + 8) + // epoch || commitments || tipset || powertable + _ = binary.Write(&buf, binary.BigEndian, ts.Epoch) + _, _ = buf.Write(ts.Commitments[:]) + _, _ = buf.Write(tsCid) + _, _ = buf.Write(ts.PowerTable) + return buf.Bytes() +} // A chain of tipsets comprising a base (the last finalised tipset from which the chain extends). // and (possibly empty) suffix. @@ -45,8 +96,8 @@ func (c ECChain) IsZero() bool { } // Returns the base tipset. -func (c ECChain) Base() TipSet { - return c[0] +func (c ECChain) Base() *TipSet { + return &c[0] } // Returns the suffix of the chain after the base. @@ -61,8 +112,8 @@ func (c ECChain) Suffix() []TipSet { // Returns the last tipset in the chain. // This could be the base tipset if there is no suffix. // This will panic on a zero value. -func (c ECChain) Head() TipSet { - return c[len(c)-1] +func (c ECChain) Head() *TipSet { + return &c[len(c)-1] } // Returns a new chain with the same base and no suffix. @@ -71,8 +122,11 @@ func (c ECChain) BaseChain() ECChain { return ECChain{c[0]} } -func (c ECChain) Extend(tip TipSet) ECChain { - return append(c, tip) +func (c ECChain) Extend(tip TipSetKey) ECChain { + return append(c, TipSet{ + Epoch: c.Head().Epoch + 1, + TipSet: tip, + }) } // Returns a chain with suffix (after the base) truncated to a maximum length. @@ -88,9 +142,7 @@ func (c ECChain) Eq(other ECChain) bool { return false } for i := range c { - if !bytes.Equal(c[i], other[i]) { - return false - } + c[i].Equal(&other[i]) } return true } @@ -101,16 +153,13 @@ func (c ECChain) SameBase(other ECChain) bool { if c.IsZero() || other.IsZero() { return false } - return bytes.Equal(c.Base(), other.Base()) + return c.Base().Equal(other.Base()) } // Check whether a chain has a specific base tipset. // Always false for a zero value. -func (c ECChain) HasBase(t TipSet) bool { - if c.IsZero() || len(t) == 0 { - return false - } - return bytes.Equal(c[0], t) +func (c ECChain) HasBase(t *TipSet) bool { + return !t.IsZero() && !c.IsZero() && c.Base().Equal(t) } // Checks whether a chain has some prefix (including the base). @@ -123,7 +172,7 @@ func (c ECChain) HasPrefix(other ECChain) bool { return false } for i := range other { - if !bytes.Equal(c[i], other[i]) { + if !c[i].Equal(&other[i]) { return false } } @@ -131,13 +180,13 @@ func (c ECChain) HasPrefix(other ECChain) bool { } // Checks whether a chain has some tipset (including as its base). -func (c ECChain) HasTipset(t TipSet) bool { - if len(t) == 0 { +func (c ECChain) HasTipset(t *TipSet) bool { + if t.IsZero() { // Chain can never contain zero-valued TipSet. return false } - for _, t2 := range c { - if bytes.Equal(t, t2) { + for i := range c { + if c[i].Equal(t) { return true } } @@ -147,7 +196,8 @@ func (c ECChain) HasTipset(t TipSet) bool { // Validates a chain value, returning an error if it finds any issues. // A chain is valid if it meets the following criteria: // 1) All contained tipsets are non-empty. -// 2) The chain is not longer than CHAIN_MAX_LEN. +// 2) All epochs are >= 0 and increasing. +// 3) The chain is not longer than CHAIN_MAX_LEN. // An entirely zero-valued chain itself is deemed valid. See ECChain.IsZero. func (c ECChain) Validate() error { if c.IsZero() { @@ -156,10 +206,16 @@ func (c ECChain) Validate() error { if len(c) > CHAIN_MAX_LEN { return errors.New("chain too long") } - for _, tipSet := range c { - if len(tipSet) == 0 { + var lastEpoch int64 = -1 + for i := range c { + ts := &c[i] + if ts.IsZero() { return errors.New("chain cannot contain zero-valued tip sets") } + if ts.Epoch <= lastEpoch { + return errors.New("chain must have increasing epochs") + } + lastEpoch = ts.Epoch } return nil } @@ -167,16 +223,19 @@ func (c ECChain) Validate() error { // Returns an identifier for the chain suitable for use as a map key. // This must completely determine the sequence of tipsets in the chain. func (c ECChain) Key() ChainKey { - var ln int - for _, t := range c { - ln += 4 // for length - ln += len(t) // for data + ln := len(c) * (8 + 32 + 4) // epoch + commitement + ts length + for i := range c { + ln += len(c[i].TipSet) + len(c[i].PowerTable) } var buf bytes.Buffer buf.Grow(ln) - for _, t := range c { - _ = binary.Write(&buf, binary.BigEndian, uint32(len(t))) - buf.Write(t) + for i := range c { + ts := &c[i] + _ = binary.Write(&buf, binary.BigEndian, ts.Epoch) + _, _ = buf.Write(ts.Commitments[:]) + _ = binary.Write(&buf, binary.BigEndian, uint32(len(ts.TipSet))) + buf.Write(ts.TipSet) + _, _ = buf.Write(ts.PowerTable) } return ChainKey(buf.String()) } @@ -184,8 +243,8 @@ func (c ECChain) Key() ChainKey { func (c ECChain) String() string { var b strings.Builder b.WriteString("[") - for i, t := range c { - b.WriteString(hex.EncodeToString(t)) + for i := range c { + b.WriteString(hex.EncodeToString(c[i].TipSet)) if i < len(c)-1 { b.WriteString(", ") } diff --git a/gpbft/chain_test.go b/gpbft/chain_test.go index 42630b63..7739f62b 100644 --- a/gpbft/chain_test.go +++ b/gpbft/chain_test.go @@ -7,56 +7,17 @@ import ( "github.com/stretchr/testify/require" ) -func TestTipSet(t *testing.T) { - t.Parallel() - tests := []struct { - name string - subject gpbft.TipSet - wantZero bool - wantString string - }{ - { - name: "zero-value struct is zero", - wantZero: true, - wantString: "", - }, - { - name: "ZeroTipSet is zero", - subject: []byte{}, - wantZero: true, - wantString: "", - }, - { - name: "NewTipSet with zero values is zero", - subject: nil, - wantZero: true, - wantString: "", - }, - { - name: "Non-zero is not zero", - subject: gpbft.TipSet("fish"), - wantString: "fish", - }, - } - for _, test := range tests { - test := test - t.Run(test.name, func(t *testing.T) { - require.Equal(t, test.wantZero, len(test.subject) == 0) - require.Equal(t, test.wantString, string(test.subject)) - }) - } -} - func TestECChain(t *testing.T) { t.Parallel() - zeroTipSet := []byte{} + zeroTipSet := gpbft.TipSet{} + oneTipSet := gpbft.TipSet{Epoch: 0, TipSet: []byte{1}} t.Run("zero-value is zero", func(t *testing.T) { var subject gpbft.ECChain require.True(t, subject.IsZero()) - require.False(t, subject.HasBase(zeroTipSet)) + require.False(t, subject.HasBase(&zeroTipSet)) require.False(t, subject.HasPrefix(subject)) - require.False(t, subject.HasTipset(zeroTipSet)) + require.False(t, subject.HasTipset(&zeroTipSet)) require.False(t, subject.SameBase(subject)) require.True(t, subject.Eq(subject)) require.True(t, subject.Eq(*new(gpbft.ECChain))) @@ -72,31 +33,31 @@ func TestECChain(t *testing.T) { require.Nil(t, subject) }) t.Run("extended chain is as expected", func(t *testing.T) { - wantBase := []byte("fish") + wantBase := gpbft.TipSet{Epoch: 0, TipSet: []byte("fish")} subject, err := gpbft.NewChain(wantBase) require.NoError(t, err) require.Len(t, subject, 1) - require.Equal(t, wantBase, subject.Base()) - require.Equal(t, wantBase, subject.Head()) + require.Equal(t, &wantBase, subject.Base()) + require.Equal(t, &wantBase, subject.Head()) require.NoError(t, subject.Validate()) - wantNext := []byte("lobster") - subjectExtended := subject.Extend(wantNext) + wantNext := gpbft.TipSet{Epoch: 1, TipSet: []byte("lobster")} + subjectExtended := subject.Extend(wantNext.TipSet) require.Len(t, subjectExtended, 2) require.NoError(t, subjectExtended.Validate()) - require.Equal(t, wantBase, subjectExtended.Base()) + require.Equal(t, &wantBase, subjectExtended.Base()) require.Equal(t, []gpbft.TipSet{wantNext}, subjectExtended.Suffix()) - require.Equal(t, wantNext, subjectExtended.Head()) - require.Equal(t, wantNext, subjectExtended.Prefix(1).Head()) - require.True(t, subjectExtended.HasTipset(wantBase)) + require.Equal(t, &wantNext, subjectExtended.Head()) + require.Equal(t, &wantNext, subjectExtended.Prefix(1).Head()) + require.True(t, subjectExtended.HasTipset(&wantBase)) require.False(t, subject.HasPrefix(subjectExtended)) require.True(t, subjectExtended.HasPrefix(subject)) - require.False(t, subject.Extend(wantBase).HasPrefix(subjectExtended.Extend(wantNext))) + require.False(t, subject.Extend(wantBase.TipSet).HasPrefix(subjectExtended.Extend(wantNext.TipSet))) }) t.Run("SameBase is false when either chain is zero", func(t *testing.T) { var zeroChain gpbft.ECChain - nonZeroChain, err := gpbft.NewChain([]byte{1}) + nonZeroChain, err := gpbft.NewChain(oneTipSet) require.NoError(t, err) require.False(t, nonZeroChain.SameBase(zeroChain)) require.False(t, zeroChain.SameBase(nonZeroChain)) @@ -104,7 +65,7 @@ func TestECChain(t *testing.T) { }) t.Run("HasPrefix is false when either chain is zero", func(t *testing.T) { var zeroChain gpbft.ECChain - nonZeroChain, err := gpbft.NewChain([]byte{1}) + nonZeroChain, err := gpbft.NewChain(oneTipSet) require.NoError(t, err) require.False(t, nonZeroChain.HasPrefix(zeroChain)) require.False(t, zeroChain.HasPrefix(nonZeroChain)) @@ -115,7 +76,7 @@ func TestECChain(t *testing.T) { require.NoError(t, zeroChain.Validate()) }) t.Run("ordered chain with zero-valued base is invalid", func(t *testing.T) { - subject := gpbft.ECChain{zeroTipSet, []byte{1}} + subject := gpbft.ECChain{zeroTipSet, oneTipSet} require.Error(t, subject.Validate()) }) } diff --git a/gpbft/gen.go b/gpbft/gen.go index 0d52e192..127e980b 100644 --- a/gpbft/gen.go +++ b/gpbft/gen.go @@ -18,6 +18,188 @@ var _ = cid.Undef var _ = math.E var _ = sort.Sort +var lengthBufTipSet = []byte{132} + +func (t *TipSet) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write(lengthBufTipSet); err != nil { + return err + } + + // t.Epoch (int64) (int64) + if t.Epoch >= 0 { + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.Epoch)); err != nil { + return err + } + } else { + if err := cw.WriteMajorTypeHeader(cbg.MajNegativeInt, uint64(-t.Epoch-1)); err != nil { + return err + } + } + + // t.TipSet ([]uint8) (slice) + if len(t.TipSet) > 2097152 { + return xerrors.Errorf("Byte array in field t.TipSet was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.TipSet))); err != nil { + return err + } + + if _, err := cw.Write(t.TipSet); err != nil { + return err + } + + // t.PowerTable ([]uint8) (slice) + if len(t.PowerTable) > 2097152 { + return xerrors.Errorf("Byte array in field t.PowerTable was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.PowerTable))); err != nil { + return err + } + + if _, err := cw.Write(t.PowerTable); err != nil { + return err + } + + // t.Commitments ([32]uint8) (array) + if len(t.Commitments) > 2097152 { + return xerrors.Errorf("Byte array in field t.Commitments was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.Commitments))); err != nil { + return err + } + + if _, err := cw.Write(t.Commitments[:]); err != nil { + return err + } + return nil +} + +func (t *TipSet) UnmarshalCBOR(r io.Reader) (err error) { + *t = TipSet{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajArray { + return fmt.Errorf("cbor input should be of type array") + } + + if extra != 4 { + return fmt.Errorf("cbor input had wrong number of fields") + } + + // t.Epoch (int64) (int64) + { + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + var extraI int64 + switch maj { + case cbg.MajUnsignedInt: + extraI = int64(extra) + if extraI < 0 { + return fmt.Errorf("int64 positive overflow") + } + case cbg.MajNegativeInt: + extraI = int64(extra) + if extraI < 0 { + return fmt.Errorf("int64 negative overflow") + } + extraI = -1 - extraI + default: + return fmt.Errorf("wrong type for int64 field: %d", maj) + } + + t.Epoch = int64(extraI) + } + // t.TipSet ([]uint8) (slice) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.TipSet: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + + if extra > 0 { + t.TipSet = make([]uint8, extra) + } + + if _, err := io.ReadFull(cr, t.TipSet); err != nil { + return err + } + + // t.PowerTable ([]uint8) (slice) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.PowerTable: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + + if extra > 0 { + t.PowerTable = make([]uint8, extra) + } + + if _, err := io.ReadFull(cr, t.PowerTable); err != nil { + return err + } + + // t.Commitments ([32]uint8) (array) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.Commitments: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + if extra != 32 { + return fmt.Errorf("expected array to have 32 elements") + } + + t.Commitments = [32]uint8{} + if _, err := io.ReadFull(cr, t.Commitments[:]); err != nil { + return err + } + return nil +} + var lengthBufGMessage = []byte{133} func (t *GMessage) MarshalCBOR(w io.Writer) error { @@ -228,15 +410,7 @@ func (t *Payload) MarshalCBOR(w io.Writer) error { return err } for _, v := range t.Value { - if len(v) > 2097152 { - return xerrors.Errorf("Byte array in field v was too long") - } - - if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(v))); err != nil { - return err - } - - if _, err := cw.Write(v); err != nil { + if err := v.MarshalCBOR(cw); err != nil { return err } @@ -324,7 +498,7 @@ func (t *Payload) UnmarshalCBOR(r io.Reader) (err error) { } if extra > 0 { - t.Value = make([][]uint8, extra) + t.Value = make([]TipSet, extra) } for i := 0; i < int(extra); i++ { @@ -336,24 +510,12 @@ func (t *Payload) UnmarshalCBOR(r io.Reader) (err error) { _ = extra _ = err - maj, extra, err = cr.ReadHeader() - if err != nil { - return err - } - - if extra > 2097152 { - return fmt.Errorf("t.Value[i]: byte array too large (%d)", extra) - } - if maj != cbg.MajByteString { - return fmt.Errorf("expected byte array") - } + { - if extra > 0 { - t.Value[i] = make([]uint8, extra) - } + if err := t.Value[i].UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Value[i]: %w", err) + } - if _, err := io.ReadFull(cr, t.Value[i]); err != nil { - return err } } diff --git a/gpbft/gpbft.go b/gpbft/gpbft.go index 0ba4c7aa..5cadd37e 100644 --- a/gpbft/gpbft.go +++ b/gpbft/gpbft.go @@ -11,6 +11,7 @@ import ( "github.com/filecoin-project/go-bitfield" rlepluslazy "github.com/filecoin-project/go-bitfield/rle" + "github.com/filecoin-project/go-f3/merkle" "golang.org/x/xerrors" ) @@ -90,26 +91,30 @@ type Payload struct { Value ECChain } -func (p Payload) Eq(other *Payload) bool { +func (p *Payload) Eq(other *Payload) bool { return p.Instance == other.Instance && p.Round == other.Round && p.Step == other.Step && p.Value.Eq(other.Value) } -func (p Payload) MarshalForSigning(nn NetworkName) []byte { +func (p *Payload) MarshalForSigning(nn NetworkName) []byte { + values := make([][]byte, len(p.Value)) + for i := range p.Value { + values[i] = p.Value[i].MarshalForSigning() + } + root := merkle.Tree(values) + var buf bytes.Buffer buf.WriteString(DOMAIN_SEPARATION_TAG) buf.WriteString(":") buf.WriteString(string(nn)) buf.WriteString(":") - _ = binary.Write(&buf, binary.BigEndian, p.Instance) - _ = binary.Write(&buf, binary.BigEndian, p.Round) + _ = binary.Write(&buf, binary.BigEndian, p.Step) - for _, t := range p.Value { - _ = binary.Write(&buf, binary.BigEndian, uint32(len(t))) - buf.Write(t) - } + _ = binary.Write(&buf, binary.BigEndian, p.Round) + _ = binary.Write(&buf, binary.BigEndian, p.Instance) + _, _ = buf.Write(root[:]) return buf.Bytes() } diff --git a/gpbft/participant_test.go b/gpbft/participant_test.go index 69d7ddaf..bf6063fa 100644 --- a/gpbft/participant_test.go +++ b/gpbft/participant_test.go @@ -39,7 +39,7 @@ type participantTestSubject struct { func newParticipantTestSubject(t *testing.T, seed int64, instance uint64) *participantTestSubject { // Generate some canonical chain. - canonicalChain, err := gpbft.NewChain([]byte("genesis")) + canonicalChain, err := gpbft.NewChain(gpbft.TipSet{Epoch: 0, TipSet: []byte("genesis")}) require.NoError(t, err) const ( @@ -258,7 +258,7 @@ func TestParticipant(t *testing.T) { }) t.Run("on invalid canonical chain", func(t *testing.T) { subject := newParticipantTestSubject(t, seed, 0) - invalidChain := gpbft.ECChain{nil} + invalidChain := gpbft.ECChain{gpbft.TipSet{}} subject.host.On("GetCanonicalChain").Return(invalidChain, *subject.powerTable, subject.beacon) require.ErrorContains(t, subject.Start(), "invalid canonical chain") subject.assertHostExpectations() @@ -291,7 +291,7 @@ func TestParticipant(t *testing.T) { Sender: somePowerEntry.ID, Vote: gpbft.Payload{ Instance: initialInstance, - Value: gpbft.ECChain{nil}, + Value: gpbft.ECChain{gpbft.TipSet{}}, }, }, false }, @@ -404,7 +404,7 @@ func TestParticipant_ValidateMessage(t *testing.T) { Sender: somePowerEntry.ID, Vote: gpbft.Payload{ Instance: initialInstanceNumber, - Value: gpbft.ECChain{[]byte("fish")}, + Value: gpbft.ECChain{gpbft.TipSet{Epoch: 0, TipSet: []byte("fish")}}, }, } }, @@ -417,7 +417,7 @@ func TestParticipant_ValidateMessage(t *testing.T) { Sender: somePowerEntry.ID, Vote: gpbft.Payload{ Instance: initialInstanceNumber, - Value: gpbft.ECChain{subject.canonicalChain.Base(), nil}, + Value: gpbft.ECChain{*subject.canonicalChain.Base(), gpbft.TipSet{}}, }, } }, @@ -511,7 +511,7 @@ func TestParticipant_ValidateMessage(t *testing.T) { Instance: initialInstanceNumber, Step: gpbft.CONVERGE_PHASE, Round: 42, - Value: gpbft.ECChain{subject.canonicalChain.Base(), nil}, + Value: gpbft.ECChain{*subject.canonicalChain.Base(), gpbft.TipSet{}}, }, } }, @@ -808,7 +808,7 @@ func TestParticipant_ValidateMessage(t *testing.T) { Justification: &gpbft.Justification{ Vote: gpbft.Payload{ Instance: initialInstanceNumber, - Value: gpbft.ECChain{subject.canonicalChain.Base(), nil}, + Value: gpbft.ECChain{*subject.canonicalChain.Base(), gpbft.TipSet{}}, }, }, } diff --git a/merkle/merkle.go b/merkle/merkle.go new file mode 100644 index 00000000..c3bd3315 --- /dev/null +++ b/merkle/merkle.go @@ -0,0 +1,117 @@ +package merkle + +import ( + "math" + "math/bits" + + "golang.org/x/crypto/sha3" +) + +// Digest is a 32-byte hash digest. +type Digest = [32]byte + +// TreeWithProofs returns a the root of the merkle-tree of the given values, along with merkle-proofs for +// each leaf. +func TreeWithProofs(values [][]byte) (Digest, [][]Digest) { + depth := depth(values) + proofs := make([][]Digest, len(values)) + for i := range proofs { + proofs[i] = make([]Digest, 0, depth) + } + return buildTree(depth, values, proofs), proofs +} + +// Tree returns a the root of the merkle-tree of the given values. +func Tree(values [][]byte) Digest { + return buildTree(bits.Len(uint(len(values))-1), values, nil) +} + +// VerifyProof verifies that the given value maps to the given index in the merkle-tree with the +// given root. It returns "more" if the value is not the last value in the merkle-tree. +func VerifyProof(root Digest, index int, value []byte, proof []Digest) (valid bool, more bool) { + // We only allow int32 items, assert that. + if index >= math.MaxInt32 || len(proof) >= 32 { + return false, false + } + + // Make sure the index is in-range for the proof. + if index > (1< 0 { + leftProofs = proofs[:split] + rightProofs = proofs[split:] + } + + leftHash := buildTree(depth-1, values[:split], leftProofs) + rightHash := buildTree(depth-1, values[split:], rightProofs) + + for i, proof := range leftProofs { + leftProofs[i] = append(proof, rightHash) + } + for i, proof := range rightProofs { + rightProofs[i] = append(proof, leftHash) + } + + return internalHash(leftHash, rightHash) +} diff --git a/merkle/merkle_test.go b/merkle/merkle_test.go new file mode 100644 index 00000000..e2137b80 --- /dev/null +++ b/merkle/merkle_test.go @@ -0,0 +1,39 @@ +package merkle + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestHashTree(t *testing.T) { + for i := 1; i < 256; i++ { + t.Run(fmt.Sprintf("Length/%d", i), func(t *testing.T) { + test := make([][]byte, i) + for j := range test { + test[j] = []byte{byte(j)} + + } + root, paths := TreeWithProofs(test) + root2 := Tree(test) + require.Equal(t, root, root2) + require.Equal(t, len(test), len(paths)) + assert.Equal(t, len(paths[0]), depth(test)) + + for i, path := range paths { + valid, more := VerifyProof(root, i, test[i], path) + assert.True(t, valid, "proof was not valid for index %d", i) + assert.Equal(t, i < len(paths)-1, more, "incorrect value for 'more' for index %d", i) + } + }) + } +} + +func TestHashZero(t *testing.T) { + test := [][]byte{} + root, paths := TreeWithProofs(test) + assert.Empty(t, paths) + assert.Equal(t, root, Digest{}) +} diff --git a/sim/cmd/f3sim/f3sim.go b/sim/cmd/f3sim/f3sim.go index 44d6e0f9..d4e21f96 100644 --- a/sim/cmd/f3sim/f3sim.go +++ b/sim/cmd/f3sim/f3sim.go @@ -36,7 +36,7 @@ func main() { } tsg := sim.NewTipSetGenerator(uint64(seed)) - baseChain, err := gpbft.NewChain(tsg.Sample()) + baseChain, err := gpbft.NewChain(gpbft.TipSet{Epoch: 0, TipSet: tsg.Sample()}) if err != nil { log.Fatalf("failed to generate base chain: %v\n", err) } diff --git a/sim/decision_log.go b/sim/decision_log.go index 4e1d3612..9bcf6c58 100644 --- a/sim/decision_log.go +++ b/sim/decision_log.go @@ -131,7 +131,7 @@ func (dl *DecisionLog) verifyDecision(decision *gpbft.Justification) error { if decision.Vote.Value.IsZero() { return fmt.Errorf("decided empty tipset: %v", decision) } - if !decision.Vote.Value.HasBase(base) { + if !decision.Vote.Value.HasBase(&base) { return fmt.Errorf("decided tipset with wrong base: %v", decision) } diff --git a/sim/host.go b/sim/host.go index 31f71f4d..08585df2 100644 --- a/sim/host.go +++ b/sim/host.go @@ -62,7 +62,7 @@ func (v *simHost) ReceiveDecision(decision *gpbft.Justification) time.Time { firstForInstance := v.sim.decisions.ReceiveDecision(v.id, decision) if firstForInstance { // When the first valid decision is received for an instance, prepare for the next one. - nextBase := decision.Vote.Value.Head() + nextBase := *decision.Vote.Value.Head() // Copy the previous instance power table. // The simulator doesn't have any facility to evolve the power table. // See https://github.com/filecoin-project/go-f3/issues/114. @@ -72,7 +72,7 @@ func (v *simHost) ReceiveDecision(decision *gpbft.Justification) time.Time { // There's no facility yet for them to observe different chains after the first instance. // See https://github.com/filecoin-project/go-f3/issues/115. newTip := v.sim.tipSetGenerator.Sample() - nextChain, _ := gpbft.NewChain(nextBase, newTip) + nextChain, _ := gpbft.NewChain(nextBase, gpbft.TipSet{Epoch: nextBase.Epoch + 1, TipSet: newTip}) v.sim.ec.AddInstance(nextChain, nextPowerTable, nextBeacon) v.sim.decisions.BeginInstance(decision.Vote.Instance+1, nextBase, nextPowerTable) diff --git a/sim/options.go b/sim/options.go index 76d5c4ef..56552acb 100644 --- a/sim/options.go +++ b/sim/options.go @@ -23,7 +23,7 @@ var ( func init() { var err error - defaultBaseChain, err = gpbft.NewChain([]byte(("genesis"))) + defaultBaseChain, err = gpbft.NewChain(gpbft.TipSet{Epoch: 0, TipSet: []byte("genesis")}) if err != nil { panic("failed to instantiate default simulation base chain") } diff --git a/sim/sim.go b/sim/sim.go index d316a91d..2f9ed3ec 100644 --- a/sim/sim.go +++ b/sim/sim.go @@ -62,7 +62,7 @@ func NewSimulation(o ...Option) (*Simulation, error) { s.ec.AddParticipant(s.adversary.ID(), s.adversary.Power, pubKey) } - s.decisions.BeginInstance(0, (*s.baseChain).Head(), s.ec.Instances[0].PowerTable) + s.decisions.BeginInstance(0, *s.baseChain.Head(), s.ec.Instances[0].PowerTable) return s, nil } diff --git a/sim/tipset_gen.go b/sim/tipset_gen.go index 5111644d..cf8b362a 100644 --- a/sim/tipset_gen.go +++ b/sim/tipset_gen.go @@ -15,7 +15,7 @@ func NewTipSetGenerator(seed uint64) *TipSetGenerator { return &TipSetGenerator{seed} } -func (c *TipSetGenerator) Sample() gpbft.TipSet { +func (c *TipSetGenerator) Sample() gpbft.TipSetKey { b := make([]byte, 8) for i := range b { b[i] = alphanum[c.nextN(len(alphanum))] diff --git a/test/honest_test.go b/test/honest_test.go index b53dadd5..9a3fc1a4 100644 --- a/test/honest_test.go +++ b/test/honest_test.go @@ -58,7 +58,7 @@ func TestHonest_ChainAgreement(t *testing.T) { sm.SetChains(sim.ChainCount{Count: sm.HonestParticipantsCount(), Chain: targetChain}) require.NoErrorf(t, sm.Run(1, maxRounds), "%s", sm.Describe()) - requireConsensus(t, sm, targetChain.Head()) + requireConsensus(t, sm, *targetChain.Head()) }) } } @@ -123,7 +123,7 @@ func TestSync_AgreementWithRepetition(t *testing.T) { sm.SetChains(sim.ChainCount{Count: sm.HonestParticipantsCount(), Chain: a}) require.NoErrorf(t, sm.Run(1, maxRounds), "%s", sm.Describe()) // Synchronous, agreeing groups always decide the candidate. - requireConsensus(t, sm, a.Head()) + requireConsensus(t, sm, *a.Head()) }) } @@ -150,7 +150,7 @@ func TestAsyncAgreement(t *testing.T) { sm.SetChains(sim.ChainCount{Count: sm.HonestParticipantsCount(), Chain: a}) require.NoErrorf(t, sm.Run(1, maxRounds), "%s", sm.Describe()) - requireConsensus(t, sm, baseChain.Head(), a.Head()) + requireConsensus(t, sm, *baseChain.Head(), *a.Head()) }) }) } @@ -174,7 +174,7 @@ func TestSyncHalves(t *testing.T) { require.NoErrorf(t, sm.Run(1, maxRounds), "%s", sm.Describe()) // Groups split 50/50 always decide the base. - requireConsensus(t, sm, baseChain.Head()) + requireConsensus(t, sm, *baseChain.Head()) }) } @@ -200,7 +200,7 @@ func TestSyncHalvesBLS(t *testing.T) { require.NoErrorf(t, sm.Run(1, maxRounds), "%s", sm.Describe()) // Groups split 50/50 always decide the base. - requireConsensus(t, sm, baseChain.Head()) + requireConsensus(t, sm, *baseChain.Head()) }) } @@ -227,7 +227,7 @@ func TestAsyncHalves(t *testing.T) { require.NoErrorf(t, sm.Run(1, maxRounds), "%s", sm.Describe()) // Groups split 50/50 always decide the base. - requireConsensus(t, sm, baseChain.Head()) + requireConsensus(t, sm, *baseChain.Head()) }) }) } @@ -254,7 +254,7 @@ func TestRequireStrongQuorumToProgress(t *testing.T) { require.NoErrorf(t, sm.Run(1, maxRounds), "%s", sm.Describe()) // Must decide base. - requireConsensus(t, sm, baseChain.Head()) + requireConsensus(t, sm, *baseChain.Head()) }) } @@ -283,5 +283,5 @@ func TestLongestCommonPrefix(t *testing.T) { require.NoErrorf(t, sm.Run(1, maxRounds), "%s", sm.Describe()) // Must decide ab, the longest common prefix. - requireConsensus(t, sm, ab.Head()) + requireConsensus(t, sm, *ab.Head()) } diff --git a/test/multi_instance_test.go b/test/multi_instance_test.go index 4cd0d9ba..a1156fee 100644 --- a/test/multi_instance_test.go +++ b/test/multi_instance_test.go @@ -26,7 +26,7 @@ func TestMultiSingleton(t *testing.T) { instance := sm.GetInstance(instanceCount) require.NotNil(t, instance) expected := instance.Base - requireConsensusAtInstance(t, sm, instanceCount-1, expected.Head()) + requireConsensusAtInstance(t, sm, instanceCount-1, *expected.Head()) } func TestMultiSyncPair(t *testing.T) { @@ -42,7 +42,7 @@ func TestMultiSyncPair(t *testing.T) { require.NoErrorf(t, sm.Run(instanceCount, maxRounds), "%s", sm.Describe()) expected := sm.GetInstance(instanceCount).Base - requireConsensusAtInstance(t, sm, instanceCount-1, expected.Head()) + requireConsensusAtInstance(t, sm, instanceCount-1, *expected.Head()) } func TestMultiASyncPair(t *testing.T) { diff --git a/test/util_test.go b/test/util_test.go index 48f45596..b33a18c3 100644 --- a/test/util_test.go +++ b/test/util_test.go @@ -1,7 +1,6 @@ package test import ( - "bytes" "os" "runtime" "strconv" @@ -39,8 +38,8 @@ nextParticipant: for _, pid := range sm.ListParticipantIDs() { decision, ok := sm.GetDecision(instance, pid) require.True(t, ok, "no decision for participant %d in instance %d", pid, instance) - for _, e := range expected { - if bytes.Equal(decision.Head(), e) { + for i := range expected { + if decision.Head().Equal(&expected[i]) { continue nextParticipant } } @@ -52,7 +51,7 @@ nextParticipant: func generateECChain(t *testing.T, tsg *sim.TipSetGenerator) gpbft.ECChain { t.Helper() // TODO: add stochastic chain generation. - chain, err := gpbft.NewChain(tsg.Sample()) + chain, err := gpbft.NewChain(gpbft.TipSet{Epoch: 0, TipSet: tsg.Sample()}) require.NoError(t, err) return chain }