mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-08-25 03:11:21 +00:00
fix(integration_tests): multi sequencer now builds the state with two accredited keys from channel creation
This commit is contained in:
@@ -3,9 +3,9 @@
|
||||
reason = "top-level test functions are conventional for integration tests"
|
||||
)]
|
||||
|
||||
//! Two sequencers share one channel: A starts solo as channel admin, live-
|
||||
//! accredits `[A, B]` with round-robin rotation, B joins and syncs, both
|
||||
//! produce on their turns, and A, B and an indexer converge on the same chain.
|
||||
//! Two sequencers share one channel: both are staked at genesis, so A creates
|
||||
//! the channel already accrediting `[A, B]`, B joins and syncs, both produce on
|
||||
//! their turns, and A, B and an indexer converge on the same chain.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -13,91 +13,110 @@ use anyhow::{Context as _, Result, ensure};
|
||||
use indexer_service_rpc::RpcClient as _;
|
||||
use integration_tests::{
|
||||
config::{self, SequencerPartialConfig},
|
||||
indexer_client::IndexerClient,
|
||||
init_logger,
|
||||
setup::{SequencerSetup, indexer_client, sequencer_client, setup_bedrock_node, setup_indexer},
|
||||
};
|
||||
use logos_blockchain_key_management_system_service::keys::{ED25519_SECRET_KEY_SIZE, Ed25519Key};
|
||||
use sequencer_core::{
|
||||
block_publisher::{Ed25519PublicKey, read_channel_state},
|
||||
config::{BedrockConfig, GenesisAction},
|
||||
sign_genesis_stake,
|
||||
};
|
||||
use sequencer_service_rpc::{RpcClient as _, SequencerClient};
|
||||
use test_fixtures::{
|
||||
MultiZoneTestContextBuilder, ZoneTestContextBuilder, config::MultiNodeTestContextConfig,
|
||||
};
|
||||
use testnet_initial_state::{initial_pub_accounts_private_keys, initial_public_user_accounts};
|
||||
use tokio::test;
|
||||
|
||||
const PHASE_TIMEOUT: Duration = Duration::from_secs(360);
|
||||
const POLL_INTERVAL: Duration = Duration::from_secs(2);
|
||||
const TRANSFER_AMOUNT: u128 = 10;
|
||||
/// ≈4 turn windows past B's join (5 s blocks, ~20 s turns → ~4 blocks/window).
|
||||
/// ≈4 turn windows past B's join, at `DEFAULT_SEQUENCER_POSTING_TIMEFRAME`
|
||||
/// (20 slots ≈ 20 s) and 5 s blocks.
|
||||
const ROTATION_BLOCKS: u64 = 8;
|
||||
|
||||
#[test]
|
||||
async fn multi_sequencer_committee_converges() -> Result<()> {
|
||||
let bedrock_channel_id = config::bedrock_channel_id();
|
||||
init_logger();
|
||||
|
||||
let (_bedrock, bedrock_addr) = setup_bedrock_node()
|
||||
.await
|
||||
.context("Failed to set up Bedrock node")?;
|
||||
|
||||
// Fixed seeds so both keys can be staked in genesis before either starts.
|
||||
let key_a = [0xA1_u8; ED25519_SECRET_KEY_SIZE];
|
||||
let key_b = [0xB2_u8; ED25519_SECRET_KEY_SIZE];
|
||||
let pub_a = Ed25519Key::from_bytes(&key_a).public_key();
|
||||
let pub_b = Ed25519Key::from_bytes(&key_b).public_key();
|
||||
|
||||
// Each operator signs its own stake offchain.
|
||||
let genesis = vec![
|
||||
founding_stake(0, pub_a.to_bytes(), [0x51_u8; 32])?,
|
||||
founding_stake(1, pub_b.to_bytes(), [0x52_u8; 32])?,
|
||||
];
|
||||
|
||||
let bedrock_config = BedrockConfig {
|
||||
channel_id: config::bedrock_channel_id(),
|
||||
node_url: config::addr_to_url(config::UrlProtocol::Http, bedrock_addr)?,
|
||||
funding_key: config::bedrock_funding_key(),
|
||||
auth: None,
|
||||
priority_fee: sequencer_core::config::default_priority_fee(),
|
||||
};
|
||||
|
||||
let partial = SequencerPartialConfig {
|
||||
block_create_timeout: Duration::from_secs(5),
|
||||
..SequencerPartialConfig::default()
|
||||
};
|
||||
|
||||
let ctx = MultiZoneTestContextBuilder::default()
|
||||
.with_zone(
|
||||
ZoneTestContextBuilder::new(MultiNodeTestContextConfig {
|
||||
num_nodes: 2,
|
||||
bedrock_channel: bedrock_channel_id,
|
||||
})
|
||||
.disable_wallet()
|
||||
.with_sequencer_partial_config(partial)
|
||||
.with_genesis(vec![]),
|
||||
)
|
||||
.build()
|
||||
.await?;
|
||||
// Phase 1: A solo (it creates the channel), plus an indexer.
|
||||
let (seq_a, _a_home) = SequencerSetup::new(partial, bedrock_addr)
|
||||
.with_genesis(genesis.clone())
|
||||
.with_bedrock_signing_key(key_a)
|
||||
.setup()
|
||||
.await
|
||||
.context("Failed to set up sequencer A")?;
|
||||
let a = sequencer_client(seq_a.addr())?;
|
||||
let (idx, _idx_home) = setup_indexer(bedrock_addr, config::bedrock_channel_id(), None)
|
||||
.await
|
||||
.context("Failed to set up indexer")?;
|
||||
let indexer = indexer_client(idx.addr()).await?;
|
||||
|
||||
let mut seq_iterator = ctx.sequencer_components_iter(bedrock_channel_id).unwrap();
|
||||
wait_for_height(&a, 2, "sequencer A to produce past genesis").await?;
|
||||
|
||||
let seq_client_a = &(seq_iterator.next().unwrap().sequencer_client);
|
||||
let seq_client_b = &(seq_iterator.next().unwrap().sequencer_client);
|
||||
|
||||
let indexer = ctx.indexer_client();
|
||||
|
||||
wait_for_height(seq_client_a, 2, "sequencer A to produce past genesis").await?;
|
||||
|
||||
log::info!("Passed wait for height A to be at least 2");
|
||||
|
||||
let height_at_config = seq_client_a.get_last_block_id().await?;
|
||||
wait_for_height(
|
||||
seq_client_a,
|
||||
height_at_config + 1,
|
||||
"A to produce after the roster change",
|
||||
)
|
||||
// Phase 2: the creating tx carried the committee, so both keys are accredited
|
||||
// from block 1 and discovery has nothing to reconcile.
|
||||
let mut want = vec![pub_a.to_bytes(), pub_b.to_bytes()];
|
||||
want.sort_unstable();
|
||||
wait_until("Bedrock to accredit both staked keys", || async {
|
||||
Ok(committee(&bedrock_config).await?.0 == want)
|
||||
})
|
||||
.await?;
|
||||
|
||||
log::info!(
|
||||
"Passed wait for height A to be at least {}",
|
||||
height_at_config + 1
|
||||
);
|
||||
// Phase 3: B joins live and syncs the existing chain.
|
||||
let (seq_b, _b_home) = SequencerSetup::new(partial, bedrock_addr)
|
||||
.with_genesis(genesis)
|
||||
.with_bedrock_signing_key(key_b)
|
||||
.setup()
|
||||
.await
|
||||
.context("Failed to set up sequencer B")?;
|
||||
let b = sequencer_client(seq_b.addr())?;
|
||||
|
||||
let join_height = seq_client_a.get_last_block_id().await?;
|
||||
wait_for_height(seq_client_b, join_height, "B to sync to A's height at join").await?;
|
||||
let join_height = a.get_last_block_id().await?;
|
||||
wait_for_height(&b, join_height, "B to sync to A's height at join").await?;
|
||||
|
||||
log::info!("Passed wait for height B to be at least {join_height}");
|
||||
|
||||
// Phase 4: rotation + convergence over ≈4 turn windows.
|
||||
// Phase 4: rotation + convergence over ≈4 turn windows. Without the turn
|
||||
// check, a chain A produces alone satisfies every assertion below.
|
||||
let rotation_target = join_height + ROTATION_BLOCKS;
|
||||
wait_for_height(
|
||||
seq_client_a,
|
||||
&a,
|
||||
rotation_target,
|
||||
"the chain to advance across turn windows",
|
||||
)
|
||||
.await?;
|
||||
|
||||
log::info!("Passed wait for height A to be at least {rotation_target}");
|
||||
|
||||
wait_for_height(
|
||||
seq_client_b,
|
||||
rotation_target,
|
||||
"B to follow across turn windows",
|
||||
)
|
||||
wait_for_height(&b, rotation_target, "B to follow across turn windows").await?;
|
||||
wait_until("the round-robin turn to reach B", || async {
|
||||
Ok(committee(&bedrock_config).await?.1 == Some(pub_b))
|
||||
})
|
||||
.await?;
|
||||
assert_same_chain(seq_client_a, seq_client_b).await?;
|
||||
|
||||
log::info!("Passed wait for height B to be at least {rotation_target}");
|
||||
assert_same_chain(&a, &b).await?;
|
||||
|
||||
// Phase 5: a tx submitted only to B is included by B and visible on A.
|
||||
let accounts = initial_public_user_accounts();
|
||||
@@ -105,8 +124,8 @@ async fn multi_sequencer_committee_converges() -> Result<()> {
|
||||
let to = accounts[1].account_id;
|
||||
let sign_key = initial_pub_accounts_private_keys()[0].pub_sign_key.clone();
|
||||
|
||||
let to_balance_before = seq_client_a.get_account_balance(to).await?;
|
||||
let nonce = seq_client_b.get_accounts_nonces(vec![from]).await?[0];
|
||||
let to_balance_before = a.get_account_balance(to).await?;
|
||||
let nonce = b.get_accounts_nonces(vec![from]).await?[0];
|
||||
let tx = common::test_utils::create_transaction_native_token_transfer(
|
||||
from,
|
||||
nonce.0,
|
||||
@@ -114,30 +133,28 @@ async fn multi_sequencer_committee_converges() -> Result<()> {
|
||||
TRANSFER_AMOUNT,
|
||||
&sign_key,
|
||||
);
|
||||
seq_client_b
|
||||
.send_transaction(tx)
|
||||
b.send_transaction(tx)
|
||||
.await
|
||||
.context("Failed to submit the transfer to B")?;
|
||||
|
||||
wait_for_balance(seq_client_a, to, to_balance_before + TRANSFER_AMOUNT).await?;
|
||||
|
||||
log::info!(
|
||||
"Passed wait for height balance {to} to be {}",
|
||||
to_balance_before + TRANSFER_AMOUNT
|
||||
);
|
||||
let expected = to_balance_before + TRANSFER_AMOUNT;
|
||||
wait_until("the cross-sequencer transfer to reach A", || async {
|
||||
Ok(a.get_account_balance(to).await? == expected)
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Phase 6: the indexer finalizes the same chain, with no stall.
|
||||
wait_for_finalized(indexer, join_height).await?;
|
||||
|
||||
log::info!("Passed indexer to see finalized {join_height}");
|
||||
|
||||
wait_until("the indexer to finalize", || async {
|
||||
Ok(indexer.get_last_finalized_block_id().await?.unwrap_or(0) >= join_height)
|
||||
})
|
||||
.await?;
|
||||
let finalized = indexer.get_last_finalized_block_id().await?.unwrap_or(0);
|
||||
for id in 1..=finalized {
|
||||
let block_i = indexer
|
||||
.get_block_by_id(id)
|
||||
.await?
|
||||
.with_context(|| format!("Indexer is missing finalized block {id}"))?;
|
||||
let block_a = seq_client_a
|
||||
let block_a = a
|
||||
.get_block(id)
|
||||
.await?
|
||||
.with_context(|| format!("A is missing block {id}"))?;
|
||||
@@ -156,59 +173,62 @@ async fn multi_sequencer_committee_converges() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Builds a founding-sequencer genesis entry, signing its stake the way an
|
||||
/// operator would before handing the entry over.
|
||||
fn founding_stake(
|
||||
index: usize,
|
||||
sequencer_key: sequencer_stake_core::SequencerKey,
|
||||
owner_seed: [u8; 32],
|
||||
) -> Result<GenesisAction> {
|
||||
let owner = lee::PrivateKey::try_new(owner_seed)?;
|
||||
Ok(GenesisAction::StakeSequencer {
|
||||
sequencer_key,
|
||||
ownership_public_key: lee::PublicKey::new_from_private_key(&owner),
|
||||
stake_signature: sign_genesis_stake(index, sequencer_key, &owner),
|
||||
})
|
||||
}
|
||||
|
||||
/// Polls `check` until it reports ready, failing with `what` on timeout.
|
||||
async fn wait_until<F, Fut>(what: &str, mut check: F) -> Result<()>
|
||||
where
|
||||
F: FnMut() -> Fut,
|
||||
Fut: Future<Output = Result<bool>>,
|
||||
{
|
||||
let wait = async {
|
||||
while !check().await? {
|
||||
tokio::time::sleep(POLL_INTERVAL).await;
|
||||
}
|
||||
Ok::<(), anyhow::Error>(())
|
||||
};
|
||||
tokio::time::timeout(PHASE_TIMEOUT, wait)
|
||||
.await
|
||||
.with_context(|| format!("Timed out waiting for {what}"))?
|
||||
}
|
||||
|
||||
/// Polls the sequencer until its chain height reaches `target`.
|
||||
async fn wait_for_height(client: &SequencerClient, target: u64, what: &str) -> Result<()> {
|
||||
log::info!("Waiting for {what:?}, target is {target}");
|
||||
|
||||
let wait = async {
|
||||
loop {
|
||||
if client.get_last_block_id().await? >= target {
|
||||
return Ok::<(), anyhow::Error>(());
|
||||
}
|
||||
tokio::time::sleep(POLL_INTERVAL).await;
|
||||
}
|
||||
};
|
||||
tokio::time::timeout(PHASE_TIMEOUT, wait)
|
||||
.await
|
||||
.with_context(|| format!("Timed out waiting for {what} (target height {target})"))?
|
||||
wait_until(&format!("{what} (target height {target})"), || async {
|
||||
Ok(client.get_last_block_id().await? >= target)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Polls the sequencer until `account`'s balance reaches `expected`.
|
||||
async fn wait_for_balance(
|
||||
client: &SequencerClient,
|
||||
account: lee::AccountId,
|
||||
expected: u128,
|
||||
) -> Result<()> {
|
||||
log::info!("Waiting for {account} to have {expected} tokens");
|
||||
|
||||
let wait = async {
|
||||
loop {
|
||||
if client.get_account_balance(account).await? == expected {
|
||||
return Ok::<(), anyhow::Error>(());
|
||||
}
|
||||
tokio::time::sleep(POLL_INTERVAL).await;
|
||||
}
|
||||
/// The channel's accredited keys, sorted, plus whose turn the tip was written on.
|
||||
async fn committee(config: &BedrockConfig) -> Result<(Vec<[u8; 32]>, Option<Ed25519PublicKey>)> {
|
||||
let Some(state) = read_channel_state(config).await? else {
|
||||
return Ok((Vec::new(), None));
|
||||
};
|
||||
tokio::time::timeout(PHASE_TIMEOUT, wait)
|
||||
.await
|
||||
.context("Timed out waiting for the cross-sequencer transfer to reach A")?
|
||||
}
|
||||
|
||||
/// Polls the indexer until its finalized height reaches `target`.
|
||||
async fn wait_for_finalized(indexer: &IndexerClient, target: u64) -> Result<()> {
|
||||
log::info!("Waiting for indexer to see target finalized, target is {target}");
|
||||
|
||||
let wait = async {
|
||||
loop {
|
||||
if indexer.get_last_finalized_block_id().await?.unwrap_or(0) >= target {
|
||||
return Ok::<(), anyhow::Error>(());
|
||||
}
|
||||
tokio::time::sleep(POLL_INTERVAL).await;
|
||||
}
|
||||
};
|
||||
tokio::time::timeout(PHASE_TIMEOUT, wait)
|
||||
.await
|
||||
.context("Timed out waiting for the indexer to finalize")?
|
||||
let turn = state
|
||||
.accredited_keys
|
||||
.get(usize::from(state.tip_sequencer))
|
||||
.copied();
|
||||
let mut keys: Vec<_> = state
|
||||
.accredited_keys
|
||||
.iter()
|
||||
.map(Ed25519PublicKey::to_bytes)
|
||||
.collect();
|
||||
keys.sort_unstable();
|
||||
Ok((keys, turn))
|
||||
}
|
||||
|
||||
/// Asserts A and B hold byte-identical block hashes over their common prefix.
|
||||
|
||||
@@ -5,11 +5,14 @@ use k256::ecdsa::signature::hazmat::PrehashVerifier as _;
|
||||
pub use private_key::PrivateKey;
|
||||
pub use public_key::PublicKey;
|
||||
use rand::{RngCore as _, rngs::OsRng};
|
||||
use serde_with::{DeserializeFromStr, SerializeDisplay};
|
||||
|
||||
mod private_key;
|
||||
mod public_key;
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
#[derive(
|
||||
Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize, SerializeDisplay, DeserializeFromStr,
|
||||
)]
|
||||
pub struct Signature {
|
||||
pub value: [u8; 64],
|
||||
}
|
||||
|
||||
@@ -11,24 +11,24 @@ pub use logos_blockchain_core::mantle::{
|
||||
use logos_blockchain_core::{
|
||||
mantle::{
|
||||
SignedMantleTx,
|
||||
channel::{SlotTimeframe, SlotTimeout},
|
||||
channel::{ChannelState, SlotTimeframe, SlotTimeout},
|
||||
gas::GasCost,
|
||||
ops::{
|
||||
Op, OpProof,
|
||||
channel::{
|
||||
ChannelId,
|
||||
config::{ChannelConfigOp, Keys},
|
||||
inscribe::Inscription,
|
||||
inscribe::{Inscription, InscriptionOp},
|
||||
},
|
||||
},
|
||||
traits::Hashable as _,
|
||||
transactions::{MantleTxBuilder, OpsProofs},
|
||||
transactions::{MantleTxBuilder, OpsProofs, states::Unverified},
|
||||
},
|
||||
proofs::channel_multi_sig_proof::{ChannelMultiSigProof, IndexedSignature},
|
||||
};
|
||||
use logos_blockchain_http_api_common::bodies::wallet::fund::WalletFundRequestBody;
|
||||
pub use logos_blockchain_key_management_system_service::keys::{
|
||||
ED25519_SECRET_KEY_SIZE, Ed25519Key, ZkKey,
|
||||
ED25519_SECRET_KEY_SIZE, Ed25519Key, ZkKey, ZkPublicKey,
|
||||
};
|
||||
pub use logos_blockchain_zone_sdk::sequencer::SequencerCheckpoint;
|
||||
use logos_blockchain_zone_sdk::{
|
||||
@@ -109,6 +109,13 @@ enum Command {
|
||||
new_keys: Keys,
|
||||
resp: oneshot::Sender<Result<()>>,
|
||||
},
|
||||
/// Hand zone-sdk a pre-built tx to track and post, keyed by the channel tip
|
||||
/// it leaves behind.
|
||||
SubmitSignedTx {
|
||||
tx: Box<SignedMantleTx<Unverified>>,
|
||||
msg_id: MsgId,
|
||||
resp: oneshot::Sender<Result<PublishOutcome>>,
|
||||
},
|
||||
}
|
||||
|
||||
type CommandSender = mpsc::Sender<Command>;
|
||||
@@ -141,6 +148,15 @@ pub trait BlockPublisherTrait: Sized + Sync {
|
||||
withdrawals: Vec<WithdrawArg>,
|
||||
) -> impl Future<Output = Result<PublishOutcome>> + Send + 'blk;
|
||||
|
||||
/// Create the channel and write `block` into it in one Mantle tx. Only valid
|
||||
/// while the channel does not exist, and `keys[0]` must be this sequencer's
|
||||
/// own key, since creation hands the first turn to index 0.
|
||||
async fn publish_genesis_creating_channel(
|
||||
&self,
|
||||
block: &Block,
|
||||
keys: Vec<Ed25519PublicKey>,
|
||||
) -> Result<PublishOutcome>;
|
||||
|
||||
/// Live (adopted, possibly not yet finalized) accredited-key snapshot for
|
||||
/// this channel, read directly from the connected Bedrock node.
|
||||
fn accredited_keys(&self) -> impl Future<Output = Result<Vec<Ed25519PublicKey>>> + Send;
|
||||
@@ -199,19 +215,31 @@ pub struct ZoneSdkPublisher {
|
||||
// path wait until it has actually stopped.
|
||||
drive_task: TaskGroup,
|
||||
indexer: ZoneIndexer<NodeHttpClient>,
|
||||
bedrock_signing_key: Ed25519Key,
|
||||
funding_key: ZkPublicKey,
|
||||
priority_fee: u64,
|
||||
}
|
||||
|
||||
impl ZoneSdkPublisher {
|
||||
/// Runs one [`Command`] on the drive task and waits for its reply.
|
||||
async fn dispatch<T>(
|
||||
&self,
|
||||
command: impl FnOnce(oneshot::Sender<Result<T>>) -> Command,
|
||||
) -> Result<T> {
|
||||
let (resp_tx, resp_rx) = oneshot::channel();
|
||||
self.command_tx
|
||||
.send(command(resp_tx))
|
||||
.await
|
||||
.map_err(|_closed| anyhow!("Drive task is no longer running"))?;
|
||||
resp_rx
|
||||
.await
|
||||
.map_err(|_closed| anyhow!("Drive task dropped the response"))?
|
||||
}
|
||||
}
|
||||
|
||||
impl BlockPublisherTrait for ZoneSdkPublisher {
|
||||
async fn channel_exists(config: &BedrockConfig) -> Result<bool> {
|
||||
let node = NodeHttpClient::new(
|
||||
CommonHttpClient::new(config.auth.clone().map(Into::into)),
|
||||
config.node_url.clone(),
|
||||
);
|
||||
Ok(node
|
||||
.channel_state(config.channel_id)
|
||||
.await
|
||||
.context("Failed to read channel state")?
|
||||
.is_some())
|
||||
Ok(read_channel_state(config).await?.is_some())
|
||||
}
|
||||
|
||||
async fn new(
|
||||
@@ -235,7 +263,7 @@ impl BlockPublisherTrait for ZoneSdkPublisher {
|
||||
|
||||
let mut sequencer = ZoneSequencer::init_with_config(
|
||||
config.channel_id,
|
||||
bedrock_signing_key,
|
||||
bedrock_signing_key.clone(),
|
||||
node.clone(),
|
||||
zone_sdk_config,
|
||||
initial_checkpoint,
|
||||
@@ -334,6 +362,21 @@ impl BlockPublisherTrait for ZoneSdkPublisher {
|
||||
|
||||
let _dontcare = resp_tx.send(result);
|
||||
}
|
||||
Command::SubmitSignedTx { tx, msg_id, resp: resp_tx } => {
|
||||
let submitted = sequencer
|
||||
.handle()
|
||||
.submit_signed_tx(*tx, msg_id)
|
||||
.context("Failed to submit pre-built channel transaction");
|
||||
let msg_result = submitted.map(|(result, checkpoint)| PublishOutcome {
|
||||
this_msg: result.tx.inscription().this_msg,
|
||||
checkpoint,
|
||||
released_notes: released_notes(&result.tx),
|
||||
});
|
||||
if let Err(e) = &msg_result {
|
||||
warn!("zone-sdk rejected the pre-built transaction: {e:?}");
|
||||
}
|
||||
let _dontcare = resp_tx.send(msg_result);
|
||||
}
|
||||
},
|
||||
event = sequencer.next_event() => {
|
||||
match event {
|
||||
@@ -419,6 +462,9 @@ impl BlockPublisherTrait for ZoneSdkPublisher {
|
||||
turn_rx,
|
||||
driver_cancellation,
|
||||
drive_task: TaskGroup::new(vec![drive_task]),
|
||||
bedrock_signing_key,
|
||||
funding_key: config.funding_key,
|
||||
priority_fee: config.priority_fee,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -432,19 +478,88 @@ impl BlockPublisherTrait for ZoneSdkPublisher {
|
||||
.try_into()
|
||||
.context("Block data exceeds maximum allowed size")?;
|
||||
|
||||
let (resp_tx, resp_rx) = oneshot::channel();
|
||||
self.command_tx
|
||||
.send(Command::Publish {
|
||||
inscription: data_bounded,
|
||||
withdrawals,
|
||||
resp: resp_tx,
|
||||
})
|
||||
.await
|
||||
.map_err(|_closed| anyhow!("Drive task is no longer running"))?;
|
||||
self.dispatch(|resp| Command::Publish {
|
||||
inscription: data_bounded,
|
||||
withdrawals,
|
||||
resp,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
resp_rx
|
||||
async fn publish_genesis_creating_channel(
|
||||
&self,
|
||||
block: &Block,
|
||||
keys: Vec<Ed25519PublicKey>,
|
||||
) -> Result<PublishOutcome> {
|
||||
let own_key = self.bedrock_signing_key.public_key();
|
||||
ensure!(
|
||||
keys.first() == Some(&own_key),
|
||||
"Creating the channel requires our own key first; creation gives the turn to index 0"
|
||||
);
|
||||
let key_count = keys.len();
|
||||
let keys =
|
||||
Keys::try_from(keys).map_err(|err| anyhow!("Invalid channel key list: {err}"))?;
|
||||
|
||||
let config_op = ChannelConfigOp {
|
||||
channel: self.channel_id,
|
||||
keys,
|
||||
posting_timeframe: SlotTimeframe::from(
|
||||
system_accounts::DEFAULT_SEQUENCER_POSTING_TIMEFRAME,
|
||||
),
|
||||
posting_timeout: SlotTimeout::from(system_accounts::DEFAULT_SEQUENCER_POSTING_TIMEOUT),
|
||||
configuration_threshold: system_accounts::DEFAULT_SEQUENCER_CONFIGURATION_THRESHOLD,
|
||||
transfer_threshold: system_accounts::DEFAULT_SEQUENCER_WITHDRAW_THRESHOLD,
|
||||
};
|
||||
|
||||
let data = borsh::to_vec(block).context("Failed to serialize genesis block")?;
|
||||
let inscription: Inscription = data
|
||||
.try_into()
|
||||
.context("Genesis block exceeds maximum allowed size")?;
|
||||
// The config op runs first and becomes the tip, so it is the parent.
|
||||
let inscribe_op = InscriptionOp {
|
||||
channel_id: self.channel_id,
|
||||
inscription,
|
||||
parent: config_op.id(),
|
||||
signer: own_key,
|
||||
};
|
||||
let msg_id = inscribe_op.id();
|
||||
|
||||
let funded = fund_ops(
|
||||
&self.node,
|
||||
self.funding_key,
|
||||
self.priority_fee,
|
||||
[
|
||||
Op::ChannelConfig(config_op),
|
||||
Op::ChannelInscribe(inscribe_op),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
let mantle_tx = funded.funded_tx;
|
||||
|
||||
let signature = self
|
||||
.bedrock_signing_key
|
||||
.sign_payload(mantle_tx.hash().as_signing_bytes().as_ref());
|
||||
// Creation skips the channel-config signature check, but the proof must
|
||||
// still be well formed; index 0 is our own key.
|
||||
let config_proof =
|
||||
ChannelMultiSigProof::try_new(IndexedSignature::new(0, signature).into())
|
||||
.map_err(|err| anyhow!("Failed to assemble channel multi-sig proof: {err:?}"))?;
|
||||
|
||||
let mut ops_proofs: OpsProofs = OpProof::ChannelMultiSigProof(config_proof).into();
|
||||
ops_proofs
|
||||
.try_push(OpProof::Ed25519Sig(signature))
|
||||
.map_err(|err| anyhow!("Too many operation proofs: {err:?}"))?;
|
||||
if let Some(transfer_proof) = funded.transfer_proof {
|
||||
ops_proofs
|
||||
.try_push(transfer_proof)
|
||||
.map_err(|err| anyhow!("Too many operation proofs: {err:?}"))?;
|
||||
}
|
||||
|
||||
info!("Creating the channel with {key_count} accredited key(s), genesis block bundled");
|
||||
|
||||
let tx = Box::new(SignedMantleTx::new(mantle_tx, ops_proofs));
|
||||
self.dispatch(|resp| Command::SubmitSignedTx { tx, msg_id, resp })
|
||||
.await
|
||||
.map_err(|_closed| anyhow!("Drive task dropped the publish response"))?
|
||||
}
|
||||
|
||||
async fn accredited_keys(&self) -> Result<Vec<Ed25519PublicKey>> {
|
||||
@@ -465,18 +580,8 @@ impl BlockPublisherTrait for ZoneSdkPublisher {
|
||||
let new_keys =
|
||||
Keys::try_from(new_keys).map_err(|err| anyhow!("Invalid channel key list: {err}"))?;
|
||||
|
||||
let (resp_tx, resp_rx) = oneshot::channel();
|
||||
self.command_tx
|
||||
.send(Command::SubmitChannelConfig {
|
||||
new_keys,
|
||||
resp: resp_tx,
|
||||
})
|
||||
self.dispatch(|resp| Command::SubmitChannelConfig { new_keys, resp })
|
||||
.await
|
||||
.map_err(|_closed| anyhow!("Drive task is no longer running"))?;
|
||||
|
||||
resp_rx
|
||||
.await
|
||||
.map_err(|_closed| anyhow!("Drive task dropped the submit response"))?
|
||||
}
|
||||
|
||||
fn channel_id(&self) -> ChannelId {
|
||||
@@ -550,6 +655,41 @@ const fn channel_update_inscription(orphan: &ChannelUpdateTx) -> Option<&Inscrip
|
||||
}
|
||||
}
|
||||
|
||||
/// Funds `ops` from the node's wallet, which appends a fee transfer (paid from
|
||||
/// `funding_key`, change back to it) and returns its proof.
|
||||
async fn fund_ops(
|
||||
node: &NodeHttpClient,
|
||||
funding_key: ZkPublicKey,
|
||||
priority_fee: u64,
|
||||
ops: impl IntoIterator<Item = Op>,
|
||||
) -> Result<logos_blockchain_http_api_common::bodies::wallet::fund::WalletFundResponseBody> {
|
||||
let tx_builder = MantleTxBuilder::new()
|
||||
.extend_ops(ops)
|
||||
.map_err(|err| anyhow!("Too many ops in channel transaction: {err:?}"))?;
|
||||
node.fund_tx(WalletFundRequestBody {
|
||||
tip: None,
|
||||
tx_builder,
|
||||
change_public_key: funding_key,
|
||||
funding_public_keys: vec![funding_key],
|
||||
max_tx_fee: GasCost::new(logos_blockchain_core::mantle::Value::MAX),
|
||||
priority_fee,
|
||||
})
|
||||
.await
|
||||
.context("Failed to fund channel transaction")
|
||||
}
|
||||
|
||||
/// Reads the channel's committee state from the bedrock node, without a running
|
||||
/// sequencer. `None` means the channel does not exist yet.
|
||||
pub async fn read_channel_state(config: &BedrockConfig) -> Result<Option<ChannelState>> {
|
||||
let node = NodeHttpClient::new(
|
||||
CommonHttpClient::new(config.auth.clone().map(Into::into)),
|
||||
config.node_url.clone(),
|
||||
);
|
||||
node.channel_state(config.channel_id)
|
||||
.await
|
||||
.context("Failed to read channel state")
|
||||
}
|
||||
|
||||
/// Signs a `ChannelConfig` op (accredited keys + rotation params) with
|
||||
/// `signing_key`, funds it from `config.funding_key` via the node's wallet,
|
||||
/// and posts it straight to the bedrock node.
|
||||
@@ -599,22 +739,13 @@ pub async fn post_channel_config(
|
||||
config.node_url.clone(),
|
||||
);
|
||||
|
||||
// Fund the op from the node's wallet: the node appends a fee transfer
|
||||
// (paid from `funding_key`, change back to it) and returns its proof.
|
||||
let tx_builder = MantleTxBuilder::new()
|
||||
.extend_ops([Op::ChannelConfig(config_op)])
|
||||
.map_err(|err| anyhow!("Too many ops in channel config transaction: {err:?}"))?;
|
||||
let funded = node
|
||||
.fund_tx(WalletFundRequestBody {
|
||||
tip: None,
|
||||
tx_builder,
|
||||
change_public_key: config.funding_key,
|
||||
funding_public_keys: vec![config.funding_key],
|
||||
max_tx_fee: GasCost::new(logos_blockchain_core::mantle::Value::MAX),
|
||||
priority_fee: config.priority_fee,
|
||||
})
|
||||
.await
|
||||
.context("Failed to fund channel config transaction")?;
|
||||
let funded = fund_ops(
|
||||
&node,
|
||||
config.funding_key,
|
||||
config.priority_fee,
|
||||
[Op::ChannelConfig(config_op)],
|
||||
)
|
||||
.await?;
|
||||
let mantle_tx = funded.funded_tx;
|
||||
|
||||
// Sign the funded tx: the appended fee transfer changes the hash.
|
||||
|
||||
@@ -11,7 +11,7 @@ use bytesize::ByteSize;
|
||||
use common::config::BasicAuth;
|
||||
pub use cross_zone_inbox_core::{CrossZoneConfig, CrossZonePeer, CrossZoneRoute};
|
||||
use humantime_serde;
|
||||
use lee::{AccountId, Balance};
|
||||
use lee::{AccountId, Balance, PublicKey, Signature};
|
||||
use logos_blockchain_core::mantle::ops::channel::ChannelId;
|
||||
use logos_blockchain_key_management_system_service::keys::ZkPublicKey;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -33,6 +33,12 @@ pub enum GenesisAction {
|
||||
holder: AccountId,
|
||||
amount: Balance,
|
||||
},
|
||||
/// Stakes `sequencer_key` at genesis.
|
||||
StakeSequencer {
|
||||
sequencer_key: sequencer_stake_core::SequencerKey,
|
||||
ownership_public_key: PublicKey,
|
||||
stake_signature: Signature,
|
||||
},
|
||||
}
|
||||
|
||||
/// Sequencer p2p gossip configuration. Absent (`None`) disables gossip
|
||||
|
||||
+188
-49
@@ -87,6 +87,13 @@ const MAX_DISPATCHES_PER_BLOCK: usize = 16;
|
||||
// once that path exists, instead of a fixed genesis-only key.
|
||||
const GENESIS_STAKE_FUNDING_KEY: [u8; 32] = [9; 32];
|
||||
|
||||
/// A founding sequencer's key, plus the ownership account attesting to its stake.
|
||||
type FoundingStake = (
|
||||
sequencer_stake_core::SequencerKey,
|
||||
lee::PublicKey,
|
||||
lee::Signature,
|
||||
);
|
||||
|
||||
/// The origin of a transaction.
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum TransactionOrigin {
|
||||
@@ -368,17 +375,27 @@ impl<BP: BlockPublisherTrait> SequencerCore<BP> {
|
||||
"First pending block on fresh start should be the genesis block"
|
||||
);
|
||||
|
||||
// The channel is born holding only its creator's key, so a configured
|
||||
// founding set is applied by the same tx that writes genesis; the
|
||||
// committee is never observable without it.
|
||||
let founding_committee = founding_committee(&config, own_sequencer_key);
|
||||
|
||||
let mut last_checkpoint = None;
|
||||
for block in &pending_blocks {
|
||||
let outcome = 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 publish = match &founding_committee {
|
||||
Some(keys) if block.header.block_id == GENESIS_BLOCK_ID => {
|
||||
block_publisher
|
||||
.publish_genesis_creating_channel(block, keys.clone())
|
||||
.await
|
||||
}
|
||||
_ => block_publisher.publish_block(block, vec![]).await,
|
||||
};
|
||||
let outcome = publish.unwrap_or_else(|err| {
|
||||
panic!(
|
||||
"Failed to publish block {} on fresh start: {err:#}",
|
||||
block.header.block_id
|
||||
)
|
||||
});
|
||||
last_checkpoint = Some(outcome.checkpoint);
|
||||
store
|
||||
.raise_published_high_water(block.header.block_id)
|
||||
@@ -1720,13 +1737,24 @@ fn build_genesis_state(
|
||||
Some(build_supply_bridge_account_genesis_transaction(*balance))
|
||||
}
|
||||
// Seeded directly in `build_initial_state` (holdings via `build_holding_account`), not a
|
||||
// genesis tx.
|
||||
GenesisAction::SupplyBridgeLockHolding { .. } => None,
|
||||
});
|
||||
let bootstrap_stake_txs = bootstrap_sequencer_key.into_iter().flat_map(|key| {
|
||||
build_bootstrap_stake_genesis_transactions(key, config.sequencer_stake_signing_key)
|
||||
// genesis tx. Stakes are built separately below.
|
||||
GenesisAction::SupplyBridgeLockHolding { .. } | GenesisAction::StakeSequencer { .. } => {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
// The creator falls back to staking itself, signing with the key it owns.
|
||||
let mut staked = founding_stakes(config);
|
||||
if staked.is_empty() {
|
||||
staked.extend(bootstrap_sequencer_key.map(|key| {
|
||||
let owner = lee::PrivateKey::try_new(config.sequencer_stake_signing_key)
|
||||
.expect("sequencer stake signing key is a valid private key");
|
||||
let signature = sign_genesis_stake(0, key, &owner);
|
||||
(key, lee::PublicKey::new_from_private_key(&owner), signature)
|
||||
}));
|
||||
}
|
||||
let bootstrap_stake_txs = build_stake_genesis_transactions(&staked);
|
||||
|
||||
let genesis_txs = wrapped_token_config_tx
|
||||
.chain(ping_sender_config_tx)
|
||||
.chain(ping_receiver_config_tx)
|
||||
@@ -1746,45 +1774,91 @@ fn build_genesis_state(
|
||||
(state, genesis_txs)
|
||||
}
|
||||
|
||||
/// The bootstrap sequencer's own `Stake`, funded via the faucet and signed
|
||||
/// with `stake_signing_key` so a later top-up/unstake use the same account.
|
||||
/// Real transactions, not raw state, so followers replay them instead of
|
||||
/// missing them.
|
||||
fn build_bootstrap_stake_genesis_transactions(
|
||||
sequencer_key: sequencer_stake_core::SequencerKey,
|
||||
stake_signing_key: [u8; 32],
|
||||
) -> [PublicTransaction; 2] {
|
||||
let stake_key = lee::PrivateKey::try_new(stake_signing_key).unwrap();
|
||||
let ownership_id = AccountId::from(&lee::PublicKey::new_from_private_key(&stake_key));
|
||||
let funding_key = lee::PrivateKey::try_new(GENESIS_STAKE_FUNDING_KEY).unwrap();
|
||||
let funding_id = AccountId::from(&lee::PublicKey::new_from_private_key(&funding_key));
|
||||
let amount = system_accounts::DEFAULT_MINIMUM_SEQUENCER_STAKE;
|
||||
fn founding_stakes(config: &SequencerConfig) -> Vec<FoundingStake> {
|
||||
config
|
||||
.genesis
|
||||
.iter()
|
||||
.filter_map(|action| match action {
|
||||
GenesisAction::StakeSequencer {
|
||||
sequencer_key,
|
||||
ownership_public_key,
|
||||
stake_signature,
|
||||
} => Some((
|
||||
*sequencer_key,
|
||||
ownership_public_key.clone(),
|
||||
stake_signature.clone(),
|
||||
)),
|
||||
GenesisAction::SupplyAccount { .. }
|
||||
| GenesisAction::SupplyBridgeAccount { .. }
|
||||
| GenesisAction::SupplyBridgeLockHolding { .. } => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
let fund_message = Message::try_new(
|
||||
programs::faucet().id(),
|
||||
vec![system_accounts::faucet_account_id(), funding_id],
|
||||
vec![lee_core::account::Nonce(0)],
|
||||
faucet_core::Instruction::GenesisTransferDirect { amount },
|
||||
/// The accredited keys a newly created channel should carry, `own_key` first
|
||||
/// because creation gives the turn to index 0. `None` leaves creation to the
|
||||
/// plain inscription path.
|
||||
fn founding_committee(
|
||||
config: &SequencerConfig,
|
||||
own_key: sequencer_stake_core::SequencerKey,
|
||||
) -> Option<Vec<block_publisher::Ed25519PublicKey>> {
|
||||
let mut keys: Vec<_> = founding_stakes(config)
|
||||
.into_iter()
|
||||
.map(|(key, ..)| key)
|
||||
.collect();
|
||||
if keys.is_empty() {
|
||||
return None;
|
||||
}
|
||||
keys.sort_unstable();
|
||||
keys.retain(|key| *key != own_key);
|
||||
|
||||
Some(
|
||||
std::iter::once(own_key)
|
||||
.chain(keys)
|
||||
.map(|key| {
|
||||
block_publisher::Ed25519PublicKey::from_bytes(&key)
|
||||
.expect("sequencer key was decoded from a valid Ed25519 public key")
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
.expect("Failed to build genesis funding message");
|
||||
let fund_witness_set =
|
||||
lee::public_transaction::WitnessSet::for_message(&fund_message, &[&funding_key]);
|
||||
let fund_tx = PublicTransaction::new(fund_message, fund_witness_set);
|
||||
}
|
||||
|
||||
fn genesis_stake_funding_account() -> AccountId {
|
||||
let key = lee::PrivateKey::try_new(GENESIS_STAKE_FUNDING_KEY)
|
||||
.expect("GENESIS_STAKE_FUNDING_KEY is a valid private key");
|
||||
AccountId::from(&lee::PublicKey::new_from_private_key(&key))
|
||||
}
|
||||
|
||||
/// The exact `Stake` message the founding sequencer at `index` must sign. Shared
|
||||
/// offchain by the genesis sequencer.
|
||||
fn genesis_stake_message(
|
||||
index: usize,
|
||||
sequencer_key: sequencer_stake_core::SequencerKey,
|
||||
ownership_id: AccountId,
|
||||
) -> Message {
|
||||
let amount = system_accounts::DEFAULT_MINIMUM_SEQUENCER_STAKE;
|
||||
let mover_instruction_data = lee::program::Program::serialize_instruction(
|
||||
authenticated_transfer_core::Instruction::Transfer { amount },
|
||||
)
|
||||
.expect("Failed to serialize genesis mover instruction");
|
||||
let stake_message = Message::try_new(
|
||||
// A nonce counts how many times an account has signed. The funding account
|
||||
// signed the faucet tx already, so its count starts at 1 here.
|
||||
let funding_nonce = u128::try_from(index)
|
||||
.expect("founding sequencer count fits in u128")
|
||||
.checked_add(1)
|
||||
.expect("genesis funding nonce overflow");
|
||||
|
||||
Message::try_new(
|
||||
programs::sequencer_stake().id(),
|
||||
vec![
|
||||
funding_id,
|
||||
genesis_stake_funding_account(),
|
||||
ownership_id,
|
||||
system_accounts::sequencer_stake_config_account_id(),
|
||||
],
|
||||
// funding_key already signed fund_tx above, so it's at nonce 1; stake_key
|
||||
// signs for the first time here, still at nonce 0.
|
||||
vec![lee_core::account::Nonce(1), lee_core::account::Nonce(0)],
|
||||
vec![
|
||||
lee_core::account::Nonce(funding_nonce),
|
||||
lee_core::account::Nonce(0),
|
||||
],
|
||||
sequencer_stake_core::Instruction::Stake {
|
||||
sequencer_key,
|
||||
amount,
|
||||
@@ -1792,14 +1866,77 @@ fn build_bootstrap_stake_genesis_transactions(
|
||||
mover_instruction_data,
|
||||
},
|
||||
)
|
||||
.expect("Failed to build genesis Stake message");
|
||||
let stake_witness_set = lee::public_transaction::WitnessSet::for_message(
|
||||
&stake_message,
|
||||
&[&funding_key, &stake_key],
|
||||
);
|
||||
let stake_tx = PublicTransaction::new(stake_message, stake_witness_set);
|
||||
.expect("Failed to build genesis Stake message")
|
||||
}
|
||||
|
||||
[fund_tx, stake_tx]
|
||||
/// Signs the founding sequencer at `index`'s genesis `Stake`, for an operator
|
||||
/// producing their `GenesisAction::StakeSequencer` entry.
|
||||
#[must_use]
|
||||
pub fn sign_genesis_stake(
|
||||
index: usize,
|
||||
sequencer_key: sequencer_stake_core::SequencerKey,
|
||||
ownership_key: &lee::PrivateKey,
|
||||
) -> lee::Signature {
|
||||
let ownership_id = AccountId::from(&lee::PublicKey::new_from_private_key(ownership_key));
|
||||
let message = genesis_stake_message(index, sequencer_key, ownership_id);
|
||||
lee::Signature::new(ownership_key, &message.hash())
|
||||
}
|
||||
|
||||
/// The founding sequencers' `Stake`s, funded via the faucet. Real transactions,
|
||||
/// not raw state, so followers replay them instead of missing them.
|
||||
fn build_stake_genesis_transactions(staked: &[FoundingStake]) -> Vec<PublicTransaction> {
|
||||
if staked.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let funding_key = lee::PrivateKey::try_new(GENESIS_STAKE_FUNDING_KEY).unwrap();
|
||||
let funding_public_key = lee::PublicKey::new_from_private_key(&funding_key);
|
||||
let amount = system_accounts::DEFAULT_MINIMUM_SEQUENCER_STAKE;
|
||||
let total = u128::try_from(staked.len())
|
||||
.ok()
|
||||
.and_then(|count| amount.checked_mul(count))
|
||||
.expect("genesis stake total overflow");
|
||||
|
||||
let fund_message = Message::try_new(
|
||||
programs::faucet().id(),
|
||||
vec![
|
||||
system_accounts::faucet_account_id(),
|
||||
genesis_stake_funding_account(),
|
||||
],
|
||||
vec![lee_core::account::Nonce(0)],
|
||||
faucet_core::Instruction::GenesisTransferDirect { amount: total },
|
||||
)
|
||||
.expect("Failed to build genesis funding message");
|
||||
// The funding account signs even though it is only receiving. It is a brand
|
||||
// new account, so the transfer claims it, and a claim needs that account's
|
||||
// own signature.
|
||||
let fund_witness_set =
|
||||
lee::public_transaction::WitnessSet::for_message(&fund_message, &[&funding_key]);
|
||||
|
||||
let mut txs = vec![PublicTransaction::new(fund_message, fund_witness_set)];
|
||||
|
||||
for (index, (sequencer_key, ownership_public_key, signature)) in staked.iter().enumerate() {
|
||||
let ownership_id = AccountId::from(ownership_public_key);
|
||||
let stake_message = genesis_stake_message(index, *sequencer_key, ownership_id);
|
||||
let stake_witness_set = lee::public_transaction::WitnessSet::from_raw_parts(vec![
|
||||
(
|
||||
lee::Signature::new(&funding_key, &stake_message.hash()),
|
||||
funding_public_key.clone(),
|
||||
),
|
||||
(signature.clone(), ownership_public_key.clone()),
|
||||
]);
|
||||
|
||||
// Redundant with the signature check every tx gets, but names the entry.
|
||||
assert!(
|
||||
stake_witness_set.is_valid_for(&stake_message),
|
||||
"genesis stake signature does not match founding sequencer {index} ({})",
|
||||
hex::encode(sequencer_key)
|
||||
);
|
||||
|
||||
txs.push(PublicTransaction::new(stake_message, stake_witness_set));
|
||||
}
|
||||
|
||||
txs
|
||||
}
|
||||
|
||||
/// Bridge-lock holder balances configured for this zone's genesis.
|
||||
@@ -1808,7 +1945,9 @@ fn bridge_lock_holdings(
|
||||
) -> impl Iterator<Item = (lee::AccountId, lee::Balance)> + '_ {
|
||||
genesis.iter().filter_map(|action| match action {
|
||||
GenesisAction::SupplyBridgeLockHolding { holder, amount } => Some((*holder, *amount)),
|
||||
GenesisAction::SupplyAccount { .. } | GenesisAction::SupplyBridgeAccount { .. } => None,
|
||||
GenesisAction::SupplyAccount { .. }
|
||||
| GenesisAction::SupplyBridgeAccount { .. }
|
||||
| GenesisAction::StakeSequencer { .. } => None,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -90,6 +90,14 @@ impl BlockPublisherTrait for MockBlockPublisher {
|
||||
})
|
||||
}
|
||||
|
||||
async fn publish_genesis_creating_channel(
|
||||
&self,
|
||||
block: &Block,
|
||||
_keys: Vec<Ed25519PublicKey>,
|
||||
) -> Result<PublishOutcome> {
|
||||
self.publish_block(block, Vec::new()).await
|
||||
}
|
||||
|
||||
async fn accredited_keys(&self) -> Result<Vec<Ed25519PublicKey>> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
@@ -229,6 +229,7 @@ fn initial_programs() -> Vec<Program> {
|
||||
programs::vault(),
|
||||
programs::faucet(),
|
||||
programs::bridge(),
|
||||
programs::sequencer_stake(),
|
||||
// Cross-zone programs are builtins: their bytecode is baked into every node,
|
||||
// so registering them in the base state (rather than shipping ELFs through
|
||||
// the genesis block, which exceeds the inscription size limit) keeps the two
|
||||
@@ -239,7 +240,6 @@ fn initial_programs() -> Vec<Program> {
|
||||
programs::ping_receiver(),
|
||||
programs::bridge_lock(),
|
||||
programs::wrapped_token(),
|
||||
programs::sequencer_stake(),
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -1043,6 +1043,12 @@ pub async fn verify_commitment_is_in_state(
|
||||
.is_some()
|
||||
}
|
||||
|
||||
/// Initializes the global logger once, for tests that build their fixtures
|
||||
/// without going through [`TestContextBuilder`].
|
||||
pub fn init_logger() {
|
||||
*LOGGER;
|
||||
}
|
||||
|
||||
fn dir_size_bytes(path: &Path) -> u64 {
|
||||
let mut total = 0_u64;
|
||||
let Ok(entries) = std::fs::read_dir(path) else {
|
||||
|
||||
Reference in New Issue
Block a user