use std::{
collections::{BTreeMap, BTreeSet},
sync::Arc,
time::Duration,
};
use linera_base::{
crypto::InMemorySigner,
data_types::{Amount, Round, TimeDelta},
identifiers::{AccountOwner, ChainId},
ownership::{ChainOwnership, TimeoutConfig},
};
use linera_core::{
client::{chain_client, Client, ListeningMode},
environment,
join_set_ext::JoinSet,
test_utils::{FaultType, MemoryStorageBuilder, TestBuilder},
worker::{DEFAULT_BLOCK_CACHE_SIZE, DEFAULT_EXECUTION_STATE_CACHE_SIZE},
};
use linera_rpc::node_provider::DEFAULT_MAX_BACKOFF;
use crate::{client_context::ClientContext, config::GenesisConfig};
async fn make_context(
builder: &mut TestBuilder<MemoryStorageBuilder>,
signer: InMemorySigner,
chain_id: ChainId,
) -> anyhow::Result<ClientContext<environment::Test>> {
let genesis_config = GenesisConfig::new_for_testing(builder);
let admin_chain_id = genesis_config.admin_chain_id();
let storage = builder.make_storage().await?;
let client = Client::new(
environment::Impl {
storage,
network: builder.make_node_provider(),
signer,
wallet: environment::TestWallet::default(),
},
admin_chain_id,
false,
[(chain_id, ListeningMode::FullChain)],
format!("Client node for {chain_id:.8}"),
Some(Duration::from_secs(30)),
Some(Duration::from_secs(1)),
1000,
chain_client::Options::test_default(),
&linera_core::client::RequestsSchedulerConfig::default(),
DEFAULT_BLOCK_CACHE_SIZE,
DEFAULT_EXECUTION_STATE_CACHE_SIZE,
);
Ok(ClientContext {
client: Arc::new(client),
genesis_config,
send_timeout: Duration::from_secs(4),
recv_timeout: Duration::from_secs(4),
retry_delay: Duration::from_secs(1),
max_retries: 10,
max_backoff: DEFAULT_MAX_BACKOFF,
chain_listeners: JoinSet::default(),
default_chain: None,
client_metrics: None,
})
}
#[test_log::test(tokio::test)]
async fn test_wallet_persists_fast_pending_proposal() -> anyhow::Result<()> {
let signer = InMemorySigner::new(None);
let mut builder =
TestBuilder::new(MemoryStorageBuilder::default(), 4, 0, signer.clone()).await?;
let mut client = builder.add_root_chain(1, Amount::from_tokens(10)).await?;
client.options_mut().allow_fast_blocks = true;
let chain_id = client.chain_id();
let owner = client.identity().await?;
let timeout_config = TimeoutConfig {
fast_round_duration: Some(TimeDelta::from_secs(5)),
..TimeoutConfig::default()
};
let ownership = ChainOwnership {
super_owners: BTreeSet::from_iter([owner]),
owners: BTreeMap::default(),
multi_leader_rounds: 10,
open_multi_leader_rounds: false,
timeout_config,
};
client.change_ownership(ownership).await?;
builder.set_fault_type([1, 2, 3], FaultType::OfflineWithInfo);
assert!(client
.burn(AccountOwner::CHAIN, Amount::from_tokens(3))
.await
.is_err());
let pending = client
.pending_proposal()
.await
.expect("expected fast pending proposal after failed burn");
assert_eq!(pending.round, Some(Round::Fast));
let context = make_context(&mut builder, signer, chain_id).await?;
context.update_wallet_from_client(&client).await?;
let stored = context
.wallet()
.get(chain_id)
.expect("wallet missing chain entry")
.pending_proposal
.expect("wallet missing fast pending proposal");
assert_eq!(stored.round, Some(Round::Fast));
Ok(())
}
#[test_log::test(tokio::test)]
async fn test_wallet_drops_non_fast_pending_proposal() -> anyhow::Result<()> {
let mut signer = InMemorySigner::new(None);
let mut builder =
TestBuilder::new(MemoryStorageBuilder::default(), 4, 0, signer.clone()).await?;
let client = builder.add_root_chain(1, Amount::from_tokens(10)).await?;
let chain_id = client.chain_id();
let owner0 = client.identity().await?;
let owner1: AccountOwner = signer.generate_new().into();
let timeout_config = TimeoutConfig {
fast_round_duration: Some(TimeDelta::from_secs(5)),
..TimeoutConfig::default()
};
let ownership = ChainOwnership::multiple([(owner0, 100), (owner1, 100)], 10, timeout_config);
client.change_ownership(ownership).await?;
builder.set_fault_type([1, 2, 3], FaultType::OfflineWithInfo);
assert!(client
.burn(AccountOwner::CHAIN, Amount::from_tokens(3))
.await
.is_err());
let pending = client
.pending_proposal()
.await
.expect("expected non-fast pending proposal after failed burn");
assert_eq!(pending.round, Some(Round::MultiLeader(0)));
let context = make_context(&mut builder, signer, chain_id).await?;
context.update_wallet_from_client(&client).await?;
let stored = context
.wallet()
.get(chain_id)
.expect("wallet missing chain entry");
assert!(stored.pending_proposal.is_none());
Ok(())
}