From 6a833fdfa79b825ddd86804e2f749934b0845bc4 Mon Sep 17 00:00:00 2001 From: Diego Date: Wed, 2 Oct 2024 11:30:56 -0300 Subject: [PATCH] feat(lib/grandpa): Add warp sync provider (#4187) --- dot/state/grandpa.go | 23 ++- dot/state/interfaces.go | 5 + .../consensus/grandpa/mocks_generate_test.go | 6 + .../client/consensus/grandpa/mocks_test.go | 139 ++++++++++++++ .../client/consensus/grandpa/warp_sync.go | 173 ++++++++++++++++++ .../consensus/grandpa/warp_sync_test.go | 126 +++++++++++++ 6 files changed, 471 insertions(+), 1 deletion(-) create mode 100644 internal/client/consensus/grandpa/mocks_generate_test.go create mode 100644 internal/client/consensus/grandpa/mocks_test.go create mode 100644 internal/client/consensus/grandpa/warp_sync.go create mode 100644 internal/client/consensus/grandpa/warp_sync_test.go diff --git a/dot/state/grandpa.go b/dot/state/grandpa.go index e5b1ad1ac1..cbea1ed38c 100644 --- a/dot/state/grandpa.go +++ b/dot/state/grandpa.go @@ -7,6 +7,7 @@ import ( "encoding/binary" "errors" "fmt" + "slices" "strconv" "github.com/ChainSafe/gossamer/dot/telemetry" @@ -37,7 +38,7 @@ var ( // GrandpaState tracks information related to grandpa type GrandpaState struct { - db GetPutDeleter + db GrandpaDatabase blockState *BlockState forcedChanges *orderedPendingChanges @@ -602,3 +603,23 @@ func (s *GrandpaState) GetPrecommits(round, setID uint64) ([]types.GrandpaSigned return pcs, nil } + +// GetAuthoritiesChangesFromBlock retrieves blocks numbers where authority set changes happened +func (s *GrandpaState) GetAuthoritiesChangesFromBlock(initialBlockNumber uint) ([]uint, error) { + blockNumbers := make([]uint, 0) + iter, err := s.db.NewPrefixIterator(setIDChangePrefix) + if err != nil { + return nil, err + } + + for iter.Next() { + blockNumber := common.BytesToUint(iter.Value()) + if blockNumber >= initialBlockNumber { + blockNumbers = append(blockNumbers, blockNumber) + } + } + + // To ensure the order of the blocks + slices.Sort(blockNumbers) + return blockNumbers, nil +} diff --git a/dot/state/interfaces.go b/dot/state/interfaces.go index 55b3f426fe..9042358003 100644 --- a/dot/state/interfaces.go +++ b/dot/state/interfaces.go @@ -10,6 +10,11 @@ import ( "github.com/ChainSafe/gossamer/internal/database" ) +type GrandpaDatabase interface { + GetPutDeleter + NewPrefixIterator(prefix []byte) (database.Iterator, error) +} + // GetPutDeleter has methods to get, put and delete key values. type GetPutDeleter interface { GetPutter diff --git a/internal/client/consensus/grandpa/mocks_generate_test.go b/internal/client/consensus/grandpa/mocks_generate_test.go new file mode 100644 index 0000000000..84d6188cef --- /dev/null +++ b/internal/client/consensus/grandpa/mocks_generate_test.go @@ -0,0 +1,6 @@ +// Copyright 2021 ChainSafe Systems (ON) +// SPDX-License-Identifier: LGPL-3.0-only + +package grandpa + +//go:generate mockgen -destination=mocks_test.go -package $GOPACKAGE . BlockState,GrandpaState diff --git a/internal/client/consensus/grandpa/mocks_test.go b/internal/client/consensus/grandpa/mocks_test.go new file mode 100644 index 0000000000..7d7a4ceb98 --- /dev/null +++ b/internal/client/consensus/grandpa/mocks_test.go @@ -0,0 +1,139 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/ChainSafe/gossamer/internal/client/consensus/grandpa (interfaces: BlockState,GrandpaState) +// +// Generated by this command: +// +// mockgen -destination=mocks_test.go -package grandpa . BlockState,GrandpaState +// + +// Package grandpa is a generated GoMock package. +package grandpa + +import ( + reflect "reflect" + + types "github.com/ChainSafe/gossamer/dot/types" + common "github.com/ChainSafe/gossamer/lib/common" + gomock "go.uber.org/mock/gomock" +) + +// MockBlockState is a mock of BlockState interface. +type MockBlockState struct { + ctrl *gomock.Controller + recorder *MockBlockStateMockRecorder +} + +// MockBlockStateMockRecorder is the mock recorder for MockBlockState. +type MockBlockStateMockRecorder struct { + mock *MockBlockState +} + +// NewMockBlockState creates a new mock instance. +func NewMockBlockState(ctrl *gomock.Controller) *MockBlockState { + mock := &MockBlockState{ctrl: ctrl} + mock.recorder = &MockBlockStateMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockBlockState) EXPECT() *MockBlockStateMockRecorder { + return m.recorder +} + +// GetHeader mocks base method. +func (m *MockBlockState) GetHeader(arg0 common.Hash) (*types.Header, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetHeader", arg0) + ret0, _ := ret[0].(*types.Header) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetHeader indicates an expected call of GetHeader. +func (mr *MockBlockStateMockRecorder) GetHeader(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetHeader", reflect.TypeOf((*MockBlockState)(nil).GetHeader), arg0) +} + +// GetHeaderByNumber mocks base method. +func (m *MockBlockState) GetHeaderByNumber(arg0 uint) (*types.Header, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetHeaderByNumber", arg0) + ret0, _ := ret[0].(*types.Header) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetHeaderByNumber indicates an expected call of GetHeaderByNumber. +func (mr *MockBlockStateMockRecorder) GetHeaderByNumber(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetHeaderByNumber", reflect.TypeOf((*MockBlockState)(nil).GetHeaderByNumber), arg0) +} + +// GetHighestFinalisedHeader mocks base method. +func (m *MockBlockState) GetHighestFinalisedHeader() (*types.Header, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetHighestFinalisedHeader") + ret0, _ := ret[0].(*types.Header) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetHighestFinalisedHeader indicates an expected call of GetHighestFinalisedHeader. +func (mr *MockBlockStateMockRecorder) GetHighestFinalisedHeader() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetHighestFinalisedHeader", reflect.TypeOf((*MockBlockState)(nil).GetHighestFinalisedHeader)) +} + +// GetJustification mocks base method. +func (m *MockBlockState) GetJustification(arg0 common.Hash) ([]byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetJustification", arg0) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetJustification indicates an expected call of GetJustification. +func (mr *MockBlockStateMockRecorder) GetJustification(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetJustification", reflect.TypeOf((*MockBlockState)(nil).GetJustification), arg0) +} + +// MockGrandpaState is a mock of GrandpaState interface. +type MockGrandpaState struct { + ctrl *gomock.Controller + recorder *MockGrandpaStateMockRecorder +} + +// MockGrandpaStateMockRecorder is the mock recorder for MockGrandpaState. +type MockGrandpaStateMockRecorder struct { + mock *MockGrandpaState +} + +// NewMockGrandpaState creates a new mock instance. +func NewMockGrandpaState(ctrl *gomock.Controller) *MockGrandpaState { + mock := &MockGrandpaState{ctrl: ctrl} + mock.recorder = &MockGrandpaStateMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockGrandpaState) EXPECT() *MockGrandpaStateMockRecorder { + return m.recorder +} + +// GetAuthoritiesChangesFromBlock mocks base method. +func (m *MockGrandpaState) GetAuthoritiesChangesFromBlock(arg0 uint) ([]uint, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAuthoritiesChangesFromBlock", arg0) + ret0, _ := ret[0].([]uint) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAuthoritiesChangesFromBlock indicates an expected call of GetAuthoritiesChangesFromBlock. +func (mr *MockGrandpaStateMockRecorder) GetAuthoritiesChangesFromBlock(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthoritiesChangesFromBlock", reflect.TypeOf((*MockGrandpaState)(nil).GetAuthoritiesChangesFromBlock), arg0) +} diff --git a/internal/client/consensus/grandpa/warp_sync.go b/internal/client/consensus/grandpa/warp_sync.go new file mode 100644 index 0000000000..2e7a1289a1 --- /dev/null +++ b/internal/client/consensus/grandpa/warp_sync.go @@ -0,0 +1,173 @@ +// Copyright 2024 ChainSafe Systems (ON) +// SPDX-License-Identifier: LGPL-3.0-only + +package grandpa + +import ( + "fmt" + + "github.com/ChainSafe/gossamer/dot/types" + "github.com/ChainSafe/gossamer/internal/primitives/core/hash" + "github.com/ChainSafe/gossamer/internal/primitives/runtime" + "github.com/ChainSafe/gossamer/lib/common" + "github.com/ChainSafe/gossamer/pkg/scale" +) + +const MaxWarpSyncProofSize = 8 * 1024 * 1024 + +var ( + errMissingStartBlock = fmt.Errorf("missing start block") + errStartBlockNotFinalized = fmt.Errorf("start block is not finalized") +) + +type BlockState interface { + GetHeader(hash common.Hash) (*types.Header, error) + GetHighestFinalisedHeader() (*types.Header, error) + GetHeaderByNumber(num uint) (*types.Header, error) + GetJustification(hash common.Hash) ([]byte, error) +} + +type GrandpaState interface { + GetAuthoritiesChangesFromBlock(blockNumber uint) ([]uint, error) +} + +type WarpSyncFragment struct { + // The last block that the given authority set finalized. This block should contain a digest + // signalling an authority set change from which we can fetch the next authority set. + Header types.Header + // A justification for the header above which proves its finality. In order to validate it the + // verifier must be aware of the authorities and set id for which the justification refers to. + Justification GrandpaJustification[hash.H256, uint64] +} + +type WarpSyncProof struct { + Proofs []WarpSyncFragment + // indicates whether the warp sync has been completed + IsFinished bool + proofsLength int +} + +func NewWarpSyncProof() WarpSyncProof { + return WarpSyncProof{ + Proofs: make([]WarpSyncFragment, 0), + IsFinished: false, + proofsLength: 0, + } +} + +func (w *WarpSyncProof) addFragment(fragment WarpSyncFragment) (limitReached bool, err error) { + encodedFragment, err := scale.Marshal(fragment) + if err != nil { + return false, err + } + + if w.proofsLength+len(encodedFragment) >= MaxWarpSyncProofSize { + return true, nil + } + + w.proofsLength += len(encodedFragment) + w.Proofs = append(w.Proofs, fragment) + + return false, nil +} + +func (w *WarpSyncProof) lastProofBlockNumber() uint64 { + if len(w.Proofs) == 0 { + return 0 + } + return w.Proofs[len(w.Proofs)-1].Justification.Justification.Commit.TargetNumber + 1 +} + +type WarpSyncProofProvider struct { + blockState BlockState + grandpaState GrandpaState +} + +// Generate build a warp sync encoded proof starting from the given block hash +func (np *WarpSyncProofProvider) Generate(start common.Hash) ([]byte, error) { + // Get and traverse all GRANDPA authorities changes from the given block hash + beginBlockHeader, err := np.blockState.GetHeader(start) + if err != nil { + return nil, fmt.Errorf("%w: %w", errMissingStartBlock, err) + } + + lastFinalizedBlockHeader, err := np.blockState.GetHighestFinalisedHeader() + if err != nil { + return nil, fmt.Errorf("getting best block header: %w", err) + } + + if beginBlockHeader.Number > lastFinalizedBlockHeader.Number { + return nil, errStartBlockNotFinalized + } + + authoritySetChanges, err := np.grandpaState.GetAuthoritiesChangesFromBlock(beginBlockHeader.Number) + if err != nil { + return nil, err + } + + limitReached := false + finalProof := NewWarpSyncProof() + for _, blockNumber := range authoritySetChanges { + header, err := np.blockState.GetHeaderByNumber(blockNumber) + if err != nil { + return nil, err + } + + encJustification, err := np.blockState.GetJustification(header.Hash()) // get the justification of such block + if err != nil { + return nil, err + } + + justification, err := decodeJustification[hash.H256, uint64, runtime.BlakeTwo256](encJustification) + if err != nil { + return nil, err + } + + fragment := WarpSyncFragment{Header: *header, Justification: *justification} + + // check the proof size + limitReached, err = finalProof.addFragment(fragment) + if err != nil { + return nil, err + } + + if limitReached { + break + } + } + + // If the limit is not reached then retrieve the latest (best) justification + // and append in the proofs + if !limitReached { + // the existing best justification must be for a block higher than the + // last authority set change. if we didn't prove any authority set + // change then we fallback to make sure it's higher or equal to the + // initial warp sync block. + lastFinalizedBlockHeader, err := np.blockState.GetHighestFinalisedHeader() + if err != nil { + return nil, fmt.Errorf("getting best block header: %w", err) + } + latestJustification, err := np.blockState.GetJustification(lastFinalizedBlockHeader.Hash()) + if err != nil { + return nil, err + } + + justification, err := decodeJustification[hash.H256, uint64, runtime.BlakeTwo256](latestJustification) + if err != nil { + return nil, err + } + + if justification.Justification.Commit.TargetNumber >= finalProof.lastProofBlockNumber() { + fragment := WarpSyncFragment{Header: *lastFinalizedBlockHeader, Justification: *justification} + _, err = finalProof.addFragment(fragment) + if err != nil { + return nil, err + } + } + + finalProof.IsFinished = true + } + + // Encode and return the proof + return scale.Marshal(finalProof) +} diff --git a/internal/client/consensus/grandpa/warp_sync_test.go b/internal/client/consensus/grandpa/warp_sync_test.go new file mode 100644 index 0000000000..3fe109397b --- /dev/null +++ b/internal/client/consensus/grandpa/warp_sync_test.go @@ -0,0 +1,126 @@ +// Copyright 2024 ChainSafe Systems (ON) +// SPDX-License-Identifier: LGPL-3.0-only + +package grandpa + +import ( + "errors" + "testing" + + "github.com/ChainSafe/gossamer/dot/types" + "github.com/ChainSafe/gossamer/lib/common" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +func TestGenerateWarpSyncProofBlockNotFound(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // Mock block state to return not found block + blockStateMock := NewMockBlockState(ctrl) + blockStateMock.EXPECT().GetHeader(common.EmptyHash).Return(nil, errors.New("not found")).AnyTimes() + + provider := &WarpSyncProofProvider{ + blockState: blockStateMock, + } + + // Check errMissingStartBlock returned by provider + _, err := provider.Generate(common.EmptyHash) + assert.Error(t, err) + assert.ErrorIs(t, err, errMissingStartBlock) +} + +func TestGenerateWarpSyncProofBlockNotFinalized(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // Mock block state to return not found block + bestBlockHeader := &types.Header{ + Number: 2, + ParentHash: common.MustBlake2bHash([]byte("1")), + } + + notFinalizedBlockHeader := &types.Header{ + Number: 3, + ParentHash: common.MustBlake2bHash([]byte("2")), + } + + blockStateMock := NewMockBlockState(ctrl) + blockStateMock.EXPECT().GetHeader(notFinalizedBlockHeader.Hash()).Return(notFinalizedBlockHeader, nil).AnyTimes() + blockStateMock.EXPECT().GetHighestFinalisedHeader().Return(bestBlockHeader, nil).AnyTimes() + + provider := &WarpSyncProofProvider{ + blockState: blockStateMock, + } + + // Check errMissingStartBlock returned by provider + _, err := provider.Generate(notFinalizedBlockHeader.Hash()) + assert.Error(t, err) + assert.ErrorIs(t, err, errStartBlockNotFinalized) +} + +//nolint:lll +func TestGenerateWarpSyncProofOk(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + encodedJustification1 := []byte{42, 0, 0, 0, 0, 0, 0, 0, 236, 148, 26, 228, 225, 134, 98, 140, 150, 154, 23, 185, 43, 231, 172, 194, 69, 150, 27, 191, 202, 50, 108, 91, 220, 57, 214, 47, 202, 62, 70, 238, 10, 0, 0, 0, 0, 0, 0, 0, 4, 236, 148, 26, 228, 225, 134, 98, 140, 150, 154, 23, 185, 43, 231, 172, 194, 69, 150, 27, 191, 202, 50, 108, 91, 220, 57, 214, 47, 202, 62, 70, 238, 10, 0, 0, 0, 0, 0, 0, 0, 201, 232, 26, 136, 31, 77, 15, 194, 34, 200, 248, 43, 219, 148, 207, 56, 240, 171, 208, 221, 162, 202, 153, 209, 150, 27, 71, 207, 227, 102, 133, 32, 206, 74, 78, 26, 148, 166, 18, 67, 188, 76, 163, 200, 68, 249, 134, 28, 122, 74, 182, 69, 135, 90, 199, 52, 72, 109, 41, 12, 37, 18, 161, 4, 136, 220, 52, 23, 213, 5, 142, 196, 180, 80, 62, 12, 18, 234, 26, 10, 137, 190, 32, 15, 233, 137, 34, 66, 61, 67, 52, 1, 79, 166, 176, 238, 0} + encodedJustification2 := []byte{50, 0, 0, 0, 0, 0, 0, 0, 236, 148, 26, 228, 225, 134, 98, 140, 150, 154, 23, 185, 43, 231, 172, 194, 69, 150, 27, 191, 202, 50, 108, 91, 220, 57, 214, 47, 202, 62, 70, 238, 10, 0, 0, 0, 0, 0, 0, 0, 4, 236, 148, 26, 228, 225, 134, 98, 140, 150, 154, 23, 185, 43, 231, 172, 194, 69, 150, 27, 191, 202, 50, 108, 91, 220, 57, 214, 47, 202, 62, 70, 238, 10, 0, 0, 0, 0, 0, 0, 0, 201, 232, 26, 136, 31, 77, 15, 194, 34, 200, 248, 43, 219, 148, 207, 56, 240, 171, 208, 221, 162, 202, 153, 209, 150, 27, 71, 207, 227, 102, 133, 32, 206, 74, 78, 26, 148, 166, 18, 67, 188, 76, 163, 200, 68, 249, 134, 28, 122, 74, 182, 69, 135, 90, 199, 52, 72, 109, 41, 12, 37, 18, 161, 4, 136, 220, 52, 23, 213, 5, 142, 196, 180, 80, 62, 12, 18, 234, 26, 10, 137, 190, 32, 15, 233, 137, 34, 66, 61, 67, 52, 1, 79, 166, 176, 238, 0} + var blockHeaders []*types.Header + + blockStateMock := NewMockBlockState(ctrl) + grandpaStateMock := NewMockGrandpaState(ctrl) + + for blockNumber := uint(1); blockNumber <= 10; blockNumber++ { + // Create block header + var header *types.Header + parentHash := common.Hash{0x00} + if blockNumber > 1 { + parentHash = blockHeaders[blockNumber-2].Hash() + } + + header = types.NewHeader( + parentHash, + common.Hash{byte(blockNumber)}, + common.Hash{byte(blockNumber)}, + blockNumber, + types.Digest{}, + ) + + blockHeaders = append(blockHeaders, header) + + // Mock block state responses + blockStateMock.EXPECT().GetHeader(header.Hash()).Return(header, nil).AnyTimes() + blockStateMock.EXPECT().GetHeaderByNumber(blockNumber).Return(header, nil).AnyTimes() + + // authorities set changes happens only in block 5 + if blockNumber < 5 { + grandpaStateMock.EXPECT().GetAuthoritiesChangesFromBlock(blockNumber).Return([]uint{5}, nil).AnyTimes() + } else if blockNumber == 5 { + blockStateMock.EXPECT().GetJustification(header.Hash()).Return(encodedJustification1, nil).AnyTimes() + } else { + grandpaStateMock.EXPECT().GetAuthoritiesChangesFromBlock(blockNumber).Return([]uint{}, nil).AnyTimes() + } + } + + blockStateMock.EXPECT().GetHighestFinalisedHeader().Return(blockHeaders[len(blockHeaders)-1], nil).AnyTimes() + blockStateMock.EXPECT().GetJustification(blockHeaders[len(blockHeaders)-1].Hash()).Return(encodedJustification2, nil).AnyTimes() + + provider := &WarpSyncProofProvider{ + blockState: blockStateMock, + grandpaState: grandpaStateMock, + } + + proof, err := provider.Generate(blockHeaders[0].Hash()) + assert.NoError(t, err) + + expectedProof := []byte{ + 0x4, 0x1c, 0xa4, 0x2, 0x25, 0x71, 0x86, 0xee, 0x43, 0x46, 0xfd, 0x2c, 0x9, 0xfe, 0xeb, 0x91, 0x17, 0x10, 0xe5, 0x88, 0x41, 0x89, 0xc3, 0xc7, 0x5f, 0xb5, 0x1, 0x1a, 0x75, 0x21, 0x37, 0x2f, 0xf9, 0x14, 0x5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2a, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xec, 0x94, 0x1a, 0xe4, 0xe1, 0x86, 0x62, 0x8c, 0x96, 0x9a, 0x17, 0xb9, 0x2b, 0xe7, 0xac, 0xc2, 0x45, 0x96, 0x1b, 0xbf, 0xca, 0x32, 0x6c, 0x5b, 0xdc, 0x39, 0xd6, 0x2f, 0xca, 0x3e, 0x46, 0xee, 0xa, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4, 0xec, 0x94, 0x1a, 0xe4, 0xe1, 0x86, 0x62, 0x8c, 0x96, 0x9a, 0x17, 0xb9, 0x2b, 0xe7, 0xac, 0xc2, 0x45, 0x96, 0x1b, 0xbf, 0xca, 0x32, 0x6c, 0x5b, 0xdc, 0x39, 0xd6, 0x2f, 0xca, 0x3e, 0x46, 0xee, 0xa, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xc9, 0xe8, 0x1a, 0x88, 0x1f, 0x4d, 0xf, 0xc2, 0x22, 0xc8, 0xf8, 0x2b, 0xdb, 0x94, 0xcf, 0x38, 0xf0, 0xab, 0xd0, 0xdd, 0xa2, 0xca, 0x99, 0xd1, 0x96, 0x1b, 0x47, 0xcf, 0xe3, 0x66, 0x85, 0x20, 0xce, 0x4a, 0x4e, 0x1a, 0x94, 0xa6, 0x12, 0x43, 0xbc, 0x4c, 0xa3, 0xc8, 0x44, 0xf9, 0x86, 0x1c, 0x7a, 0x4a, 0xb6, 0x45, 0x87, 0x5a, 0xc7, 0x34, 0x48, 0x6d, 0x29, 0xc, 0x25, 0x12, 0xa1, 0x4, 0x88, 0xdc, 0x34, 0x17, 0xd5, 0x5, 0x8e, 0xc4, 0xb4, 0x50, 0x3e, 0xc, 0x12, 0xea, 0x1a, 0xa, 0x89, 0xbe, 0x20, 0xf, 0xe9, 0x89, 0x22, 0x42, 0x3d, 0x43, 0x34, 0x1, 0x4f, 0xa6, 0xb0, 0xee, 0x0, 0x1, + } + assert.Equal(t, expectedProof, proof) +}