From bc3aaaa5f8a2a823ebe30e35691c64e3eae8c9b9 Mon Sep 17 00:00:00 2001 From: Andreas Fackler Date: Tue, 29 Apr 2025 12:32:38 +0200 Subject: [PATCH 1/2] Automatically rotate the faucet chain once it has a maximum length. --- CLI.md | 3 + linera-faucet/server/src/lib.rs | 87 +++++++++++++++++------ linera-faucet/server/src/tests.rs | 5 +- linera-service/src/cli_wrappers/wallet.rs | 13 +++- linera-service/src/linera/command.rs | 4 ++ linera-service/src/linera/main.rs | 4 +- linera-service/src/linera/net_up_utils.rs | 2 +- linera-service/tests/linera_net_tests.rs | 19 ++++- linera-service/tests/local_net_tests.rs | 12 ++-- 9 files changed, 113 insertions(+), 36 deletions(-) diff --git a/CLI.md b/CLI.md index 278becbde994..8e6f8ff29f16 100644 --- a/CLI.md +++ b/CLI.md @@ -603,6 +603,9 @@ Run a GraphQL service that exposes a faucet where users can claim tokens. This g Default value: `8080` * `--amount ` — The number of tokens to send to each new chain * `--limit-rate-until ` — The end timestamp: The faucet will rate-limit the token supply so it runs out of money no earlier than this +* `--max-chain-length ` — The maximum number of blocks in the faucet chain, before a new one is created + + Default value: `100` * `--listener-skip-process-inbox` — Do not create blocks automatically to receive incoming messages. Instead, wait for an explicit mutation `processInbox` * `--listener-delay-before-ms ` — Wait before processing any notification (useful for testing) diff --git a/linera-faucet/server/src/lib.rs b/linera-faucet/server/src/lib.rs index 752e8415e244..b0e76f35e4ae 100644 --- a/linera-faucet/server/src/lib.rs +++ b/linera-faucet/server/src/lib.rs @@ -11,7 +11,7 @@ use axum::{Extension, Router}; use futures::{lock::Mutex, FutureExt as _}; use linera_base::{ crypto::{CryptoHash, ValidatorPublicKey}, - data_types::{Amount, ApplicationPermissions, Timestamp}, + data_types::{Amount, ApplicationPermissions, BlockHeight, Timestamp}, identifiers::{AccountOwner, ChainId, MessageId}, ownership::ChainOwnership, }; @@ -43,14 +43,15 @@ mod tests; pub struct QueryRoot { context: Arc>, genesis_config: Arc, - chain_id: ChainId, + chain_id: Arc>, } /// The root GraphQL mutation type. pub struct MutationRoot { - chain_id: ChainId, + chain_id: Arc>, context: Arc>, amount: Amount, + end_block_height: BlockHeight, end_timestamp: Timestamp, start_timestamp: Timestamp, start_balance: Amount, @@ -90,7 +91,8 @@ where /// Returns the current committee's validators. async fn current_validators(&self) -> Result, Error> { - let client = self.context.lock().await.make_chain_client(self.chain_id)?; + let chain_id = *self.chain_id.lock().await; + let client = self.context.lock().await.make_chain_client(chain_id)?; let committee = client.local_committee().await?; Ok(committee .validators() @@ -119,7 +121,8 @@ where C: ClientContext, { async fn do_claim(&self, owner: AccountOwner) -> Result { - let client = self.context.lock().await.make_chain_client(self.chain_id)?; + let chain_id = *self.chain_id.lock().await; + let client = self.context.lock().await.make_chain_client(chain_id)?; if self.start_timestamp < self.end_timestamp { let local_time = client.storage_client().clock().current_time(); @@ -150,16 +153,31 @@ where .open_chain(ownership, ApplicationPermissions::default(), self.amount) .await; self.context.lock().await.update_wallet(&client).await?; - let (message_id, certificate) = match result? { - ClientOutcome::Committed(result) => result, - ClientOutcome::WaitForTimeout(timeout) => { - return Err(Error::new(format!( - "This faucet is using a multi-owner chain and is not the leader right now. \ - Try again at {}", - timeout.timestamp, - ))); - } - }; + let (message_id, certificate) = result?.try_unwrap()?; + + if client.next_block_height() >= self.end_block_height { + let key_pair = client.key_pair().await?; + let balance = client.local_balance().await?.try_sub(Amount::ONE)?; + let ownership = client.chain_state_view().await?.ownership().clone(); + let (message_id, certificate) = client + .open_chain(ownership, ApplicationPermissions::default(), balance) + .await? + .try_unwrap()?; + client.close_chain().await?.try_unwrap()?; + let chain_id = ChainId::child(message_id); + info!("Switching to a new faucet chain {chain_id:8}; remaining balance: {balance}"); + self.context + .lock() + .await + .update_wallet_for_new_chain( + chain_id, + Some(key_pair), + certificate.block().header.timestamp, + ) + .await?; + *self.chain_id.lock().await = chain_id; + } + let chain_id = ChainId::child(message_id); Ok(ClaimOutcome { message_id, @@ -186,7 +204,7 @@ pub struct FaucetService where C: ClientContext, { - chain_id: ChainId, + chain_id: Arc>, context: Arc>, genesis_config: Arc, config: ChainListenerConfig, @@ -194,6 +212,7 @@ where port: NonZeroU16, amount: Amount, end_timestamp: Timestamp, + end_block_height: BlockHeight, start_timestamp: Timestamp, start_balance: Amount, } @@ -204,13 +223,14 @@ where { fn clone(&self) -> Self { Self { - chain_id: self.chain_id, + chain_id: self.chain_id.clone(), context: Arc::clone(&self.context), genesis_config: Arc::clone(&self.genesis_config), config: self.config.clone(), storage: self.storage.clone(), port: self.port, amount: self.amount, + end_block_height: self.end_block_height, end_timestamp: self.end_timestamp, start_timestamp: self.start_timestamp, start_balance: self.start_balance, @@ -229,6 +249,7 @@ where chain_id: ChainId, context: C, amount: Amount, + end_block_height: BlockHeight, end_timestamp: Timestamp, genesis_config: Arc, config: ChainListenerConfig, @@ -240,13 +261,14 @@ where client.process_inbox().await?; let start_balance = client.local_balance().await?; Ok(Self { - chain_id, + chain_id: Arc::new(Mutex::new(chain_id)), context, genesis_config, config, storage, port, amount, + end_block_height, end_timestamp, start_timestamp, start_balance, @@ -255,9 +277,10 @@ where pub fn schema(&self) -> Schema, MutationRoot, EmptySubscription> { let mutation_root = MutationRoot { - chain_id: self.chain_id, + chain_id: self.chain_id.clone(), context: Arc::clone(&self.context), amount: self.amount, + end_block_height: self.end_block_height, end_timestamp: self.end_timestamp, start_timestamp: self.start_timestamp, start_balance: self.start_balance, @@ -265,7 +288,7 @@ where let query_root = QueryRoot { genesis_config: Arc::clone(&self.genesis_config), context: Arc::clone(&self.context), - chain_id: self.chain_id, + chain_id: self.chain_id.clone(), }; Schema::build(query_root, mutation_root, EmptySubscription).finish() } @@ -304,3 +327,27 @@ where schema.execute(request.into_inner()).await.into() } } + +trait ClientOutcomeExt { + type Output; + + /// Returns the committed result or an error if we are not the leader. + /// + /// It is recommended to use single-owner chains for the faucet to avoid this error. + fn try_unwrap(self) -> Result; +} + +impl ClientOutcomeExt for ClientOutcome { + type Output = T; + + fn try_unwrap(self) -> Result { + match self { + ClientOutcome::Committed(result) => Ok(result), + ClientOutcome::WaitForTimeout(timeout) => Err(Error::new(format!( + "This faucet is using a multi-owner chain and is not the leader right now. \ + Try again at {}", + timeout.timestamp, + ))), + } + } +} diff --git a/linera-faucet/server/src/tests.rs b/linera-faucet/server/src/tests.rs index 6fffa900dc0c..98d066bc3478 100644 --- a/linera-faucet/server/src/tests.rs +++ b/linera-faucet/server/src/tests.rs @@ -9,7 +9,7 @@ use async_trait::async_trait; use futures::lock::Mutex; use linera_base::{ crypto::{AccountPublicKey, AccountSecretKey}, - data_types::{Amount, Timestamp}, + data_types::{Amount, BlockHeight, Timestamp}, identifiers::ChainId, }; use linera_client::{chain_listener, wallet::Wallet}; @@ -82,9 +82,10 @@ async fn test_faucet_rate_limiting() { }; let context = Arc::new(Mutex::new(context)); let root = MutationRoot { - chain_id, + chain_id: Arc::new(Mutex::new(chain_id)), context: context.clone(), amount: Amount::from_tokens(1), + end_block_height: BlockHeight::from(10), end_timestamp: Timestamp::from(6000), start_timestamp: Timestamp::from(0), start_balance: Amount::from_tokens(6), diff --git a/linera-service/src/cli_wrappers/wallet.rs b/linera-service/src/cli_wrappers/wallet.rs index 962f15cc24d6..c32c8ddd098f 100644 --- a/linera-service/src/cli_wrappers/wallet.rs +++ b/linera-service/src/cli_wrappers/wallet.rs @@ -519,15 +519,22 @@ impl ClientWrapper { port: impl Into>, chain_id: ChainId, amount: Amount, + max_chain_length: Option, ) -> Result { let port = port.into().unwrap_or(8080); let mut command = self.command().await?; - let child = command + command .arg("faucet") .arg(chain_id.to_string()) .args(["--port".to_string(), port.to_string()]) - .args(["--amount".to_string(), amount.to_string()]) - .spawn_into()?; + .args(["--amount".to_string(), amount.to_string()]); + if let Some(max_chain_length) = max_chain_length { + command.args([ + "--max-chain-length".to_string(), + max_chain_length.to_string(), + ]); + } + let child = command.spawn_into()?; let client = reqwest_client(); for i in 0..10 { linera_base::time::timer::sleep(Duration::from_secs(i)).await; diff --git a/linera-service/src/linera/command.rs b/linera-service/src/linera/command.rs index 127bb9629c6a..485a79ebf28d 100644 --- a/linera-service/src/linera/command.rs +++ b/linera-service/src/linera/command.rs @@ -620,6 +620,10 @@ pub enum ClientCommand { #[arg(long)] limit_rate_until: Option>, + /// The maximum number of blocks in the faucet chain, before a new one is created. + #[arg(long, default_value = "100")] + max_chain_length: u64, + /// Configuration for the faucet chain listener. #[command(flatten)] config: ChainListenerConfig, diff --git a/linera-service/src/linera/main.rs b/linera-service/src/linera/main.rs index e9d2af678466..959201550483 100644 --- a/linera-service/src/linera/main.rs +++ b/linera-service/src/linera/main.rs @@ -22,7 +22,7 @@ use command::{ClientCommand, DatabaseToolCommand, NetCommand, ProjectCommand, Wa use futures::{lock::Mutex, FutureExt as _, StreamExt}; use linera_base::{ crypto::{AccountSecretKey, CryptoHash, CryptoRng, Ed25519SecretKey}, - data_types::{ApplicationPermissions, Timestamp}, + data_types::{ApplicationPermissions, BlockHeight, Timestamp}, identifiers::{AccountOwner, ChainDescription, ChainId}, listen_for_shutdown_signals, ownership::ChainOwnership, @@ -834,6 +834,7 @@ impl Runnable for Job { port, amount, limit_rate_until, + max_chain_length, config, } => { let chain_id = chain_id.unwrap_or_else(|| context.default_chain()); @@ -851,6 +852,7 @@ impl Runnable for Job { chain_id, context, amount, + BlockHeight(max_chain_length), end_timestamp, genesis_config, config, diff --git a/linera-service/src/linera/net_up_utils.rs b/linera-service/src/linera/net_up_utils.rs index 46607d33d69d..0afe98238ee0 100644 --- a/linera-service/src/linera/net_up_utils.rs +++ b/linera-service/src/linera/net_up_utils.rs @@ -294,7 +294,7 @@ async fn print_messages_and_create_faucet( ChainId::root(1) }; let service = client - .run_faucet(Some(faucet_port.into()), faucet_chain, faucet_amount) + .run_faucet(Some(faucet_port.into()), faucet_chain, faucet_amount, None) .await?; Some(service) } else { diff --git a/linera-service/tests/linera_net_tests.rs b/linera-service/tests/linera_net_tests.rs index b74f1bf682f3..1c8845afeaf9 100644 --- a/linera-service/tests/linera_net_tests.rs +++ b/linera-service/tests/linera_net_tests.rs @@ -3166,7 +3166,7 @@ async fn test_end_to_end_faucet(config: impl LineraNetConfig) -> Result<()> { let owner2 = client2.keygen().await?; let mut faucet_service = client1 - .run_faucet(None, chain1, Amount::from_tokens(2)) + .run_faucet(None, chain1, Amount::from_tokens(2), None) .await?; let faucet = faucet_service.instance(); let outcome = faucet.claim(&owner2).await?; @@ -3259,9 +3259,18 @@ async fn test_end_to_end_faucet_with_long_chains(config: impl LineraNetConfig) - } let amount = Amount::ONE; - let mut faucet_service = faucet_client.run_faucet(None, faucet_chain, amount).await?; + let mut faucet_service = faucet_client + .run_faucet(None, faucet_chain, amount, Some(chain_count as u64)) + .await?; let faucet = faucet_service.instance(); + // Create a new wallet using the faucet + let other_client = net.make_client().await; + let (other_outcome, _) = other_client + .wallet_init(&[], FaucetOption::NewChain(&faucet)) + .await? + .unwrap(); + // Create a new wallet using the faucet let client = net.make_client().await; let (outcome, _) = client @@ -3269,6 +3278,10 @@ async fn test_end_to_end_faucet_with_long_chains(config: impl LineraNetConfig) - .await? .unwrap(); + // Since the faucet chain exceeds the configured maximum length, the faucet should have + // switched after the first new chain. + assert!(other_outcome.message_id.chain_id != outcome.message_id.chain_id); + let chain = outcome.chain_id; assert_eq!(chain, client.load_wallet()?.default_chain().unwrap()); @@ -3311,7 +3324,7 @@ async fn test_end_to_end_fungible_client_benchmark(config: impl LineraNetConfig) let chain1 = client1.load_wallet()?.default_chain().unwrap(); - let mut faucet_service = client1.run_faucet(None, chain1, Amount::ONE).await?; + let mut faucet_service = client1.run_faucet(None, chain1, Amount::ONE, None).await?; let faucet = faucet_service.instance(); let path = diff --git a/linera-service/tests/local_net_tests.rs b/linera-service/tests/local_net_tests.rs index 71092d1d40f3..8a399a1f8b02 100644 --- a/linera-service/tests/local_net_tests.rs +++ b/linera-service/tests/local_net_tests.rs @@ -69,7 +69,7 @@ async fn test_end_to_end_reconfiguration(config: LocalNetConfig) -> Result<()> { .await?; let mut faucet_service = faucet_client - .run_faucet(None, faucet_chain, Amount::from_tokens(2)) + .run_faucet(None, faucet_chain, Amount::from_tokens(2), None) .await?; faucet_service.ensure_is_running()?; @@ -246,7 +246,7 @@ async fn test_end_to_end_receipt_of_old_create_committee_messages( if matches!(network, Network::Grpc) { let mut faucet_service = faucet_client - .run_faucet(None, faucet_chain, Amount::from_tokens(2)) + .run_faucet(None, faucet_chain, Amount::from_tokens(2), None) .await?; faucet_service.ensure_is_running()?; @@ -280,7 +280,7 @@ async fn test_end_to_end_receipt_of_old_create_committee_messages( faucet_client.process_inbox(faucet_chain).await?; let mut faucet_service = faucet_client - .run_faucet(None, faucet_chain, Amount::from_tokens(2)) + .run_faucet(None, faucet_chain, Amount::from_tokens(2), None) .await?; faucet_service.ensure_is_running()?; @@ -338,7 +338,7 @@ async fn test_end_to_end_receipt_of_old_remove_committee_messages( if matches!(network, Network::Grpc) { let mut faucet_service = faucet_client - .run_faucet(None, faucet_chain, Amount::from_tokens(2)) + .run_faucet(None, faucet_chain, Amount::from_tokens(2), None) .await?; faucet_service.ensure_is_running()?; @@ -374,7 +374,7 @@ async fn test_end_to_end_receipt_of_old_remove_committee_messages( if matches!(network, Network::Grpc) { let mut faucet_service = faucet_client - .run_faucet(None, faucet_chain, Amount::from_tokens(2)) + .run_faucet(None, faucet_chain, Amount::from_tokens(2), None) .await?; faucet_service.ensure_is_running()?; @@ -410,7 +410,7 @@ async fn test_end_to_end_receipt_of_old_remove_committee_messages( faucet_client.process_inbox(faucet_chain).await?; let mut faucet_service = faucet_client - .run_faucet(None, faucet_chain, Amount::from_tokens(2)) + .run_faucet(None, faucet_chain, Amount::from_tokens(2), None) .await?; faucet_service.ensure_is_running()?; From d48a4f4ec64003a24244bb4efc07d0fb74133b24 Mon Sep 17 00:00:00 2001 From: Andreas Fackler Date: Tue, 29 Apr 2025 12:55:32 +0200 Subject: [PATCH 2/2] Add TODO to move the remaining tokens. --- linera-faucet/server/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/linera-faucet/server/src/lib.rs b/linera-faucet/server/src/lib.rs index b0e76f35e4ae..5ad2599805cc 100644 --- a/linera-faucet/server/src/lib.rs +++ b/linera-faucet/server/src/lib.rs @@ -163,6 +163,7 @@ where .open_chain(ownership, ApplicationPermissions::default(), balance) .await? .try_unwrap()?; + // TODO(#1795): Move the remaining tokens to the new chain. client.close_chain().await?.try_unwrap()?; let chain_id = ChainId::child(message_id); info!("Switching to a new faucet chain {chain_id:8}; remaining balance: {balance}");