Skip to content

Commit

Permalink
Custom JSON serialization of TipSetKey for array-of-CIDs (#756)
Browse files Browse the repository at this point in the history
* Custom JSON serialization of TipSetKey for array-of-CIDs

* fixup! Custom JSON serialization of TipSetKey for array-of-CIDs

* fixup! Custom JSON serialization of TipSetKey for array-of-CIDs
  • Loading branch information
rvagg authored Nov 28, 2024
1 parent c2f99cf commit 99ae2fc
Show file tree
Hide file tree
Showing 5 changed files with 254 additions and 1 deletion.
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:"PowerTableDelta,omitempty"`
}

// NewFinalityCertificate constructs a new finality certificate from the given power delta (from
Expand Down
63 changes: 63 additions & 0 deletions gpbft/chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"encoding/base32"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"strings"
Expand Down Expand Up @@ -111,6 +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:
// 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) {
cids, err := cidsFromTipSetKey(ts.Key)
if err != nil {
return nil, err
}
return json.Marshal(&tipSetJson{
Key: cids,
Commitments: ts.Commitments[:],
tipSetSub: (*tipSetSub)(&ts),
})
}

func (ts *TipSet) UnmarshalJSON(b []byte) error {
aux := &tipSetJson{tipSetSub: (*tipSetSub)(ts)}
var err error
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,
Expand Down
105 changes: 105 additions & 0 deletions gpbft/chain_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package gpbft_test

import (
"bytes"
"encoding/json"
"testing"

"github.com/filecoin-project/go-f3/gpbft"
"github.com/ipfs/go-cid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -184,3 +187,105 @@ func TestECChain_Eq(t *testing.T) {
})
}
}

func TestTipSetSerialization(t *testing.T) {
t.Parallel()
var (
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},
},
}
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 testCases {
var buf bytes.Buffer
req.NoError(ts.MarshalCBOR(&buf))
t.Logf("cbor: %x", buf.Bytes())
var rt gpbft.TipSet
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.TipSet
req.NoError(rt.UnmarshalJSON(data))
req.Equal(ts, rt)

// check that we serialized the CIDs in the standard dag-json form
var bareMap map[string]any
req.NoError(json.Unmarshal(data, &bareMap))
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]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)
}
})

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)
}
})
}
30 changes: 30 additions & 0 deletions gpbft/gpbft.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"context"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"math"
Expand Down Expand Up @@ -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.
Expand Down
55 changes: 55 additions & 0 deletions gpbft/gpbft_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package gpbft_test
import (
"bytes"
"crypto/rand"
"encoding/json"
"io"
"testing"

Expand Down Expand Up @@ -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")
})
}

0 comments on commit 99ae2fc

Please sign in to comment.