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

minimal snapshot #2904

Open
wants to merge 67 commits into
base: master
Choose a base branch
from
Open

minimal snapshot #2904

wants to merge 67 commits into from

Conversation

magicxyyz
Copy link
Contributor

@magicxyyz magicxyyz commented Jan 28, 2025

Resolves NIT-3057

This PR adds database snapshotter that allows exporting a minimal execution database (here called snapshot) that consists of:

  • chain config entry
  • genesis state
  • head state
  • blocks and receipts up to the head block

In connection with manual copying of consensus database (arbitrumdata) this can be used to create a complete databases snapshot for a full node.

The snapshotter can be enabled with flag: --execution.database-snaphotter.enable (disabled by default).
Currently there are two ways to trigger the snapshot export:

  • sending SIGUSR2 to nitro process - starts the export with snapshot head set to latest block which state is found in disk database (e.g. docker kill -s "SIGUSR2" nitro-node)
  • calling snapshotter_snapshot rpc method that takes block number or block tag parameter - starts the export with snapshot head set to latest block that is not newer then specified block and has state available in disk database; snapshotter_result might be called to check the export status (the method takes "rewind" boolean parameter if set true and previous snapshotter run finished it discards cached result and new snapshot can be started)

Initial version supports exporting only to geth format database. The output database is specified by following options:

  • --execution.database-snapshotter.geth-exporter.output.data
    directory of stored chain state (default "snapshot")
  • --execution.database-snapshotter.geth-exporter.output.ancient
    directory of ancient where the chain freezer can be opened (default "ancient")
  • --execution.database-snapshotter.geth-exporter.output.cache
    the capacity(in megabytes) of the data caching (default 2048)
  • --execution.database-snapshotter.geth-exporter.output.db-engine
    backing database implementation to use ('leveldb' or 'pebble')
  • --execution.database-snapshotter.geth-exporter.output.handles
    number of files to be open simultaneously (default 512)
  • --execution.database-snapshotter.geth-exporter.output.namespace
    metrics namespace (default "l2chaindata_export")
  • --execution.database-snapshotter.geth-exporter.output.pebble.*
    extra options for pebble engine, see --help for details
  • --execution.database-snapshotter.geth-exporter.ideal-batch-size
    ideal write batch size in bytes (default 104857600)

Number of threads used by the snapshotter is specified by --execution.database-snapshoter.threads option.

Pulls: OffchainLabs/go-ethereum#402

@magicxyyz magicxyyz changed the title Minimal snapshot minimal snapshot Jan 28, 2025
@magicxyyz magicxyyz marked this pull request as ready for review February 18, 2025 15:45
@magicxyyz magicxyyz requested review from eljobe, tsahee and ganeshvanahalli and removed request for eljobe and tsahee February 18, 2025 15:45
Copy link
Collaborator

@tsahee tsahee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't yet go through the bulk of the logic, but the API.

@@ -0,0 +1,96 @@
package snapshotter
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's put snapshotter as a subdir of execution for good order

case rpc.FinalizedBlockNumber:
header = a.bc.CurrentFinalBlock()
case rpc.SafeBlockNumber:
header = a.bc.CurrentSafeBlock()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not for this PR: we should also have an option for latest accepted assertion

if a.promise.Ready() && rewind {
a.promise = nil
}
return promise.Current()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there could be a weird race condition between promise.Ready above and this promise.Current() which will be confusing.
Need to only read promise.Current() once, then if err == nil & rewind set a.promise=nil ( a ready promise cannot become errored, but a non-ready could become ready)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed, thanks!

added (err == nil || errors.Is(err, containers.NotReady), to not "rewind" and loose the promise in case the job hasn't finished yet

ExportTD(number uint64, hash common.Hash, tdRlp []byte) error
ExportBlockHeader(number uint64, hash common.Hash, headerRlp []byte) error
ExportBlockBody(number uint64, hash common.Hash, bodyRlp []byte) error
ExportBlockReceipts(number uint64, hash common.Hash, receiptsRlp []byte) error
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think all from ExportCanonical to ExportBlockReceipts should be a single function that writes a single block which is also assumed to be canonical.
Not 100% certain but I think I would make this function accept a types.Block. Conversion to/from RLP is probably negligable compared to i/o reads/writes in this function.
As for Td - one option is to read it with the block and pass it to this write function.
Specifically for nitro, though, there is another interesting option - difficulty for classic was always zero and for nitro always 1, so Td should always be block_number - genesis (I think, check me) and can be written without ever reading it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, it felt a little like pre-mature optimization to avoid RLP decoding-encoding. I collapsed block/header/receipt/difficulty/canonical hash export methods into one method and it is much simpler and cleaner now

}
}

func (a *DatabaseSnapshotterAPI) Snapshot(ctx context.Context, number rpc.BlockNumber) error {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

possibly for a different PR:
I see a few main options:

  1. opening a separate dir per activation of "Snapshot"
  2. having a separate "prepare" command. Prepare stores (about in that order) genesis, blocks, and head - but not state in head. Prepare might be called multiple times. If prepare finds an existing database that had prepared called a few times - it updates it by adding blocks and moving forward head. "Snapshot" does prepare + state of head + chain config (as a marker, or some other thing). It fails if it's called for an existing database with a chain config.

Should probably also talk with SRE about their preferance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants