Skip to content

Commit

Permalink
fixup! Custom JSON serialization of TipSetKey for array-of-CIDs
Browse files Browse the repository at this point in the history
  • Loading branch information
rvagg committed Nov 27, 2024
1 parent 5b8af30 commit 53c3ad1
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 60 deletions.
2 changes: 1 addition & 1 deletion certs/certs.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ type FinalityCertificate struct {
Signature []byte
// Changes between the power table used to validate this finality certificate and the power
// used to validate the next finality certificate. Sorted by ParticipantID, ascending.
PowerTableDelta PowerTableDiff
PowerTableDelta PowerTableDiff `json:"omitempty"`
}

// NewFinalityCertificate constructs a new finality certificate from the given power delta (from
Expand Down
85 changes: 44 additions & 41 deletions gpbft/chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,29 +57,6 @@ type TipSet struct {
Commitments [32]byte
}

func cidsFromTipSetKey(encoded []byte) ([]cid.Cid, error) {
var cids []cid.Cid
for nextIdx := 0; nextIdx < len(encoded); {
nr, c, err := cid.CidFromBytes(encoded[nextIdx:])
if err != nil {
return nil, err
}
cids = append(cids, c)
nextIdx += nr
}
return cids, nil
}

func tipSetKeyFromCids(cids []cid.Cid) (TipSetKey, error) {
var buf bytes.Buffer
for _, c := range cids {
if _, err := buf.Write(c.Bytes()); err != nil {
return nil, err
}
}
return buf.Bytes(), nil
}

