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

Custom JSON serialization of TipSetKey for array-of-CIDs #756

Merged
merged 3 commits into from
Nov 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
"bytes"
"encoding/base32"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"strings"
Expand Down Expand Up @@ -111,6 +112,68 @@
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
}

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
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 @@
"bytes"
"context"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"math"
Expand Down Expand Up @@ -94,6 +95,35 @@
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
}

Check warning on line 119 in gpbft/gpbft.go

View check run for this annotation

Codecov / codecov/patch

gpbft/gpbft.go#L118-L119

Added lines #L118 - L119 were not covered by tests
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")
})
}
Loading