pub trait ForksHandler<T: Config, S: ForkScopes>: Logging<BlockNumberFor<T>, Logger = DispatchError> + Sized {
const TAG: &[u8];
const MAX_FORKS: u32;
const MAX_RECOVER_TRAVERSAL: u32;
Show 22 methods
// Required methods
fn forks_not_enabled() -> DispatchError;
fn inconsistent_forks() -> DispatchError;
fn max_forks_error() -> DispatchError;
// Provided methods
fn start<F: FnOnce()>(
target: Option<&str>,
fmt: Option<LogFormatter<BlockNumberFor<T>, Self::Level>>,
ocw: F,
) { ... }
fn get_head() -> Option<BlockNumberFor<T>> { ... }
fn get_branch_hash(hash: T::Hash) -> Option<[u8; 32]> { ... }
fn get_block_branch(hash: T::Hash) -> Option<Branch<T, S>> { ... }
fn get_branch(branch_hash: &[u8]) -> Option<Branch<T, S>> { ... }
fn get_prev_branch(hash: T::Hash) -> Option<Branch<T, S>> { ... }
fn get_prev_block_branch() -> Option<Branch<T, S>> { ... }
fn get_divider(hash: T::Hash) -> Option<[u8; 32]> { ... }
fn transition(
branch: &Branch<T, S>,
action: ForkAction,
) -> Option<Branch<T, S>> { ... }
fn gen_scope_item_key(item: &S::Item) -> [u8; 32] { ... }
fn scope_item_exists(
key: &[u8; 32],
target: Option<&str>,
fmt: Option<LogFormatter<BlockNumberFor<T>, Self::Level>>,
) -> Result<bool, Self::Logger> { ... }
fn add_to_scope(
item: S::Item,
target: Option<&str>,
fmt: Option<LogFormatter<BlockNumberFor<T>, Self::Level>>,
) -> Result<[u8; 32], Self::Logger> { ... }
fn remove_from_scope(
key: &[u8; 32],
target: Option<&str>,
fmt: Option<LogFormatter<BlockNumberFor<T>, Self::Level>>,
) -> Result<(), Self::Logger> { ... }
fn parent_branch_hash_unavailable(
block: BlockNumberFor<T>,
_target: Option<&str>,
_fmt: Option<LogFormatter<BlockNumberFor<T>, Self::Level>>,
) -> Result<(), Self::Logger> { ... }
fn parent_branch_unavailable(
block: BlockNumberFor<T>,
target: Option<&str>,
fmt: Option<LogFormatter<BlockNumberFor<T>, Self::Level>>,
) -> Result<(), Self::Logger> { ... }
fn inherited_branch_mutation_conflict(
block: BlockNumberFor<T>,
target: Option<&str>,
fmt: Option<LogFormatter<BlockNumberFor<T>, Self::Level>>,
) -> Result<(), Self::Logger> { ... }
fn parent_divider_unavailable(
block: BlockNumberFor<T>,
target: Option<&str>,
fmt: Option<LogFormatter<BlockNumberFor<T>, Self::Level>>,
) -> Result<(), Self::Logger> { ... }
fn inherited_branch_decode_error(
block: BlockNumberFor<T>,
target: Option<&str>,
fmt: Option<LogFormatter<BlockNumberFor<T>, Self::Level>>,
) -> Result<(), Self::Logger> { ... }
fn max_forks(
block: BlockNumberFor<T>,
target: Option<&str>,
fmt: Option<LogFormatter<BlockNumberFor<T>, Self::Level>>,
) -> Result<(), Self::Logger> { ... }
}Expand description
Fork-aware local branch collection framework for pallet/module state.
This trait allows a pallet (or module using OCWs) to maintain its own deterministic local fork graph independent of chain consensus.
It is tightly coupled to FRAME System and uses:
- current block number
- parent hash
- historical block hashes
to resolve and recover local branch state.
Each implementation defines:
- its own
TAG(storage namespace) - its own
ForkScope(local state tracked per branch)
This lets every pallet answer:
"what was my local state on this branch?"Useful for:
- OCW-derived state
- local indexing and aggregation
- branch-aware replay
- deterministic recovery after storage loss
§OCW execution model
Offchain workers should always begin with:
fn offchain_worker(block_number: BlockNumberFor<T>) {
<Self as ForksHandler<T, MyForkScope>>::start(
Some("my-pallet"),
Some(LogFormatter::default()),
|| {
// pallet-specific OCW logic here
}
);
}Self::start is the only valid entry point.
It handles:
- longest-chain extension vs sibling fork creation
- recovery of missing or corrupted branch state
- conditions where OCW execution should be skipped
The OCW closure executes only after branch resolution and recovery have completed.
§Scope safety
Fork scope state is recoverable, but not permanently trusted.
Recovery may recreate synthetic forks and restore only the minimum valid state required for continued execution, not exact historical lineage.
Fork recovery guarantees execution continuity, not perfect reconstruction.
§Query model
Branch resolution is intentionally delayed by one block.
The current executing block is not inserted into the fork graph during its own OCW execution.
Instead:
block N OCW executes
-> block N - 1 is resolved and persistedbecause the current block hash is not yet safely available for deterministic fork routing.
This means queries during OCW execution resolve the previous block’s branch by default.
Additional traversal across parent, sibling, and canonical ancestry is available through branch access helpers.
Required Associated Constants§
Sourceconst TAG: &[u8]
const TAG: &[u8]
Storage namespace prefix for all fork-local keys.
Used to isolate multiple fork-graphs with special scopes.
Sourceconst MAX_FORKS: u32
const MAX_FORKS: u32
Maximum sibling forks allowed from a single branch point.
Once exceeded, no additional sibling branch is created
and Self::max_forks is triggered.
Sourceconst MAX_RECOVER_TRAVERSAL: u32
const MAX_RECOVER_TRAVERSAL: u32
Maximum reverse traversal attempts during recovery.
Used when branch resolution fails and the system walks backward to find the nearest recoverable branch state.
Prevents unbounded historical scanning.
Required Methods§
Sourcefn forks_not_enabled() -> DispatchError
fn forks_not_enabled() -> DispatchError
Returns the error used when fork-aware storage is accessed before
ForksHandler::start has initialized the fork graph.
Sourcefn inconsistent_forks() -> DispatchError
fn inconsistent_forks() -> DispatchError
Returns the error used when the fork graph is in an inconsistent state.
Sourcefn max_forks_error() -> DispatchError
fn max_forks_error() -> DispatchError
Error returned when fork creation exceeds
Self::MAX_FORKS.
Provided Methods§
Sourcefn start<F: FnOnce()>(
target: Option<&str>,
fmt: Option<LogFormatter<BlockNumberFor<T>, Self::Level>>,
ocw: F,
)
fn start<F: FnOnce()>( target: Option<&str>, fmt: Option<LogFormatter<BlockNumberFor<T>, Self::Level>>, ocw: F, )
Start fork resolution for the previous block and execute OCW logic inside the resolved branch environment.
This is the only valid OCW entry point.
It determines:
- longest-chain extension vs sibling fork creation
- divider / branch recovery when storage is missing
- corruption handling for decode failures
- mutation conflict promotion into sibling forks
- conditions where OCW execution should be skipped
Resolution is intentionally delayed by one block:
block N OCW executes
-> block N - 1 is resolved and persistedbecause the current block hash is not yet safely available for deterministic fork routing.
The provided ocw closure runs only after:
- branch resolution is complete
- recovery (if needed) has finished
- block -> divider -> branch invariants are restored
This guarantees OCW logic executes only inside a valid fork-aware branch context.
Sourcefn get_head() -> Option<BlockNumberFor<T>>
fn get_head() -> Option<BlockNumberFor<T>>
Returns the highest known longest-chain boundary used for fork detection.
This is local fork-tracking state, not consensus finality.
Used by Self::start to decide:
block <= HEAD_BLOCK -> sibling fork path
block > HEAD_BLOCK -> longest-chain extensionSourcefn get_branch_hash(hash: T::Hash) -> Option<[u8; 32]>
fn get_branch_hash(hash: T::Hash) -> Option<[u8; 32]>
Returns the fork-branch key-hash for a persisted block hash.
This should be queried using already finalized block hashes, typically the previous block during OCW execution.
Sourcefn get_block_branch(hash: T::Hash) -> Option<Branch<T, S>>
fn get_block_branch(hash: T::Hash) -> Option<Branch<T, S>>
Returns the resolved fork-branch data for a persisted block hash.
This should be queried using already finalized block hashes, typically the previous block during OCW execution.
Sourcefn get_branch(branch_hash: &[u8]) -> Option<Branch<T, S>>
fn get_branch(branch_hash: &[u8]) -> Option<Branch<T, S>>
Loads a branch directly from its branch hash (key).
Sourcefn get_prev_branch(hash: T::Hash) -> Option<Branch<T, S>>
fn get_prev_branch(hash: T::Hash) -> Option<Branch<T, S>>
Returns the structural parent branch of a resolved block.
Useful for manual traversal across fork ancestry.
This should be queried using already finalized block hashes, typically the previous block during OCW execution.
fn get_prev_block_branch() -> Option<Branch<T, S>>
Sourcefn get_divider(hash: T::Hash) -> Option<[u8; 32]>
fn get_divider(hash: T::Hash) -> Option<[u8; 32]>
Returns the divider for a persisted block hash.
Divider is the routing layer that allows multiple sibling branches to coexist from the same parent ancestry.
This should be queried using already finalized block hashes, typically the previous block during OCW execution.
Sourcefn transition(branch: &Branch<T, S>, action: ForkAction) -> Option<Branch<T, S>>
fn transition(branch: &Branch<T, S>, action: ForkAction) -> Option<Branch<T, S>>
Deterministic traversal across persisted local fork branches.
This moves between already resolved Branch states created by
ForksHandler::start during pallet OCW execution.
Since fork resolution is delayed by one block:
block N executes
-> block N - 1 is resolved and persistedthe current executing block is not yet inserted into the local fork graph during its own OCW execution.
For normal OCW access, use ForksHandler::get_prev_block_branch
to retrieve the previous block’s (N - 1) resolved branch.
From that branch, navigation can continue using ForkAction and
ForksHandler::transition, or other helpers such as:
A branch represents a continuous stream of blocks on the same path:
A -> B -> C -> D
same branchWhen a division occurs, a new sibling branch is created:
A -> B -> C
|-- D
|-- D'Here:
Dis the original branch continuationD'is the new sibling branch
The branch that existed before the split (A -> B -> C)
becomes the parent branch for both paths.
A child branch is any branch created from that fork point.
Example:
parent branch
A -> B -> C
child branches
|-- D [0]
|-- D' [1]Nested forks create deeper child branches:
A -> B -> C
|-- D' [0]
|-- E' [0,0]This function performs branch navigation, not direct block traversal, so block numbers are not passed here.
If block position is needed, it can be inferred from:
- the branch
Branch::head - the previously resolved branch head
Traversal uses ForkAction to move between:
- parent branches
- child branches
- sibling branches
- root ancestry
Returns Some(branch) if the target exists,
otherwise None.
Sourcefn gen_scope_item_key(item: &S::Item) -> [u8; 32]
fn gen_scope_item_key(item: &S::Item) -> [u8; 32]
Derives a 32-byte scope key for a given scope item.
Delegates to Accrete::make_key which produces a stable, content-addressed
key from the item.
The same item always produces the same key regardless of the block or fork it is called from, making scope keys fork-independent.
Sourcefn scope_item_exists(
key: &[u8; 32],
target: Option<&str>,
fmt: Option<LogFormatter<BlockNumberFor<T>, Self::Level>>,
) -> Result<bool, Self::Logger>
fn scope_item_exists( key: &[u8; 32], target: Option<&str>, fmt: Option<LogFormatter<BlockNumberFor<T>, Self::Level>>, ) -> Result<bool, Self::Logger>
Returns true if the given scope key exists in the current fork’s branch.
Resolves the branch for block - 1 via Self::get_prev_block_branch
and checks both the local scope (items written on this exact branch)
and the inherited scope (items promoted from ancestor branches via
accrete()).
Returns Err(OCWForksNotEnabled) if no branch exists for the previous block,
which indicates the fork graph has not been initialized via ForksHandler::start.
Sourcefn add_to_scope(
item: S::Item,
target: Option<&str>,
fmt: Option<LogFormatter<BlockNumberFor<T>, Self::Level>>,
) -> Result<[u8; 32], Self::Logger>
fn add_to_scope( item: S::Item, target: Option<&str>, fmt: Option<LogFormatter<BlockNumberFor<T>, Self::Level>>, ) -> Result<[u8; 32], Self::Logger>
Registers a scope item in the local scope of the current branch.
Resolves the branch for block - 1 using block_hash(block - 1) as
the lookup key.
The item is added to branch.scope.local_keys so it is visible to
Self::scope_item_exists on the same branch and propagates into
inherited_keys of any child branch created via accrete() during
the next ForksHandler::start call.
Returns the 32-byte scope key assigned to the item.
Returns:
Err(OCWForksNotEnabled)ifblock_hash(block - 1)has no corresponding branch entry, meaningForksHandler::starthas not yet run at the current block.Err(OCWForksInconsistent)if the branch hash resolves but the branch itself cannot be loaded.
Sourcefn remove_from_scope(
key: &[u8; 32],
target: Option<&str>,
fmt: Option<LogFormatter<BlockNumberFor<T>, Self::Level>>,
) -> Result<(), Self::Logger>
fn remove_from_scope( key: &[u8; 32], target: Option<&str>, fmt: Option<LogFormatter<BlockNumberFor<T>, Self::Level>>, ) -> Result<(), Self::Logger>
Removes a scope item from the current branch’s local or inherited scope.
Resolves the branch for block - 1 and removes the key from whichever
scope layer it occupies:
- If the key is in
local_keys, it is removed directly and the branch is persisted. - If the key is in
inherited_keys, it is removed from the inherited layer and the branch is persisted. - If the key is in neither layer, the call is a no-op and returns
Ok(()).
Returns:
Err(OCWForksNotEnabled)if the branch cannot be resolved.Err(OCWForksInconsistent)if the branch hash exists but the branch itself cannot be loaded.
Recovery path when a target block’s parent block does not have a resolvable branch available.
The target block is usually (N-1) and parent (N-2)
Storage corruption may permanently lose the parent’s local fork scope for that generation, which cannot be reconstructed from chain history alone.
Recovery walks backward to the nearest recoverable branch and restores only the minimum valid scope required for execution continuity.
This is intentionally scope-first, not lineage-first: synthetic recovery branches may be created instead of exact historical fork reconstruction.
Recovery path when a divider exists for a target block’s (N-1) parent (N-2) but the resolved branch data is missing.
The stale divider is cleared first, then recovery
falls back to Self::parent_branch_hash_unavailable
to rebuild the minimum valid branch state.
Sourcefn inherited_branch_mutation_conflict(
block: BlockNumberFor<T>,
target: Option<&str>,
fmt: Option<LogFormatter<BlockNumberFor<T>, Self::Level>>,
) -> Result<(), Self::Logger>
fn inherited_branch_mutation_conflict( block: BlockNumberFor<T>, target: Option<&str>, fmt: Option<LogFormatter<BlockNumberFor<T>, Self::Level>>, ) -> Result<(), Self::Logger>
Recovery path when optimistic branch mutation fails due to concurrent OCW modification.
Unlike other recovery paths, this handles the target block (N-1)
itself, not its missing parent (N-2) ancestry. It does not return
control to Self::start for retry, because no parent recovery
is needed.
Instead, it directly performs branch update for the target block by cloning the conflicting structure - the inherited branch from parent into a new sibling branch.
The conflicting writer keeps the original branch, while the later writer continues on a cloned sibling fork.
This preserves deterministic execution without retrying mutation on already committed branch state.
Recovery path when divider routing is missing for the target block’s (N-1) parent (N-2).
Since divider loss breaks ancestry routing entirely,
recovery falls back directly to
Self::parent_branch_hash_unavailable.
Sourcefn inherited_branch_decode_error(
block: BlockNumberFor<T>,
target: Option<&str>,
fmt: Option<LogFormatter<BlockNumberFor<T>, Self::Level>>,
) -> Result<(), Self::Logger>
fn inherited_branch_decode_error( block: BlockNumberFor<T>, target: Option<&str>, fmt: Option<LogFormatter<BlockNumberFor<T>, Self::Level>>, ) -> Result<(), Self::Logger>
Recovery path when branch decoding fails for the target block’s (N-1) inherited branch from parent (N-2) for extension.
This means the branch exists in storage, but its payload is corrupted or unreadable.
It is treated the same as a missing branch and
delegated to Self::parent_branch_unavailable.
Sourcefn max_forks(
block: BlockNumberFor<T>,
target: Option<&str>,
fmt: Option<LogFormatter<BlockNumberFor<T>, Self::Level>>,
) -> Result<(), Self::Logger>
fn max_forks( block: BlockNumberFor<T>, target: Option<&str>, fmt: Option<LogFormatter<BlockNumberFor<T>, Self::Level>>, ) -> Result<(), Self::Logger>
Triggered when no additional sibling branch slot can be allocated under the configured limit as the sibling of the target block’s (N-1) parent (N-2).
This occurs when:
sibling_count > MAX_FORKSand branch creation must stop.
Dyn Compatibility§
This trait is not dyn compatible.
In older versions of Rust, dyn compatibility was called "object safety", so this trait is not object safe.