From 21f1a0eb9635567792665c38f89114d091efc09b Mon Sep 17 00:00:00 2001 From: Shashank Date: Wed, 1 Apr 2026 10:00:28 +0530 Subject: [PATCH 1/5] Impl nonce fix cmd --- docs/docs/users/reference/cli.md | 25 ++- src/cli/subcommands/mpool_cmd.rs | 294 ++++++++++++++++++++++++++++++- 2 files changed, 314 insertions(+), 5 deletions(-) diff --git a/docs/docs/users/reference/cli.md b/docs/docs/users/reference/cli.md index 294d478e329c..adee7db5569c 100644 --- a/docs/docs/users/reference/cli.md +++ b/docs/docs/users/reference/cli.md @@ -617,10 +617,11 @@ Interact with the message pool Usage: forest-cli mpool Commands: - pending Get pending messages - nonce Get the current nonce for an address - stat Print mempool stats - help Print this message or the help of the given subcommand(s) + pending Get pending messages + nonce Get the current nonce for an address + stat Print mempool stats + nonce-fix Fill an on-chain nonce gap by pushing signed self-transfer messages + help Print this message or the help of the given subcommand(s) Options: -h, --help Print help @@ -671,6 +672,22 @@ Options: -h, --help Print help ``` +### `forest-cli mpool nonce-fix` + +``` +Fill an on-chain nonce gap by pushing signed self-transfer messages + +Usage: forest-cli mpool nonce-fix --addr [OPTIONS] + +Options: + --addr Address to fill nonces for (must be signable by the node's wallet) + --auto Derive the fill range from chain state and the mempool (ignores `--start` / `--end`) + --start First sequence to fill (inclusive); required unless `--auto` + --end End of range (exclusive); required unless `--auto` + --gas-fee-cap Gas fee cap for filler messages, in `attoFIL`. Default: twice the parent base fee from chain head + -h, --help Print help +``` + ### `forest-cli state` ``` diff --git a/src/cli/subcommands/mpool_cmd.rs b/src/cli/subcommands/mpool_cmd.rs index c83a814c2cbd..ab2c68d11149 100644 --- a/src/cli/subcommands/mpool_cmd.rs +++ b/src/cli/subcommands/mpool_cmd.rs @@ -6,12 +6,15 @@ use crate::lotus_json::{HasLotusJson as _, NotNullVec}; use crate::message::{MessageRead as _, SignedMessage}; use crate::rpc::{self, prelude::*, types::ApiTipsetKey}; use crate::shim::address::StrictAddress; -use crate::shim::message::Message; +use crate::shim::message::{METHOD_SEND, Message}; use crate::shim::{address::Address, econ::TokenAmount}; use ahash::{HashMap, HashSet}; +use anyhow::Context as _; use clap::Subcommand; +use fvm_ipld_encoding::RawBytes; use num::BigInt; +use std::ops::Range; #[derive(Debug, Subcommand)] pub enum MpoolCommands { @@ -44,6 +47,24 @@ pub enum MpoolCommands { #[arg(long)] local: bool, }, + /// Fill an on-chain nonce gap by pushing signed self-transfer messages. + NonceFix { + /// Address to fill nonces for (must be signable by the node's wallet). + #[arg(long)] + addr: StrictAddress, + /// Derive the fill range from chain state and the mempool (ignores `--start` / `--end`). + #[arg(long)] + auto: bool, + /// First sequence to fill (inclusive); required unless `--auto`. + #[arg(long)] + start: Option, + /// End of range (exclusive); required unless `--auto`. + #[arg(long)] + end: Option, + /// Gas fee cap for filler messages, in `attoFIL`. Default: twice the parent base fee from chain head. + #[arg(long)] + gas_fee_cap: Option, + }, } fn filter_messages( @@ -69,6 +90,62 @@ fn filter_messages( Ok(filtered) } +enum NonceFixFillRangeInput { + Auto { + addr: Address, + next_on_chain_nonce: u64, + pending: Vec, + }, + Manual { + start: Option, + end: Option, + }, +} + +fn get_nonce_fix_fill_range(input: NonceFixFillRangeInput) -> anyhow::Result>> { + match input { + NonceFixFillRangeInput::Auto { + addr, + next_on_chain_nonce, + pending, + } => { + let Some(pending_nonce) = pending + .iter() + .filter(|m| m.from() == addr) + .map(|m| m.sequence()) + .filter(|&seq| seq >= next_on_chain_nonce) + .min() + else { + return Ok(None); + }; + if pending_nonce == next_on_chain_nonce { + return Ok(None); + } + Ok(Some(next_on_chain_nonce..pending_nonce)) + } + NonceFixFillRangeInput::Manual { start, end } => { + let start = start.context("manual mode requires --start")?; + let end = end.context("manual mode requires --end")?; + anyhow::ensure!(end > start, "--end must be greater than --start"); + Ok(Some(start..end)) + } + } +} + +fn get_nonce_fix_gas_fee_cap( + gas_fee_cap: Option<&str>, + parent_base_fee: TokenAmount, +) -> anyhow::Result { + if let Some(cap) = gas_fee_cap { + Ok(TokenAmount::from_atto( + cap.parse::() + .context("invalid --gas-fee-cap value")?, + )) + } else { + Ok(parent_base_fee * 2u64) + } +} + async fn get_actor_sequence( message: &Message, tipset: &Tipset, @@ -273,6 +350,64 @@ impl MpoolCommands { let nonce = MpoolGetNonce::call(&client, (address.into(),)).await?; println!("{nonce}"); + Ok(()) + } + Self::NonceFix { + addr, + auto, + start, + end, + gas_fee_cap, + } => { + let addr: Address = addr.into(); + + let fill_range = if auto { + let actor = StateGetActor::call(&client, (addr, ApiTipsetKey(None))) + .await? + .with_context(|| format!("no on-chain actor found for {addr}"))?; + let next_nonce = actor.sequence; + let NotNullVec(pending) = + MpoolPending::call(&client, (ApiTipsetKey(None),)).await?; + get_nonce_fix_fill_range(NonceFixFillRangeInput::Auto { + addr, + next_on_chain_nonce: next_nonce, + pending, + })? + } else { + get_nonce_fix_fill_range(NonceFixFillRangeInput::Manual { start, end })? + }; + + let Some(fill_range) = fill_range else { + println!("No nonce gap found or no --end flag specified"); + return Ok(()); + }; + + let tipset = ChainHead::call(&client, ()).await?; + let parent_base_fee = tipset.block_headers().first().parent_base_fee.clone(); + let fee_cap = get_nonce_fix_gas_fee_cap(gas_fee_cap.as_deref(), parent_base_fee)?; + let n = fill_range.end.saturating_sub(fill_range.start); + println!( + "Creating {n} filler messages ({} ~ {})", + fill_range.start, fill_range.end + ); + + for sequence in fill_range { + let msg = Message { + version: 0, + from: addr, + to: addr, + sequence, + value: TokenAmount::default(), + method_num: METHOD_SEND, + params: RawBytes::new(vec![]), + gas_limit: 1_000_000, + gas_fee_cap: fee_cap.clone(), + gas_premium: TokenAmount::from_atto(5u64), + }; + let smsg = WalletSignMessage::call(&client, (addr, msg)).await?; + MpoolPush::call(&client, (smsg,)).await?; + } + Ok(()) } } @@ -422,6 +557,163 @@ mod tests { } } + #[test] + fn nonce_fix_auto_no_pending() { + let keystore = KeyStore::new(KeyStoreConfig::Memory).unwrap(); + let mut wallet = Wallet::new(keystore); + let addr = wallet.generate_addr(SignatureType::Secp256k1).unwrap(); + let r = get_nonce_fix_fill_range(NonceFixFillRangeInput::Auto { + addr, + next_on_chain_nonce: 0, + pending: vec![], + }) + .unwrap(); + assert_eq!(r, None); + } + + #[test] + fn nonce_fix_auto_other_sender() { + let keystore = KeyStore::new(KeyStoreConfig::Memory).unwrap(); + let mut wallet = Wallet::new(keystore); + let addr = wallet.generate_addr(SignatureType::Secp256k1).unwrap(); + let other = wallet.generate_addr(SignatureType::Secp256k1).unwrap(); + let target = wallet.generate_addr(SignatureType::Secp256k1).unwrap(); + let m = create_smsg(&target, &other, wallet.borrow_mut(), 10, 1000000, 1); + let r = get_nonce_fix_fill_range(NonceFixFillRangeInput::Auto { + addr, + next_on_chain_nonce: 5, + pending: vec![m], + }) + .unwrap(); + assert_eq!(r, None); + } + + #[test] + fn nonce_fix_auto_fill_range_gap() { + let keystore = KeyStore::new(KeyStoreConfig::Memory).unwrap(); + let mut wallet = Wallet::new(keystore); + let addr = wallet.generate_addr(SignatureType::Secp256k1).unwrap(); + let target = wallet.generate_addr(SignatureType::Secp256k1).unwrap(); + let m = create_smsg(&target, &addr, wallet.borrow_mut(), 7, 1000000, 1); + let r = get_nonce_fix_fill_range(NonceFixFillRangeInput::Auto { + addr, + next_on_chain_nonce: 5, + pending: vec![m], + }) + .unwrap(); + assert_eq!(r, Some(5..7)); + } + + #[test] + fn nonce_fix_auto_fill_range_min_pending_nonce() { + let keystore = KeyStore::new(KeyStoreConfig::Memory).unwrap(); + let mut wallet = Wallet::new(keystore); + let addr = wallet.generate_addr(SignatureType::Secp256k1).unwrap(); + let target = wallet.generate_addr(SignatureType::Secp256k1).unwrap(); + let m10 = create_smsg(&target, &addr, wallet.borrow_mut(), 10, 1000000, 1); + let m8 = create_smsg(&target, &addr, wallet.borrow_mut(), 8, 1000000, 1); + let r = get_nonce_fix_fill_range(NonceFixFillRangeInput::Auto { + addr, + next_on_chain_nonce: 5, + pending: vec![m10, m8], + }) + .unwrap(); + assert_eq!(r, Some(5..8)); + } + + #[test] + fn nonce_fix_auto_next_nonce_exist_in_mpool() { + let keystore = KeyStore::new(KeyStoreConfig::Memory).unwrap(); + let mut wallet = Wallet::new(keystore); + let addr = wallet.generate_addr(SignatureType::Secp256k1).unwrap(); + let target = wallet.generate_addr(SignatureType::Secp256k1).unwrap(); + let m = create_smsg(&target, &addr, wallet.borrow_mut(), 5, 1000000, 1); + let r = get_nonce_fix_fill_range(NonceFixFillRangeInput::Auto { + addr, + next_on_chain_nonce: 5, + pending: vec![m], + }) + .unwrap(); + assert_eq!(r, None); + } + + #[test] + fn nonce_fix_manual_fill_range_missing_start() { + let e = get_nonce_fix_fill_range(NonceFixFillRangeInput::Manual { + start: None, + end: Some(10), + }) + .unwrap_err(); + assert!( + e.to_string().contains("manual mode requires --start"), + "{e}" + ); + } + + #[test] + fn nonce_fix_manual_fill_range_missing_end() { + let e = get_nonce_fix_fill_range(NonceFixFillRangeInput::Manual { + start: Some(1), + end: None, + }) + .unwrap_err(); + assert!(e.to_string().contains("manual mode requires --end"), "{e}"); + } + + #[test] + fn nonce_fix_invalid_fill_range() { + let e = get_nonce_fix_fill_range(NonceFixFillRangeInput::Manual { + start: Some(5), + end: Some(5), + }) + .unwrap_err(); + assert!( + e.to_string().contains("--end must be greater than --start"), + "{e}" + ); + + let e = get_nonce_fix_fill_range(NonceFixFillRangeInput::Manual { + start: Some(5), + end: Some(3), + }) + .unwrap_err(); + assert!( + e.to_string().contains("--end must be greater than --start"), + "{e}" + ); + } + + #[test] + fn nonce_fix_manual_fill_range() { + let r = get_nonce_fix_fill_range(NonceFixFillRangeInput::Manual { + start: Some(2), + end: Some(5), + }) + .unwrap(); + assert_eq!(r, Some(2..5)); + } + + #[test] + fn nonce_fix_default_fee_cap() { + let parent = TokenAmount::from_atto(100u64); + let cap = get_nonce_fix_gas_fee_cap(None, parent.clone()).unwrap(); + assert_eq!(cap, parent * 2u64); + } + + #[test] + fn nonce_fix_explicit_fee_cap() { + let parent = TokenAmount::from_atto(999u64); + let cap = get_nonce_fix_gas_fee_cap(Some("42"), parent).unwrap(); + assert_eq!(cap, TokenAmount::from_atto(42u64)); + } + + #[test] + fn nonce_fix_invalid_fee_cap() { + let parent = TokenAmount::from_atto(1u64); + let e = get_nonce_fix_gas_fee_cap(Some("not-a-number"), parent).unwrap_err(); + assert!(e.to_string().contains("invalid --gas-fee-cap value"), "{e}"); + } + #[test] fn compute_statistics() { use crate::shim::message::Message; From 833e540b74de2d76a356765fbf048e983fcab5be Mon Sep 17 00:00:00 2001 From: Shashank Date: Mon, 6 Apr 2026 20:10:25 +0530 Subject: [PATCH 2/5] Impl mpool replace cmd --- docs/docs/users/reference/cli.md | 19 ++ src/cli/subcommands/mpool_cmd.rs | 394 ++++++++++++++++++++++++++++++- src/message_pool/mod.rs | 2 + src/message_pool/msgpool/mod.rs | 4 +- 4 files changed, 416 insertions(+), 3 deletions(-) diff --git a/docs/docs/users/reference/cli.md b/docs/docs/users/reference/cli.md index 122f98159347..7d544b99e577 100644 --- a/docs/docs/users/reference/cli.md +++ b/docs/docs/users/reference/cli.md @@ -688,6 +688,25 @@ Options: -h, --help Print help ``` +### `forest-cli mpool replace` + +``` +Replace a pending message in the mempool with updated gas parameters (replace-by-fee) + +Usage: forest-cli mpool replace [OPTIONS] + +Options: + --from Address that sent the message (required unless `--cid` is used) + --nonce Nonce of the message to replace (required unless `--cid` is used) + --cid CID of the message to replace (alternative to `--from`/`--nonce`) + --auto Automatically re-estimate gas, ensuring the RBF minimum premium is met + --max-fee Maximum total fee in `attoFIL`; only used with `--auto` + --gas-premium Gas premium in `attoFIL` (manual mode) + --gas-feecap Gas fee cap in `attoFIL` (manual mode) + --gas-limit Gas limit (manual mode; keeps original value if unset) + -h, --help Print help +``` + ### `forest-cli state` ``` diff --git a/src/cli/subcommands/mpool_cmd.rs b/src/cli/subcommands/mpool_cmd.rs index ab2c68d11149..cf7992008473 100644 --- a/src/cli/subcommands/mpool_cmd.rs +++ b/src/cli/subcommands/mpool_cmd.rs @@ -4,13 +4,15 @@ use crate::blocks::Tipset; use crate::lotus_json::{HasLotusJson as _, NotNullVec}; use crate::message::{MessageRead as _, SignedMessage}; -use crate::rpc::{self, prelude::*, types::ApiTipsetKey}; +use crate::message_pool::{RBF_DENOM, RBF_NUM}; +use crate::rpc::{self, prelude::*, types::ApiTipsetKey, types::MessageSendSpec}; use crate::shim::address::StrictAddress; use crate::shim::message::{METHOD_SEND, Message}; use crate::shim::{address::Address, econ::TokenAmount}; use ahash::{HashMap, HashSet}; use anyhow::Context as _; +use cid::Cid; use clap::Subcommand; use fvm_ipld_encoding::RawBytes; use num::BigInt; @@ -65,6 +67,33 @@ pub enum MpoolCommands { #[arg(long)] gas_fee_cap: Option, }, + /// Replace a pending message in the mempool with updated gas parameters (replace-by-fee). + Replace { + /// Address that sent the message (required unless `--cid` is used). + #[arg(long, required_unless_present = "cid")] + from: Option, + /// Nonce of the message to replace (required unless `--cid` is used). + #[arg(long, required_unless_present = "cid")] + nonce: Option, + /// CID of the message to replace (alternative to `--from`/`--nonce`). + #[arg(long, conflicts_with_all = ["from", "nonce"])] + cid: Option, + /// Automatically re-estimate gas, ensuring the RBF minimum premium is met. + #[arg(long)] + auto: bool, + /// Maximum total fee in `attoFIL`; only used with `--auto`. + #[arg(long)] + max_fee: Option, + /// Gas premium in `attoFIL` (manual mode). + #[arg(long)] + gas_premium: Option, + /// Gas fee cap in `attoFIL` (manual mode). + #[arg(long)] + gas_feecap: Option, + /// Gas limit (manual mode; keeps original value if unset). + #[arg(long)] + gas_limit: Option, + }, } fn filter_messages( @@ -146,6 +175,85 @@ fn get_nonce_fix_gas_fee_cap( } } +/// Minimum gas premium required to replace a message (RBF floor). +/// Mirrors the check in `MsgSet::add`: `old + old * RBF_NUM / RBF_DENOM + 1`. +fn compute_rbf_minimum_premium(original_premium: &TokenAmount) -> TokenAmount { + original_premium.clone() + + (original_premium * RBF_NUM).div_floor(RBF_DENOM) + + TokenAmount::from_atto(1u8) +} + +fn find_pending_message( + from: Address, + nonce: u64, + pending: &[SignedMessage], +) -> anyhow::Result { + pending + .iter() + .find(|m| m.from() == from && m.sequence() == nonce) + .cloned() + .with_context(|| format!("no pending message found from {from} with nonce {nonce}")) +} + +enum ReplaceGasInput { + Auto { + estimated_msg: Message, + original_premium: TokenAmount, + }, + Manual { + gas_premium: Option, + gas_feecap: Option, + gas_limit: Option, + original_msg: Message, + }, +} + +fn compute_replacement_gas(input: ReplaceGasInput) -> anyhow::Result { + match input { + ReplaceGasInput::Auto { + mut estimated_msg, + original_premium, + } => { + let min_premium = compute_rbf_minimum_premium(&original_premium); + if estimated_msg.gas_premium < min_premium { + estimated_msg.gas_premium = min_premium; + } + Ok(estimated_msg) + } + ReplaceGasInput::Manual { + gas_premium, + gas_feecap, + gas_limit, + mut original_msg, + } => { + if let Some(premium_str) = gas_premium { + let new_premium = TokenAmount::from_atto( + premium_str + .parse::() + .context("invalid --gas-premium value")?, + ); + let min_premium = compute_rbf_minimum_premium(&original_msg.gas_premium); + anyhow::ensure!( + new_premium >= min_premium, + "replacement gas premium {new_premium} is below the RBF minimum {min_premium}" + ); + original_msg.gas_premium = new_premium; + } + if let Some(feecap_str) = gas_feecap { + original_msg.gas_fee_cap = TokenAmount::from_atto( + feecap_str + .parse::() + .context("invalid --gas-feecap value")?, + ); + } + if let Some(limit) = gas_limit { + original_msg.gas_limit = limit; + } + Ok(original_msg) + } + } +} + async fn get_actor_sequence( message: &Message, tipset: &Tipset, @@ -408,6 +516,78 @@ impl MpoolCommands { MpoolPush::call(&client, (smsg,)).await?; } + Ok(()) + } + Self::Replace { + from, + nonce, + cid, + auto, + max_fee, + gas_premium, + gas_feecap, + gas_limit, + } => { + let (sender, sequence) = if let Some(msg_cid) = cid { + let api_msg = ChainGetMessage::call(&client, (msg_cid,)).await?; + (api_msg.message.from, api_msg.message.sequence) + } else { + let sender: Address = from + .context("--from is required when --cid is not provided")? + .into(); + let seq = nonce.context("--nonce is required when --cid is not provided")?; + (sender, seq) + }; + + let NotNullVec(pending) = + MpoolPending::call(&client, (ApiTipsetKey(None),)).await?; + let found = find_pending_message(sender, sequence, &pending)?; + let original_msg = found.into_message(); + + let replacement = if auto { + let mut msg_for_estimate = original_msg.clone(); + msg_for_estimate.gas_limit = 0; + msg_for_estimate.gas_fee_cap = TokenAmount::default(); + msg_for_estimate.gas_premium = TokenAmount::default(); + + let spec = if let Some(ref fee_str) = max_fee { + let max = TokenAmount::from_atto( + fee_str + .parse::() + .context("invalid --max-fee value")?, + ); + Some(MessageSendSpec { + max_fee: max, + msg_uuid: uuid::Uuid::nil(), + maximize_fee_cap: false, + }) + } else { + None + }; + + let estimated = GasEstimateMessageGas::call( + &client, + (msg_for_estimate, spec, ApiTipsetKey(None)), + ) + .await?; + + compute_replacement_gas(ReplaceGasInput::Auto { + estimated_msg: estimated.message, + original_premium: original_msg.gas_premium, + })? + } else { + compute_replacement_gas(ReplaceGasInput::Manual { + gas_premium, + gas_feecap, + gas_limit, + original_msg, + })? + }; + + let smsg = WalletSignMessage::call(&client, (sender, replacement)).await?; + let new_cid = MpoolPush::call(&client, (smsg,)).await?; + println!("new message cid: {new_cid}"); + Ok(()) } } @@ -789,4 +969,216 @@ mod tests { assert_eq!(stats, expected); } + + #[test] + fn find_pending_message_found() { + let keystore = KeyStore::new(KeyStoreConfig::Memory).unwrap(); + let mut wallet = Wallet::new(keystore); + let sender = wallet.generate_addr(SignatureType::Secp256k1).unwrap(); + let target = wallet.generate_addr(SignatureType::Secp256k1).unwrap(); + + let m5 = create_smsg(&target, &sender, wallet.borrow_mut(), 5, 1000000, 1); + let m6 = create_smsg(&target, &sender, wallet.borrow_mut(), 6, 1000000, 1); + let pending = vec![m5.clone(), m6]; + + let found = find_pending_message(sender, 5, &pending).unwrap(); + assert_eq!(found.cid(), m5.cid()); + } + + #[test] + fn find_pending_message_wrong_nonce() { + let keystore = KeyStore::new(KeyStoreConfig::Memory).unwrap(); + let mut wallet = Wallet::new(keystore); + let sender = wallet.generate_addr(SignatureType::Secp256k1).unwrap(); + let target = wallet.generate_addr(SignatureType::Secp256k1).unwrap(); + + let m5 = create_smsg(&target, &sender, wallet.borrow_mut(), 5, 1000000, 1); + let pending = vec![m5]; + + let e = find_pending_message(sender, 99, &pending).unwrap_err(); + assert!(e.to_string().contains("no pending message found"), "{e}"); + } + + #[test] + fn find_pending_message_wrong_sender() { + let keystore = KeyStore::new(KeyStoreConfig::Memory).unwrap(); + let mut wallet = Wallet::new(keystore); + let sender = wallet.generate_addr(SignatureType::Secp256k1).unwrap(); + let other = wallet.generate_addr(SignatureType::Secp256k1).unwrap(); + let target = wallet.generate_addr(SignatureType::Secp256k1).unwrap(); + + let m = create_smsg(&target, &sender, wallet.borrow_mut(), 5, 1000000, 1); + let pending = vec![m]; + + let e = find_pending_message(other, 5, &pending).unwrap_err(); + assert!(e.to_string().contains("no pending message found"), "{e}"); + } + + #[test] + fn find_pending_message_empty_pool() { + let keystore = KeyStore::new(KeyStoreConfig::Memory).unwrap(); + let mut wallet = Wallet::new(keystore); + let addr = wallet.generate_addr(SignatureType::Secp256k1).unwrap(); + + let e = find_pending_message(addr, 0, &[]).unwrap_err(); + assert!(e.to_string().contains("no pending message found"), "{e}"); + } + + #[test] + fn rbf_minimum_premium_known_values() { + let original = TokenAmount::from_atto(100u64); + assert_eq!( + compute_rbf_minimum_premium(&original), + TokenAmount::from_atto(126u64) // 100 + 100*64/256 + 1 = 100 + 25 + 1 = 126 + ); + } + + #[test] + fn rbf_minimum_premium_zero() { + let original = TokenAmount::from_atto(0u64); + assert_eq!( + compute_rbf_minimum_premium(&original), + TokenAmount::from_atto(1u64) + ); + } + + fn make_test_message( + from: Address, + to: Address, + nonce: u64, + gas_limit: u64, + gas_premium: u64, + gas_fee_cap: u64, + ) -> Message { + Message { + version: 0, + from, + to, + sequence: nonce, + value: TokenAmount::default(), + method_num: METHOD_SEND, + params: RawBytes::new(vec![]), + gas_limit, + gas_fee_cap: TokenAmount::from_atto(gas_fee_cap), + gas_premium: TokenAmount::from_atto(gas_premium), + } + } + + #[test] + fn replace_auto_estimated_above_rbf_floor() { + let keystore = KeyStore::new(KeyStoreConfig::Memory).unwrap(); + let mut wallet = Wallet::new(keystore); + let addr = wallet.generate_addr(SignatureType::Secp256k1).unwrap(); + let target = wallet.generate_addr(SignatureType::Secp256k1).unwrap(); + + let original_premium = TokenAmount::from_atto(100u64); + let rbf_floor = compute_rbf_minimum_premium(&original_premium); + + let estimated = make_test_message(addr, target, 5, 2000000, 200, 500); + assert!(estimated.gas_premium > rbf_floor); + + let result = compute_replacement_gas(ReplaceGasInput::Auto { + estimated_msg: estimated.clone(), + original_premium, + }) + .unwrap(); + assert_eq!(result.gas_premium, estimated.gas_premium); + } + + #[test] + fn replace_auto_estimated_below_rbf_floor() { + let keystore = KeyStore::new(KeyStoreConfig::Memory).unwrap(); + let mut wallet = Wallet::new(keystore); + let addr = wallet.generate_addr(SignatureType::Secp256k1).unwrap(); + let target = wallet.generate_addr(SignatureType::Secp256k1).unwrap(); + + let original_premium = TokenAmount::from_atto(1000u64); + let rbf_floor = compute_rbf_minimum_premium(&original_premium); + + let estimated = make_test_message(addr, target, 5, 2000000, 50, 500); + assert!(estimated.gas_premium < rbf_floor); + + let result = compute_replacement_gas(ReplaceGasInput::Auto { + estimated_msg: estimated, + original_premium, + }) + .unwrap(); + assert_eq!(result.gas_premium, rbf_floor); + } + + #[test] + fn replace_manual_valid_premium() { + let keystore = KeyStore::new(KeyStoreConfig::Memory).unwrap(); + let mut wallet = Wallet::new(keystore); + let addr = wallet.generate_addr(SignatureType::Secp256k1).unwrap(); + let target = wallet.generate_addr(SignatureType::Secp256k1).unwrap(); + + let original = make_test_message(addr, target, 5, 1000000, 100, 300); + let result = compute_replacement_gas(ReplaceGasInput::Manual { + gas_premium: Some("200".to_string()), + gas_feecap: Some("600".to_string()), + gas_limit: None, + original_msg: original.clone(), + }) + .unwrap(); + assert_eq!(result.gas_premium, TokenAmount::from_atto(200u64)); + assert_eq!(result.gas_fee_cap, TokenAmount::from_atto(600u64)); + assert_eq!(result.gas_limit, original.gas_limit); + } + + #[test] + fn replace_manual_premium_below_rbf_floor() { + let keystore = KeyStore::new(KeyStoreConfig::Memory).unwrap(); + let mut wallet = Wallet::new(keystore); + let addr = wallet.generate_addr(SignatureType::Secp256k1).unwrap(); + let target = wallet.generate_addr(SignatureType::Secp256k1).unwrap(); + + let original = make_test_message(addr, target, 5, 1000000, 100, 300); + let e = compute_replacement_gas(ReplaceGasInput::Manual { + gas_premium: Some("110".to_string()), + gas_feecap: None, + gas_limit: None, + original_msg: original, + }) + .unwrap_err(); + assert!(e.to_string().contains("below the RBF minimum"), "{e}"); + } + + #[test] + fn replace_manual_no_overrides_keeps_original() { + let keystore = KeyStore::new(KeyStoreConfig::Memory).unwrap(); + let mut wallet = Wallet::new(keystore); + let addr = wallet.generate_addr(SignatureType::Secp256k1).unwrap(); + let target = wallet.generate_addr(SignatureType::Secp256k1).unwrap(); + + let original = make_test_message(addr, target, 5, 1000000, 100, 300); + let result = compute_replacement_gas(ReplaceGasInput::Manual { + gas_premium: None, + gas_feecap: None, + gas_limit: None, + original_msg: original.clone(), + }) + .unwrap(); + assert_eq!(result.gas_premium, original.gas_premium); + assert_eq!(result.gas_fee_cap, original.gas_fee_cap); + assert_eq!(result.gas_limit, original.gas_limit); + } + + #[test] + fn replace_manual_custom_gas_limit() { + let keystore = KeyStore::new(KeyStoreConfig::Memory).unwrap(); + let mut wallet = Wallet::new(keystore); + let addr = wallet.generate_addr(SignatureType::Secp256k1).unwrap(); + let target = wallet.generate_addr(SignatureType::Secp256k1).unwrap(); + + let original = make_test_message(addr, target, 5, 1000000, 100, 300); + let result = compute_replacement_gas(ReplaceGasInput::Manual { + gas_premium: None, + gas_feecap: None, + gas_limit: Some(5000000), + original_msg: original, + }) + .unwrap(); + assert_eq!(result.gas_limit, 5000000); + } } diff --git a/src/message_pool/mod.rs b/src/message_pool/mod.rs index e30066e2449e..009c4a5ccbe7 100644 --- a/src/message_pool/mod.rs +++ b/src/message_pool/mod.rs @@ -12,4 +12,6 @@ pub use self::{ msgpool::{msg_pool::MessagePool, *}, }; +pub(crate) use self::msgpool::{RBF_DENOM, RBF_NUM}; + pub use block_prob::block_probabilities; diff --git a/src/message_pool/msgpool/mod.rs b/src/message_pool/msgpool/mod.rs index 63ed23c8c85f..c1c34545c03a 100644 --- a/src/message_pool/msgpool/mod.rs +++ b/src/message_pool/msgpool/mod.rs @@ -33,8 +33,8 @@ use crate::message_pool::{ }; const REPLACE_BY_FEE_RATIO: f32 = 1.25; -const RBF_NUM: u64 = ((REPLACE_BY_FEE_RATIO - 1f32) * 256f32) as u64; -const RBF_DENOM: u64 = 256; +pub(crate) const RBF_NUM: u64 = ((REPLACE_BY_FEE_RATIO - 1f32) * 256f32) as u64; +pub(crate) const RBF_DENOM: u64 = 256; const BASE_FEE_LOWER_BOUND_FACTOR_CONSERVATIVE: i64 = 100; const BASE_FEE_LOWER_BOUND_FACTOR: i64 = 10; const REPUB_MSG_LIMIT: usize = 30; From 7c9a9ff56976b412d69f7ebf541d1e9fdedc51c1 Mon Sep 17 00:00:00 2001 From: Shashank Date: Mon, 6 Apr 2026 22:50:20 +0530 Subject: [PATCH 3/5] Add calibnet mpool tool check --- .github/workflows/forest.yml | 27 +++++ scripts/tests/calibnet_mpool_check.sh | 168 ++++++++++++++++++++++++++ 2 files changed, 195 insertions(+) create mode 100755 scripts/tests/calibnet_mpool_check.sh diff --git a/.github/workflows/forest.yml b/.github/workflows/forest.yml index 679b8322bdeb..a44355e4f839 100644 --- a/.github/workflows/forest.yml +++ b/.github/workflows/forest.yml @@ -245,6 +245,32 @@ jobs: ./scripts/tests/calibnet_wallet_check.sh "$CALIBNET_WALLET" fi timeout-minutes: ${{ fromJSON(env.SCRIPT_TIMEOUT_MINUTES) }} + calibnet-mpool-check: + needs: + - build-ubuntu + name: Mpool nonce-fix and replace tests + runs-on: ubuntu-24.04 + steps: + - uses: actions/cache@v5 + with: + path: "${{ env.FIL_PROOFS_PARAMETER_CACHE }}" + key: proof-params-keys + - uses: actions/checkout@v6 + - uses: actions/download-artifact@v8 + with: + name: "forest-${{ runner.os }}" + path: ~/.cargo/bin + - name: Set permissions + run: | + chmod +x ~/.cargo/bin/forest* + - name: Mpool nonce-fix and replace check + env: + CALIBNET_WALLET: "${{ secrets.CALIBNET_WALLET }}" + run: | + if [[ "$CALIBNET_WALLET" != "" ]]; then + ./scripts/tests/calibnet_mpool_check.sh "$CALIBNET_WALLET" + fi + timeout-minutes: ${{ fromJSON(env.SCRIPT_TIMEOUT_MINUTES) }} calibnet-delegated-wallet-check: # Disabling this job until message pool nonce calculation is fixed. See: https://github.com/ChainSafe/forest/issues/4899 if: false @@ -631,6 +657,7 @@ jobs: - calibnet-stateless-rpc-check - state-migrations-check - calibnet-wallet-check + - calibnet-mpool-check - calibnet-no-discovery-checks - calibnet-kademlia-checks - calibnet-eth-mapping-check diff --git a/scripts/tests/calibnet_mpool_check.sh b/scripts/tests/calibnet_mpool_check.sh new file mode 100755 index 000000000000..a813b363decf --- /dev/null +++ b/scripts/tests/calibnet_mpool_check.sh @@ -0,0 +1,168 @@ +#!/usr/bin/env bash +# This script tests forest-cli mpool nonce-fix and forest-cli mpool replace commands. +# It requires both `forest` and `forest-cli` to be in the PATH, plus a funded wallet. + +set -euxo pipefail + +source "$(dirname "$0")/harness.sh" + +forest_wallet_init "$@" + +ADDR=$($FOREST_WALLET_PATH list | tail -1 | cut -d ' ' -f1) + +sleep 5s + +# nonce-fix: CLI argument validation +: "nonce-fix: missing --addr should fail" +if $FOREST_CLI_PATH mpool nonce-fix 2>&1; then + echo "FAIL: expected error without --addr" + exit 1 +fi + +: "nonce-fix: manual mode missing --start should fail" +if $FOREST_CLI_PATH mpool nonce-fix --addr "$ADDR" --end 10 2>&1; then + echo "FAIL: expected error without --start" + exit 1 +fi + +: "nonce-fix: manual mode missing --end should fail" +if $FOREST_CLI_PATH mpool nonce-fix --addr "$ADDR" --start 5 2>&1; then + echo "FAIL: expected error without --end" + exit 1 +fi + +: "nonce-fix: end equals start should fail" +if $FOREST_CLI_PATH mpool nonce-fix --addr "$ADDR" --start 5 --end 5 2>&1; then + echo "FAIL: expected error with --end == --start" + exit 1 +fi + +: "nonce-fix: end less than start should fail" +if $FOREST_CLI_PATH mpool nonce-fix --addr "$ADDR" --start 5 --end 3 2>&1; then + echo "FAIL: expected error with --end < --start" + exit 1 +fi + +: "nonce-fix: invalid gas-fee-cap should fail" +if $FOREST_CLI_PATH mpool nonce-fix --addr "$ADDR" --start 0 --end 1 --gas-fee-cap "not-a-number" 2>&1; then + echo "FAIL: expected error with invalid --gas-fee-cap" + exit 1 +fi + +# nonce-fix: auto mode -- no gap +: "nonce-fix: auto mode with no nonce gap" +OUTPUT=$($FOREST_CLI_PATH mpool nonce-fix --addr "$ADDR" --auto) +echo "$OUTPUT" +echo "$OUTPUT" | grep -q "No nonce gap found" + +# nonce-fix: auto mode -- with gap +: "nonce-fix: create a nonce gap then auto-fill it" +NONCE=$($FOREST_CLI_PATH mpool nonce "$ADDR") +echo "Current nonce before gap: $NONCE" + +$FOREST_CLI_PATH mpool nonce-fix --addr "$ADDR" --start "$((NONCE + 2))" --end "$((NONCE + 3))" +sleep 2s + +OUTPUT=$($FOREST_CLI_PATH mpool nonce-fix --addr "$ADDR" --auto) +echo "$OUTPUT" +echo "$OUTPUT" | grep -q "Creating 2 filler messages" + +# nonce-fix: manual mode -- happy path +: "nonce-fix: manual mode creates filler messages" +NONCE=$($FOREST_CLI_PATH mpool nonce "$ADDR") +END=$((NONCE + 2)) +OUTPUT=$($FOREST_CLI_PATH mpool nonce-fix --addr "$ADDR" --start "$NONCE" --end "$END") +echo "$OUTPUT" +echo "$OUTPUT" | grep -q "Creating 2 filler messages" + +# nonce-fix: manual mode -- custom gas-fee-cap +: "nonce-fix: manual mode with custom gas-fee-cap" +NONCE=$($FOREST_CLI_PATH mpool nonce "$ADDR") +END=$((NONCE + 1)) +OUTPUT=$($FOREST_CLI_PATH mpool nonce-fix --addr "$ADDR" --start "$NONCE" --end "$END" --gas-fee-cap "1000000000") +echo "$OUTPUT" +echo "$OUTPUT" | grep -q "Creating 1 filler messages" + +# nonce-fix: verify pending messages +: "nonce-fix: verify filler messages appear in pending" +PENDING=$($FOREST_CLI_PATH mpool pending --from "$ADDR" --cids) +echo "$PENDING" +if [ -z "$PENDING" ]; then + echo "FAIL: expected pending messages from $ADDR" + exit 1 +fi + +# replace: CLI argument validation +: "replace: missing required args should fail" +if $FOREST_CLI_PATH mpool replace 2>&1; then + echo "FAIL: expected error without --from or --cid" + exit 1 +fi + +: "replace: conflicting --cid and --from should fail" +if $FOREST_CLI_PATH mpool replace --cid bafy2bzaceaxm23epjsmh75yvzcecsrbavlmkcxnvuzock6waew7l7piyn2bkji --from "$ADDR" 2>&1; then + echo "FAIL: expected error with conflicting --cid and --from" + exit 1 +fi + +: "replace: --from without --nonce should fail" +if $FOREST_CLI_PATH mpool replace --from "$ADDR" 2>&1; then + echo "FAIL: expected error without --nonce" + exit 1 +fi + +: "replace: non-existent pending message should fail" +if $FOREST_CLI_PATH mpool replace --from "$ADDR" --nonce 999999999 --auto 2>&1; then + echo "FAIL: expected error for non-existent pending message" + exit 1 +fi + +: "replace: invalid --max-fee should fail" +if $FOREST_CLI_PATH mpool replace --from "$ADDR" --nonce 0 --auto --max-fee "abc" 2>&1; then + echo "FAIL: expected error with invalid --max-fee" + exit 1 +fi + +# replace: happy path -- send messages then replace immediately +TARGET=$($FOREST_WALLET_PATH new) +$FOREST_WALLET_PATH set-default "$ADDR" + +: "replace: auto mode by --from and --nonce" +NONCE=$($FOREST_CLI_PATH mpool nonce "$ADDR") +$FOREST_WALLET_PATH send "$TARGET" "100 atto FIL" +OUTPUT=$($FOREST_CLI_PATH mpool replace --from "$ADDR" --nonce "$NONCE" --auto) +echo "$OUTPUT" +echo "$OUTPUT" | grep -q "new message cid:" + +: "replace: manual mode with gas params" +NONCE=$($FOREST_CLI_PATH mpool nonce "$ADDR") +$FOREST_WALLET_PATH send "$TARGET" "100 atto FIL" +OUTPUT=$($FOREST_CLI_PATH mpool replace --from "$ADDR" --nonce "$NONCE" \ + --gas-premium "100000000000" --gas-feecap "1000000000000" --gas-limit 2000000) +echo "$OUTPUT" +echo "$OUTPUT" | grep -q "new message cid:" + +: "replace: auto mode by --cid" +NONCE=$($FOREST_CLI_PATH mpool nonce "$ADDR") +MSG_CID=$($FOREST_WALLET_PATH send "$TARGET" "100 atto FIL") +echo "Message CID for replace: $MSG_CID" +OUTPUT=$($FOREST_CLI_PATH mpool replace --cid "$MSG_CID" --auto) +echo "$OUTPUT" +echo "$OUTPUT" | grep -q "new message cid:" + +: "replace: auto mode with --max-fee" +NONCE=$($FOREST_CLI_PATH mpool nonce "$ADDR") +$FOREST_WALLET_PATH send "$TARGET" "100 atto FIL" +OUTPUT=$($FOREST_CLI_PATH mpool replace --from "$ADDR" --nonce "$NONCE" --auto --max-fee "10000000000000") +echo "$OUTPUT" +echo "$OUTPUT" | grep -q "new message cid:" + +: "replace: manual gas premium below RBF minimum should fail" +NONCE=$($FOREST_CLI_PATH mpool nonce "$ADDR") +$FOREST_WALLET_PATH send "$TARGET" "100 atto FIL" +if $FOREST_CLI_PATH mpool replace --from "$ADDR" --nonce "$NONCE" --gas-premium "1" 2>&1; then + echo "FAIL: expected error with gas premium below RBF minimum" + exit 1 +fi + +: "All mpool nonce-fix and replace tests passed" From 5fa6a4b929266455258d94cdd3a4c62de6487fb5 Mon Sep 17 00:00:00 2001 From: Shashank Date: Wed, 15 Apr 2026 14:56:42 +0530 Subject: [PATCH 4/5] cleanup --- .github/workflows/forest.yml | 27 ----- scripts/tests/calibnet_mpool_check.sh | 168 -------------------------- 2 files changed, 195 deletions(-) delete mode 100755 scripts/tests/calibnet_mpool_check.sh diff --git a/.github/workflows/forest.yml b/.github/workflows/forest.yml index ecf38d0a627a..d04e473e1441 100644 --- a/.github/workflows/forest.yml +++ b/.github/workflows/forest.yml @@ -246,32 +246,6 @@ jobs: ./scripts/tests/calibnet_wallet_check.sh "$CALIBNET_WALLET" fi timeout-minutes: ${{ fromJSON(env.SCRIPT_TIMEOUT_MINUTES) }} - calibnet-mpool-check: - needs: - - build-ubuntu - name: Mpool nonce-fix and replace tests - runs-on: ubuntu-24.04 - steps: - - uses: actions/cache@v5 - with: - path: "${{ env.FIL_PROOFS_PARAMETER_CACHE }}" - key: proof-params-keys - - uses: actions/checkout@v6 - - uses: actions/download-artifact@v8 - with: - name: "forest-${{ runner.os }}" - path: ~/.cargo/bin - - name: Set permissions - run: | - chmod +x ~/.cargo/bin/forest* - - name: Mpool nonce-fix and replace check - env: - CALIBNET_WALLET: "${{ secrets.CALIBNET_WALLET }}" - run: | - if [[ "$CALIBNET_WALLET" != "" ]]; then - ./scripts/tests/calibnet_mpool_check.sh "$CALIBNET_WALLET" - fi - timeout-minutes: ${{ fromJSON(env.SCRIPT_TIMEOUT_MINUTES) }} calibnet-delegated-wallet-check: concurrency: group: calibnet-wallet-tests @@ -661,7 +635,6 @@ jobs: - state-migrations-check - calibnet-wallet-check - calibnet-delegated-wallet-check - - calibnet-mpool-check - calibnet-no-discovery-checks - calibnet-kademlia-checks - calibnet-eth-mapping-check diff --git a/scripts/tests/calibnet_mpool_check.sh b/scripts/tests/calibnet_mpool_check.sh deleted file mode 100755 index a813b363decf..000000000000 --- a/scripts/tests/calibnet_mpool_check.sh +++ /dev/null @@ -1,168 +0,0 @@ -#!/usr/bin/env bash -# This script tests forest-cli mpool nonce-fix and forest-cli mpool replace commands. -# It requires both `forest` and `forest-cli` to be in the PATH, plus a funded wallet. - -set -euxo pipefail - -source "$(dirname "$0")/harness.sh" - -forest_wallet_init "$@" - -ADDR=$($FOREST_WALLET_PATH list | tail -1 | cut -d ' ' -f1) - -sleep 5s - -# nonce-fix: CLI argument validation -: "nonce-fix: missing --addr should fail" -if $FOREST_CLI_PATH mpool nonce-fix 2>&1; then - echo "FAIL: expected error without --addr" - exit 1 -fi - -: "nonce-fix: manual mode missing --start should fail" -if $FOREST_CLI_PATH mpool nonce-fix --addr "$ADDR" --end 10 2>&1; then - echo "FAIL: expected error without --start" - exit 1 -fi - -: "nonce-fix: manual mode missing --end should fail" -if $FOREST_CLI_PATH mpool nonce-fix --addr "$ADDR" --start 5 2>&1; then - echo "FAIL: expected error without --end" - exit 1 -fi - -: "nonce-fix: end equals start should fail" -if $FOREST_CLI_PATH mpool nonce-fix --addr "$ADDR" --start 5 --end 5 2>&1; then - echo "FAIL: expected error with --end == --start" - exit 1 -fi - -: "nonce-fix: end less than start should fail" -if $FOREST_CLI_PATH mpool nonce-fix --addr "$ADDR" --start 5 --end 3 2>&1; then - echo "FAIL: expected error with --end < --start" - exit 1 -fi - -: "nonce-fix: invalid gas-fee-cap should fail" -if $FOREST_CLI_PATH mpool nonce-fix --addr "$ADDR" --start 0 --end 1 --gas-fee-cap "not-a-number" 2>&1; then - echo "FAIL: expected error with invalid --gas-fee-cap" - exit 1 -fi - -# nonce-fix: auto mode -- no gap -: "nonce-fix: auto mode with no nonce gap" -OUTPUT=$($FOREST_CLI_PATH mpool nonce-fix --addr "$ADDR" --auto) -echo "$OUTPUT" -echo "$OUTPUT" | grep -q "No nonce gap found" - -# nonce-fix: auto mode -- with gap -: "nonce-fix: create a nonce gap then auto-fill it" -NONCE=$($FOREST_CLI_PATH mpool nonce "$ADDR") -echo "Current nonce before gap: $NONCE" - -$FOREST_CLI_PATH mpool nonce-fix --addr "$ADDR" --start "$((NONCE + 2))" --end "$((NONCE + 3))" -sleep 2s - -OUTPUT=$($FOREST_CLI_PATH mpool nonce-fix --addr "$ADDR" --auto) -echo "$OUTPUT" -echo "$OUTPUT" | grep -q "Creating 2 filler messages" - -# nonce-fix: manual mode -- happy path -: "nonce-fix: manual mode creates filler messages" -NONCE=$($FOREST_CLI_PATH mpool nonce "$ADDR") -END=$((NONCE + 2)) -OUTPUT=$($FOREST_CLI_PATH mpool nonce-fix --addr "$ADDR" --start "$NONCE" --end "$END") -echo "$OUTPUT" -echo "$OUTPUT" | grep -q "Creating 2 filler messages" - -# nonce-fix: manual mode -- custom gas-fee-cap -: "nonce-fix: manual mode with custom gas-fee-cap" -NONCE=$($FOREST_CLI_PATH mpool nonce "$ADDR") -END=$((NONCE + 1)) -OUTPUT=$($FOREST_CLI_PATH mpool nonce-fix --addr "$ADDR" --start "$NONCE" --end "$END" --gas-fee-cap "1000000000") -echo "$OUTPUT" -echo "$OUTPUT" | grep -q "Creating 1 filler messages" - -# nonce-fix: verify pending messages -: "nonce-fix: verify filler messages appear in pending" -PENDING=$($FOREST_CLI_PATH mpool pending --from "$ADDR" --cids) -echo "$PENDING" -if [ -z "$PENDING" ]; then - echo "FAIL: expected pending messages from $ADDR" - exit 1 -fi - -# replace: CLI argument validation -: "replace: missing required args should fail" -if $FOREST_CLI_PATH mpool replace 2>&1; then - echo "FAIL: expected error without --from or --cid" - exit 1 -fi - -: "replace: conflicting --cid and --from should fail" -if $FOREST_CLI_PATH mpool replace --cid bafy2bzaceaxm23epjsmh75yvzcecsrbavlmkcxnvuzock6waew7l7piyn2bkji --from "$ADDR" 2>&1; then - echo "FAIL: expected error with conflicting --cid and --from" - exit 1 -fi - -: "replace: --from without --nonce should fail" -if $FOREST_CLI_PATH mpool replace --from "$ADDR" 2>&1; then - echo "FAIL: expected error without --nonce" - exit 1 -fi - -: "replace: non-existent pending message should fail" -if $FOREST_CLI_PATH mpool replace --from "$ADDR" --nonce 999999999 --auto 2>&1; then - echo "FAIL: expected error for non-existent pending message" - exit 1 -fi - -: "replace: invalid --max-fee should fail" -if $FOREST_CLI_PATH mpool replace --from "$ADDR" --nonce 0 --auto --max-fee "abc" 2>&1; then - echo "FAIL: expected error with invalid --max-fee" - exit 1 -fi - -# replace: happy path -- send messages then replace immediately -TARGET=$($FOREST_WALLET_PATH new) -$FOREST_WALLET_PATH set-default "$ADDR" - -: "replace: auto mode by --from and --nonce" -NONCE=$($FOREST_CLI_PATH mpool nonce "$ADDR") -$FOREST_WALLET_PATH send "$TARGET" "100 atto FIL" -OUTPUT=$($FOREST_CLI_PATH mpool replace --from "$ADDR" --nonce "$NONCE" --auto) -echo "$OUTPUT" -echo "$OUTPUT" | grep -q "new message cid:" - -: "replace: manual mode with gas params" -NONCE=$($FOREST_CLI_PATH mpool nonce "$ADDR") -$FOREST_WALLET_PATH send "$TARGET" "100 atto FIL" -OUTPUT=$($FOREST_CLI_PATH mpool replace --from "$ADDR" --nonce "$NONCE" \ - --gas-premium "100000000000" --gas-feecap "1000000000000" --gas-limit 2000000) -echo "$OUTPUT" -echo "$OUTPUT" | grep -q "new message cid:" - -: "replace: auto mode by --cid" -NONCE=$($FOREST_CLI_PATH mpool nonce "$ADDR") -MSG_CID=$($FOREST_WALLET_PATH send "$TARGET" "100 atto FIL") -echo "Message CID for replace: $MSG_CID" -OUTPUT=$($FOREST_CLI_PATH mpool replace --cid "$MSG_CID" --auto) -echo "$OUTPUT" -echo "$OUTPUT" | grep -q "new message cid:" - -: "replace: auto mode with --max-fee" -NONCE=$($FOREST_CLI_PATH mpool nonce "$ADDR") -$FOREST_WALLET_PATH send "$TARGET" "100 atto FIL" -OUTPUT=$($FOREST_CLI_PATH mpool replace --from "$ADDR" --nonce "$NONCE" --auto --max-fee "10000000000000") -echo "$OUTPUT" -echo "$OUTPUT" | grep -q "new message cid:" - -: "replace: manual gas premium below RBF minimum should fail" -NONCE=$($FOREST_CLI_PATH mpool nonce "$ADDR") -$FOREST_WALLET_PATH send "$TARGET" "100 atto FIL" -if $FOREST_CLI_PATH mpool replace --from "$ADDR" --nonce "$NONCE" --gas-premium "1" 2>&1; then - echo "FAIL: expected error with gas premium below RBF minimum" - exit 1 -fi - -: "All mpool nonce-fix and replace tests passed" From f7f12ffe034168b93d73b7f722414ab3265ca9fc Mon Sep 17 00:00:00 2001 From: Shashank Date: Thu, 16 Apr 2026 05:43:03 +0530 Subject: [PATCH 5/5] fix spellcheck --- .config/forest.dic | 4 +++- src/cli/subcommands/mpool_cmd.rs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.config/forest.dic b/.config/forest.dic index 751957b10484..91d1e1e83b3a 100644 --- a/.config/forest.dic +++ b/.config/forest.dic @@ -1,4 +1,4 @@ -270 +272 Algorand/M API's API/SM @@ -189,6 +189,7 @@ precommit preloaded pubsub R2 +RBF README repo/S retag @@ -210,6 +211,7 @@ semver serializable serializer/SM serverless +signable Skellam skippable Sqlx diff --git a/src/cli/subcommands/mpool_cmd.rs b/src/cli/subcommands/mpool_cmd.rs index cf7992008473..ec0f187fdfde 100644 --- a/src/cli/subcommands/mpool_cmd.rs +++ b/src/cli/subcommands/mpool_cmd.rs @@ -51,7 +51,7 @@ pub enum MpoolCommands { }, /// Fill an on-chain nonce gap by pushing signed self-transfer messages. NonceFix { - /// Address to fill nonces for (must be signable by the node's wallet). + /// Address to fill nonce's for (must be signable by the node's wallet). #[arg(long)] addr: StrictAddress, /// Derive the fill range from chain state and the mempool (ignores `--start` / `--end`).