Skip to content

Commit

Permalink
Makes ECChain an entirely opaque [][byte.
Browse files Browse the repository at this point in the history
  • Loading branch information
anorth committed Apr 8, 2024
1 parent 2548c4d commit 594a683
Show file tree
Hide file tree
Showing 16 changed files with 125 additions and 483 deletions.
2 changes: 1 addition & 1 deletion cmd/f3sim/f3sim.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func main() {
sm := sim.NewSimulation(simConfig, graniteConfig, *traceLevel)

// Same chain for everyone.
candidate := sm.Base(0).Extend(sm.CIDGen.Sample())
candidate := sm.Base(0).Extend(sm.TipGen.Sample())
sm.SetChains(sim.ChainCount{Count: *participantCount, Chain: candidate})

err := sm.Run(1, *maxRounds)
Expand Down
1 change: 0 additions & 1 deletion gen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ func main() {
gpbft.GMessage{},
gpbft.Payload{},
gpbft.Justification{},
gpbft.TipSet{},
)
if err != nil {
fmt.Println(err)
Expand Down
107 changes: 24 additions & 83 deletions gpbft/chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,73 +2,25 @@ package gpbft

import (
"bytes"
"encoding/binary"
"encoding/hex"
"errors"
"io"
"strconv"
"strings"
)

// Information about a tipset that is relevant to the F3 protocol.
// This is a lightweight value type comprising 3 machine words.
// Fields are exported for CBOR generation, but are opqaue and should not be accessed
// within the protocol implementation.
type TipSet struct {
// The epoch of the blocks in the tipset.
Epoch int64
// The identifier of the tipset.
ID TipSetID
}

// Creates a new tipset.
func NewTipSet(epoch int64, cid TipSetID) TipSet {
return TipSet{
Epoch: epoch,
ID: cid,
}
}

// Returns a zero value tipset.
// The zero value is not a meaningful tipset and may be used to represent bottom.
func ZeroTipSet() TipSet {
return TipSet{}
}

func (t TipSet) IsZero() bool {
return t.Epoch == 0 && t.ID.IsZero()
}

func (t TipSet) Eq(other TipSet) bool {
return t.Epoch == other.Epoch && t.ID.Eq(other.ID)
}

// An identifier for this tipset suitable for use as a map key.
// This must completely and verifiably determine the tipset data; it is not sufficient to use
// the block CIDs and rely on external verification of the attached metadata.
func (t TipSet) Key() string {
buf := bytes.Buffer{}
buf.Write(t.ID.Bytes())
_ = binary.Write(&buf, binary.BigEndian, t.Epoch)
return buf.String()
}

func (t TipSet) String() string {
var b strings.Builder
b.Write(t.ID.Bytes())
b.WriteString("@")
b.WriteString(strconv.FormatInt(t.Epoch, 10))
return b.String()
}

func (t TipSet) MarshalForSigning(w io.Writer) {
_ = binary.Write(w, binary.BigEndian, t.Epoch)
_, _ = w.Write(t.ID.Bytes())
}
// 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

// A chain of tipsets comprising a base (the last finalised tipset from which the chain extends).
// and (possibly empty) suffix.
// Tipsets are assumed to be built contiguously on each other, though epochs may be missing due to null rounds.
// The zero value is not a valid chain, and represents a "bottom" value when used in a Granite message.
// Tipsets are assumed to be built contiguously on each other,
// though epochs may be missing due to null rounds.
// The zero value is not a valid chain, and represents a "bottom" value
// when used in a Granite message.
type ECChain []TipSet

// A map key for a chain. The zero value means "bottom".
Expand Down Expand Up @@ -115,13 +67,8 @@ func (c ECChain) BaseChain() ECChain {
return ECChain{c[0]}
}

// Returns a new chain extending this chain with one tipset.
// The new tipset is given an epoch and weight one greater than the previous head.
func (c ECChain) Extend(cid TipSetID) ECChain {
return append(c, TipSet{
Epoch: c.Head().Epoch + 1,
ID: cid,
})
func (c ECChain) Extend(tip TipSet) ECChain {
return append(c, tip)
}

// Returns a chain with suffix (after the base) truncated to a maximum length.
Expand All @@ -137,7 +84,7 @@ func (c ECChain) Eq(other ECChain) bool {
return false
}
for i := range c {
if !c[i].Eq(other[i]) {
if !bytes.Equal(c[i], other[i]) {
return false
}
}
Expand All @@ -150,16 +97,16 @@ func (c ECChain) SameBase(other ECChain) bool {
if c.IsZero() || other.IsZero() {
return false
}
return c.Base().Eq(other.Base())
return bytes.Equal(c.Base(), 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() || t.IsZero() {
if c.IsZero() || len(t) == 0 {
return false
}
return c[0].Eq(t)
return bytes.Equal(c[0], t)
}

// Checks whether a chain has some prefix (including the base).
Expand All @@ -172,7 +119,7 @@ func (c ECChain) HasPrefix(other ECChain) bool {
return false
}
for i := range other {
if !c[i].Eq(other[i]) {
if !bytes.Equal(c[i], other[i]) {
return false
}
}
Expand All @@ -181,12 +128,12 @@ 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 t.IsZero() {
if len(t) == 0 {
// Chain can never contain zero-valued TipSet.
return false
}
for _, t2 := range c {
if t2.Eq(t) {
if bytes.Equal(t, t2) {
return true
}
}
Expand All @@ -202,15 +149,9 @@ func (c ECChain) Validate() error {
if c.IsZero() {
return nil
}
var epochSoFar int64
for _, tipSet := range c {
switch {
case tipSet.IsZero():
if len(tipSet) == 0 {
return errors.New("chain cannot contain zero-valued tip sets")
case tipSet.Epoch <= epochSoFar:
return errors.New("chain epoch must be in order and unique")
default:
epochSoFar = tipSet.Epoch
}
}
return nil
Expand All @@ -221,7 +162,7 @@ func (c ECChain) Validate() error {
func (c ECChain) Key() ChainKey {
buf := bytes.Buffer{}
for _, t := range c {
buf.Write([]byte(t.Key()))
buf.Write(t)
}
return ChainKey(buf.String())
}
Expand All @@ -230,7 +171,7 @@ func (c ECChain) String() string {
var b strings.Builder
b.WriteString("[")
for i, t := range c {
b.WriteString(t.String())
b.WriteString(hex.EncodeToString(t))
if i < len(c)-1 {
b.WriteString(", ")
}
Expand Down
108 changes: 21 additions & 87 deletions gpbft/chain_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package gpbft_test

import (
"bytes"
"encoding/binary"
"testing"

"github.com/filecoin-project/go-f3/gpbft"
Expand All @@ -20,56 +18,39 @@ func TestTipSet(t *testing.T) {
{
name: "zero-value struct is zero",
wantZero: true,
wantString: "@0",
wantString: "",
},
{
name: "ZeroTipSet is zero",
subject: gpbft.ZeroTipSet(),
subject: []byte{},
wantZero: true,
wantString: "@0",
wantString: "",
},
{
name: "NewTipSet with zero values is zero",
subject: gpbft.NewTipSet(0, gpbft.NewTipSetID(nil)),
subject: nil,
wantZero: true,
wantString: "@0",
wantString: "",
},
{
name: "Non-zero is not zero",
subject: gpbft.NewTipSet(1413, gpbft.NewTipSetID([]byte("fish"))),
wantString: "fish@1413",
},
{
name: "negative epoch is accepted",
subject: gpbft.NewTipSet(-1413, gpbft.NewTipSetID([]byte("fish"))),
wantString: "fish@-1413",
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, test.subject.IsZero())
require.Equal(t, test.wantString, test.subject.String())
requireTipSetMarshaledForSigning(t, test.subject)
require.Equal(t, test.wantZero, len(test.subject) == 0)
require.Equal(t, test.wantString, string(test.subject))
})
}
}

func requireTipSetMarshaledForSigning(t *testing.T, subject gpbft.TipSet) {
t.Helper()
var gotSigningMarshal bytes.Buffer
subject.MarshalForSigning(&gotSigningMarshal)
wantPrefix := binary.BigEndian.AppendUint64(nil, uint64(subject.Epoch))
wantSuffix := subject.ID.Bytes()
require.Equal(t, len(wantPrefix)+len(wantSuffix), gotSigningMarshal.Len())
require.True(t, bytes.HasPrefix(gotSigningMarshal.Bytes(), wantPrefix))
require.True(t, bytes.HasSuffix(gotSigningMarshal.Bytes(), wantSuffix))
}

func TestECChain(t *testing.T) {
t.Parallel()

zeroTipSet := gpbft.ZeroTipSet()
zeroTipSet := []byte{}
t.Run("zero-value is zero", func(t *testing.T) {
var subject gpbft.ECChain
require.True(t, subject.IsZero())
Expand All @@ -83,81 +64,47 @@ func TestECChain(t *testing.T) {
require.Panics(t, func() { subject.Prefix(0) })
require.Panics(t, func() { subject.Base() })
require.Panics(t, func() { subject.Head() })
require.Equal(t, zeroTipSet, subject.HeadOrZero())
require.NoError(t, subject.Validate())
requireMonotonicallyIncreasingEpochs(t, subject)
})
t.Run("NewChain with zero-value base is error", func(t *testing.T) {
subject, err := gpbft.NewChain(zeroTipSet)
require.Error(t, err)
require.Nil(t, subject)
})
t.Run("NewChain with repeated epochs is error", func(t *testing.T) {
tipSet := gpbft.NewTipSet(1413, gpbft.ZeroTipSetID())
subject, err := gpbft.NewChain(tipSet, tipSet)
require.Error(t, err)
require.Nil(t, subject)
})
t.Run("NewChain with epoch gaps is not error", func(t *testing.T) {
subject, err := gpbft.NewChain(gpbft.NewTipSet(1413, gpbft.ZeroTipSetID()), gpbft.NewTipSet(1414, gpbft.ZeroTipSetID()))
require.NoError(t, err)
require.NoError(t, subject.Validate())
requireMonotonicallyIncreasingEpochs(t, subject)
})
t.Run("NewChain with unordered tipSets is error", func(t *testing.T) {
subject, err := gpbft.NewChain(gpbft.NewTipSet(2, gpbft.ZeroTipSetID()), zeroTipSet)
require.Error(t, err)
require.Nil(t, subject)
})
t.Run("NewChain with ordered duplicate epoch is error", func(t *testing.T) {
subject, err := gpbft.NewChain(zeroTipSet,
gpbft.NewTipSet(2, gpbft.ZeroTipSetID()),
gpbft.NewTipSet(2, gpbft.NewTipSetID([]byte("fish"))),
gpbft.NewTipSet(2, gpbft.NewTipSetID([]byte("lobster"))))
require.Error(t, err)
require.Nil(t, subject)
require.NoError(t, subject.Validate())
requireMonotonicallyIncreasingEpochs(t, subject)
})
t.Run("extended chain is as expected", func(t *testing.T) {
wantBase := gpbft.NewTipSet(1413, gpbft.NewTipSetID([]byte("fish")))
wantBase := []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.HeadOrZero())
require.NoError(t, subject.Validate())
requireMonotonicallyIncreasingEpochs(t, subject)

wantNextID := gpbft.NewTipSetID([]byte("lobster"))
wantNextTipSet := gpbft.NewTipSet(wantBase.Epoch+1, wantNextID)
subjectExtended := subject.Extend(wantNextID)
wantNext := []byte("lobster")
subjectExtended := subject.Extend(wantNext)
require.Len(t, subjectExtended, 2)
require.NoError(t, subjectExtended.Validate())
requireMonotonicallyIncreasingEpochs(t, subjectExtended)
require.Equal(t, wantBase, subjectExtended.Base())
require.Equal(t, []gpbft.TipSet{wantNextTipSet}, subjectExtended.Suffix())
require.Equal(t, wantNextTipSet, subjectExtended.Head())
require.Equal(t, wantNextTipSet, subjectExtended.HeadOrZero())
require.Equal(t, wantNextTipSet, subjectExtended.Prefix(1).Head())
require.True(t, subjectExtended.HasTipset(gpbft.NewTipSet(wantBase.Epoch+1, wantNextID)))
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.False(t, subject.HasPrefix(subjectExtended))
require.True(t, subjectExtended.HasPrefix(subject))

require.False(t, subject.Extend(wantBase.ID).HasPrefix(subjectExtended.Extend(wantNextID)))
require.False(t, subject.Extend(wantBase).HasPrefix(subjectExtended.Extend(wantNext)))
})
t.Run("SameBase is false when either chain is zero", func(t *testing.T) {
var zeroChain gpbft.ECChain
nonZeroChain, err := gpbft.NewChain(gpbft.NewTipSet(2, gpbft.ZeroTipSetID()))
nonZeroChain, err := gpbft.NewChain([]byte{1})
require.NoError(t, err)
require.False(t, nonZeroChain.SameBase(zeroChain))
require.False(t, zeroChain.SameBase(nonZeroChain))
require.False(t, zeroChain.SameBase(zeroChain))
})
t.Run("HasPrefix is false when either chain is zero", func(t *testing.T) {
var zeroChain gpbft.ECChain
nonZeroChain, err := gpbft.NewChain(gpbft.NewTipSet(2, gpbft.ZeroTipSetID()))
nonZeroChain, err := gpbft.NewChain([]byte{1})
require.NoError(t, err)
require.False(t, nonZeroChain.HasPrefix(zeroChain))
require.False(t, zeroChain.HasPrefix(nonZeroChain))
Expand All @@ -168,20 +115,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, gpbft.NewTipSet(1, gpbft.ZeroTipSetID())}
subject := gpbft.ECChain{zeroTipSet, []byte{1}}
require.Error(t, subject.Validate())
})
t.Run("unordered chain is invalid", func(t *testing.T) {
subject := gpbft.ECChain{gpbft.NewTipSet(2, gpbft.ZeroTipSetID()), gpbft.NewTipSet(1, gpbft.ZeroTipSetID())}
require.Error(t, subject.Validate())
})
}

func requireMonotonicallyIncreasingEpochs(t *testing.T, subject gpbft.ECChain) {
t.Helper()
var latestEpoch int64
for index, tipSet := range subject {
require.Less(t, latestEpoch, tipSet.Epoch, "not monotonically increasing at index %d", index)
latestEpoch = tipSet.Epoch
}
}
Loading

0 comments on commit 594a683

Please sign in to comment.