Skip to content
This repository was archived by the owner on Nov 15, 2023. It is now read-only.
This repository was archived by the owner on Nov 15, 2023. It is now read-only.

Re-rooting the state of a block #2832

@rphmeier

Description

@rphmeier

Substrate chains may occasionally need to do state rollbacks.
In Polkadot, that's the case when we have a fully-malicious validator committee whose misbehavior is discovered.

We introduce ext_reroot(ancestor: BlockNumber) -> Hash for this purpose.
This will set the current storage to be equal to the post-state of the ancestor of the current block with the given block number.

Some pseudocode for an implementation:

// substrate-executor
fn ext_reroot(ancestor: BlockNumber) -> Hash {
    ext.reroot(ancestor);
}

// substrate-state-machine

struct Ext<'a, B: 'a + Backend> {
     backend: Cow<'a, B>,
     prospective: &'a mut OverlayedChanges,
     // ...
}

impl Ext {
    fn reroot(number: BlockNumber) {
        // prospective is an overlay of all storage items changed so far within the runtime execution
        // of the block.
        prospective.clear();
        if current_number <= ancestor { trap() }
        self.backend = Cow::Owned(self.backend.reroot(number));
    }
}

struct TrieBackend {
    fn reroot(&self, number) -> TrieBackend {
        let db_overlay = MemoryDB::new();   

        // reverse, inserting all deletions of ancestors and deleting all insertions.
        for (num, hash) in ancestry_iter(parent_hash) {
            if num == ancestor { break }
            let pruning_record = db.pruning_record(hash);
            for (inserted_key, _val) in pruning_record.insertions {
                db_overlay.remove(&inserted_key); // undo an insertion.
            }

            // values might not actually be available in the current implementation of pruning.
            // but the value should still be available within the DB because we are assuming this block hasn't been canonicalized yet (so deletions haven't been applied yet).
            for (_deleted_key, val) in pruning_record.deletions {
                db_overlay.insert(val); // undo a deletion.
            }
        };

        
        // we used a memorydb for `db_overlay` because it's very likely that many of the storage operations cancel out.
        // now, we have to make sure that the `update` produced by `Backend::root` and passed to `client::BlockImportOperation::update_storage` is based on top of the `db_overlay`, but still includes any further deltas.
    }
}

I would expect that most of this implementation is within state_machine::Ext and state_machine::Backend. Ext needs to clear the prospective overlay, and we would make the backend field on Ext into a Cow. Backend gets a new Backend::reroot(new_number) -> Self. For the trie implementation, this would do all of the pruning-overlay logic mentioned above, and create a new Backend with the new state root and a pre-update to apply the del

This won't play that nicely with the storage changes root or light clients for consensus. What we can do is introduce a special storage changes root digest which says "everything changed". To maintain good light client compatibility on the consensus layer, the caller of this function should force-issue a change digest to the same set as is currently enabled afterwards.

Metadata

Metadata

Assignees

Labels

J0-enhancementAn additional feature request.

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions