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

docs(design): GRANDPA client implementation design #4519

Open
wants to merge 16 commits into
base: refactor/client-db
Choose a base branch
from
Open
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: 0 additions & 2 deletions .github/workflows/copyright.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
name: Copyright check
on:
pull_request:
branches:
- development
# Commented paths to avoid skipping required workflow
# See https://github.community/t/feature-request-conditional-required-checks/16761
# paths:
Expand Down
2 changes: 0 additions & 2 deletions .github/workflows/mocks.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
name: Mocks check
on:
pull_request:
branches:
- development
# Commented paths to avoid skipping required workflow
# See https://github.community/t/feature-request-conditional-required-checks/16761
# paths:
Expand Down
3 changes: 3 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ linters-settings:
- finalize
- initialize
- color
- finalization
- behavior
- finalizing

linters:
enable:
Expand Down
Binary file added docs/docs/design/assets/img/client-traits.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
333 changes: 333 additions & 0 deletions docs/docs/design/client-db-integration.md

Large diffs are not rendered by default.

301 changes: 301 additions & 0 deletions docs/docs/design/grandpa-client.md

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ require (
github.com/dgraph-io/badger/v4 v4.5.1
github.com/dgraph-io/ristretto/v2 v2.1.0
github.com/disiqueira/gotree v1.0.0
github.com/dolthub/maphash v0.1.0
github.com/elastic/go-freelru v0.15.0
github.com/ethereum/go-ethereum v1.15.0
github.com/fatih/color v1.18.0
github.com/gammazero/deque v1.0.0
Expand All @@ -22,6 +24,7 @@ require (
github.com/gorilla/rpc v1.2.1
github.com/gorilla/websocket v1.5.3
github.com/gtank/merlin v0.1.1
github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/ipfs/go-ds-badger4 v0.1.5
github.com/jpillora/backoff v1.0.0
github.com/jpillora/ipfilter v1.2.9
Expand All @@ -41,6 +44,7 @@ require (
github.com/tetratelabs/wazero v1.1.0
github.com/tidwall/btree v1.7.0
github.com/tyler-smith/go-bip39 v1.1.0
github.com/ugurcsen/gods-generic v0.10.4
go.uber.org/mock v0.5.0
golang.org/x/crypto v0.33.0
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8
Expand Down Expand Up @@ -225,3 +229,7 @@ toolchain go1.23.2
replace github.com/tetratelabs/wazero => github.com/ChainSafe/wazero v0.0.0-20240319130522-78b21a59bd5f

replace github.com/centrifuge/go-substrate-rpc-client/v4 => github.com/timwu20/go-substrate-rpc-client/v4 v4.0.0-20231110032757-3d8e441b7303

replace github.com/elastic/go-freelru => github.com/timwu20/go-freelru v0.0.0-20241023201517-deb64adeae4c

replace github.com/ugurcsen/gods-generic => github.com/timwu20/gods-generic v0.0.0-20241206024616-791a209639f8
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ github.com/disiqueira/gotree v1.0.0/go.mod h1:7CwL+VWsWAU95DovkdRZAtA7YbtHwGk+tL
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ=
github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
Expand Down Expand Up @@ -260,6 +262,8 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA=
Expand Down Expand Up @@ -622,8 +626,12 @@ github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
github.com/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI=
github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY=
github.com/timwu20/go-freelru v0.0.0-20241023201517-deb64adeae4c h1:WKEqrNAA0DIVpTHGV70TonOX7pN0tB8CSkT8Z8Jbxjw=
github.com/timwu20/go-freelru v0.0.0-20241023201517-deb64adeae4c/go.mod h1:bSdWT4M0lW79K8QbX6XY2heQYSCqD7THoYf82pT/H3I=
github.com/timwu20/go-substrate-rpc-client/v4 v4.0.0-20231110032757-3d8e441b7303 h1:FX7wMjDD0sWGWsC9k+stJaYwThbaq6BDT7ArlInU0KI=
github.com/timwu20/go-substrate-rpc-client/v4 v4.0.0-20231110032757-3d8e441b7303/go.mod h1:1p5145LS4BacYYKFstnHScydK9MLjZ15l72v8mbngPQ=
github.com/timwu20/gods-generic v0.0.0-20241206024616-791a209639f8 h1:woES76T+jY3c6JCKAsJGi4X7DdNoBzhnK3xVm3kqvSM=
github.com/timwu20/gods-generic v0.0.0-20241206024616-791a209639f8/go.mod h1:mGYOa88Y5sbw+ADXLpScxjJ7s5iHoWya/YHyeQ4f6c4=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
Expand Down
291 changes: 291 additions & 0 deletions internal/client/api/backend.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
// Copyright 2024 ChainSafe Systems (ON)
// SPDX-License-Identifier: LGPL-3.0-only

package api

import (
"sync"

"github.com/ChainSafe/gossamer/internal/client/consensus"
"github.com/ChainSafe/gossamer/internal/primitives/blockchain"
"github.com/ChainSafe/gossamer/internal/primitives/core/offchain"
"github.com/ChainSafe/gossamer/internal/primitives/runtime"
statemachine "github.com/ChainSafe/gossamer/internal/primitives/state-machine"
"github.com/ChainSafe/gossamer/internal/primitives/storage"
)

// ImportNotificationAction describes which block import notification stream should be notified.
type ImportNotificationAction uint

const (
// RecentBlockImportNotificationAction notifies only when the node has synced to the tip or there is a re-org.
RecentBlockImportNotificationAction ImportNotificationAction = iota
// EveryBlockImportNotificationAction notifies for every single block no matter what the sync state is.
EveryBlockImportNotificationAction
// BothBlockImportNotificationAction means both [RecentBlockImportNotificationAction] and
// [EveryBlockImportNotificationAction] should be fired.
BothBlockImportNotificationAction
// NoneBlockImportNotificationAction means no block import notification should be fired.
NoneBlockImportNotificationAction
)

// StorageChanges contains a [statemachine.StorageCollection] and [statemachine.ChildStorageCollection]
type StorageChanges struct {
statemachine.StorageCollection
statemachine.ChildStorageCollection
}

// ImportSummary contains information about the block that just got imported,
// including storage changes, reorged blocks, etc.
type ImportSummary[
H runtime.Hash,
N runtime.Number,
Header runtime.Header[N, H],
] struct {
Hash H // Block hash of the imported block.
Origin consensus.BlockOrigin // Import origin.
Header Header // Header of the imported block.
IsNewBest bool // Is this block a new best block.
StorageChanges *StorageChanges // Optional storage changes.
// TreeRoute from old best to new best.
// If nil, there was no re-org while importing.
TreeRoute *blockchain.TreeRoute[H, N]
ImportNotificationAction ImportNotificationAction // Which notify action to take for this import.
}

// FinalizeSummary contains information about the block that just got finalized, including tree heads that became
// stale at the moment of finalization.
type FinalizeSummary[
H runtime.Hash,
N runtime.Number,
Header runtime.Header[N, H],
] struct {
// Last finalized block header.
Header Header
// Blocks that were finalized.
// The last entry is the one that has been explicitly finalized.
Finalized []H
// Heads that became stale during this finalization operation.
StaleHeads []H
}

// ClientImportOperation is an import operation wrapper.
type ClientImportOperation[
H runtime.Hash,
Hasher runtime.Hasher[H],
N runtime.Number,
Header runtime.Header[N, H],
E runtime.Extrinsic,
] struct {
Op BlockImportOperation[N, H, Hasher, Header, E] // DB Operation.
NotifyImported *ImportSummary[H, N, Header] // Summary of imported block.
NotifyFinalized *FinalizeSummary[H, N, Header] // Summary of finalized block.
}

// NewBlockState is the state of a new block.
type NewBlockState uint8

const (
// NewBlockStateNormal is a normal block.
NewBlockStateNormal NewBlockState = iota
// NewBlockStateBest is a new best block.
NewBlockStateBest
// NewBlockStateFinal is a newly finalized block.
NewBlockStateFinal
)

// IsBest returns true if equal to [NewBlockStateBest]
func (nbs NewBlockState) IsBest() bool {
return nbs == NewBlockStateBest
}

// IsFinal returns true if equal to [NewBlockStateFinal]
func (nbs NewBlockState) IsFinal() bool {
return nbs == NewBlockStateFinal
}

// BlockImportOperation keeps hold of the inserted block state and data.
type BlockImportOperation[
N runtime.Number,
H runtime.Hash,
Hasher runtime.Hasher[H],
Header runtime.Header[N, H],
E runtime.Extrinsic,
] interface {
// State returns the pending state.
// Returns nil for backends with locally-unavailable state data.
State() (statemachine.Backend[H, Hasher], error)

// SetBlockData will set block data to the transaction.
SetBlockData(
header Header,
body []E,
indexedBody [][]byte,
justifications runtime.Justifications,
state NewBlockState,
) error

// UpdateDBStorage will inject storage data into the database.
UpdateDBStorage(update statemachine.BackendTransaction[H, Hasher]) error

// SetGenesisState will set genesis state. If commit is false the state is saved in memory, but is not written
// to the database.
SetGenesisState(storage storage.Storage, commit bool, stateVersion storage.StateVersion) (H, error)

// ResetStorage will inject storage data into the database replacing any existing data.
ResetStorage(storage storage.Storage, stateVersion storage.StateVersion) (H, error)

// UpdateStorage will set storage changes.
UpdateStorage(update statemachine.StorageCollection, childUpdate statemachine.ChildStorageCollection) error

// UpdateOffchainStorage will write offchain storage changes to the database.
UpdateOffchainStorage(offchainUpdate statemachine.OffchainChangesCollection) error

// InsertAux will insert auxiliary keys.
// Values that are nil respresent the keys should be deleted.
InsertAux(ops AuxDataOperations) error

// MarkFinalized marks a block as finalized.
MarkFinalized(hash H, justification *runtime.Justification) error

// MarkHead marks a block as the new head. If the block import changes the head and MarkHead is called with
// a different block hash, MarkHead will override the changed head as a result of the block import.
MarkHead(hash H) error

// UpdateTransactionIndex adds a transaction index operation.
UpdateTransactionIndex(index []statemachine.IndexOperation) error
}

// LockImportRun is the interface for performing operations on the backend.
type LockImportRun[
H runtime.Hash,
N runtime.Number,
Hasher runtime.Hasher[H],
Header runtime.Header[N, H],
E runtime.Extrinsic,
] interface {
/// LockImportRun locks the import lock, and run operations inside.
LockImportRun(
f func(*ClientImportOperation[H, Hasher, N, Header, E]) error,
) error
}

// KeyValue is used in [AuxStore.InsertAux]. Key and Value should not be nil.
type KeyValue struct {
Key []byte
Value []byte
}

// AuxStore provides access to an auxiliary database.
//
// This is a simple global database not aware of forks. Can be used for storing auxiliary
// information like total block weight/difficulty for fork resolution purposes as a common use
// case.
type AuxStore interface {
// Insert auxiliary data into key-value store.
//
// Deletions occur after insertions.
InsertAux(insert []KeyValue, delete [][]byte) error

// Query auxiliary data from key-value store.
GetAux(key []byte) ([]byte, error)
}

// Backend is the client backend.
//
// Manages the data layer.
//
// # State Pruning
//
// While an object from StateAt is alive, the state
// should not be pruned. The backend should internally reference-count
// its state objects.
//
// The same applies for live BlockImportOperation instances: while an import operation building on a
// parent P is alive, the state for P should not be pruned.
//
// # Block Pruning
//
// Users can pin blocks in memory by calling PinBlock. When
// a block would be pruned, its value is kept in an in-memory cache
// until it is unpinned via UnpinBlock.
//
// While a block is pinned, its state is also preserved.
//
// The backend should internally reference count the number of pin / unpin calls.
type Backend[
H runtime.Hash,
N runtime.Number,
Hasher runtime.Hasher[H],
Header runtime.Header[N, H],
E runtime.Extrinsic,
] interface {
AuxStore // Insert auxiliary data into key-value store.

// BeginOperation begins a new block insertion transaction with given parent block id.
// When constructing the genesis, this is called with all-zero hash.
BeginOperation() (BlockImportOperation[N, H, Hasher, Header, E], error)

// BeginStateOperation notes an operation to contain state transition.
BeginStateOperation(operation BlockImportOperation[N, H, Hasher, Header, E], block H) error

// CommitOperation will commit block insertion.
CommitOperation(transaction BlockImportOperation[N, H, Hasher, Header, E]) error

// FinalizeBlock will finalize block with given hash.
//
// This should only be called if the parent of the given block has been finalized.
FinalizeBlock(hash H, justification *runtime.Justification) error

// AppendJustification appends justification to the block with the given hash.
//
// This should only be called for blocks that are already finalized.
AppendJustification(hash H, justification runtime.Justification) error

// Blockchain returns reference to blockchain backend.
Blockchain() blockchain.Backend[H, N, Header, E]

// OffchainStorage returns a pointer to offchain storage.
OffchainStorage() offchain.OffchainStorage

// PinBlock pins the block to keep body, justification and state available after pruning.
// Number of pins are reference counted. Users need to make sure to perform
// one call to UnpinBlock per call to PinBlock.
PinBlock(hash H) error

// Unpin the block to allow pruning.
UnpinBlock(hash H)

// HaveStateAt returns true if state for given block is available.
HaveStateAt(hash H, number N) bool

// StateAt returns state backend with post-state of given block.
StateAt(hash H) (statemachine.Backend[H, Hasher], error)

// Revert attempts to revert the chain by n blocks. If revertFinalized is set it will attempt to
// revert past any finalized block. This is unsafe and can potentially leave the node in an
// inconsistent state. All blocks higher than the best block are also reverted and not counting
// towards n.
//
// Returns the number of blocks that were successfully reverted and the list of finalized
// blocks that has been reverted.
Revert(n N, revertFinalized bool) (N, map[H]any, error)

// RemoveLeafBlock discards non-best, unfinalized leaf block.
RemoveLeafBlock(hash H) error

// GetImportLock returns access to the import lock for this backend.
//
// NOTE: Backend isn't expected to acquire the lock by itself ever. Rather
// the using components should acquire and hold the lock whenever they do
// something that the import of a block would interfere with, e.g. importing
// a new block or calculating the best head.
GetImportLock() *sync.RWMutex

// RequiresFullSync tells whether the backend requires full-sync mode.
RequiresFullSync() bool

// UsageInfo returns current usage statistics.
// TODO: implement UsageInfo if we require it
// UsageInfo() *UsageInfo
}
Loading
Loading