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

Fuzz and property tests for json codec #91

Merged
merged 4 commits into from
Dec 2, 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
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ toolchain go1.22.5

require (
github.com/hashicorp/go-plugin v1.6.2-0.20240829161738-06afb6d7ae99
github.com/leanovate/gopter v0.2.11
github.com/shopspring/decimal v1.4.0
github.com/smartcontractkit/chainlink-common v0.3.1-0.20241106142051-c7bded1c08ae
github.com/smartcontractkit/libocr v0.0.0-20241007185508-adbe57025f12
Expand Down
486 changes: 486 additions & 0 deletions go.sum

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions llo/json_report_codec.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func UnmarshalJSONStreamValue(enc *JSONStreamValue) (StreamValue, error) {

type JSONReportCodec struct{}

func (cdc JSONReportCodec) Encode(ctx context.Context, r Report, _ llotypes.ChannelDefinition) ([]byte, error) {
func (cdc JSONReportCodec) Encode(_ context.Context, r Report, _ llotypes.ChannelDefinition) ([]byte, error) {
type encode struct {
ConfigDigest types.ConfigDigest
SeqNr uint64
Expand Down Expand Up @@ -81,7 +81,6 @@ func (cdc JSONReportCodec) Encode(ctx context.Context, r Report, _ llotypes.Chan
return json.Marshal(e)
}

// Fuzz testing: MERC-6522
func (cdc JSONReportCodec) Decode(b []byte) (r Report, err error) {
type decode struct {
ConfigDigest string
Expand All @@ -97,6 +96,11 @@ func (cdc JSONReportCodec) Decode(b []byte) (r Report, err error) {
if err != nil {
return r, fmt.Errorf("failed to decode report: expected JSON (got: %s); %w", b, err)
}
if d.SeqNr == 0 {
// catch obviously bad inputs, since a valid report can never have SeqNr == 0
return r, fmt.Errorf("missing SeqNr")
}

cdBytes, err := hex.DecodeString(d.ConfigDigest)
if err != nil {
return r, fmt.Errorf("invalid ConfigDigest; %w", err)
Expand All @@ -112,10 +116,6 @@ func (cdc JSONReportCodec) Decode(b []byte) (r Report, err error) {
return r, fmt.Errorf("failed to decode StreamValue: %w", err)
}
}
if d.SeqNr == 0 {
// catch obviously bad inputs, since a valid report can never have SeqNr == 0
return r, fmt.Errorf("missing SeqNr")
}

return Report{
ConfigDigest: cd,
Expand Down
256 changes: 253 additions & 3 deletions llo/json_report_codec_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
package llo

import (
"bytes"
"fmt"
"math"
reflect "reflect"
"testing"

"github.com/leanovate/gopter"
"github.com/leanovate/gopter/gen"
"github.com/leanovate/gopter/prop"
"github.com/shopspring/decimal"
"github.com/smartcontractkit/libocr/commontypes"
"github.com/smartcontractkit/libocr/offchainreporting2/types"
ocr2types "github.com/smartcontractkit/libocr/offchainreporting2/types"

Expand All @@ -16,6 +23,250 @@ import (
"github.com/stretchr/testify/require"
)

func FuzzJSONCodec_Decode_Unpack(f *testing.F) {
validJSON := []byte(`{"foo":"bar"}`)
emptyInput := []byte(``)
nilInput := []byte(nil)
nullJSON := []byte(`null`)
incompleteJSON := []byte(`{`)
notJSON := []byte(`"random string"`)
unprintable := []byte{1, 2, 3}
validJSONReport := []byte(`{"ConfigDigest":"0102030000000000000000000000000000000000000000000000000000000000","SeqNr":43,"ChannelID":46,"ValidAfterSeconds":44,"ObservationTimestampSeconds":45,"Values":[{"Type":0,"Value":"1"},{"Type":0,"Value":"2"},{"Type":1,"Value":"Q{Bid: 3.13, Benchmark: 4.4, Ask: 5.12}"}],"Specimen":true}`)
invalidConfigDigest := []byte(`{"SeqNr":42,"ConfigDigest":"foo"}`)
invalidConfigDigestNotEnoughBytes := []byte(`{"SeqNr":42,"ConfigDigest":"0xdead"}`)
badStreamValues := []byte(`{"SeqNr":42,"ConfigDigest":"0102030000000000000000000000000000000000000000000000000000000000", "Values":[{"Type":0,"Value":null},{"Type":-1,"Value":"2"}]}`)

f.Add(validJSON)
f.Add(emptyInput)
f.Add(nilInput)
f.Add(nullJSON)
f.Add(incompleteJSON)
f.Add(notJSON)
f.Add(unprintable)
f.Add(validJSONReport)
f.Add(invalidConfigDigest)
f.Add(invalidConfigDigestNotEnoughBytes)
f.Add(badStreamValues)

validPackedJSONTemplate := `{"configDigest":"0102030000000000000000000000000000000000000000000000000000000000","seqNr":43,"report":%s,"sigs":[{"Signature":"AgME","Signer":2}]}`
packedJSONReports := [][]byte{
[]byte(fmt.Sprintf(validPackedJSONTemplate, validJSON)),
[]byte(fmt.Sprintf(validPackedJSONTemplate, emptyInput)),
[]byte(fmt.Sprintf(validPackedJSONTemplate, nilInput)),
[]byte(fmt.Sprintf(validPackedJSONTemplate, nullJSON)),
[]byte(fmt.Sprintf(validPackedJSONTemplate, incompleteJSON)),
[]byte(fmt.Sprintf(validPackedJSONTemplate, notJSON)),
[]byte(fmt.Sprintf(validPackedJSONTemplate, unprintable)),
[]byte(fmt.Sprintf(validPackedJSONTemplate, validJSONReport)),
[]byte(fmt.Sprintf(validPackedJSONTemplate, invalidConfigDigest)),
[]byte(fmt.Sprintf(validPackedJSONTemplate, invalidConfigDigestNotEnoughBytes)),
[]byte(fmt.Sprintf(validPackedJSONTemplate, badStreamValues)),
}
for _, packedJSONReport := range packedJSONReports {
f.Add(packedJSONReport)
}

packedJSONSigTemplate := `{"configDigest":"0102030000000000000000000000000000000000000000000000000000000000","seqNr":43,"report":{},"sigs":[{"Signature":%s,"Signer":2}]}`
badSigs := [][]byte{
[]byte(fmt.Sprintf(packedJSONSigTemplate, `null`)),
[]byte(fmt.Sprintf(packedJSONSigTemplate, `""`)),
[]byte(fmt.Sprintf(packedJSONSigTemplate, `1`)),
[]byte(fmt.Sprintf(packedJSONSigTemplate, `[]`)),
[]byte(fmt.Sprintf(packedJSONSigTemplate, `"abc$def#ghi!"`)),
}
for _, badSig := range badSigs {
f.Add(badSig)
}

var codec JSONReportCodec
f.Fuzz(func(t *testing.T, data []byte) {
// test that it doesn't panic, don't care about errors
codec.Decode(data) //nolint:errcheck
codec.Unpack(data) //nolint:errcheck
codec.UnpackDecode(data) //nolint:errcheck
})
}

func Test_JSONCodec_Properties(t *testing.T) {
properties := gopter.NewProperties(nil)

ctx := tests.Context(t)
cd := llotypes.ChannelDefinition{}
codec := JSONReportCodec{}

properties.Property("Encode/Decode", prop.ForAll(
func(r Report) bool {
b, err := codec.Encode(ctx, r, cd)
require.NoError(t, err)
r2, err := codec.Decode(b)
require.NoError(t, err)
return equalReports(r, r2)
},
gen.StrictStruct(reflect.TypeOf(&Report{}), map[string]gopter.Gen{
"ConfigDigest": genConfigDigest(),
"SeqNr": genSeqNr(),
"ChannelID": gen.UInt32(),
"ValidAfterSeconds": gen.UInt32(),
"ObservationTimestampSeconds": gen.UInt32(),
"Values": genStreamValues(),
"Specimen": gen.Bool(),
}),
))

properties.Property("Pack/Unpack", prop.ForAll(
func(digest types.ConfigDigest, seqNr uint64, report ocr2types.Report, sigs []types.AttributedOnchainSignature) bool {
b, err := codec.Pack(digest, seqNr, report, sigs)
require.NoError(t, err)
digest2, seqNr2, report2, sigs2, err := codec.Unpack(b)
require.NoError(t, err)

if digest != digest2 {
return false
}
if seqNr != seqNr2 {
return false
}
if !bytes.Equal(report, report2) {
return false
}
if len(sigs) != len(sigs2) {
return false
}
for i := range sigs {
if sigs[i].Signer != sigs2[i].Signer || !bytes.Equal(sigs[i].Signature, sigs2[i].Signature) {
return false
}
}
return true
},
genConfigDigest(),
genSeqNr(),
genSerializedReport(),
genSigs(),
))

properties.TestingRun(t)
}

func equalReports(r, r2 Report) bool {
if r.ConfigDigest != r2.ConfigDigest {
return false
}
if r.SeqNr != r2.SeqNr {
return false
}
if r.ChannelID != r2.ChannelID {
return false
}
if r.ValidAfterSeconds != r2.ValidAfterSeconds {
return false
}
if r.ObservationTimestampSeconds != r2.ObservationTimestampSeconds {
return false
}
if len(r.Values) != len(r2.Values) {
return false
}
for i := range r.Values {
if !equalStreamValues(r.Values[i], r2.Values[i]) {
return false
}
}
return r.Specimen == r2.Specimen
}

func equalStreamValues(sv, sv2 StreamValue) bool {
if sv.Type() != sv2.Type() {
return false
}
m1, err := sv.MarshalBinary()
if err != nil {
// should be impossible
panic(err)
}
m2, err := sv2.MarshalBinary()
if err != nil {
// should be impossible
panic(err)
}
return bytes.Equal(m1, m2)
}

func genConfigDigest() gopter.Gen {
return func(p *gopter.GenParameters) *gopter.GenResult {
var cd types.ConfigDigest
p.Rng.Read(cd[:])
return gopter.NewGenResult(cd, gopter.NoShrinker)
}
}

func genSeqNr() gopter.Gen {
return gen.UInt64Range(1, math.MaxUint64)
}

func genSerializedReport() gopter.Gen {
return gen.Const(ocr2types.Report(`{"foo":"bar"}`))
}

func genSigs() gopter.Gen {
return gen.SliceOf(genSig())
}

func genSig() gopter.Gen {
return gen.StrictStruct(reflect.TypeOf(&types.AttributedOnchainSignature{}), map[string]gopter.Gen{
"Signature": genSigBytes(),
"Signer": genSigner(),
})
}

func genSigner() gopter.Gen {
return gen.UInt8().Map(func(v uint8) commontypes.OracleID {
return commontypes.OracleID(v)
})
}

func genSigBytes() gopter.Gen {
return gen.SliceOf(gen.UInt8())
}

func genDecimalValue() gopter.Gen {
return func(p *gopter.GenParameters) *gopter.GenResult {
var sv StreamValue = ToDecimal(decimal.NewFromFloat(p.Rng.Float64()))
return gopter.NewGenResult(sv, gopter.NoShrinker)
}
}

func genQuote() gopter.Gen {
return func(p *gopter.GenParameters) *gopter.GenResult {
var sv StreamValue = &Quote{
Bid: decimal.NewFromFloat(p.Rng.Float64()),
Benchmark: decimal.NewFromFloat(p.Rng.Float64()),
Ask: decimal.NewFromFloat(p.Rng.Float64()),
}
return gopter.NewGenResult(sv, gopter.NoShrinker)
}
}

func genStreamValue() gopter.Gen {
return func(p *gopter.GenParameters) *gopter.GenResult {
switch p.Rng.Intn(3) {
case 0:
return genDecimalValue()(p)
case 1:
return genQuote()(p)
case 2:
return gopter.NewGenResult((StreamValue)(nil), gopter.NoShrinker)
}
return nil
}
}

var streamValueSliceType = reflect.TypeOf((*StreamValue)(nil)).Elem()

func genStreamValues() gopter.Gen {
return gen.SliceOf(genStreamValue(), streamValueSliceType)
}

func Test_JSONCodec(t *testing.T) {
t.Run("Encode=>Decode", func(t *testing.T) {
ctx := tests.Context(t)
Expand All @@ -34,7 +285,6 @@ func Test_JSONCodec(t *testing.T) {
encoded, err := cdc.Encode(ctx, r, llo.ChannelDefinition{})
require.NoError(t, err)

fmt.Println("encoded", string(encoded))
assert.Equal(t, `{"ConfigDigest":"0102030000000000000000000000000000000000000000000000000000000000","SeqNr":43,"ChannelID":46,"ValidAfterSeconds":44,"ObservationTimestampSeconds":45,"Values":[{"Type":0,"Value":"1"},{"Type":0,"Value":"2"},{"Type":1,"Value":"Q{Bid: 3.13, Benchmark: 4.4, Ask: 5.12}"}],"Specimen":true}`, string(encoded))

decoded, err := cdc.Decode(encoded)
Expand Down Expand Up @@ -97,8 +347,8 @@ func Test_JSONCodec(t *testing.T) {
t.Run("invalid input fails decode", func(t *testing.T) {
cdc := JSONReportCodec{}
_, err := cdc.Decode([]byte(`{}`))
assert.EqualError(t, err, "invalid ConfigDigest; cannot convert bytes to ConfigDigest. bytes have wrong length 0")
_, err = cdc.Decode([]byte(`{"ConfigDigest":"0102030000000000000000000000000000000000000000000000000000000000"}`))
assert.EqualError(t, err, "missing SeqNr")
_, err = cdc.Decode([]byte(`{"seqNr":1}`))
assert.EqualError(t, err, "invalid ConfigDigest; cannot convert bytes to ConfigDigest. bytes have wrong length 0")
})
}
2 changes: 0 additions & 2 deletions llo/onchain_config_codec.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ var _ OnchainConfigCodec = EVMOnchainConfigCodec{}
// returned by EncodeValueInt192.
type EVMOnchainConfigCodec struct{}

// TODO: Needs fuzz testing - MERC-6522
func (EVMOnchainConfigCodec) Decode(b []byte) (OnchainConfig, error) {
if len(b) != onchainConfigEncodedLength {
return OnchainConfig{}, fmt.Errorf("unexpected length of OnchainConfig, expected %v, got %v", onchainConfigEncodedLength, len(b))
Expand All @@ -60,7 +59,6 @@ func (EVMOnchainConfigCodec) Decode(b []byte) (OnchainConfig, error) {
return o, nil
}

// TODO: Needs fuzz testing - MERC-6522
func (EVMOnchainConfigCodec) Encode(c OnchainConfig) ([]byte, error) {
if c.Version != onchainConfigVersion {
return nil, fmt.Errorf("unexpected version of OnchainConfig, expected %v, got %v", onchainConfigVersion, c.Version)
Expand Down
Loading
Loading