mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-08-25 03:11:21 +00:00
214 lines
7.3 KiB
Rust
214 lines
7.3 KiB
Rust
#![expect(
|
|
clippy::tests_outside_test_module,
|
|
reason = "top-level test functions are conventional for integration tests"
|
|
)]
|
|
|
|
//! B unstakes in full, leaves the committee, gets its stake back, then rejoins.
|
|
|
|
use std::time::Duration;
|
|
|
|
use anyhow::{Context as _, Result, ensure};
|
|
use integration_tests::{
|
|
account_balance, assert_same_chain, committee, get_account, init_logger, wait_until,
|
|
};
|
|
use lee::{AccountId, PublicKey, program::Program};
|
|
use log::info;
|
|
use logos_blockchain_key_management_system_service::keys::Ed25519Key;
|
|
use sequencer_core::config::BedrockConfig;
|
|
use sequencer_service_rpc::RpcClient as _;
|
|
use test_fixtures::{
|
|
MultiZoneTestContextBuilder, TestContext, ZoneTestContextBuilder,
|
|
config::{self, MultiNodeTestContextConfig, SequencerPartialConfig},
|
|
};
|
|
use tokio::test;
|
|
use wallet::AccountIdentity;
|
|
|
|
/// What genesis stakes each founding sequencer.
|
|
const STAKE: u128 = system_accounts::DEFAULT_MINIMUM_SEQUENCER_STAKE;
|
|
|
|
#[test]
|
|
async fn a_sequencer_leaves_the_committee_and_rejoins() -> Result<()> {
|
|
init_logger();
|
|
|
|
let channel = config::bedrock_channel_id();
|
|
let partial = SequencerPartialConfig {
|
|
block_create_timeout: Duration::from_secs(2),
|
|
..SequencerPartialConfig::default()
|
|
};
|
|
|
|
let mut ctx = MultiZoneTestContextBuilder::default()
|
|
.with_zone(
|
|
ZoneTestContextBuilder::new(MultiNodeTestContextConfig {
|
|
num_nodes: 2,
|
|
bedrock_channel: channel,
|
|
})
|
|
.with_sequencer_partial_config(partial),
|
|
)
|
|
.build()
|
|
.await
|
|
.context("Failed to build the two-sequencer test context")?;
|
|
|
|
let key_b = Ed25519Key::from_bytes(&config::sequencer_signing_key_from_seed(1)).public_key();
|
|
let stake_key_b = sequencer_stake_core::SequencerKey::new(key_b.to_bytes())
|
|
.context("Sequencer B's Bedrock key is not a valid Ed25519 point")?;
|
|
|
|
let bedrock_config = BedrockConfig {
|
|
channel_id: channel,
|
|
node_url: config::addr_to_url(config::UrlProtocol::Http, ctx.bedrock_addr())?,
|
|
funding_key: config::bedrock_funding_key(),
|
|
auth: None,
|
|
priority_fee: sequencer_core::config::default_priority_fee(),
|
|
};
|
|
|
|
// B's genesis stake sits on an account only this key can sign for.
|
|
let owner_b = config::founding_stake_owner_key(1)?;
|
|
let ownership_b = AccountId::from(&PublicKey::new_from_private_key(&owner_b));
|
|
ctx.wallet_mut()
|
|
.storage_mut()
|
|
.key_chain_mut()
|
|
.add_imported_public_account(owner_b);
|
|
|
|
let config_id = system_accounts::sequencer_stake_config_account_id();
|
|
|
|
let settlement = AccountId::from(&PublicKey::new_from_private_key(
|
|
&config::default_public_accounts_for_wallet()[0].0,
|
|
));
|
|
|
|
wait_until("both staked keys to be accredited", || async {
|
|
Ok(committee(&bedrock_config)
|
|
.await?
|
|
.0
|
|
.contains(&key_b.to_bytes()))
|
|
})
|
|
.await?;
|
|
info!("Both sequencers accredited from channel creation");
|
|
|
|
// B leaves.
|
|
let settlement_before = account_balance(&ctx, settlement).await?;
|
|
send_stake_tx(
|
|
&ctx,
|
|
vec![
|
|
AccountIdentity::Public(ownership_b),
|
|
AccountIdentity::PublicNoSign(config_id),
|
|
],
|
|
&sequencer_stake_core::Instruction::UnstakeRequest {
|
|
amount: STAKE,
|
|
destination: settlement,
|
|
},
|
|
)
|
|
.await
|
|
.context("Failed to submit B's UnstakeRequest")?;
|
|
info!("B requested a full unstake");
|
|
|
|
wait_until("B to leave the committee", || async {
|
|
Ok(!committee(&bedrock_config)
|
|
.await?
|
|
.0
|
|
.contains(&key_b.to_bytes()))
|
|
})
|
|
.await?;
|
|
info!("B removed from the Bedrock committee");
|
|
|
|
wait_until("B's stake to be released", || async {
|
|
Ok(get_account(&ctx, ownership_b).await?.balance == 0)
|
|
})
|
|
.await?;
|
|
ensure!(
|
|
account_balance(&ctx, settlement).await? == settlement_before + STAKE,
|
|
"the released stake should have reached the settlement account"
|
|
);
|
|
ensure!(
|
|
stake_entry(&ctx, stake_key_b).await?.is_none(),
|
|
"a fully released key should have no config entry left"
|
|
);
|
|
info!("B's stake released in full");
|
|
|
|
// B rejoins on the same ownership account, which stays claimed after an exit.
|
|
let mover_instruction_data =
|
|
Program::serialize_instruction(authenticated_transfer_core::Instruction::Transfer {
|
|
amount: STAKE,
|
|
})
|
|
.context("Failed to serialize the mover instruction")?;
|
|
send_stake_tx(
|
|
&ctx,
|
|
vec![
|
|
AccountIdentity::Public(settlement),
|
|
AccountIdentity::Public(ownership_b),
|
|
AccountIdentity::PublicNoSign(config_id),
|
|
],
|
|
&sequencer_stake_core::Instruction::Stake {
|
|
sequencer_key: stake_key_b,
|
|
amount: STAKE,
|
|
mover_program_id: programs::authenticated_transfer().id(),
|
|
mover_instruction_data,
|
|
},
|
|
)
|
|
.await
|
|
.context("Failed to submit B's re-stake")?;
|
|
|
|
wait_until("B's re-stake to land", || async {
|
|
Ok(get_account(&ctx, ownership_b).await?.balance == STAKE)
|
|
})
|
|
.await?;
|
|
info!("B staked again");
|
|
|
|
wait_until("B to be accredited again", || async {
|
|
Ok(committee(&bedrock_config)
|
|
.await?
|
|
.0
|
|
.contains(&key_b.to_bytes()))
|
|
})
|
|
.await?;
|
|
info!("B back in the Bedrock committee");
|
|
|
|
// Rejoining is only real if B writes to the channel again.
|
|
wait_until("the round-robin turn to reach B again", || async {
|
|
Ok(committee(&bedrock_config).await?.1 == Some(key_b))
|
|
})
|
|
.await?;
|
|
|
|
let a = ctx
|
|
.sequencer_client_by_node_ids(channel, 0)
|
|
.context("Missing sequencer A")?;
|
|
let b = ctx
|
|
.sequencer_client_by_node_ids(channel, 1)
|
|
.context("Missing sequencer B")?;
|
|
let resumed_at = a.get_last_block_id().await?;
|
|
wait_until("the chain to advance past B's rejoin", || async {
|
|
Ok(a.get_last_block_id().await? > resumed_at)
|
|
})
|
|
.await?;
|
|
assert_same_chain(a, b).await?;
|
|
info!("B produces again and both sequencers agree on the chain");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Sends `instruction` to `sequencer_stake` over `accounts`.
|
|
async fn send_stake_tx(
|
|
ctx: &TestContext,
|
|
accounts: Vec<AccountIdentity>,
|
|
instruction: &sequencer_stake_core::Instruction,
|
|
) -> Result<()> {
|
|
let data = Program::serialize_instruction(instruction.clone())
|
|
.context("Failed to serialize the sequencer_stake instruction")?;
|
|
ctx.wallet()
|
|
.send_pub_tx(accounts, data, programs::sequencer_stake().id())
|
|
.await
|
|
.map_err(|err| anyhow::anyhow!("Failed to submit sequencer_stake transaction: {err:?}"))?;
|
|
Ok(())
|
|
}
|
|
|
|
/// The `sequencer_stake` config entry for `sequencer_key`, if any.
|
|
async fn stake_entry(
|
|
ctx: &TestContext,
|
|
sequencer_key: sequencer_stake_core::SequencerKey,
|
|
) -> Result<Option<sequencer_stake_core::SequencerEntry>> {
|
|
let account = get_account(ctx, system_accounts::sequencer_stake_config_account_id())
|
|
.await
|
|
.context("Failed to read the sequencer_stake config account")?;
|
|
let config = sequencer_stake_core::SequencerStakeConfig::from_bytes(account.data.as_ref())
|
|
.context("config account data did not decode as a SequencerStakeConfig")?;
|
|
Ok(config.entries.get(&sequencer_key).copied())
|
|
}
|