Skip to content

Commit 8e005e9

Browse files
committed
Rotate the faucet chain automatically.
1 parent 1f6ad63 commit 8e005e9

14 files changed

Lines changed: 270 additions & 51 deletions

File tree

CLI.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,11 @@ Run a GraphQL service that exposes a faucet where users can claim tokens. This g
603603
Default value: `8080`
604604
* `--amount <AMOUNT>` — The number of tokens to send to each new chain
605605
* `--limit-rate-until <LIMIT_RATE_UNTIL>` — The end timestamp: The faucet will rate-limit the token supply so it runs out of money no earlier than this
606+
* `--max-claims-per-chain <MAX_CLAIMS_PER_CHAIN>` — The maximum number of claims per faucet chain, after which a new one is created.
607+
608+
A lower number improves performance for clients but creates overhead in the faucet.
609+
610+
Default value: `100`
606611
* `--listener-skip-process-inbox` — Do not create blocks automatically to receive incoming messages. Instead, wait for an explicit mutation `processInbox`
607612
* `--listener-delay-before-ms <DELAY_BEFORE_MS>` — Wait before processing any notification (useful for testing)
608613

linera-base/src/data_types.rs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,7 @@ macro_rules! impl_wrapped_number {
371371
}
372372

373373
/// Saturating addition.
374-
pub fn saturating_add(self, other: Self) -> Self {
374+
pub const fn saturating_add(self, other: Self) -> Self {
375375
let val = self.0.saturating_add(other.0);
376376
Self(val)
377377
}
@@ -392,7 +392,7 @@ macro_rules! impl_wrapped_number {
392392
}
393393

394394
/// Saturating subtraction.
395-
pub fn saturating_sub(self, other: Self) -> Self {
395+
pub const fn saturating_sub(self, other: Self) -> Self {
396396
let val = self.0.saturating_sub(other.0);
397397
Self(val)
398398
}
@@ -413,7 +413,7 @@ macro_rules! impl_wrapped_number {
413413
}
414414

415415
/// Saturating in-place addition.
416-
pub fn saturating_add_assign(&mut self, other: Self) {
416+
pub const fn saturating_add_assign(&mut self, other: Self) {
417417
self.0 = self.0.saturating_add(other.0);
418418
}
419419

@@ -427,7 +427,7 @@ macro_rules! impl_wrapped_number {
427427
}
428428

429429
/// Saturating multiplication.
430-
pub fn saturating_mul(&self, other: $wrapped) -> Self {
430+
pub const fn saturating_mul(&self, other: $wrapped) -> Self {
431431
Self(self.0.saturating_mul(other))
432432
}
433433

@@ -662,37 +662,37 @@ impl Amount {
662662
pub const ONE: Amount = Amount(10u128.pow(Amount::DECIMAL_PLACES as u32));
663663

664664
/// Returns an `Amount` corresponding to that many tokens, or `Amount::MAX` if saturated.
665-
pub fn from_tokens(tokens: u128) -> Amount {
665+
pub const fn from_tokens(tokens: u128) -> Amount {
666666
Self::ONE.saturating_mul(tokens)
667667
}
668668

669669
/// Returns an `Amount` corresponding to that many millitokens, or `Amount::MAX` if saturated.
670-
pub fn from_millis(millitokens: u128) -> Amount {
670+
pub const fn from_millis(millitokens: u128) -> Amount {
671671
Amount(10u128.pow(Amount::DECIMAL_PLACES as u32 - 3)).saturating_mul(millitokens)
672672
}
673673

674674
/// Returns an `Amount` corresponding to that many microtokens, or `Amount::MAX` if saturated.
675-
pub fn from_micros(microtokens: u128) -> Amount {
675+
pub const fn from_micros(microtokens: u128) -> Amount {
676676
Amount(10u128.pow(Amount::DECIMAL_PLACES as u32 - 6)).saturating_mul(microtokens)
677677
}
678678

679679
/// Returns an `Amount` corresponding to that many nanotokens, or `Amount::MAX` if saturated.
680-
pub fn from_nanos(nanotokens: u128) -> Amount {
680+
pub const fn from_nanos(nanotokens: u128) -> Amount {
681681
Amount(10u128.pow(Amount::DECIMAL_PLACES as u32 - 9)).saturating_mul(nanotokens)
682682
}
683683

684684
/// Returns an `Amount` corresponding to that many attotokens.
685-
pub fn from_attos(attotokens: u128) -> Amount {
685+
pub const fn from_attos(attotokens: u128) -> Amount {
686686
Amount(attotokens)
687687
}
688688

689689
/// Helper function to obtain the 64 most significant bits of the balance.
690-
pub fn upper_half(self) -> u64 {
690+
pub const fn upper_half(self) -> u64 {
691691
(self.0 >> 64) as u64
692692
}
693693

694694
/// Helper function to obtain the 64 least significant bits of the balance.
695-
pub fn lower_half(self) -> u64 {
695+
pub const fn lower_half(self) -> u64 {
696696
self.0 as u64
697697
}
698698

linera-client/src/chain_listener.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ pub trait ClientContext: 'static {
8181
}
8282
Ok(clients)
8383
}
84+
85+
async fn forget_chain(&mut self, chain_id: &ChainId) -> Result<(), Error>;
8486
}
8587

8688
/// A `ChainListener` is a process that listens to notifications from validators and reacts

linera-client/src/client_context.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,14 @@ where
111111
async fn update_wallet(&mut self, client: &ChainClient<NodeProvider, S>) -> Result<(), Error> {
112112
self.update_wallet_from_client(client).await
113113
}
114+
115+
async fn forget_chain(&mut self, chain_id: &ChainId) -> Result<(), Error> {
116+
self.wallet
117+
.mutate(|w| w.forget_chain(chain_id))
118+
.await
119+
.map_err(|e| error::Inner::Persistence(Box::new(e)))??;
120+
Ok(())
121+
}
114122
}
115123

116124
impl<S, W> ClientContext<S, W>

linera-client/src/unit_tests/chain_listener.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,11 @@ impl chain_listener::ClientContext for ClientContext {
101101
self.wallet.update_from_state(client).await;
102102
Ok(())
103103
}
104+
105+
async fn forget_chain(&mut self, chain_id: &ChainId) -> Result<(), Error> {
106+
self.wallet.forget_chain(chain_id)?;
107+
Ok(())
108+
}
104109
}
105110

106111
/// Tests that the chain listener, if there is a message in the inbox, will continue requesting

linera-faucet/server/src/lib.rs

Lines changed: 109 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ use serde::Deserialize;
2525
use tower_http::cors::CorsLayer;
2626
use tracing::info;
2727

28+
/// A rough estimate of the maximum fee for a block.
29+
const MAX_FEE: Amount = Amount::from_millis(100);
30+
2831
/// Returns an HTML response constructing the GraphiQL web page for the given URI.
2932
pub(crate) async fn graphiql(uri: axum::http::Uri) -> impl axum::response::IntoResponse {
3033
axum::response::Html(
@@ -47,9 +50,11 @@ pub struct QueryRoot<C> {
4750

4851
/// The root GraphQL mutation type.
4952
pub struct MutationRoot<C> {
50-
chain_id: ChainId,
53+
main_chain_id: ChainId,
54+
tmp_chain_id: Arc<Mutex<Option<ChainId>>>,
5155
context: Arc<Mutex<C>>,
5256
amount: Amount,
57+
max_claims_per_chain: u32,
5358
end_timestamp: Timestamp,
5459
start_timestamp: Timestamp,
5560
start_balance: Amount,
@@ -118,17 +123,55 @@ where
118123
C: ClientContext,
119124
{
120125
async fn do_claim(&self, owner: AccountOwner) -> Result<ClaimOutcome, Error> {
121-
let client = self.context.lock().await.make_chain_client(self.chain_id)?;
126+
let main_client = self
127+
.context
128+
.lock()
129+
.await
130+
.make_chain_client(self.main_chain_id)?;
131+
let maybe_tmp_chain_id = *self.tmp_chain_id.lock().await;
132+
let tmp_chain_id = match maybe_tmp_chain_id {
133+
Some(tmp_chain_id) => tmp_chain_id,
134+
None => {
135+
let key_pair = main_client.key_pair().await?;
136+
let balance = self
137+
.amount
138+
.try_add(MAX_FEE)?
139+
.try_mul(u128::from(self.max_claims_per_chain))?
140+
.try_add(MAX_FEE)?; // One more block fee for closing the chain.
141+
let ownership = main_client.chain_state_view().await?.ownership().clone();
142+
let (message_id, certificate) = main_client
143+
.open_chain(ownership, ApplicationPermissions::default(), balance)
144+
.await?
145+
.try_unwrap()?;
146+
let chain_id = ChainId::child(message_id);
147+
info!("Switching to a new faucet chain {chain_id:8}");
148+
self.context
149+
.lock()
150+
.await
151+
.update_wallet_for_new_chain(
152+
chain_id,
153+
Some(key_pair),
154+
certificate.block().header.timestamp,
155+
)
156+
.await?;
157+
*self.tmp_chain_id.lock().await = Some(chain_id);
158+
chain_id
159+
}
160+
};
161+
let tmp_client = self.context.lock().await.make_chain_client(tmp_chain_id)?;
122162

123163
if self.start_timestamp < self.end_timestamp {
124-
let local_time = client.storage_client().clock().current_time();
164+
let local_time = tmp_client.storage_client().clock().current_time();
125165
if local_time < self.end_timestamp {
126166
let full_duration = self
127167
.end_timestamp
128168
.delta_since(self.start_timestamp)
129169
.as_micros();
130170
let remaining_duration = self.end_timestamp.delta_since(local_time).as_micros();
131-
let balance = client.local_balance().await?;
171+
let balance = tmp_client
172+
.local_balance()
173+
.await?
174+
.try_add(main_client.local_balance().await?)?;
132175
let Ok(remaining_balance) = balance.try_sub(self.amount) else {
133176
return Err(Error::new("The faucet is empty."));
134177
};
@@ -145,20 +188,29 @@ where
145188
}
146189

147190
let ownership = ChainOwnership::single(owner);
148-
let result = client
191+
let result = tmp_client
149192
.open_chain(ownership, ApplicationPermissions::default(), self.amount)
150193
.await;
151-
self.context.lock().await.update_wallet(&client).await?;
152-
let (message_id, certificate) = match result? {
153-
ClientOutcome::Committed(result) => result,
154-
ClientOutcome::WaitForTimeout(timeout) => {
155-
return Err(Error::new(format!(
156-
"This faucet is using a multi-owner chain and is not the leader right now. \
157-
Try again at {}",
158-
timeout.timestamp,
159-
)));
194+
self.context.lock().await.update_wallet(&tmp_client).await?;
195+
let (message_id, certificate) = result?.try_unwrap()?;
196+
197+
// Only keep using this chain if there will still be enough balance to close it.
198+
if tmp_client.local_balance().await? < self.amount.try_add(MAX_FEE.try_mul(2)?)? {
199+
// TODO(#1795): Move the remaining tokens back to the main chain.
200+
match tmp_client.close_chain().await {
201+
Ok(outcome) => {
202+
outcome.try_unwrap()?;
203+
}
204+
Err(err) => tracing::warn!("Failed to close the chain: {err:?}"),
160205
}
161-
};
206+
self.context
207+
.lock()
208+
.await
209+
.forget_chain(&tmp_chain_id)
210+
.await?;
211+
self.tmp_chain_id.lock().await.take();
212+
}
213+
162214
let chain_id = ChainId::child(message_id);
163215
Ok(ClaimOutcome {
164216
message_id,
@@ -185,14 +237,16 @@ pub struct FaucetService<C>
185237
where
186238
C: ClientContext,
187239
{
188-
chain_id: ChainId,
240+
main_chain_id: ChainId,
241+
tmp_chain_id: Arc<Mutex<Option<ChainId>>>,
189242
context: Arc<Mutex<C>>,
190243
genesis_config: Arc<GenesisConfig>,
191244
config: ChainListenerConfig,
192245
storage: C::Storage,
193246
port: NonZeroU16,
194247
amount: Amount,
195248
end_timestamp: Timestamp,
249+
max_claims_per_chain: u32,
196250
start_timestamp: Timestamp,
197251
start_balance: Amount,
198252
}
@@ -203,13 +257,15 @@ where
203257
{
204258
fn clone(&self) -> Self {
205259
Self {
206-
chain_id: self.chain_id,
260+
main_chain_id: self.main_chain_id,
261+
tmp_chain_id: Arc::clone(&self.tmp_chain_id),
207262
context: Arc::clone(&self.context),
208263
genesis_config: Arc::clone(&self.genesis_config),
209264
config: self.config.clone(),
210265
storage: self.storage.clone(),
211266
port: self.port,
212267
amount: self.amount,
268+
max_claims_per_chain: self.max_claims_per_chain,
213269
end_timestamp: self.end_timestamp,
214270
start_timestamp: self.start_timestamp,
215271
start_balance: self.start_balance,
@@ -228,6 +284,7 @@ where
228284
chain_id: ChainId,
229285
context: C,
230286
amount: Amount,
287+
max_claims_per_chain: u32,
231288
end_timestamp: Timestamp,
232289
genesis_config: Arc<GenesisConfig>,
233290
config: ChainListenerConfig,
@@ -239,13 +296,15 @@ where
239296
client.process_inbox().await?;
240297
let start_balance = client.local_balance().await?;
241298
Ok(Self {
242-
chain_id,
299+
main_chain_id: chain_id,
300+
tmp_chain_id: Arc::new(Mutex::new(None)),
243301
context,
244302
genesis_config,
245303
config,
246304
storage,
247305
port,
248306
amount,
307+
max_claims_per_chain,
249308
end_timestamp,
250309
start_timestamp,
251310
start_balance,
@@ -254,23 +313,28 @@ where
254313

255314
pub fn schema(&self) -> Schema<QueryRoot<C>, MutationRoot<C>, EmptySubscription> {
256315
let mutation_root = MutationRoot {
257-
chain_id: self.chain_id,
316+
main_chain_id: self.main_chain_id,
317+
tmp_chain_id: Arc::clone(&self.tmp_chain_id),
258318
context: Arc::clone(&self.context),
259319
amount: self.amount,
320+
max_claims_per_chain: self.max_claims_per_chain,
260321
end_timestamp: self.end_timestamp,
261322
start_timestamp: self.start_timestamp,
262323
start_balance: self.start_balance,
263324
};
264325
let query_root = QueryRoot {
265326
genesis_config: Arc::clone(&self.genesis_config),
266327
context: Arc::clone(&self.context),
267-
chain_id: self.chain_id,
328+
chain_id: self.main_chain_id,
268329
};
269330
Schema::build(query_root, mutation_root, EmptySubscription).finish()
270331
}
271332

272333
/// Runs the faucet.
273-
#[tracing::instrument(name = "FaucetService::run", skip_all, fields(port = self.port, chain_id = ?self.chain_id))]
334+
#[tracing::instrument(
335+
name = "FaucetService::run",
336+
skip_all, fields(port = self.port, chain_id = ?self.main_chain_id))
337+
]
274338
pub async fn run(self) -> anyhow::Result<()> {
275339
let port = self.port.get();
276340
let index_handler = axum::routing::get(graphiql).post(Self::index_handler);
@@ -303,3 +367,27 @@ where
303367
schema.execute(request.into_inner()).await.into()
304368
}
305369
}
370+
371+
trait ClientOutcomeExt {
372+
type Output;
373+
374+
/// Returns the committed result or an error if we are not the leader.
375+
///
376+
/// It is recommended to use single-owner chains for the faucet to avoid this error.
377+
fn try_unwrap(self) -> Result<Self::Output, Error>;
378+
}
379+
380+
impl<T> ClientOutcomeExt for ClientOutcome<T> {
381+
type Output = T;
382+
383+
fn try_unwrap(self) -> Result<Self::Output, Error> {
384+
match self {
385+
ClientOutcome::Committed(result) => Ok(result),
386+
ClientOutcome::WaitForTimeout(timeout) => Err(Error::new(format!(
387+
"This faucet is using a multi-owner chain and is not the leader right now. \
388+
Try again at {}",
389+
timeout.timestamp,
390+
))),
391+
}
392+
}
393+
}

0 commit comments

Comments
 (0)