Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EVM-friendly TipSet and ECChain formats #216

Merged
merged 12 commits into from
May 17, 2024
1 change: 1 addition & 0 deletions gen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

func main() {
err := gen.WriteTupleEncodersToFile("../gpbft/gen.go", "gpbft",
gpbft.TipSet{},
gpbft.GMessage{},
gpbft.Payload{},
gpbft.Justification{},
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ require (
github.com/stretchr/testify v1.9.0
github.com/whyrusleeping/cbor-gen v0.1.0
go.uber.org/multierr v1.11.0
golang.org/x/crypto v0.21.0
golang.org/x/sync v0.3.0
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028
)
Expand Down Expand Up @@ -45,7 +46,6 @@ require (
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
go.uber.org/zap v1.25.0 // indirect
golang.org/x/crypto v0.21.0 // indirect
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 // indirect
golang.org/x/sys v0.18.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
Expand Down
13 changes: 11 additions & 2 deletions gpbft/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,16 @@ type Verifier interface {
VerifyAggregate(payload, aggSig []byte, signers []PubKey) error
}

type Signatures interface {
Signer
Verifier

// MarshalPayloadForSigning marshals the given payload into the bytes that should be signed.
// This should usually call `Payload.MarshalForSigning(NetworkName)` except when testing as
// that method is slow (computes a merkle tree that's necessary for testing).
MarshalPayloadForSigning(*Payload) []byte
}

type DecisionReceiver interface {
// Receives a finality decision from the instance, with signatures from a strong quorum
// of participants justifying it.
Expand All @@ -89,7 +99,6 @@ type Host interface {
Chain
Network
Clock
Signer
Verifier
Signatures
DecisionReceiver
}
157 changes: 118 additions & 39 deletions gpbft/chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,82 @@ import (
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"strings"

cbg "github.com/whyrusleeping/cbor-gen"
"golang.org/x/crypto/blake2b"
)

// 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
// TipSetKey is the canonically ordered concatenation of the block CIDs in a tipset.
type TipSetKey = []byte

type CID = []byte

// This the CID "prefix" of a v1-DagCBOR-Blake2b256-32 CID. That is:
//
// - 0x01 CIDv1
// - 0x71 DagCBOR
// - 0xA0E402 LEB128 encoded Blake2b256 multicodec
// - 0x20 32 (length of the digest)
var cidPrefix = []byte{0x01, 0x71, 0xA0, 0xE4, 0x02, 0x20}
Stebalien marked this conversation as resolved.
Show resolved Hide resolved

// Hashes the given data and returns a CBOR + blake2b-256 CID.
func MakeCid(data []byte) []byte {
// We construct this CID manually to avoid depending on go-cid (it's also a _bit_ faster).
digest := blake2b.Sum256(data)

out := make([]byte, 0, 38)
out = append(out, cidPrefix...)
out = append(out, digest[:]...)
return out
}

// TipSet represents a single EC tipset.
type TipSet struct {
// The EC epoch (strictly increasing).
Epoch int64
// The tipset's key (canonically ordered concatenated block-header CIDs).
Key TipSetKey
// Blake2b256-32 CID of the CBOR-encoded power table.
PowerTable CID // []PowerEntry
// Keccak256 root hash of the commitments merkle tree.
Commitments [32]byte
}

func (ts *TipSet) IsZero() bool {
return len(ts.Key) == 0
}

func (ts *TipSet) Equal(b *TipSet) bool {
return ts.Epoch == b.Epoch &&
bytes.Equal(ts.Key, b.Key) &&
bytes.Equal(ts.PowerTable, b.PowerTable) &&
ts.Commitments == b.Commitments
}

func (ts *TipSet) MarshalForSigning() []byte {
var buf bytes.Buffer
buf.Grow(len(ts.Key) + 4) // slight over-estimation
_ = cbg.WriteByteArray(&buf, ts.Key)
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()
Stebalien marked this conversation as resolved.
Show resolved Hide resolved
}

func (ts *TipSet) String() string {
if ts == nil {
return "<nil>"
}

return fmt.Sprintf("%d@%s", ts.Epoch, hex.EncodeToString(ts.Key))
}

// A chain of tipsets comprising a base (the last finalised tipset from which the chain extends).
// and (possibly empty) suffix.
Expand Down Expand Up @@ -45,8 +111,8 @@ func (c ECChain) IsZero() bool {
}

// Returns the base tipset.
func (c ECChain) Base() TipSet {
return c[0]
func (c ECChain) Base() *TipSet {
Stebalien marked this conversation as resolved.
Show resolved Hide resolved
return &c[0]
}

// Returns the suffix of the chain after the base.
Expand All @@ -61,8 +127,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.
Expand All @@ -71,8 +137,16 @@ func (c ECChain) BaseChain() ECChain {
return ECChain{c[0]}
}

func (c ECChain) Extend(tip ...TipSet) ECChain {
return append(c[:len(c):len(c)], tip...)
func (c ECChain) Extend(tips ...TipSetKey) ECChain {
c = c[:len(c):len(c)]
offset := c.Head().Epoch + 1
for i, tip := range tips {
c = append(c, TipSet{
Epoch: offset + int64(i),
Key: tip,
})
}
return c
}

// Returns a chain with suffix (after the base) truncated to a maximum length.
Expand All @@ -92,9 +166,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
}
Expand All @@ -105,16 +177,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).
Expand All @@ -127,21 +196,21 @@ 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
}
}
return true
}

// 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
}
}
Expand All @@ -151,7 +220,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() {
Expand All @@ -160,36 +230,45 @@ 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
}

// 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].Key) + 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.Key)))
buf.Write(ts.Key)
_, _ = buf.Write(ts.PowerTable)
}
return ChainKey(buf.String())
}

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(c[i].String())
if i < len(c)-1 {
b.WriteString(", ")
}
Expand Down
Loading
Loading