diff --git a/certs/certs.go b/certs/certs.go index f6dfef59..fef67a69 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 `json:"omitempty"` + PowerTableDelta PowerTableDiff `json:"PowerTableDelta,omitempty"` } // NewFinalityCertificate constructs a new finality certificate from the given power delta (from diff --git a/gpbft/chain_test.go b/gpbft/chain_test.go index bbf4c179..17b273f2 100644 --- a/gpbft/chain_test.go +++ b/gpbft/chain_test.go @@ -188,7 +188,7 @@ func TestECChain_Eq(t *testing.T) { } } -func TestTipSetKeySerialization(t *testing.T) { +func TestTipSetSerialization(t *testing.T) { t.Parallel() var ( c1 = gpbft.MakeCid([]byte("barreleye1")) @@ -268,6 +268,11 @@ func TestTipSetKeySerialization(t *testing.T) { for j, c := range []cid.Cid{c1, c2, c3}[:len(ts.Key)/38] { req.Equal(map[string]any{"/": c.String()}, keyField[j]) } + + // check that the supplemental data is a base64 string + commitField, ok := bareMap["Commitments"].(string) + req.True(ok) + req.Len(commitField, 44) } }) diff --git a/gpbft/gpbft.go b/gpbft/gpbft.go index 37ab5bb3..8918cd5b 100644 --- a/gpbft/gpbft.go +++ b/gpbft/gpbft.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/binary" + "encoding/json" "errors" "fmt" "math" @@ -94,6 +95,35 @@ func (d *SupplementalData) Eq(other *SupplementalData) bool { return d.Commitments == other.Commitments && d.PowerTable == other.PowerTable } +// Custom JSON marshalling for SupplementalData to achieve a commitment field +// that is a base64-encoded string. + +type supplementalDataSub SupplementalData +type supplementalDataJson struct { + Commitments []byte + *supplementalDataSub +} + +func (sd SupplementalData) MarshalJSON() ([]byte, error) { + return json.Marshal(&supplementalDataJson{ + Commitments: sd.Commitments[:], + supplementalDataSub: (*supplementalDataSub)(&sd), + }) +} + +func (sd *SupplementalData) UnmarshalJSON(b []byte) error { + aux := &supplementalDataJson{supplementalDataSub: (*supplementalDataSub)(sd)} + var err error + if err = json.Unmarshal(b, &aux); err != nil { + return err + } + if len(aux.Commitments) != 32 { + return errors.New("commitments must be 32 bytes") + } + copy(sd.Commitments[:], aux.Commitments) + return nil +} + // Fields of the message that make up the signature payload. type Payload struct { // GossiPBFT instance (epoch) number. diff --git a/gpbft/gpbft_test.go b/gpbft/gpbft_test.go index 4bde70fa..afeeb3e0 100644 --- a/gpbft/gpbft_test.go +++ b/gpbft/gpbft_test.go @@ -3,6 +3,7 @@ package gpbft_test import ( "bytes" "crypto/rand" + "encoding/json" "io" "testing" @@ -1734,3 +1735,57 @@ func TestGPBFT_Sway(t *testing.T) { require.Fail(t, "after 10 tries did not swayed to proposals 1 and 2 at CONVERGE and COMMIT, respectively.") }) } + +func TestSupplementalDataSerialization(t *testing.T) { + t.Parallel() + var ( + testCases = []gpbft.SupplementalData{ + { + PowerTable: gpbft.MakeCid([]byte("fish")), + Commitments: [32]byte{0x01}, + }, + { + PowerTable: gpbft.MakeCid([]byte("lobster")), + Commitments: [32]byte{0x02}, + }, + } + ) + + t.Run("cbor round trip", func(t *testing.T) { + req := require.New(t) + for _, ts := range testCases { + var buf bytes.Buffer + req.NoError(ts.MarshalCBOR(&buf)) + t.Logf("cbor: %x", buf.Bytes()) + var rt gpbft.SupplementalData + req.NoError(rt.UnmarshalCBOR(&buf)) + req.Equal(ts, rt) + } + }) + + t.Run("json round trip", func(t *testing.T) { + req := require.New(t) + for _, ts := range testCases { + data, err := ts.MarshalJSON() + req.NoError(err) + t.Logf("json: %s", data) + var rt gpbft.SupplementalData + req.NoError(rt.UnmarshalJSON(data)) + req.Equal(ts, rt) + + // check that the supplemental data is a base64 string + var bareMap map[string]any + req.NoError(json.Unmarshal(data, &bareMap)) + commitField, ok := bareMap["Commitments"].(string) + req.True(ok) + req.Len(commitField, 44) + } + }) + + t.Run("json error cases", func(t *testing.T) { + req := require.New(t) + var ts gpbft.SupplementalData + err := ts.UnmarshalJSON([]byte(`{"Commitments":"bm9wZQ==","PowerTable":{"/":"bafy2bzaced5zqzzbxzyzuq2tcxhuclnvdn3y6ijhurgaapnbayul2dd5gspc4"}}`)) + req.ErrorContains(err, "32 bytes") + }) +}