Skip to content

Commit

Permalink
feat(lib/grandpa): Add warp sync provider (#4187)
Browse files Browse the repository at this point in the history
  • Loading branch information
dimartiro authored Oct 2, 2024
1 parent 3aaac6b commit 6a833fd
Show file tree
Hide file tree
Showing 6 changed files with 471 additions and 1 deletion.
23 changes: 22 additions & 1 deletion dot/state/grandpa.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"encoding/binary"
"errors"
"fmt"
"slices"
"strconv"

"github.com/ChainSafe/gossamer/dot/telemetry"
Expand Down Expand Up @@ -37,7 +38,7 @@ var (

// GrandpaState tracks information related to grandpa
type GrandpaState struct {
db GetPutDeleter
db GrandpaDatabase
blockState *BlockState

forcedChanges *orderedPendingChanges
Expand Down Expand Up @@ -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
}
5 changes: 5 additions & 0 deletions dot/state/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions internal/client/consensus/grandpa/mocks_generate_test.go
Original file line number Diff line number Diff line change
@@ -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
139 changes: 139 additions & 0 deletions internal/client/consensus/grandpa/mocks_test.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

173 changes: 173 additions & 0 deletions internal/client/consensus/grandpa/warp_sync.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading

0 comments on commit 6a833fd

Please sign in to comment.