mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-07-09 15:29:34 +00:00
Merge 89d248697085452554b3c094bd27cd037f7a7a77 into 2921438e932becd0ec69acd6eaa92317620106e8
This commit is contained in:
commit
d0dfa680bd
34
.github/workflows/ci.yml
vendored
34
.github/workflows/ci.yml
vendored
@ -135,7 +135,37 @@ jobs:
|
||||
env:
|
||||
RISC0_DEV_MODE: "1"
|
||||
RUST_LOG: "info"
|
||||
run: cargo nextest run --workspace --exclude integration_tests --all-features
|
||||
run: cargo nextest run --workspace --exclude integration_tests --exclude test_fixtures --all-features
|
||||
|
||||
test-fixtures-tests:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
|
||||
|
||||
- uses: ./.github/actions/install-system-deps
|
||||
|
||||
- uses: ./.github/actions/install-risc0
|
||||
|
||||
- name: Install active toolchain
|
||||
run: rustup install
|
||||
|
||||
- name: Restore Rust cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
shared-key: ci-rust-cache
|
||||
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' }}
|
||||
|
||||
- name: Install nextest
|
||||
run: cargo install --locked cargo-nextest
|
||||
|
||||
- name: Run test_fixtures tests
|
||||
env:
|
||||
RISC0_DEV_MODE: "1"
|
||||
RUST_LOG: "info"
|
||||
run: cargo nextest run -p test_fixtures
|
||||
|
||||
integration-tests-prebuild:
|
||||
runs-on: ubuntu-latest
|
||||
@ -193,7 +223,7 @@ jobs:
|
||||
echo "Discovered integration targets: $targets_json"
|
||||
|
||||
integration-tests:
|
||||
needs: integration-tests-prebuild
|
||||
needs: [test-fixtures-tests, integration-tests-prebuild]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 90
|
||||
strategy:
|
||||
|
||||
33
Cargo.lock
generated
33
Cargo.lock
generated
@ -2076,7 +2076,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccc2776f0c61eca1ca32528f85548abd1a4be8fb53d1b21c013e4f18da1e7090"
|
||||
dependencies = [
|
||||
"data-encoding",
|
||||
"syn 1.0.109",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -8896,6 +8896,7 @@ dependencies = [
|
||||
"futures",
|
||||
"hex",
|
||||
"humantime-serde",
|
||||
"itertools 0.14.0",
|
||||
"key_protocol",
|
||||
"lee",
|
||||
"lee_core",
|
||||
@ -9462,6 +9463,7 @@ dependencies = [
|
||||
"system_accounts",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"zstd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -9735,6 +9737,7 @@ name = "test_fixtures"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bip39",
|
||||
"bytesize",
|
||||
"common",
|
||||
"env_logger",
|
||||
@ -11797,3 +11800,31 @@ dependencies = [
|
||||
"log",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd"
|
||||
version = "0.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
|
||||
dependencies = [
|
||||
"zstd-safe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-safe"
|
||||
version = "7.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
|
||||
dependencies = [
|
||||
"zstd-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-sys"
|
||||
version = "2.0.16+zstd.1.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
@ -147,6 +147,7 @@ bip39 = "2.2.0"
|
||||
hmac-sha512 = "1.1.7"
|
||||
chrono = "0.4.41"
|
||||
borsh = "1.5.7"
|
||||
zstd = "0.13"
|
||||
base58 = "0.2.0"
|
||||
itertools = "0.14.0"
|
||||
num-bigint = "0.4.6"
|
||||
|
||||
5
Justfile
5
Justfile
@ -35,6 +35,11 @@ test:
|
||||
@echo "🧪 Running tests"
|
||||
RISC0_DEV_MODE=1 cargo nextest run --no-fail-fast
|
||||
|
||||
# Regenerate the prebuilt sequencer db dump for fast TestContext::new() (needs Docker; commit the dump).
|
||||
regenerate-test-fixture:
|
||||
@echo "🧪 Regenerating test fixtures"
|
||||
RISC0_DEV_MODE=1 cargo run -p test_fixtures --bin regenerate_test_fixture
|
||||
|
||||
# Run criterion benches: fast crypto primitives, then the slow PPE verify (real proving setup).
|
||||
bench:
|
||||
@echo "📊 Running criterion benches"
|
||||
|
||||
@ -39,6 +39,7 @@ url.workspace = true
|
||||
num-bigint.workspace = true
|
||||
risc0-zkvm.workspace = true
|
||||
futures.workspace = true
|
||||
itertools.workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
@ -10,11 +10,11 @@ use lee::V03State;
|
||||
use lee_core::BlockId;
|
||||
use log::info;
|
||||
use logos_blockchain_zone_sdk::sequencer::SequencerCheckpoint;
|
||||
pub use storage::DbResult;
|
||||
use storage::sequencer::{
|
||||
RocksDBIO,
|
||||
sequencer_cells::{PendingDepositEventRecord, WithdrawalReconciliationKey},
|
||||
};
|
||||
pub use storage::{DbResult, sequencer::DbDump};
|
||||
|
||||
pub struct SequencerStore {
|
||||
dbio: Arc<RocksDBIO>,
|
||||
@ -150,6 +150,28 @@ impl SequencerStore {
|
||||
self.dbio.get_lee_state()
|
||||
}
|
||||
|
||||
/// Remove the persisted zone-sdk checkpoint so the next startup is treated as a fresh start.
|
||||
pub fn delete_zone_checkpoint(&self) -> DbResult<()> {
|
||||
self.dbio.delete_zone_sdk_checkpoint_bytes()
|
||||
}
|
||||
|
||||
/// Reset every stored block to `Pending` so the next fresh start republishes the whole chain.
|
||||
pub fn reset_all_blocks_to_pending(&self) -> DbResult<()> {
|
||||
self.dbio.reset_all_blocks_to_pending()
|
||||
}
|
||||
|
||||
/// Single-blob [`DbDump`] of the whole store; restore with [`Self::restore_db_from_dump`].
|
||||
pub fn dump(&self) -> DbResult<DbDump> {
|
||||
self.dbio.dump_all()
|
||||
}
|
||||
|
||||
/// Create a fresh rocksdb at `location` from `dump`, closing it before returning so a sequencer
|
||||
/// can open it normally afterwards.
|
||||
pub fn restore_db_from_dump(location: &Path, dump: &DbDump) -> DbResult<()> {
|
||||
RocksDBIO::restore_from_dump(location, dump)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_zone_checkpoint(&self) -> Result<Option<SequencerCheckpoint>> {
|
||||
let Some(bytes) = self.dbio.get_zone_sdk_checkpoint_bytes()? else {
|
||||
return Ok(None);
|
||||
|
||||
@ -8,6 +8,7 @@ use common::{
|
||||
transaction::{LeeTransaction, clock_invocation},
|
||||
};
|
||||
use config::{GenesisAction, SequencerConfig};
|
||||
use itertools::Itertools as _;
|
||||
use lee::{AccountId, PublicTransaction, public_transaction::Message};
|
||||
use lee_core::GENESIS_BLOCK_ID;
|
||||
use log::{error, info, warn};
|
||||
@ -124,7 +125,7 @@ impl<BP: BlockPublisherTrait> SequencerCore<BP> {
|
||||
load_or_create_signing_key(&config.home.join("bedrock_signing_key"))
|
||||
.expect("Failed to load or create bedrock signing key");
|
||||
|
||||
let (store, state, genesis_block) = Self::open_or_create_store(&config);
|
||||
let (store, state, _genesis_block) = Self::open_or_create_store(&config);
|
||||
|
||||
let latest_block_meta = store
|
||||
.latest_block_meta()
|
||||
@ -151,15 +152,33 @@ impl<BP: BlockPublisherTrait> SequencerCore<BP> {
|
||||
.await
|
||||
.expect("Failed to initialize Block Publisher");
|
||||
|
||||
// On a truly fresh start (no checkpoint persisted yet), publish the
|
||||
// genesis block so the indexer can find the channel start. After the
|
||||
// first publish, zone-sdk's checkpoint persistence covers further
|
||||
// restarts.
|
||||
// Fresh start (no checkpoint): republish all pending blocks
|
||||
if is_fresh_start {
|
||||
block_publisher
|
||||
.publish_block(&genesis_block, vec![])
|
||||
.await
|
||||
.expect("Failed to publish genesis block");
|
||||
let mut pending_blocks = store
|
||||
.get_all_blocks()
|
||||
.filter_ok(|block| matches!(block.bedrock_status, BedrockStatus::Pending))
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.expect("Failed to read blocks from store while republishing on fresh start");
|
||||
pending_blocks.sort_unstable_by_key(|block| block.header.block_id);
|
||||
|
||||
assert!(
|
||||
pending_blocks
|
||||
.first()
|
||||
.is_none_or(|block| block.header.block_id == GENESIS_BLOCK_ID),
|
||||
"First pending block on fresh start should be the genesis block"
|
||||
);
|
||||
|
||||
for block in &pending_blocks {
|
||||
block_publisher
|
||||
.publish_block(block, vec![])
|
||||
.await
|
||||
.unwrap_or_else(|err| {
|
||||
panic!(
|
||||
"Failed to publish block {} on fresh start: {err:#}",
|
||||
block.header.block_id
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let sequencer_core = Self {
|
||||
|
||||
@ -15,6 +15,7 @@ thiserror.workspace = true
|
||||
borsh.workspace = true
|
||||
rocksdb.workspace = true
|
||||
tempfile.workspace = true
|
||||
zstd.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
programs.workspace = true
|
||||
|
||||
@ -12,6 +12,12 @@ pub enum DbError {
|
||||
error: borsh::io::Error,
|
||||
additional_info: Option<String>,
|
||||
},
|
||||
#[error("Compression error: {}", additional_info.as_deref().unwrap_or("No additional info"))]
|
||||
CompressionError {
|
||||
#[source]
|
||||
error: std::io::Error,
|
||||
additional_info: Option<String>,
|
||||
},
|
||||
#[error("Logic Error: {additional_info}")]
|
||||
DbInteractionError { additional_info: String },
|
||||
}
|
||||
@ -33,6 +39,14 @@ impl DbError {
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn compression_error(err: std::io::Error, message: Option<String>) -> Self {
|
||||
Self::CompressionError {
|
||||
error: err,
|
||||
additional_info: message,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn db_interaction_error(message: String) -> Self {
|
||||
Self::DbInteractionError {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
use rocksdb::{DBWithThreadMode, MultiThreaded, WriteBatch};
|
||||
|
||||
use crate::{
|
||||
cells::{SimpleReadableCell, SimpleWritableCell},
|
||||
cells::{SimpleReadableCell, SimpleStorableCell, SimpleWritableCell},
|
||||
error::DbError,
|
||||
};
|
||||
|
||||
@ -66,4 +66,17 @@ pub trait DBIO {
|
||||
) -> DbResult<()> {
|
||||
cell.put_batch(self.db(), params, write_batch)
|
||||
}
|
||||
|
||||
/// Delete a cell. Deleting an absent key is a no-op (rocksdb semantics).
|
||||
fn del<T: SimpleStorableCell>(&self, params: T::KeyParams) -> DbResult<()> {
|
||||
let cf_ref = T::column_ref(self.db());
|
||||
self.db()
|
||||
.delete_cf(&cf_ref, T::key_constructor(params)?)
|
||||
.map_err(|rerr| {
|
||||
DbError::rocksdb_cast_message(
|
||||
rerr,
|
||||
Some(format!("Failed to delete {:?}", T::CELL_NAME)),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
use std::{path::Path, sync::Arc};
|
||||
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use common::{
|
||||
HashType,
|
||||
block::{BedrockStatus, Block, BlockMeta},
|
||||
};
|
||||
use lee::V03State;
|
||||
use rocksdb::{
|
||||
BoundColumnFamily, ColumnFamilyDescriptor, DBWithThreadMode, MultiThreaded, Options, WriteBatch,
|
||||
BoundColumnFamily, ColumnFamilyDescriptor, DBWithThreadMode, IteratorMode, MultiThreaded,
|
||||
Options, WriteBatch,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@ -44,6 +46,47 @@ pub const DB_LEE_STATE_KEY: &str = "lee_state";
|
||||
/// Name of state column family.
|
||||
pub const CF_LEE_STATE_NAME: &str = "cf_lee_state";
|
||||
|
||||
/// A single key/value entry from a column family, used inside [`DbDump`].
|
||||
#[derive(BorshSerialize, BorshDeserialize)]
|
||||
pub struct DbDumpEntry {
|
||||
pub cf_name: String,
|
||||
pub key: Vec<u8>,
|
||||
pub value: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Schema-agnostic single-blob snapshot of a store: every key/value pair across all column
|
||||
/// families. Lets a prebuilt store ship as one committed file instead of a rocksdb directory.
|
||||
#[derive(BorshSerialize, BorshDeserialize)]
|
||||
pub struct DbDump {
|
||||
pub entries: Vec<DbDumpEntry>,
|
||||
}
|
||||
|
||||
impl DbDump {
|
||||
/// Serialize the dump to a zstd-compressed borsh blob.
|
||||
pub fn to_bytes(&self) -> DbResult<Vec<u8>> {
|
||||
/// zstd compression level for [`DbDump::to_bytes`]. Level 19 keeps the committed fixture
|
||||
/// small without a meaningful decompression cost.
|
||||
const DUMP_ZSTD_LEVEL: i32 = 19;
|
||||
|
||||
let borsh = borsh::to_vec(self).map_err(|err| {
|
||||
DbError::borsh_cast_message(err, Some("Failed to serialize DbDump".to_owned()))
|
||||
})?;
|
||||
zstd::encode_all(borsh.as_slice(), DUMP_ZSTD_LEVEL).map_err(|err| {
|
||||
DbError::compression_error(err, Some("Failed to compress DbDump".to_owned()))
|
||||
})
|
||||
}
|
||||
|
||||
/// Deserialize a dump produced by [`Self::to_bytes`].
|
||||
pub fn from_bytes(bytes: &[u8]) -> DbResult<Self> {
|
||||
let borsh = zstd::decode_all(bytes).map_err(|err| {
|
||||
DbError::db_interaction_error(format!("Failed to decompress DbDump: {err}"))
|
||||
})?;
|
||||
borsh::from_slice(&borsh).map_err(|err| {
|
||||
DbError::compression_error(err, Some("Failed to deserialize DbDump".to_owned()))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RocksDBIO {
|
||||
pub db: DBWithThreadMode<MultiThreaded>,
|
||||
}
|
||||
@ -84,6 +127,71 @@ impl RocksDBIO {
|
||||
Ok(dbio)
|
||||
}
|
||||
|
||||
/// Dump every key/value pair across all column families into a [`DbDump`]. Column families are
|
||||
/// discovered from disk, so new ones are captured without a hardcoded list.
|
||||
pub fn dump_all(&self) -> DbResult<DbDump> {
|
||||
let cf_names =
|
||||
DBWithThreadMode::<MultiThreaded>::list_cf(&Options::default(), self.db.path())
|
||||
.map_err(|rerr| {
|
||||
DbError::rocksdb_cast_message(
|
||||
rerr,
|
||||
Some("Failed to list column families for dump".to_owned()),
|
||||
)
|
||||
})?;
|
||||
|
||||
let mut entries = Vec::new();
|
||||
for cf_name in cf_names {
|
||||
let cf = self.db.cf_handle(&cf_name).ok_or_else(|| {
|
||||
DbError::db_interaction_error(format!(
|
||||
"Column family {cf_name:?} listed on disk but not opened; add it to `open_inner`"
|
||||
))
|
||||
})?;
|
||||
for item in self.db.iterator_cf(&cf, IteratorMode::Start) {
|
||||
let (key, value) = item.map_err(|rerr| {
|
||||
DbError::rocksdb_cast_message(
|
||||
rerr,
|
||||
Some(format!(
|
||||
"Failed to iterate column family {cf_name:?} for dump"
|
||||
)),
|
||||
)
|
||||
})?;
|
||||
entries.push(DbDumpEntry {
|
||||
cf_name: cf_name.clone(),
|
||||
key: key.into_vec(),
|
||||
value: value.into_vec(),
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(DbDump { entries })
|
||||
}
|
||||
|
||||
/// Create a fresh rocksdb at `path` populated from a [`DbDump`].
|
||||
pub fn restore_from_dump(path: &Path, dump: &DbDump) -> DbResult<Self> {
|
||||
let mut db_opts = Options::default();
|
||||
db_opts.create_missing_column_families(true);
|
||||
db_opts.create_if_missing(true);
|
||||
let dbio = Self::open_inner(path, &db_opts)?;
|
||||
|
||||
let mut batch = WriteBatch::default();
|
||||
for entry in &dump.entries {
|
||||
let cf = dbio.db.cf_handle(&entry.cf_name).ok_or_else(|| {
|
||||
DbError::db_interaction_error(format!(
|
||||
"Unknown column family {:?} in dump",
|
||||
entry.cf_name
|
||||
))
|
||||
})?;
|
||||
batch.put_cf(&cf, &entry.key, &entry.value);
|
||||
}
|
||||
dbio.db.write(batch).map_err(|rerr| {
|
||||
DbError::rocksdb_cast_message(
|
||||
rerr,
|
||||
Some("Failed to write dump restore batch".to_owned()),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(dbio)
|
||||
}
|
||||
|
||||
fn open_inner(path: &Path, db_opts: &Options) -> DbResult<Self> {
|
||||
let mut cf_opts = Options::default();
|
||||
cf_opts.set_max_write_buffer_number(16);
|
||||
@ -246,6 +354,11 @@ impl RocksDBIO {
|
||||
self.put(&ZoneSdkCheckpointCellRef(bytes), ())
|
||||
}
|
||||
|
||||
/// Remove the persisted zone-sdk checkpoint so the next startup is treated as a fresh start.
|
||||
pub fn delete_zone_sdk_checkpoint_bytes(&self) -> DbResult<()> {
|
||||
self.del::<ZoneSdkCheckpointCellOwned>(())
|
||||
}
|
||||
|
||||
pub fn get_pending_deposit_events(&self) -> DbResult<Vec<PendingDepositEventRecord>> {
|
||||
Ok(self
|
||||
.get_opt::<PendingDepositEventsCellOwned>(())?
|
||||
@ -448,10 +561,29 @@ impl RocksDBIO {
|
||||
}
|
||||
|
||||
pub fn mark_block_as_finalized(&self, block_id: u64) -> DbResult<()> {
|
||||
self.set_block_bedrock_status(block_id, BedrockStatus::Finalized)
|
||||
}
|
||||
|
||||
/// Reset every stored block to [`BedrockStatus::Pending`], for snapshotting a store to replay
|
||||
/// against a fresh Bedrock instance that knows none of the blocks yet.
|
||||
pub fn reset_all_blocks_to_pending(&self) -> DbResult<()> {
|
||||
let block_ids: Vec<u64> = self
|
||||
.get_all_blocks()
|
||||
.filter_map(Result::ok)
|
||||
.filter(|block| !matches!(block.bedrock_status, BedrockStatus::Pending))
|
||||
.map(|block| block.header.block_id)
|
||||
.collect();
|
||||
for id in block_ids {
|
||||
self.set_block_bedrock_status(id, BedrockStatus::Pending)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_block_bedrock_status(&self, block_id: u64, status: BedrockStatus) -> DbResult<()> {
|
||||
let mut block = self.get_block(block_id)?.ok_or_else(|| {
|
||||
DbError::db_interaction_error(format!("Block with id {block_id} not found"))
|
||||
})?;
|
||||
block.bedrock_status = BedrockStatus::Finalized;
|
||||
block.bedrock_status = status;
|
||||
|
||||
let cf_block = self.block_column();
|
||||
self.db
|
||||
@ -473,7 +605,7 @@ impl RocksDBIO {
|
||||
.map_err(|rerr| {
|
||||
DbError::rocksdb_cast_message(
|
||||
rerr,
|
||||
Some(format!("Failed to mark block {block_id} as finalized")),
|
||||
Some(format!("Failed to set block {block_id} bedrock status")),
|
||||
)
|
||||
})?;
|
||||
|
||||
|
||||
@ -21,6 +21,7 @@ wallet.workspace = true
|
||||
programs.workspace = true
|
||||
|
||||
anyhow.workspace = true
|
||||
bip39.workspace = true
|
||||
bytesize.workspace = true
|
||||
env_logger.workspace = true
|
||||
futures.workspace = true
|
||||
|
||||
31
test_fixtures/README.md
Normal file
31
test_fixtures/README.md
Normal file
@ -0,0 +1,31 @@
|
||||
# test_fixtures
|
||||
|
||||
Shared test/bench fixtures that spin up bedrock + sequencer + indexer + wallet end-to-end.
|
||||
|
||||
## Library
|
||||
|
||||
`TestContext` drives the full stack for integration tests and benches:
|
||||
|
||||
```rust
|
||||
let ctx = TestContext::new().await?; // fast: restores the prebuilt db dump
|
||||
let ctx = TestContext::builder()
|
||||
.from_scratch() // apply genesis + claim supply live
|
||||
.with_genesis(actions) // custom genesis (implies from_scratch)
|
||||
.disable_indexer()
|
||||
.build().await?;
|
||||
```
|
||||
|
||||
`TestContext::new()` restores `fixtures/prebuilt_sequencer_db.dump` (genesis + initial-supply claim
|
||||
blocks) instead of re-applying genesis and re-proving the private claims, then syncs the wallet. Use
|
||||
`from_scratch()` to build genesis live.
|
||||
|
||||
## Binary
|
||||
|
||||
`regenerate_test_fixture` regenerates that dump (needs Docker):
|
||||
|
||||
```sh
|
||||
just regenerate-test-fixture # RISC0_DEV_MODE=1 cargo run -p test_fixtures --bin regenerate_test_fixture
|
||||
```
|
||||
|
||||
Rerun and commit `fixtures/prebuilt_sequencer_db.dump` after changing genesis, the default accounts,
|
||||
block format, or initial state.
|
||||
BIN
test_fixtures/fixtures/prebuilt_sequencer_db.dump
Normal file
BIN
test_fixtures/fixtures/prebuilt_sequencer_db.dump
Normal file
Binary file not shown.
124
test_fixtures/src/bin/regenerate_test_fixture.rs
Normal file
124
test_fixtures/src/bin/regenerate_test_fixture.rs
Normal file
@ -0,0 +1,124 @@
|
||||
//! Regenerate the prebuilt sequencer database dump for the fast `TestContext::new()` path.
|
||||
//! Needs Docker. Run via `just regenerate-test-fixture`, then commit the dump.
|
||||
|
||||
#![expect(clippy::print_stdout, reason = "It's normal in this small cli")]
|
||||
|
||||
use std::{path::Path, time::Duration};
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use lee::PrivateKey;
|
||||
use sequencer_core::block_store::SequencerStore;
|
||||
use test_fixtures::{
|
||||
config,
|
||||
setup::{
|
||||
prebuilt_sequencer_db_dump_path, setup_bedrock_node,
|
||||
setup_private_accounts_with_initial_supply, setup_public_accounts_with_initial_supply,
|
||||
setup_sequencer, setup_wallet,
|
||||
},
|
||||
};
|
||||
use wallet::config::WalletConfigOverrides;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
env_logger::init();
|
||||
|
||||
let dest = prebuilt_sequencer_db_dump_path();
|
||||
println!(
|
||||
"🗃️ Regenerating prebuilt sequencer db fixture at {}",
|
||||
dest.display()
|
||||
);
|
||||
|
||||
generate_prebuilt_fixture(&dest)
|
||||
.await
|
||||
.context("Failed to regenerate prebuilt sequencer database fixture")?;
|
||||
|
||||
println!("✅ Wrote fixture dump to {}", dest.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run a real sequencer with the default accounts, apply genesis + claim the initial supply
|
||||
/// (genesis block + claim block), then strip the checkpoint and reset blocks to `Pending` so the
|
||||
/// dump replays cleanly against a fresh Bedrock. Writes the dump to `dest`.
|
||||
async fn generate_prebuilt_fixture(dest: &Path) -> Result<()> {
|
||||
let (_bedrock_compose, bedrock_addr) = setup_bedrock_node()
|
||||
.await
|
||||
.context("Failed to setup Bedrock node")?;
|
||||
|
||||
let initial_public_accounts = config::default_public_accounts_for_wallet();
|
||||
let initial_private_accounts = config::default_private_accounts_for_wallet();
|
||||
let genesis =
|
||||
config::genesis_from_accounts(&initial_public_accounts, &initial_private_accounts);
|
||||
|
||||
let (sequencer_handle, temp_sequencer_dir) = setup_sequencer(
|
||||
config::SequencerPartialConfig::default(),
|
||||
bedrock_addr,
|
||||
genesis,
|
||||
)
|
||||
.await
|
||||
.context("Failed to setup Sequencer for fixture generation")?;
|
||||
|
||||
let (mut wallet, _temp_wallet_dir, _wallet_password) = setup_wallet(
|
||||
sequencer_handle.addr(),
|
||||
&initial_public_accounts,
|
||||
&initial_private_accounts,
|
||||
WalletConfigOverrides::default(),
|
||||
)
|
||||
.context("Failed to setup wallet for fixture generation")?;
|
||||
|
||||
setup_public_accounts_with_initial_supply(&mut wallet, &initial_public_accounts)
|
||||
.await
|
||||
.context("Failed to initialize public accounts for fixture generation")?;
|
||||
setup_private_accounts_with_initial_supply(&mut wallet, &initial_private_accounts)
|
||||
.await
|
||||
.context("Failed to initialize private accounts for fixture generation")?;
|
||||
|
||||
// Shut down gracefully to release the rocksdb lock before reopening the store.
|
||||
drop(wallet);
|
||||
drop(sequencer_handle);
|
||||
|
||||
let db_path = temp_sequencer_dir.path().join("rocksdb");
|
||||
let store = open_store_with_retry(&db_path)
|
||||
.await
|
||||
.context("Failed to reopen sequencer store after shutdown")?;
|
||||
store
|
||||
.delete_zone_checkpoint()
|
||||
.context("Failed to strip zone-sdk checkpoint from fixture database")?;
|
||||
store
|
||||
.reset_all_blocks_to_pending()
|
||||
.context("Failed to reset fixture blocks to pending")?;
|
||||
let dump = store.dump().context("Failed to dump fixture database")?;
|
||||
drop(store);
|
||||
|
||||
if let Some(parent) = dest.parent() {
|
||||
std::fs::create_dir_all(parent)
|
||||
.with_context(|| format!("Failed to create fixture directory {}", parent.display()))?;
|
||||
}
|
||||
let bytes = dump
|
||||
.to_bytes()
|
||||
.context("Failed to serialize fixture dump")?;
|
||||
std::fs::write(dest, bytes)
|
||||
.with_context(|| format!("Failed to write fixture dump to {}", dest.display()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reopen the store, retrying while the shut-down sequencer's aborted tasks are still releasing
|
||||
/// their db handles (the process holds the file lock until then). Must `await` between attempts.
|
||||
async fn open_store_with_retry(db_path: &Path) -> Result<SequencerStore> {
|
||||
let signing_key = PrivateKey::try_new(config::SEQUENCER_SIGNING_KEY)
|
||||
.expect("Fixed sequencer signing key must be valid");
|
||||
|
||||
let mut last_err = None;
|
||||
for _ in 0..100 {
|
||||
// Let the runtime drop the aborted sequencer tasks before each attempt.
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
match SequencerStore::open_db(db_path, signing_key.clone()) {
|
||||
Ok(store) => return Ok(store),
|
||||
Err(err) => last_err = Some(err),
|
||||
}
|
||||
}
|
||||
Err(anyhow::anyhow!(
|
||||
"Failed to open sequencer store at {} after retries: {last_err:?}",
|
||||
db_path.display()
|
||||
))
|
||||
}
|
||||
@ -3,7 +3,7 @@ use std::{net::SocketAddr, path::PathBuf, time::Duration};
|
||||
use anyhow::{Context as _, Result};
|
||||
use bytesize::ByteSize;
|
||||
use indexer_service::{ChannelId, ClientConfig, IndexerConfig};
|
||||
use key_protocol::key_management::KeyChain;
|
||||
use key_protocol::key_management::{KeyChain, secret_holders::SeedHolder};
|
||||
use lee::{AccountId, PrivateKey, PublicKey};
|
||||
use lee_core::Identifier;
|
||||
use sequencer_core::config::{BedrockConfig, GenesisAction, SequencerConfig};
|
||||
@ -13,6 +13,15 @@ use wallet::config::WalletConfig;
|
||||
pub const INITIAL_PUBLIC_BALANCES_FOR_WALLET: [u128; 2] = [10_000, 20_000];
|
||||
pub const INITIAL_PRIVATE_BALANCES_FOR_WALLET: [u128; 2] = [10_000, 20_000];
|
||||
|
||||
/// Fixed sequencer signing key; exposed so the fixture generator can reopen the produced store.
|
||||
pub const SEQUENCER_SIGNING_KEY: [u8; 32] = [37; 32];
|
||||
|
||||
// Fixed entropy seeds for the default accounts: deterministic so one prebuilt database is reusable,
|
||||
// and distinct from the `testnet_initial_state` accounts to avoid depending on / double-funding
|
||||
// them.
|
||||
const DEFAULT_PUBLIC_ACCOUNT_SEEDS: [[u8; 32]; 2] = [[0x11; 32], [0x22; 32]];
|
||||
const DEFAULT_PRIVATE_ACCOUNT_SEEDS: [[u8; 32]; 2] = [[0x33; 32], [0x44; 32]];
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct InitialPrivateAccountForWallet {
|
||||
pub key_chain: KeyChain,
|
||||
@ -87,7 +96,7 @@ pub fn sequencer_config(
|
||||
block_create_timeout,
|
||||
retry_pending_blocks_timeout: Duration::from_secs(5),
|
||||
genesis: genesis_transactions,
|
||||
signing_key: [37; 32],
|
||||
signing_key: SEQUENCER_SIGNING_KEY,
|
||||
bedrock_config: BedrockConfig {
|
||||
channel_id: bedrock_channel_id(),
|
||||
node_url: addr_to_url(UrlProtocol::Http, bedrock_addr)
|
||||
@ -99,7 +108,10 @@ pub fn sequencer_config(
|
||||
|
||||
#[must_use]
|
||||
pub fn default_public_accounts_for_wallet() -> Vec<(PrivateKey, u128)> {
|
||||
let mut private_keys = vec![PrivateKey::new_os_random(), PrivateKey::new_os_random()];
|
||||
let mut private_keys = DEFAULT_PUBLIC_ACCOUNT_SEEDS
|
||||
.iter()
|
||||
.map(|seed| PrivateKey::try_new(*seed).expect("Fixed public account seed must be valid"))
|
||||
.collect::<Vec<_>>();
|
||||
private_keys.sort_unstable_by_key(|private_key| {
|
||||
AccountId::from(&PublicKey::new_from_private_key(private_key))
|
||||
});
|
||||
@ -112,7 +124,10 @@ pub fn default_public_accounts_for_wallet() -> Vec<(PrivateKey, u128)> {
|
||||
|
||||
#[must_use]
|
||||
pub fn default_private_accounts_for_wallet() -> Vec<InitialPrivateAccountForWallet> {
|
||||
let mut key_chains = vec![KeyChain::new_os_random(), KeyChain::new_os_random()];
|
||||
let mut key_chains = DEFAULT_PRIVATE_ACCOUNT_SEEDS
|
||||
.iter()
|
||||
.map(|seed| deterministic_private_key_chain(*seed))
|
||||
.collect::<Vec<_>>();
|
||||
key_chains.sort_unstable();
|
||||
|
||||
key_chains
|
||||
@ -126,6 +141,25 @@ pub fn default_private_accounts_for_wallet() -> Vec<InitialPrivateAccountForWall
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Deterministic [`KeyChain`] from fixed entropy (mirrors `KeyChain::new_os_random`, seeded).
|
||||
fn deterministic_private_key_chain(entropy: [u8; 32]) -> KeyChain {
|
||||
let mnemonic =
|
||||
bip39::Mnemonic::from_entropy(&entropy).expect("32 bytes of entropy is valid for bip39");
|
||||
let seed_holder = SeedHolder::from_mnemonic(&mnemonic, "");
|
||||
|
||||
let secret_spending_key = seed_holder.produce_top_secret_key_holder();
|
||||
let private_key_holder = secret_spending_key.produce_private_key_holder(None);
|
||||
let nullifier_public_key = private_key_holder.generate_nullifier_public_key();
|
||||
let viewing_public_key = private_key_holder.generate_viewing_public_key();
|
||||
|
||||
KeyChain {
|
||||
secret_spending_key,
|
||||
private_key_holder,
|
||||
nullifier_public_key,
|
||||
viewing_public_key,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn genesis_from_accounts(
|
||||
public_accounts: &[(PrivateKey, u128)],
|
||||
|
||||
@ -25,7 +25,8 @@ use crate::{
|
||||
indexer_client::IndexerClient,
|
||||
setup::{
|
||||
setup_bedrock_node, setup_indexer, setup_private_accounts_with_initial_supply,
|
||||
setup_public_accounts_with_initial_supply, setup_sequencer, setup_wallet,
|
||||
setup_public_accounts_with_initial_supply, setup_sequencer, setup_sequencer_from_prebuilt,
|
||||
setup_wallet, sync_wallet_from_prebuilt,
|
||||
},
|
||||
};
|
||||
|
||||
@ -254,6 +255,7 @@ pub struct TestContextBuilder {
|
||||
sequencer_partial_config: Option<config::SequencerPartialConfig>,
|
||||
enable_indexer: bool,
|
||||
wallet_config_overrides: WalletConfigOverrides,
|
||||
from_scratch: bool,
|
||||
}
|
||||
|
||||
impl TestContextBuilder {
|
||||
@ -263,6 +265,7 @@ impl TestContextBuilder {
|
||||
sequencer_partial_config: None,
|
||||
enable_indexer: true,
|
||||
wallet_config_overrides: WalletConfigOverrides::default(),
|
||||
from_scratch: false,
|
||||
}
|
||||
}
|
||||
|
||||
@ -291,6 +294,14 @@ impl TestContextBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Build from genesis live instead of loading the prebuilt fixture. Implied by
|
||||
/// [`Self::with_genesis`].
|
||||
#[must_use]
|
||||
pub const fn from_scratch(mut self) -> Self {
|
||||
self.from_scratch = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Exclude Indexer from test context.
|
||||
/// Indexer is enabled by default.
|
||||
///
|
||||
@ -308,6 +319,7 @@ impl TestContextBuilder {
|
||||
sequencer_partial_config,
|
||||
enable_indexer,
|
||||
wallet_config_overrides,
|
||||
from_scratch,
|
||||
} = self;
|
||||
|
||||
// Ensure logger is initialized only once
|
||||
@ -315,6 +327,10 @@ impl TestContextBuilder {
|
||||
|
||||
debug!("Test context setup");
|
||||
|
||||
// The fixture bakes in the default accounts + genesis, so custom genesis / from_scratch
|
||||
// must build live. Otherwise load the fixture (fails if it is missing).
|
||||
let use_prebuilt = !from_scratch && genesis_transactions.is_none();
|
||||
|
||||
let (bedrock_compose, bedrock_addr) = setup_bedrock_node()
|
||||
.await
|
||||
.context("Failed to setup Bedrock node")?;
|
||||
@ -339,25 +355,30 @@ impl TestContextBuilder {
|
||||
|
||||
let initial_public_accounts = config::default_public_accounts_for_wallet();
|
||||
let initial_private_accounts = config::default_private_accounts_for_wallet();
|
||||
// Wallet genesis must always be present so that
|
||||
// setup_public/private_accounts_with_initial_supply can claim from the vault PDAs.
|
||||
// When a test supplies custom genesis, merge rather than replace.
|
||||
let wallet_genesis =
|
||||
config::genesis_from_accounts(&initial_public_accounts, &initial_private_accounts);
|
||||
let genesis = match genesis_transactions {
|
||||
Some(mut custom) => {
|
||||
custom.extend(wallet_genesis);
|
||||
custom
|
||||
}
|
||||
None => wallet_genesis,
|
||||
|
||||
let partial_config = sequencer_partial_config.unwrap_or_default();
|
||||
|
||||
let (sequencer_handle, temp_sequencer_dir) = if use_prebuilt {
|
||||
setup_sequencer_from_prebuilt(partial_config, bedrock_addr)
|
||||
.await
|
||||
.context("Failed to setup Sequencer from prebuilt database")?
|
||||
} else {
|
||||
// Wallet genesis must always be present so that
|
||||
// setup_public/private_accounts_with_initial_supply can claim from the vault PDAs.
|
||||
// When a test supplies custom genesis, merge rather than replace.
|
||||
let wallet_genesis =
|
||||
config::genesis_from_accounts(&initial_public_accounts, &initial_private_accounts);
|
||||
let genesis = match genesis_transactions {
|
||||
Some(mut custom) => {
|
||||
custom.extend(wallet_genesis);
|
||||
custom
|
||||
}
|
||||
None => wallet_genesis,
|
||||
};
|
||||
setup_sequencer(partial_config, bedrock_addr, genesis)
|
||||
.await
|
||||
.context("Failed to setup Sequencer")?
|
||||
};
|
||||
let (sequencer_handle, temp_sequencer_dir) = setup_sequencer(
|
||||
sequencer_partial_config.unwrap_or_default(),
|
||||
bedrock_addr,
|
||||
genesis,
|
||||
)
|
||||
.await
|
||||
.context("Failed to setup Sequencer")?;
|
||||
|
||||
let (mut wallet, temp_wallet_dir, wallet_password) = setup_wallet(
|
||||
sequencer_handle.addr(),
|
||||
@ -367,13 +388,20 @@ impl TestContextBuilder {
|
||||
)
|
||||
.context("Failed to setup wallet")?;
|
||||
|
||||
setup_public_accounts_with_initial_supply(&mut wallet, &initial_public_accounts)
|
||||
.await
|
||||
.context("Failed to initialize public accounts in wallet")?;
|
||||
if use_prebuilt {
|
||||
// Funds already exist on-chain in the prebuilt blocks; sync instead of claiming live.
|
||||
sync_wallet_from_prebuilt(&mut wallet)
|
||||
.await
|
||||
.context("Failed to sync wallet from prebuilt database")?;
|
||||
} else {
|
||||
setup_public_accounts_with_initial_supply(&mut wallet, &initial_public_accounts)
|
||||
.await
|
||||
.context("Failed to initialize public accounts in wallet")?;
|
||||
|
||||
setup_private_accounts_with_initial_supply(&mut wallet, &initial_private_accounts)
|
||||
.await
|
||||
.context("Failed to initialize private accounts in wallet")?;
|
||||
setup_private_accounts_with_initial_supply(&mut wallet, &initial_private_accounts)
|
||||
.await
|
||||
.context("Failed to initialize private accounts in wallet")?;
|
||||
}
|
||||
|
||||
let sequencer_url = config::addr_to_url(config::UrlProtocol::Http, sequencer_handle.addr())
|
||||
.context("Failed to convert sequencer addr to URL")?;
|
||||
@ -405,6 +433,7 @@ impl TestContextBuilder {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A test context to be used in normal #[test] tests.
|
||||
pub struct BlockingTestContext {
|
||||
ctx: Option<TestContext>,
|
||||
|
||||
@ -4,6 +4,7 @@ use anyhow::{Context as _, Result, bail};
|
||||
use indexer_service::IndexerHandle;
|
||||
use lee::{AccountId, PrivateKey, PublicKey};
|
||||
use log::{debug, warn};
|
||||
use sequencer_core::block_store::{DbDump, SequencerStore};
|
||||
use sequencer_service::{GenesisAction, SequencerHandle};
|
||||
use tempfile::TempDir;
|
||||
use testcontainers::compose::DockerCompose;
|
||||
@ -19,6 +20,28 @@ use crate::{
|
||||
private_mention, public_mention,
|
||||
};
|
||||
|
||||
/// How to initialize the sequencer's database.
|
||||
pub enum SequencerInit<'dump> {
|
||||
/// Apply these genesis actions from scratch.
|
||||
Genesis(Vec<GenesisAction>),
|
||||
/// Restore an existing chain from a prebuilt dump (genesis actions are then irrelevant).
|
||||
Prebuilt(&'dump DbDump),
|
||||
}
|
||||
|
||||
/// Committed single-file dump of the prebuilt sequencer database (`just regenerate-test-fixture`).
|
||||
#[must_use]
|
||||
pub fn prebuilt_sequencer_db_dump_path() -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("fixtures/prebuilt_sequencer_db.dump")
|
||||
}
|
||||
|
||||
/// Load and deserialize the committed prebuilt-database dump.
|
||||
fn load_prebuilt_dump() -> Result<DbDump> {
|
||||
let path = prebuilt_sequencer_db_dump_path();
|
||||
let bytes = std::fs::read(&path)
|
||||
.with_context(|| format!("Failed to read prebuilt db dump at {}", path.display()))?;
|
||||
DbDump::from_bytes(&bytes).context("Failed to deserialize prebuilt db dump")
|
||||
}
|
||||
|
||||
pub async fn setup_bedrock_node() -> Result<(DockerCompose, SocketAddr)> {
|
||||
let manifest_dir = env!("CARGO_MANIFEST_DIR");
|
||||
let bedrock_compose_path = PathBuf::from(manifest_dir).join("../bedrock/docker-compose.yml");
|
||||
@ -111,6 +134,29 @@ pub async fn setup_sequencer(
|
||||
partial: config::SequencerPartialConfig,
|
||||
bedrock_addr: SocketAddr,
|
||||
genesis_transactions: Vec<GenesisAction>,
|
||||
) -> Result<(SequencerHandle, TempDir)> {
|
||||
setup_sequencer_inner(
|
||||
partial,
|
||||
bedrock_addr,
|
||||
SequencerInit::Genesis(genesis_transactions),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Like [`setup_sequencer`], but rebuilds the sequencer database from the committed prebuilt dump
|
||||
/// so it starts from an existing chain instead of applying genesis from scratch.
|
||||
pub async fn setup_sequencer_from_prebuilt(
|
||||
partial: config::SequencerPartialConfig,
|
||||
bedrock_addr: SocketAddr,
|
||||
) -> Result<(SequencerHandle, TempDir)> {
|
||||
let dump = load_prebuilt_dump()?;
|
||||
setup_sequencer_inner(partial, bedrock_addr, SequencerInit::Prebuilt(&dump)).await
|
||||
}
|
||||
|
||||
async fn setup_sequencer_inner(
|
||||
partial: config::SequencerPartialConfig,
|
||||
bedrock_addr: SocketAddr,
|
||||
init: SequencerInit<'_>,
|
||||
) -> Result<(SequencerHandle, TempDir)> {
|
||||
let temp_sequencer_dir =
|
||||
tempfile::tempdir().context("Failed to create temp dir for sequencer home")?;
|
||||
@ -120,6 +166,17 @@ pub async fn setup_sequencer(
|
||||
temp_sequencer_dir.path().display()
|
||||
);
|
||||
|
||||
let genesis_transactions = match init {
|
||||
SequencerInit::Genesis(genesis) => genesis,
|
||||
SequencerInit::Prebuilt(dump) => {
|
||||
// `SequencerCore::open_or_create_store` looks for `<home>/rocksdb`.
|
||||
let dst = temp_sequencer_dir.path().join("rocksdb");
|
||||
SequencerStore::restore_db_from_dump(&dst, dump)
|
||||
.context("Failed to restore prebuilt sequencer database from dump")?;
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
|
||||
let config = config::sequencer_config(
|
||||
partial,
|
||||
temp_sequencer_dir.path().to_owned(),
|
||||
@ -224,6 +281,15 @@ pub async fn setup_private_accounts_with_initial_supply(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn sync_wallet_from_prebuilt(wallet: &mut WalletCore) -> Result<()> {
|
||||
wallet
|
||||
.sync_to_latest_block()
|
||||
.await
|
||||
.context("Failed to sync wallet from prebuilt chain")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn claim_funds_from_vault_to_private(
|
||||
wallet: &mut WalletCore,
|
||||
owner_id: AccountId,
|
||||
|
||||
58
test_fixtures/tests/prebuilt_fixture.rs
Normal file
58
test_fixtures/tests/prebuilt_fixture.rs
Normal file
@ -0,0 +1,58 @@
|
||||
//! "Genesis follows config" checks for both the from-scratch and prebuilt `TestContext` paths.
|
||||
//! Need Docker/Bedrock, like the integration tests.
|
||||
#![expect(clippy::tests_outside_test_module, reason = "It's a tests module")]
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use lee::{AccountId, PublicKey};
|
||||
use sequencer_service_rpc::RpcClient as _;
|
||||
use test_fixtures::{
|
||||
TestContext,
|
||||
config::{default_private_accounts_for_wallet, default_public_accounts_for_wallet},
|
||||
verify_commitment_is_in_state,
|
||||
};
|
||||
|
||||
/// Builds from genesis (no prebuilt database) and checks the on-chain state follows the config.
|
||||
#[tokio::test]
|
||||
async fn genesis_from_scratch_follows_config() -> Result<()> {
|
||||
let ctx = TestContext::builder().from_scratch().build().await?;
|
||||
assert_context_follows_config(&ctx).await
|
||||
}
|
||||
|
||||
/// Loads the prebuilt database via [`TestContext::new`] and checks the restored state follows the
|
||||
/// config — i.e. the dump round-trips and all expected data persists.
|
||||
#[tokio::test]
|
||||
async fn prebuilt_context_follows_config() -> Result<()> {
|
||||
let ctx = TestContext::new().await?;
|
||||
assert_context_follows_config(&ctx).await
|
||||
}
|
||||
|
||||
/// Assert every configured default account is funded with its configured balance, and every
|
||||
/// configured private account's commitment is present in sequencer state. (The wallet also has an
|
||||
/// unfunded default root account, so we check the configured accounts specifically.)
|
||||
async fn assert_context_follows_config(ctx: &TestContext) -> Result<()> {
|
||||
for (private_key, expected_balance) in default_public_accounts_for_wallet() {
|
||||
let account_id = AccountId::from(&PublicKey::new_from_private_key(&private_key));
|
||||
let balance = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(account_id)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
balance, expected_balance,
|
||||
"public account {account_id} balance"
|
||||
);
|
||||
}
|
||||
|
||||
for account in default_private_accounts_for_wallet() {
|
||||
let account_id = account.account_id();
|
||||
let commitment = ctx
|
||||
.wallet()
|
||||
.get_private_account_commitment(account_id)
|
||||
.with_context(|| format!("commitment for private account {account_id}"))?;
|
||||
assert!(
|
||||
verify_commitment_is_in_state(commitment, ctx.sequencer_client()).await,
|
||||
"private commitment for {account_id} should be in sequencer state"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user