// Validates a tipset.
// Note the zero value is invalid.
func (ts *TipSet) Validate() error {
Expand Down Expand Up @@ -135,42 +112,68 @@ func (ts *TipSet) String() string {
return fmt.Sprintf("%s@%d", encTs[:min(16, len(encTs))], ts.Epoch)
}

// Custom JSON marshalling for TipSet to achieve a standard TipSetKey
// representation that presents an array of dag-json CIDs.
// Custom JSON marshalling for TipSet to achieve:
// 1. a standard TipSetKey representation that presents an array of dag-json CIDs.
// 2. a commitment field that is a base64-encoded string.

type tipSetSub TipSet
type tipSetJson struct {
Key []cid.Cid
Commitments []byte
*tipSetSub
}

func (ts TipSet) MarshalJSON() ([]byte, error) {
type TipSetSub TipSet
cids, err := cidsFromTipSetKey(ts.Key)
if err != nil {
return nil, err
}
return json.Marshal(&struct {
Key []cid.Cid
TipSetSub
}{
Key: cids,
TipSetSub: (TipSetSub)(ts),
return json.Marshal(&tipSetJson{
Key: cids,
Commitments: ts.Commitments[:],
tipSetSub: (*tipSetSub)(&ts),
})
}

func (ts *TipSet) UnmarshalJSON(data []byte) error {
type TipSetSub TipSet
aux := &struct {
Key []cid.Cid
*TipSetSub
}{
TipSetSub: (*TipSetSub)(ts),
}
func (ts *TipSet) UnmarshalJSON(b []byte) error {
aux := &tipSetJson{tipSetSub: (*tipSetSub)(ts)}
var err error
if err = json.Unmarshal(data, &aux); err != nil {
if err = json.Unmarshal(b, &aux); err != nil {
return err
}
if ts.Key, err = tipSetKeyFromCids(aux.Key); err != nil {
return err
}

Check warning on line 146 in gpbft/chain.go

View check run for this annotation

Codecov / codecov/patch

gpbft/chain.go#L145-L146

Added lines #L145 - L146 were not covered by tests
if len(aux.Commitments) != 32 {
return errors.New("commitments must be 32 bytes")
}
copy(ts.Commitments[:], aux.Commitments)
return nil
}

func cidsFromTipSetKey(encoded []byte) ([]cid.Cid, error) {
var cids []cid.Cid
for nextIdx := 0; nextIdx < len(encoded); {
nr, c, err := cid.CidFromBytes(encoded[nextIdx:])
if err != nil {
return nil, err
}
cids = append(cids, c)
nextIdx += nr
}
return cids, nil
}

func tipSetKeyFromCids(cids []cid.Cid) (TipSetKey, error) {
var buf bytes.Buffer
for _, c := range cids {
if _, err := buf.Write(c.Bytes()); err != nil {
return nil, err
}

Check warning on line 172 in gpbft/chain.go

View check run for this annotation

Codecov / codecov/patch

gpbft/chain.go#L171-L172

Added lines #L171 - L172 were not covered by tests
}
return buf.Bytes(), nil
}

// 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,
Expand Down
78 changes: 60 additions & 18 deletions gpbft/chain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,26 +191,55 @@ func TestECChain_Eq(t *testing.T) {
func TestTipSetKeySerialization(t *testing.T) {
t.Parallel()
var (
c1 = gpbft.MakeCid([]byte("barreleye1"))
c2 = gpbft.MakeCid([]byte("barreleye2"))
c3 = gpbft.MakeCid([]byte("barreleye3"))
ts1 = gpbft.TipSet{
Epoch: 1,
Key: append(append(c1.Bytes(), c2.Bytes()...), c3.Bytes()...),
PowerTable: gpbft.MakeCid([]byte("fish")),
Commitments: [32]byte{0x01},
c1 = gpbft.MakeCid([]byte("barreleye1"))
c2 = gpbft.MakeCid([]byte("barreleye2"))
c3 = gpbft.MakeCid([]byte("barreleye3"))
testCases = []gpbft.TipSet{
{
Epoch: 1,
Key: append(append(c1.Bytes(), c2.Bytes()...), c3.Bytes()...),
PowerTable: gpbft.MakeCid([]byte("fish")),
Commitments: [32]byte{0x01},
},
{
Epoch: 101,
Key: c1.Bytes(),
PowerTable: gpbft.MakeCid([]byte("lobster")),
Commitments: [32]byte{0x02},
},
}
ts2 = gpbft.TipSet{
Epoch: 101,
Key: c1.Bytes(),
PowerTable: gpbft.MakeCid([]byte("lobster")),
Commitments: [32]byte{0x02},
badJsonEncodable = []struct {
ts gpbft.TipSet
err string
}{
{
ts: gpbft.TipSet{
Epoch: 1,
Key: []byte("nope"),
PowerTable: gpbft.MakeCid([]byte("fish")),
Commitments: [32]byte{0x01},
},
err: "invalid cid",
},
}
badJsonDecodable = []struct {
json string
err string
}{
{
json: `{"Key":["nope"],"Commitments":"AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=","Epoch":101,"PowerTable":{"/":"bafy2bzaced5zqzzbxzyzuq2tcxhuclnvdn3y6ijhurgaapnbayul2dd5gspc4"}}`,
err: "invalid cid",
},
{
json: `{"Key":[{"/":"bafy2bzacecp4qqs334yrvzxsnlolskbtvyc3ub7k5tzx4s2m77vimzzkduj3g"}],"Commitments":"bm9wZQ==","Epoch":101,"PowerTable":{"/":"bafy2bzaced5zqzzbxzyzuq2tcxhuclnvdn3y6ijhurgaapnbayul2dd5gspc4"}}`,
err: "32 bytes",
},
}
)

t.Run("cbor round trip", func(t *testing.T) {
req := require.New(t)
for _, ts := range []gpbft.TipSet{ts1, ts2} {
for _, ts := range testCases {
var buf bytes.Buffer
req.NoError(ts.MarshalCBOR(&buf))
t.Logf("cbor: %x", buf.Bytes())
Expand All @@ -222,7 +251,7 @@ func TestTipSetKeySerialization(t *testing.T) {

t.Run("json round trip", func(t *testing.T) {
req := require.New(t)
for _, ts := range []gpbft.TipSet{ts1, ts2} {
for _, ts := range testCases {
data, err := ts.MarshalJSON()
req.NoError(err)
t.Logf("json: %s", data)
Expand All @@ -231,14 +260,27 @@ func TestTipSetKeySerialization(t *testing.T) {
req.Equal(ts, rt)

// check that we serialized the CIDs in the standard dag-json form
var bareMap map[string]interface{}
var bareMap map[string]any
req.NoError(json.Unmarshal(data, &bareMap))
keyField, ok := bareMap["Key"].([]interface{})
keyField, ok := bareMap["Key"].([]any)
req.True(ok)
req.Len(keyField, len(ts.Key)/38)
for j, c := range []cid.Cid{c1, c2, c3}[:len(ts.Key)/38] {
req.Equal(map[string]interface{}{"/": c.String()}, keyField[j])
req.Equal(map[string]any{"/": c.String()}, keyField[j])
}
}
})

t.Run("json error cases", func(t *testing.T) {
req := require.New(t)
for i, tc := range badJsonEncodable {
_, err := tc.ts.MarshalJSON()
req.ErrorContains(err, tc.err, "expected error for test case %d", i)
}
for i, tc := range badJsonDecodable {
var ts gpbft.TipSet
err := ts.UnmarshalJSON([]byte(tc.json))
req.ErrorContains(err, tc.err, "expected error for test case %d", i)
}
})
}

0 comments on commit 53c3ad1

Please sign in to comment.