diff --git a/certs/certs.go b/certs/certs.go index 4d91fa32..f6dfef59 100644 --- a/certs/certs.go +++ b/certs/certs.go @@ -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 diff --git a/gpbft/chain.go b/gpbft/chain.go index 5acd0209..cac191f8 100644 --- a/gpbft/chain.go +++ b/gpbft/chain.go @@ -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 { @@ -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 } + 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 + } + } + 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, diff --git a/gpbft/chain_test.go b/gpbft/chain_test.go index 506d51d0..bbf4c179 100644 --- a/gpbft/chain_test.go +++ b/gpbft/chain_test.go @@ -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()) @@ -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) @@ -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) + } + }) }