mirror of
https://github.com/logos-co/nomos-node.git
synced 2026-08-27 09:31:10 +00:00
fix(e2e): non-zero gas price (#3083)
Co-authored-by: Youngjoon Lee <5462944+youngjoon-lee@users.noreply.github.com>
This commit is contained in:
co-authored by
Youngjoon Lee
parent
22574c25fa
commit
368f9a4872
@@ -84,8 +84,8 @@ pub fn predict_signed_mantle_tx_size(tx: &MantleTx, context: &MantleTxGasContext
|
||||
calculate_channel_multi_sig_proof_byte_size(channel_withdraw_threshold)
|
||||
}
|
||||
|
||||
// None
|
||||
Op::ChannelDeposit(_) => 0,
|
||||
// ZkSigProof = ZkSignature = Groth16
|
||||
Op::ChannelDeposit(_) => COMPRESSED_PROOF_SIZE,
|
||||
})
|
||||
.sum::<usize>();
|
||||
|
||||
|
||||
@@ -301,11 +301,23 @@ impl CommonHttpClient {
|
||||
self.get::<(), ChainServiceInfo>(request_url, None).await
|
||||
}
|
||||
|
||||
/// Get the current gas prices from the ledger state at the tip.
|
||||
pub async fn gas_prices(&self, base_url: Url) -> Result<GasPricesResponseBody, Error> {
|
||||
let request_url = base_url
|
||||
/// Get the gas prices from the ledger state at `tip`, or at the current
|
||||
/// tip when `tip` is `None`.
|
||||
pub async fn gas_prices(
|
||||
&self,
|
||||
base_url: Url,
|
||||
tip: Option<HeaderId>,
|
||||
) -> Result<GasPricesResponseBody, Error> {
|
||||
let mut request_url = base_url
|
||||
.join(MANTLE_GAS_PRICES.trim_start_matches('/'))
|
||||
.map_err(Error::Url)?;
|
||||
|
||||
if let Some(t) = tip {
|
||||
request_url
|
||||
.query_pairs_mut()
|
||||
.append_pair("tip", &t.to_string());
|
||||
}
|
||||
|
||||
self.get::<(), GasPricesResponseBody>(request_url, None)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ Feature: Fees
|
||||
| node_name | account_index | wallet_name | connected_to |
|
||||
| NODE_1 | 1 | WALLET_1A | |
|
||||
When node "NODE_1" is at height 1 in 180 seconds
|
||||
Then gas prices on node "NODE_1" equal the genesis gas prices
|
||||
Then gas prices on node "NODE_1" at the genesis block equal the genesis gas prices
|
||||
Then I stop all nodes
|
||||
|
||||
@transactions_ci
|
||||
|
||||
@@ -27,7 +27,7 @@ Feature: Transactions
|
||||
| account_index | token_count | token_amount |
|
||||
| 1 | 2 | 1000 |
|
||||
| 2 | 0 | 0 |
|
||||
And we have a sponsored genesis fee account with 2 tokens of 997 value each
|
||||
And we have a sponsored genesis fee account with 2 tokens of 20000 value each
|
||||
And I have a cluster with capacity of 2 nodes
|
||||
And we use IBD peers
|
||||
And all peers must be mode online after startup in 30 seconds
|
||||
@@ -50,7 +50,7 @@ Feature: Transactions
|
||||
| account_index | token_count | token_amount |
|
||||
| 1 | 2 | 1000 |
|
||||
| 2 | 0 | 0 |
|
||||
And we have a sponsored genesis fee account with 2 tokens of 997 value each
|
||||
And we have a sponsored genesis fee account with 2 tokens of 20000 value each
|
||||
And I have a cluster with capacity of 2 nodes
|
||||
And we use IBD peers
|
||||
And all peers must be mode online after startup in 30 seconds
|
||||
@@ -70,7 +70,7 @@ Feature: Transactions
|
||||
Scenario: Many nodes with wallets startup
|
||||
Given the genesis block has the following wallet resources:
|
||||
| account_index | token_count | token_amount |
|
||||
| 1 | 2 | 1000 |
|
||||
| 1 | 2 | 2000 |
|
||||
| 2 | 0 | 0 |
|
||||
| 3 | 0 | 0 |
|
||||
| 4 | 0 | 0 |
|
||||
@@ -132,7 +132,7 @@ Feature: Transactions
|
||||
| NODE_1 | 1 | WALLET_1A | |
|
||||
| NODE_2 | 2 | WALLET_2A | NODE_1 |
|
||||
When node "NODE_1" is at height 2 in 300 seconds
|
||||
When I perform continuous transactions on user wallets with 5 coin split outputs of 1000 LGO, 5 transactions of 900 LGO each for 3 cycles and timeout of 300 seconds
|
||||
When I perform continuous transactions on user wallets with 5 coin split outputs of 2500 LGO, 5 transactions of 900 LGO each for 3 cycles and timeout of 300 seconds
|
||||
Then I stop all nodes
|
||||
|
||||
@local_transactions
|
||||
@@ -170,15 +170,15 @@ Feature: Transactions
|
||||
Scenario: Continuous transactions next wallet with coin split
|
||||
Given the genesis block has the following wallet resources:
|
||||
| account_index | token_count | token_amount |
|
||||
| 1 | 2 | 12000 |
|
||||
| 2 | 2 | 12000 |
|
||||
| 1 | 2 | 50000 |
|
||||
| 2 | 2 | 50000 |
|
||||
And I have a cluster with capacity of 2 nodes
|
||||
And I start nodes with wallet resources:
|
||||
| node_name | account_index | wallet_name | connected_to |
|
||||
| NODE_1 | 1 | WALLET_1A | |
|
||||
| NODE_2 | 2 | WALLET_2A | NODE_1 |
|
||||
When all nodes have at least 2 blocks and converged to within 1 blocks in 300 seconds
|
||||
And I perform 2 coin split transactions for each user wallet with 10 outputs of 1000 LGO each
|
||||
And I perform 2 coin split transactions for each user wallet with 10 outputs of 4000 LGO each
|
||||
And I verify each wallet has minimum 20 outputs "available" in 300 seconds
|
||||
And I perform 3 stress continuous cycles with 20 transactions of 1000 LGO to the next user wallet
|
||||
Then I stop all nodes
|
||||
@@ -220,7 +220,7 @@ Feature: Transactions
|
||||
Scenario: Coin split with many transfers to other
|
||||
Given the genesis block has the following wallet resources:
|
||||
| account_index | token_count | token_amount |
|
||||
| 1 | 4 | 26000 |
|
||||
| 1 | 4 | 30000 |
|
||||
| 2 | 0 | 0 |
|
||||
And I have a cluster with capacity of 2 nodes
|
||||
And I start nodes with wallet resources:
|
||||
@@ -238,7 +238,7 @@ Feature: Transactions
|
||||
And I send 50 transactions of 1000 LGO each from wallet "WALLET_1A" to wallet "WALLET_2A"
|
||||
When wallet "WALLET_2A" has 50 or more outputs in 240 seconds
|
||||
# All outputs accounted for
|
||||
When wallet "WALLET_1A" has 56000 or less LGO in 180 seconds
|
||||
When wallet "WALLET_1A" has 70000 or less LGO in 180 seconds
|
||||
When wallet "WALLET_1A" has 0 or less encumbered outputs in 60 seconds
|
||||
Then I stop all nodes
|
||||
|
||||
@@ -246,10 +246,10 @@ Feature: Transactions
|
||||
Scenario: Two fork chains join later and preserve persisted state wallet balances
|
||||
Given the genesis block has the following wallet resources:
|
||||
| account_index | token_count | token_amount |
|
||||
| 1 | 2 | 1400 |
|
||||
| 2 | 2 | 1400 |
|
||||
| 4 | 2 | 1400 |
|
||||
| 5 | 2 | 1400 |
|
||||
| 1 | 2 | 2500 |
|
||||
| 2 | 2 | 2500 |
|
||||
| 4 | 2 | 2500 |
|
||||
| 5 | 2 | 2500 |
|
||||
| 7 | 0 | 0 |
|
||||
| 8 | 0 | 0 |
|
||||
And I have a cluster with capacity of 5 nodes
|
||||
@@ -305,10 +305,10 @@ Feature: Transactions
|
||||
And wallet "WALLET_2A" has 3 or more outputs in 10 seconds
|
||||
And wallet "WALLET_4A" has 3 or more outputs in 10 seconds
|
||||
And wallet "WALLET_5A" has 3 or more outputs in 10 seconds
|
||||
When wallet "WALLET_1A" has 2100 or less LGO in 10 seconds
|
||||
And wallet "WALLET_2A" has 2100 or less LGO in 10 seconds
|
||||
And wallet "WALLET_4A" has 2100 or less LGO in 10 seconds
|
||||
And wallet "WALLET_5A" has 2100 or less LGO in 10 seconds
|
||||
When wallet "WALLET_1A" has 4300 or less LGO in 10 seconds
|
||||
And wallet "WALLET_2A" has 4300 or less LGO in 10 seconds
|
||||
And wallet "WALLET_4A" has 4300 or less LGO in 10 seconds
|
||||
And wallet "WALLET_5A" has 4300 or less LGO in 10 seconds
|
||||
Then I stop all nodes
|
||||
|
||||
@transactions_ci
|
||||
@@ -317,7 +317,7 @@ Feature: Transactions
|
||||
| account_index | token_count | token_amount |
|
||||
| 1 | 10 | 25000 |
|
||||
| 2 | 0 | 0 |
|
||||
And we have a sponsored genesis fee account with 200 tokens of 100 value each
|
||||
And we have a sponsored genesis fee account with 4 tokens of 20000 value each
|
||||
And I have a cluster with capacity of 2 nodes
|
||||
And I start nodes with wallet resources:
|
||||
| node_name | account_index | wallet_name | connected_to |
|
||||
@@ -346,7 +346,7 @@ Feature: Transactions
|
||||
| account_index | token_count | token_amount |
|
||||
| 1 | 3 | 100000 |
|
||||
| 2 | 0 | 0 |
|
||||
And we have a sponsored genesis fee account with 5 tokens of 100 value each
|
||||
And we have a sponsored genesis fee account with 2 tokens of 20000 value each
|
||||
And I have a cluster with capacity of 2 nodes
|
||||
And I start nodes with wallet resources:
|
||||
| node_name | account_index | wallet_name | connected_to |
|
||||
|
||||
@@ -2,7 +2,6 @@ use std::collections::HashMap;
|
||||
|
||||
use lb_core::mantle::{
|
||||
OpProof, TxHash,
|
||||
gas::GasPrice,
|
||||
ops::{
|
||||
Op,
|
||||
channel::{
|
||||
@@ -11,7 +10,8 @@ use lb_core::mantle::{
|
||||
},
|
||||
},
|
||||
transactions::{
|
||||
GENESIS_STORAGE_GAS_PRICE, GasPrices, MantleTxBuilder, MantleTxContext, MantleTxGasContext,
|
||||
GENESIS_EXECUTION_GAS_PRICE, GENESIS_STORAGE_GAS_PRICE, GasPrices, MantleTxBuilder,
|
||||
MantleTxContext, MantleTxGasContext,
|
||||
},
|
||||
};
|
||||
use lb_key_management_system_service::keys::{Ed25519Key, Ed25519Signature};
|
||||
@@ -27,7 +27,7 @@ pub fn build_inscription_tx_builder(
|
||||
HashMap::new(),
|
||||
HashMap::new(),
|
||||
GasPrices {
|
||||
execution_base_gas_price: GasPrice::new(0),
|
||||
execution_base_gas_price: GENESIS_EXECUTION_GAS_PRICE,
|
||||
storage_gas_price: GENESIS_STORAGE_GAS_PRICE,
|
||||
},
|
||||
),
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use lb_core::mantle::{GenesisTx as _, Utxo};
|
||||
use lb_key_management_system_service::keys::ZkPublicKey;
|
||||
use lb_libp2p::Multiaddr;
|
||||
use lb_node::{UserConfig, config::RunConfig};
|
||||
@@ -347,6 +348,19 @@ pub fn api_url(node: &NodeHttpClient, path: &str) -> Url {
|
||||
.expect("manual-cluster client base URL should join API path")
|
||||
}
|
||||
|
||||
/// UTXOs created by the genesis transfer of the deployment's genesis block.
|
||||
#[must_use]
|
||||
pub fn genesis_wallet_utxos(config: &TopologyConfig) -> Vec<Utxo> {
|
||||
let genesis_tx = config
|
||||
.genesis_block
|
||||
.as_ref()
|
||||
.expect("manual-cluster deployment should include genesis tx")
|
||||
.genesis_tx();
|
||||
let genesis_transfer = genesis_tx.genesis_transfer();
|
||||
|
||||
genesis_transfer.outputs.utxos(genesis_transfer).collect()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn unique_scenario_base_dir(label: &str, scenario_dir_override: Option<PathBuf>) -> PathBuf {
|
||||
let nanos = std::time::SystemTime::now()
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
use std::collections::HashSet;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use lb_common_http_client::{ApiBlock, Error as HttpClientError};
|
||||
use lb_core::mantle::{TxHash, Utxo};
|
||||
use lb_core::mantle::{
|
||||
Op, OpProof, SignedMantleTx, Transaction as _, TxHash, Utxo,
|
||||
gas::MainnetGasConstants,
|
||||
ops::channel::{ChannelId, ChannelKeyIndex},
|
||||
transactions::{GasPrices, MantleTxBuilder, MantleTxContext, MantleTxGasContext},
|
||||
};
|
||||
use lb_testing_framework::{NodeHttpClient, configs::wallet::WalletAccount};
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -9,7 +14,9 @@ use super::{
|
||||
NodeHttpWalletChainSource, WalletChainSource, WalletId, WalletUtxos,
|
||||
chain::state::{TrackedWalletKeys, TrackedWalletKeysError, WalletChainState},
|
||||
};
|
||||
use crate::common::wallet::WalletFundingSource;
|
||||
use crate::common::wallet::{
|
||||
WalletFundingSource, fund_builder_from_wallet_source, transfer_proofs_for_funded_wallet_tx,
|
||||
};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum DirectWalletSourceError {
|
||||
@@ -29,6 +36,64 @@ pub enum WalletFundingSourceFromChainError<FetchError> {
|
||||
FetchBlock(FetchError),
|
||||
}
|
||||
|
||||
/// Build, fund, and sign a single-op transaction.
|
||||
///
|
||||
/// The op fee is paid from the funding wallet (synced from chain), whose
|
||||
/// trailing transfer op gets its own proof. The op proof is built via
|
||||
/// `op_proof` from the funded transaction hash. `withdraw_thresholds` is
|
||||
/// needed by the gas-size predictor for `ChannelWithdraw` ops. Returns the
|
||||
/// signed transaction and its fee at genesis gas prices.
|
||||
#[expect(
|
||||
clippy::implicit_hasher,
|
||||
reason = "The thresholds map is forwarded to MantleTxGasContext, which requires the default hasher."
|
||||
)]
|
||||
pub async fn funded_signed_tx(
|
||||
node: &NodeHttpClient,
|
||||
genesis_utxos: &[Utxo],
|
||||
funding_account: &WalletAccount,
|
||||
withdraw_thresholds: HashMap<ChannelId, ChannelKeyIndex>,
|
||||
op: Op,
|
||||
op_proof: impl FnOnce(TxHash) -> OpProof,
|
||||
) -> (SignedMantleTx, u64) {
|
||||
let funding_source =
|
||||
current_wallet_funding_source(node, genesis_utxos, funding_account.clone())
|
||||
.await
|
||||
.expect("funding wallet source should sync from chain");
|
||||
|
||||
let tx_context = MantleTxContext {
|
||||
gas_context: MantleTxGasContext::new(
|
||||
withdraw_thresholds,
|
||||
HashMap::new(),
|
||||
GasPrices::default(),
|
||||
),
|
||||
leader_reward_amount: 0,
|
||||
};
|
||||
let tx_builder = MantleTxBuilder::new()
|
||||
.push_op(op)
|
||||
.expect("op should fit op bounds");
|
||||
|
||||
let funded_builder = fund_builder_from_wallet_source(&funding_source, &tx_builder, &tx_context)
|
||||
.expect("funding transaction should succeed");
|
||||
let fee = funded_builder
|
||||
.gas_cost::<MainnetGasConstants>(&tx_context)
|
||||
.expect("funded tx gas cost should calculate")
|
||||
.into_inner();
|
||||
|
||||
let mantle_tx = funded_builder.build().expect("funded builder should build");
|
||||
let tx_hash = mantle_tx.hash();
|
||||
|
||||
let mut proofs = vec![op_proof(tx_hash)];
|
||||
proofs.extend(
|
||||
transfer_proofs_for_funded_wallet_tx(&mantle_tx, &funding_account.secret_key)
|
||||
.expect("transfer proofs should build"),
|
||||
);
|
||||
|
||||
let signed_tx =
|
||||
SignedMantleTx::new(mantle_tx, proofs).expect("funded transaction should be valid");
|
||||
|
||||
(signed_tx, fee)
|
||||
}
|
||||
|
||||
pub async fn current_wallet_funding_source(
|
||||
client: &NodeHttpClient,
|
||||
genesis_utxos: &[Utxo],
|
||||
|
||||
@@ -30,7 +30,7 @@ pub(crate) use funding::{
|
||||
};
|
||||
pub use funding_from_chain::{
|
||||
DirectWalletSourceError, WalletFundingSourceFromChainError, current_wallet_funding_source,
|
||||
wallet_funding_source_from_chain, wallet_utxos_from_chain,
|
||||
funded_signed_tx, wallet_funding_source_from_chain, wallet_utxos_from_chain,
|
||||
};
|
||||
pub use ids::{WalletChainSourceId, WalletId, wallet_id_for_chain_source};
|
||||
pub use tracked::{
|
||||
@@ -42,7 +42,7 @@ pub use tracked_wallet::{TrackedWalletState, WalletBalance, WalletOutputState, W
|
||||
pub use transaction::{
|
||||
PreparedWalletTransaction, SignedWalletTransaction, WalletTransactionError,
|
||||
WalletTransactionIntent, fund_builder_from_wallet_source, prepare_wallet_transaction,
|
||||
wallet_state_from_utxos,
|
||||
transfer_proofs_for_funded_wallet_tx, wallet_state_from_utxos,
|
||||
};
|
||||
pub(crate) use transaction::{
|
||||
PreparedWalletTransactionWorkItem, finalize_prepared_wallet_transaction,
|
||||
|
||||
@@ -335,25 +335,39 @@ fn funding_delta_for_chunked_builder(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use lb_core::mantle::ops::channel::inscribe::Inscription;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use lb_core::mantle::{
|
||||
ops::channel::{
|
||||
ChannelId, MsgId,
|
||||
inscribe::{Inscription, InscriptionOp},
|
||||
},
|
||||
transactions::{GasPrices, MantleTxGasContext},
|
||||
};
|
||||
use lb_key_management_system_service::keys::Ed25519Key;
|
||||
use lb_testing_framework::configs::wallet::WalletAccount;
|
||||
|
||||
use super::*;
|
||||
use crate::common::mantle_inscription::{
|
||||
build_inscription_tx_builder, channel_id_for_payload_size,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn zero_cost_wallet_transaction_still_uses_funding_input() {
|
||||
let payload_size = 1024;
|
||||
let signing_key = Ed25519Key::from_bytes(&[0u8; 32]);
|
||||
let (tx_builder, context) = build_inscription_tx_builder(
|
||||
Inscription::new_unchecked(vec![0xab; payload_size]),
|
||||
&signing_key,
|
||||
channel_id_for_payload_size(payload_size),
|
||||
None,
|
||||
);
|
||||
let context = MantleTxContext {
|
||||
gas_context: MantleTxGasContext::new(
|
||||
HashMap::new(),
|
||||
HashMap::new(),
|
||||
GasPrices::new(0, 0),
|
||||
),
|
||||
leader_reward_amount: 0,
|
||||
};
|
||||
let tx_builder = MantleTxBuilder::new()
|
||||
.push_op(Op::ChannelInscribe(InscriptionOp {
|
||||
channel_id: ChannelId::from([0xAA; 32]),
|
||||
inscription: Inscription::new_unchecked(vec![0xab; 1024]),
|
||||
parent: MsgId::root(),
|
||||
signer: signing_key.public_key(),
|
||||
}))
|
||||
.expect("inscription test builder should fit op bounds");
|
||||
assert_eq!(
|
||||
tx_builder
|
||||
.funding_delta::<MainnetGasConstants>(&context)
|
||||
|
||||
@@ -4,7 +4,10 @@ use std::collections::HashMap;
|
||||
|
||||
use lb_core::mantle::{
|
||||
Note, Op,
|
||||
transactions::{GasPrices, MantleTxBuilder, MantleTxContext, MantleTxGasContext},
|
||||
transactions::{
|
||||
GENESIS_EXECUTION_GAS_PRICE, GasPrices, MantleTxBuilder, MantleTxContext,
|
||||
MantleTxGasContext,
|
||||
},
|
||||
};
|
||||
use lb_key_management_system_service::keys::ZkPublicKey;
|
||||
|
||||
@@ -47,7 +50,7 @@ impl WalletTransactionIntent {
|
||||
gas_context: MantleTxGasContext::new(
|
||||
HashMap::new(),
|
||||
HashMap::new(),
|
||||
GasPrices::new(0, storage_gas_price),
|
||||
GasPrices::new(GENESIS_EXECUTION_GAS_PRICE.into_inner(), storage_gas_price),
|
||||
),
|
||||
..MantleTxContext::default()
|
||||
};
|
||||
|
||||
@@ -21,3 +21,4 @@ pub use prepare::{
|
||||
};
|
||||
pub use prepared::PreparedWalletTransaction;
|
||||
pub use signed::SignedWalletTransaction;
|
||||
pub use signing::transfer_proofs_for_funded_wallet_tx;
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use lb_core::mantle::{
|
||||
AuthenticatedMantleTx as _, NoteId, Op, OpProof, SignedMantleTx, TxHash,
|
||||
AuthenticatedMantleTx as _, MantleTx, NoteId, Op, OpProof, SignedMantleTx, Transaction as _,
|
||||
TxHash,
|
||||
gas::MainnetGasConstants,
|
||||
transactions::{MantleTxBuilder, MantleTxContext},
|
||||
};
|
||||
@@ -41,6 +42,34 @@ pub(super) fn sign_prepared_wallet_transaction(
|
||||
))
|
||||
}
|
||||
|
||||
/// Build one `ZkSig` proof per transfer op in a funded transaction, signing
|
||||
/// every input with the same wallet key. Suitable for transactions whose
|
||||
/// funding inputs all come from a single wallet account.
|
||||
pub fn transfer_proofs_for_funded_wallet_tx(
|
||||
tx: &MantleTx,
|
||||
signing_key: &ZkKey,
|
||||
) -> Result<Vec<OpProof>, WalletTransactionError> {
|
||||
let tx_hash = tx.hash();
|
||||
tx.ops()
|
||||
.iter()
|
||||
.filter_map(|op| match op {
|
||||
Op::Transfer(transfer_op) => Some(transfer_op),
|
||||
_ => None,
|
||||
})
|
||||
.map(|transfer_op| {
|
||||
let signing_keys = transfer_op
|
||||
.inputs
|
||||
.iter()
|
||||
.map(|_| signing_key.clone())
|
||||
.collect::<Vec<_>>();
|
||||
Ok(OpProof::ZkSig(ZkKey::multi_sign(
|
||||
&signing_keys,
|
||||
&tx_hash.to_fr(),
|
||||
)?))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(super) fn build_transfer_proofs(
|
||||
ops: &[Op],
|
||||
tx_hash: &TxHash,
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::{
|
||||
num::NonZero,
|
||||
};
|
||||
|
||||
use lb_core::mantle::{NoteId, Utxo};
|
||||
use lb_core::mantle::{NoteId, Utxo, transactions::GENESIS_STORAGE_GAS_PRICE};
|
||||
use lb_testing_framework::configs::wallet::WalletAccount;
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -14,7 +14,7 @@ use crate::{
|
||||
cucumber::{error::StepError, wallet::WalletStateView},
|
||||
};
|
||||
|
||||
pub const DEFAULT_STORAGE_GAS_PRICE: u64 = 0;
|
||||
pub const DEFAULT_STORAGE_GAS_PRICE: u64 = GENESIS_STORAGE_GAS_PRICE.into_inner();
|
||||
pub const SCENARIO_FEE_ACCOUNT_NAME: &str = "__SCENARIO_FEE_ACCOUNT__";
|
||||
|
||||
const SCENARIO_FEE_ACCOUNT_INDEX: u64 = 1 << 63;
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use cucumber::{Parameter, gherkin::Step, then};
|
||||
use lb_core::mantle::{
|
||||
gas::GasPrice,
|
||||
transactions::genesis_tx::{GENESIS_EXECUTION_GAS_PRICE, GENESIS_STORAGE_GAS_PRICE},
|
||||
use lb_core::{
|
||||
header::HeaderId,
|
||||
mantle::{
|
||||
gas::GasPrice,
|
||||
transactions::genesis_tx::{GENESIS_EXECUTION_GAS_PRICE, GENESIS_STORAGE_GAS_PRICE},
|
||||
},
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
@@ -56,6 +59,43 @@ async fn step_gas_prices_equal_reference(
|
||||
world,
|
||||
step,
|
||||
&node_name,
|
||||
None,
|
||||
expected_execution,
|
||||
expected_storage,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[then(
|
||||
expr = "gas prices on node {string} at the genesis block equal the {gas_prices_reference} \
|
||||
gas prices"
|
||||
)]
|
||||
#[expect(
|
||||
clippy::needless_pass_by_ref_mut,
|
||||
reason = "cucumber step entrypoints must take `&mut World`"
|
||||
)]
|
||||
async fn step_gas_prices_at_genesis_equal_reference(
|
||||
world: &mut CucumberWorld,
|
||||
step: &Step,
|
||||
node_name: String,
|
||||
reference: GasPricesReference,
|
||||
) -> StepResult {
|
||||
let genesis_block_id = world
|
||||
.genesis_block_id
|
||||
.ok_or_else(|| StepError::LogicalError {
|
||||
message: format!(
|
||||
"Step `{}` error: genesis block id is not available for this cluster",
|
||||
step.value
|
||||
),
|
||||
})?;
|
||||
|
||||
let (expected_execution, expected_storage) = reference.expected();
|
||||
|
||||
assert_gas_prices(
|
||||
world,
|
||||
step,
|
||||
&node_name,
|
||||
Some(genesis_block_id),
|
||||
expected_execution,
|
||||
expected_storage,
|
||||
)
|
||||
@@ -78,6 +118,7 @@ async fn step_gas_prices_are(
|
||||
world,
|
||||
step,
|
||||
&node_name,
|
||||
None,
|
||||
GasPrice::new(execution),
|
||||
GasPrice::new(storage),
|
||||
)
|
||||
@@ -88,13 +129,14 @@ async fn assert_gas_prices(
|
||||
world: &CucumberWorld,
|
||||
step: &Step,
|
||||
node_name: &str,
|
||||
tip: Option<HeaderId>,
|
||||
expected_execution: GasPrice,
|
||||
expected_storage: GasPrice,
|
||||
) -> StepResult {
|
||||
let client = world.resolve_node_http_client(node_name)?;
|
||||
|
||||
let gas_prices = client
|
||||
.gas_prices()
|
||||
.gas_prices(tip)
|
||||
.await
|
||||
.map_err(|source| StepError::StepFail {
|
||||
message: format!(
|
||||
|
||||
@@ -98,6 +98,7 @@ pub fn build_manual_cluster_deployment(
|
||||
crate::cucumber::steps::manual_nodes::utils::genesis_block_utxos(
|
||||
&genesis_block.genesis_tx(),
|
||||
);
|
||||
world.genesis_block_id = Some(genesis_block.header().id());
|
||||
}
|
||||
|
||||
Ok(deployment)
|
||||
@@ -127,6 +128,7 @@ fn build_devnet_manual_cluster_deployment(
|
||||
// settings, so locally generated genesis outputs are not meaningful for
|
||||
// wallet tracking.
|
||||
world.genesis_block_utxos.clear();
|
||||
world.genesis_block_id = None;
|
||||
world.wallet_accounts.clear();
|
||||
world.node_provisioned_wallet_pks.clear();
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ use cucumber::World;
|
||||
use derivative::Derivative;
|
||||
use lb_core::{
|
||||
codec::DeserializeOp as _,
|
||||
header::HeaderId,
|
||||
mantle::{
|
||||
SignedMantleTx, TxHash, Utxo, Value,
|
||||
ops::channel::{
|
||||
@@ -855,6 +856,9 @@ pub struct CucumberWorld {
|
||||
/// Manual: List of genesis block UTXOs allocated in the genesis
|
||||
/// configuration.
|
||||
pub genesis_block_utxos: Vec<Utxo>,
|
||||
/// Manual: Header id of the locally generated genesis block, when the
|
||||
/// cluster deployment carries one.
|
||||
pub genesis_block_id: Option<HeaderId>,
|
||||
/// Manual: Optional local cluster instance for scenarios that use the local
|
||||
/// deployer.
|
||||
#[derivative(Default(value = "None"))]
|
||||
@@ -1045,6 +1049,7 @@ impl Debug for CucumberWorld {
|
||||
"genesis_block_utxos",
|
||||
&format!("{:?}", self.genesis_block_utxos),
|
||||
)
|
||||
.field("genesis_block_id", &self.genesis_block_id)
|
||||
.field("local_cluster", {
|
||||
if self.local_cluster.is_some() {
|
||||
&"Has LbcManualCluster"
|
||||
@@ -2025,6 +2030,11 @@ impl CucumberWorld {
|
||||
format!("{:?}", FullDebugInfo(self))
|
||||
}
|
||||
|
||||
#[expect(
|
||||
clippy::too_many_lines,
|
||||
reason = "Flat field-by-field dump of the whole world state; splitting it would not \
|
||||
improve readability"
|
||||
)]
|
||||
pub fn full_debug_info(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let wallet_diagnostics = self
|
||||
.wallet_diagnostics_for_debug()
|
||||
@@ -2054,6 +2064,7 @@ impl CucumberWorld {
|
||||
"genesis_block_utxos",
|
||||
&format!("{:?}", self.genesis_block_utxos),
|
||||
)
|
||||
.field("genesis_block_id", &self.genesis_block_id)
|
||||
.field("local_cluster", {
|
||||
if self.local_cluster.is_some() {
|
||||
&"Has LbcManualCluster"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{num::NonZero, path::PathBuf, time::Duration};
|
||||
use std::{collections::HashMap, num::NonZero, path::PathBuf, time::Duration};
|
||||
|
||||
use futures::StreamExt as _;
|
||||
use lb_common_http_client::ProcessedBlockEvent;
|
||||
@@ -6,7 +6,7 @@ use lb_core::{
|
||||
events::{Event, Events, TxEvent, TxEventPayload},
|
||||
header::HeaderId,
|
||||
mantle::{
|
||||
GenesisTx as _, MantleTx, Note, NoteId, OpProof, SignedMantleTx, Transaction as _, TxHash,
|
||||
GenesisTx as _, Note, NoteId, OpProof, Transaction as _, TxHash,
|
||||
gas::GasCost,
|
||||
ledger::{Inputs, Outputs},
|
||||
ops::{
|
||||
@@ -31,9 +31,12 @@ use lb_testing_framework::{
|
||||
};
|
||||
use lb_utils::math::NonNegativeRatio;
|
||||
use logos_blockchain_tests::{
|
||||
common::manual_cluster::{
|
||||
ManualNodeLayout, api_url, get_wallet_balance, start_local_manual_cluster_with_layout,
|
||||
wait_for_nodes_height,
|
||||
common::{
|
||||
manual_cluster::{
|
||||
ManualNodeLayout, api_url, genesis_wallet_utxos, get_wallet_balance,
|
||||
start_local_manual_cluster_with_layout, wait_for_nodes_height,
|
||||
},
|
||||
wallet::funded_signed_tx,
|
||||
},
|
||||
cucumber::defaults::E2E_ARTIFACTS_DIR,
|
||||
};
|
||||
@@ -41,6 +44,14 @@ use serial_test::serial;
|
||||
use testing_framework_core::scenario::DynError;
|
||||
use tokio::time::{sleep, timeout};
|
||||
|
||||
/// Generous cap on channel transaction fees at genesis gas prices; actual fees
|
||||
/// are a few hundred gas units for these small transactions.
|
||||
const MAX_CHANNEL_TX_FEE: u64 = 10_000;
|
||||
/// Covers all channel transaction fees in these tests with wide margin while
|
||||
/// staying small relative to leader notes: genesis total stake is the sum of
|
||||
/// all note values, so large fee notes dilute leader election.
|
||||
const FEE_NOTE_AMOUNT: u64 = 50_000;
|
||||
|
||||
/// End-to-end test for the channel deposit flow:
|
||||
///
|
||||
/// 1. Spawn validators that produce blocks.
|
||||
@@ -55,7 +66,8 @@ use tokio::time::{sleep, timeout};
|
||||
#[serial]
|
||||
async fn channel_deposit() {
|
||||
let deposit_amount = 1;
|
||||
let (wallet_config, funding_pk) = channel_deposit_wallet_config(deposit_amount, 100);
|
||||
let (wallet_config, funding_pk, _funding_account) =
|
||||
channel_deposit_wallet_config(deposit_amount, FEE_NOTE_AMOUNT);
|
||||
let (base, nodes) = start_local_manual_cluster_with_layout(
|
||||
"channel-deposit",
|
||||
"mantle-channel",
|
||||
@@ -121,7 +133,7 @@ async fn channel_deposit() {
|
||||
deposit: deposit_op.clone(),
|
||||
change_public_key: funding_pk,
|
||||
funding_public_keys: vec![funding_pk],
|
||||
max_tx_fee: GasCost::new(10),
|
||||
max_tx_fee: GasCost::new(MAX_CHANNEL_TX_FEE),
|
||||
};
|
||||
let response = reqwest::Client::new()
|
||||
.post(api_url(&validator.client, "channel/deposit"))
|
||||
@@ -181,10 +193,10 @@ async fn channel_deposit() {
|
||||
assert_eq!(metadata, deposit_op.metadata);
|
||||
|
||||
let balance_after = get_wallet_balance(&validator.client, funding_pk).await;
|
||||
assert_eq!(
|
||||
balance_after,
|
||||
balance_before - deposit_amount,
|
||||
"wallet balance should decrease after deposit: before={balance_before}, after={balance_after}, deposit_amount={deposit_amount}",
|
||||
let spent = balance_before - balance_after;
|
||||
assert!(
|
||||
spent >= deposit_amount && spent <= deposit_amount + MAX_CHANNEL_TX_FEE,
|
||||
"wallet balance should decrease by deposit plus fee: before={balance_before}, after={balance_after}, deposit_amount={deposit_amount}",
|
||||
);
|
||||
|
||||
let channel_balance_after = get_channel_balance(&validator.client, channel_id).await;
|
||||
@@ -208,8 +220,9 @@ async fn channel_deposit() {
|
||||
async fn channel_withdraw_updates_wallet_balance() {
|
||||
let deposit_amount = 5;
|
||||
let withdraw_amount = 2;
|
||||
let (wallet_config, funding_pk) = channel_deposit_wallet_config(deposit_amount, 100);
|
||||
let (_base, nodes) = start_local_manual_cluster_with_layout(
|
||||
let (wallet_config, funding_pk, funding_account) =
|
||||
channel_deposit_wallet_config(deposit_amount, FEE_NOTE_AMOUNT);
|
||||
let (base, nodes) = start_local_manual_cluster_with_layout(
|
||||
"channel-withdraw-wallet-balance",
|
||||
"mantle-channel",
|
||||
DeploymentBuilder::new(
|
||||
@@ -238,9 +251,33 @@ async fn channel_withdraw_updates_wallet_balance() {
|
||||
)
|
||||
.await;
|
||||
|
||||
let genesis_utxos = genesis_wallet_utxos(&base.deployment().config);
|
||||
let initial_balance = get_wallet_balance(&validator.client, funding_pk).await;
|
||||
|
||||
let channel_id = ChannelId::from([42; 32]);
|
||||
let channel_signing_key = Ed25519Key::from_bytes(&[7; 32]);
|
||||
let signed_inscription_tx = signed_channel_inscription(channel_id, &channel_signing_key);
|
||||
let inscription_op = InscriptionOp {
|
||||
channel_id,
|
||||
inscription: b"channel withdraw wallet balance test"
|
||||
.to_vec()
|
||||
.try_into()
|
||||
.expect("inscription payload should fit"),
|
||||
parent: MsgId::root(),
|
||||
signer: channel_signing_key.public_key(),
|
||||
};
|
||||
let (signed_inscription_tx, inscription_fee) = funded_signed_tx(
|
||||
&validator.client,
|
||||
&genesis_utxos,
|
||||
&funding_account,
|
||||
HashMap::new(),
|
||||
Op::ChannelInscribe(inscription_op),
|
||||
|tx_hash| {
|
||||
OpProof::Ed25519Sig(
|
||||
channel_signing_key.sign_payload(tx_hash.as_signing_bytes().as_ref()),
|
||||
)
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut block_stream = validator.client.blocks_stream().await.unwrap();
|
||||
let inscription_tx_hash = signed_inscription_tx.hash();
|
||||
@@ -253,12 +290,32 @@ async fn channel_withdraw_updates_wallet_balance() {
|
||||
|
||||
wait_for_tx_inclusion(&mut block_stream, inscription_tx_hash, "inscription").await;
|
||||
|
||||
let balance_after_inscription = wait_for_wallet_balance(
|
||||
&validator.client,
|
||||
funding_pk,
|
||||
initial_balance - inscription_fee,
|
||||
Duration::from_mins(2),
|
||||
)
|
||||
.await;
|
||||
|
||||
let (deposit_op, deposit_tx_hash) =
|
||||
submit_channel_deposit(&validator.client, channel_id, funding_pk, deposit_amount).await;
|
||||
wait_for_tx_inclusion(&mut block_stream, deposit_tx_hash, "deposit").await;
|
||||
|
||||
let balance_after_deposit =
|
||||
wait_for_wallet_balance(&validator.client, funding_pk, 100, Duration::from_mins(2)).await;
|
||||
// The deposit is funded server-side, so its exact fee is unknown here; it
|
||||
// may even be zero once gas prices decay, so wait only for the deposit
|
||||
// amount to leave the wallet.
|
||||
let balance_after_deposit = wait_for_wallet_balance_at_most(
|
||||
&validator.client,
|
||||
funding_pk,
|
||||
balance_after_inscription - deposit_amount,
|
||||
Duration::from_mins(2),
|
||||
)
|
||||
.await;
|
||||
assert!(
|
||||
balance_after_deposit >= balance_after_inscription - deposit_amount - MAX_CHANNEL_TX_FEE,
|
||||
"deposit fee should stay within the max fee bound: after_inscription={balance_after_inscription}, after_deposit={balance_after_deposit}",
|
||||
);
|
||||
let channel_balance_after_deposit = get_channel_balance(&validator.client, channel_id).await;
|
||||
assert_eq!(
|
||||
channel_balance_after_deposit, deposit_amount,
|
||||
@@ -270,7 +327,26 @@ async fn channel_withdraw_updates_wallet_balance() {
|
||||
outputs: Outputs::new([Note::new(withdraw_amount, funding_pk)]),
|
||||
withdraw_nonce: 0,
|
||||
};
|
||||
let signed_withdraw_tx = signed_channel_withdraw(withdraw.clone(), &channel_signing_key);
|
||||
let (signed_withdraw_tx, withdraw_fee) = funded_signed_tx(
|
||||
&validator.client,
|
||||
&genesis_utxos,
|
||||
&funding_account,
|
||||
HashMap::from([(channel_id, 1)]),
|
||||
Op::ChannelWithdraw(withdraw.clone()),
|
||||
|tx_hash| {
|
||||
OpProof::ChannelMultiSigProof(
|
||||
ChannelMultiSigProof::try_new(
|
||||
[IndexedSignature::new(
|
||||
0,
|
||||
channel_signing_key.sign_payload(tx_hash.as_signing_bytes().as_ref()),
|
||||
)]
|
||||
.into(),
|
||||
)
|
||||
.expect("withdraw proof should be valid"),
|
||||
)
|
||||
},
|
||||
)
|
||||
.await;
|
||||
let withdraw_tx_hash = signed_withdraw_tx.hash();
|
||||
|
||||
validator
|
||||
@@ -281,17 +357,17 @@ async fn channel_withdraw_updates_wallet_balance() {
|
||||
|
||||
wait_for_tx_inclusion(&mut block_stream, withdraw_tx_hash, "withdraw").await;
|
||||
|
||||
let expected_after_withdraw = balance_after_deposit + withdraw_amount - withdraw_fee;
|
||||
let balance_after_withdraw = wait_for_wallet_balance(
|
||||
&validator.client,
|
||||
funding_pk,
|
||||
balance_after_deposit + withdraw_amount,
|
||||
expected_after_withdraw,
|
||||
Duration::from_mins(2),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(
|
||||
balance_after_withdraw,
|
||||
balance_after_deposit + withdraw_amount,
|
||||
"wallet balance should increase after withdraw: before={balance_after_deposit}, after={balance_after_withdraw}, withdraw_amount={withdraw_amount}",
|
||||
balance_after_withdraw, expected_after_withdraw,
|
||||
"wallet balance should increase by withdraw minus fee: before={balance_after_deposit}, after={balance_after_withdraw}, withdraw_amount={withdraw_amount}, withdraw_fee={withdraw_fee}",
|
||||
);
|
||||
|
||||
let channel_balance_after_withdraw = get_channel_balance(&validator.client, channel_id).await;
|
||||
@@ -307,7 +383,7 @@ async fn channel_withdraw_updates_wallet_balance() {
|
||||
fn channel_deposit_wallet_config(
|
||||
deposit_note_amount: u64,
|
||||
fee_note_amount: u64,
|
||||
) -> (WalletConfig, ZkPublicKey) {
|
||||
) -> (WalletConfig, ZkPublicKey, WalletAccount) {
|
||||
let deposit_note = WalletAccount::deterministic(0, deposit_note_amount, false)
|
||||
.expect("deposit wallet should be valid");
|
||||
|
||||
@@ -319,8 +395,13 @@ fn channel_deposit_wallet_config(
|
||||
)
|
||||
.expect("fee wallet should be valid");
|
||||
let funding_pk = deposit_note.public_key();
|
||||
let funding_account = deposit_note.clone();
|
||||
|
||||
(WalletConfig::new(vec![deposit_note, fee_note]), funding_pk)
|
||||
(
|
||||
WalletConfig::new(vec![deposit_note, fee_note]),
|
||||
funding_pk,
|
||||
funding_account,
|
||||
)
|
||||
}
|
||||
|
||||
fn channel_test_config(mut config: RunConfig) -> RunConfig {
|
||||
@@ -353,7 +434,7 @@ async fn submit_channel_deposit(
|
||||
deposit: deposit_op.clone(),
|
||||
change_public_key: funding_pk,
|
||||
funding_public_keys: vec![funding_pk],
|
||||
max_tx_fee: GasCost::new(10),
|
||||
max_tx_fee: GasCost::new(MAX_CHANNEL_TX_FEE),
|
||||
};
|
||||
let response = reqwest::Client::new()
|
||||
.post(api_url(node, "channel/deposit"))
|
||||
@@ -378,47 +459,6 @@ async fn submit_channel_deposit(
|
||||
(deposit_op, deposit_tx_hash)
|
||||
}
|
||||
|
||||
fn signed_channel_inscription(channel_id: ChannelId, signing_key: &Ed25519Key) -> SignedMantleTx {
|
||||
let inscription = InscriptionOp {
|
||||
channel_id,
|
||||
inscription: b"channel withdraw wallet balance test"
|
||||
.to_vec()
|
||||
.try_into()
|
||||
.expect("inscription payload should fit"),
|
||||
parent: MsgId::root(),
|
||||
signer: signing_key.public_key(),
|
||||
};
|
||||
let mantle_tx = MantleTx([Op::ChannelInscribe(inscription)].into());
|
||||
let tx_hash = mantle_tx.hash();
|
||||
let inscription_proof =
|
||||
OpProof::Ed25519Sig(signing_key.sign_payload(tx_hash.as_signing_bytes().as_ref()));
|
||||
|
||||
SignedMantleTx::new(mantle_tx, vec![inscription_proof])
|
||||
.expect("inscription transaction should be valid")
|
||||
}
|
||||
|
||||
fn signed_channel_withdraw(
|
||||
withdraw: ChannelWithdrawOp,
|
||||
signing_key: &Ed25519Key,
|
||||
) -> SignedMantleTx {
|
||||
let mantle_tx = MantleTx([Op::ChannelWithdraw(withdraw)].into());
|
||||
let tx_hash = mantle_tx.hash();
|
||||
let withdraw_proof = ChannelMultiSigProof::try_new(
|
||||
[IndexedSignature::new(
|
||||
0,
|
||||
signing_key.sign_payload(tx_hash.as_signing_bytes().as_ref()),
|
||||
)]
|
||||
.into(),
|
||||
)
|
||||
.expect("withdraw proof should be valid");
|
||||
|
||||
SignedMantleTx::new(
|
||||
mantle_tx,
|
||||
vec![OpProof::ChannelMultiSigProof(withdraw_proof)],
|
||||
)
|
||||
.expect("withdraw transaction should be valid")
|
||||
}
|
||||
|
||||
async fn wait_for_tx_inclusion(
|
||||
block_stream: &mut (impl futures::Stream<Item = ProcessedBlockEvent> + Unpin),
|
||||
tx_hash: TxHash,
|
||||
@@ -490,6 +530,29 @@ async fn wait_for_wallet_balance(
|
||||
panic!("timed out waiting for wallet balance {expected}, last balance was {last_balance}");
|
||||
}
|
||||
|
||||
async fn wait_for_wallet_balance_at_most(
|
||||
node: &NodeHttpClient,
|
||||
pk: ZkPublicKey,
|
||||
threshold: u64,
|
||||
wait: Duration,
|
||||
) -> u64 {
|
||||
let start = tokio::time::Instant::now();
|
||||
let mut last_balance = get_wallet_balance(node, pk).await;
|
||||
|
||||
while start.elapsed() < wait {
|
||||
if last_balance <= threshold {
|
||||
return last_balance;
|
||||
}
|
||||
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
last_balance = get_wallet_balance(node, pk).await;
|
||||
}
|
||||
|
||||
panic!(
|
||||
"timed out waiting for wallet balance at most {threshold}, last balance was {last_balance}"
|
||||
);
|
||||
}
|
||||
|
||||
async fn fetch_block_events(node: &NodeHttpClient, block_id: HeaderId) -> Events {
|
||||
let url = api_url(node, &format!("cryptarchia/blocks/{block_id}/events"));
|
||||
let response = reqwest::Client::new()
|
||||
|
||||
@@ -12,11 +12,7 @@ use std::{
|
||||
use lb_chain_service::Epoch;
|
||||
use lb_common_http_client::Error;
|
||||
use lb_core::{
|
||||
mantle::{
|
||||
GenesisTx as _, MantleTx, NoteId, OpProof, SignedMantleTx, Transaction as _, Utxo,
|
||||
ops::Op,
|
||||
transactions::{GENESIS_STORAGE_GAS_PRICE, GasPrices, MantleTxBuilder, MantleTxGasContext},
|
||||
},
|
||||
mantle::{NoteId, OpProof, Transaction as _, Utxo, ops::Op},
|
||||
sdp::{Declaration, DeclarationMessage, Locator, ProviderId, ServiceType, WithdrawMessage},
|
||||
};
|
||||
use lb_key_management_system_service::keys::{Ed25519Key, Ed25519Signature, ZkKey};
|
||||
@@ -32,10 +28,11 @@ use logos_blockchain_tests::{
|
||||
common::{
|
||||
chain::wait_for_transactions_inclusion,
|
||||
manual_cluster::{
|
||||
LocalManualClusterHarnessBase, build_local_manual_cluster, read_manual_node_logs,
|
||||
wait_for_height as wait_for_manual_cluster_height, wait_for_tip_slot,
|
||||
LocalManualClusterHarnessBase, build_local_manual_cluster, genesis_wallet_utxos,
|
||||
read_manual_node_logs, wait_for_height as wait_for_manual_cluster_height,
|
||||
wait_for_tip_slot,
|
||||
},
|
||||
wallet::{current_wallet_funding_source, fund_builder_from_wallet_source},
|
||||
wallet::funded_signed_tx,
|
||||
},
|
||||
cucumber::defaults::E2E_ARTIFACTS_DIR,
|
||||
};
|
||||
@@ -104,32 +101,32 @@ async fn sdp_ops_e2e() {
|
||||
};
|
||||
let declaration_id = declaration.id();
|
||||
|
||||
let declare_mantle_tx = fund_sdp_transaction(
|
||||
let (declare_tx, _declare_fee) = funded_signed_tx(
|
||||
&node0,
|
||||
&genesis_utxos,
|
||||
&funding_wallet,
|
||||
HashMap::new(),
|
||||
Op::SDPDeclare(declaration),
|
||||
|tx_hash| {
|
||||
let ed25519_sig = Ed25519Signature::from_bytes(
|
||||
&provider_signing_key
|
||||
.sign_payload(tx_hash.as_signing_bytes().as_ref())
|
||||
.to_bytes(),
|
||||
);
|
||||
let zk_sig = ZkKey::multi_sign(
|
||||
&[spare_note_secret_key.clone(), provider_zk_key.clone()],
|
||||
&tx_hash.to_fr(),
|
||||
)
|
||||
.expect("SDP declare zk proof should build");
|
||||
|
||||
OpProof::ZkAndEd25519Sigs {
|
||||
zk_sig,
|
||||
ed25519_sig,
|
||||
}
|
||||
},
|
||||
)
|
||||
.await;
|
||||
let declare_hash = declare_mantle_tx.hash();
|
||||
let declare_ed25519_sig = Ed25519Signature::from_bytes(
|
||||
&provider_signing_key
|
||||
.sign_payload(declare_hash.as_signing_bytes().as_ref())
|
||||
.to_bytes(),
|
||||
);
|
||||
let declare_zk_sig = ZkKey::multi_sign(
|
||||
&[spare_note_secret_key.clone(), provider_zk_key.clone()],
|
||||
&declare_hash.to_fr(),
|
||||
)
|
||||
.expect("SDP declare zk proof should build");
|
||||
let declare_tx = SignedMantleTx::new(
|
||||
declare_mantle_tx,
|
||||
vec![OpProof::ZkAndEd25519Sigs {
|
||||
zk_sig: declare_zk_sig,
|
||||
ed25519_sig: declare_ed25519_sig,
|
||||
}],
|
||||
)
|
||||
.expect("funded SDP declare transaction should be valid");
|
||||
let declare_hash = declare_tx.hash();
|
||||
|
||||
node0
|
||||
.submit_transaction(&declare_tx)
|
||||
@@ -153,24 +150,24 @@ async fn sdp_ops_e2e() {
|
||||
nonce: declaration_created.nonce + 1,
|
||||
};
|
||||
|
||||
let withdraw_mantle_tx = fund_sdp_transaction(
|
||||
let (withdraw_tx, _withdraw_fee) = funded_signed_tx(
|
||||
&node0,
|
||||
&genesis_utxos,
|
||||
&funding_wallet,
|
||||
HashMap::new(),
|
||||
Op::SDPWithdraw(withdraw_message),
|
||||
|tx_hash| {
|
||||
OpProof::ZkSig(
|
||||
ZkKey::multi_sign(
|
||||
&[spare_note_secret_key.clone(), provider_zk_key.clone()],
|
||||
&tx_hash.to_fr(),
|
||||
)
|
||||
.expect("SDP withdraw zk proof should build"),
|
||||
)
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
let withdraw_hash = withdraw_mantle_tx.hash();
|
||||
let withdraw_zk_sig = ZkKey::multi_sign(
|
||||
&[spare_note_secret_key.clone(), provider_zk_key.clone()],
|
||||
&withdraw_hash.to_fr(),
|
||||
)
|
||||
.expect("SDP withdraw zk proof should build");
|
||||
|
||||
let withdraw_tx =
|
||||
SignedMantleTx::new(withdraw_mantle_tx, vec![OpProof::ZkSig(withdraw_zk_sig)])
|
||||
.expect("funded SDP withdraw transaction should be valid");
|
||||
let withdraw_hash = withdraw_tx.hash();
|
||||
|
||||
node0
|
||||
.submit_transaction(&withdraw_tx)
|
||||
@@ -372,26 +369,7 @@ async fn start_sdp_manual_cluster(
|
||||
.await
|
||||
.expect("node-0 should produce the first block");
|
||||
|
||||
let genesis_utxos: Vec<_> = cluster_harness
|
||||
.deployment()
|
||||
.config
|
||||
.genesis_block
|
||||
.clone()
|
||||
.expect("manual-cluster deployment should include genesis tx")
|
||||
.genesis_tx()
|
||||
.genesis_transfer()
|
||||
.outputs
|
||||
.utxos(
|
||||
cluster_harness
|
||||
.deployment()
|
||||
.config
|
||||
.genesis_block
|
||||
.as_ref()
|
||||
.expect("manual-cluster deployment should include genesis tx")
|
||||
.genesis_tx()
|
||||
.genesis_transfer(),
|
||||
)
|
||||
.collect();
|
||||
let genesis_utxos = genesis_wallet_utxos(&cluster_harness.deployment().config);
|
||||
|
||||
let spare_note_id = genesis_utxos
|
||||
.iter()
|
||||
@@ -454,39 +432,3 @@ fn patch_sdp_manual_cluster_config(mut config: RunConfig) -> RunConfig {
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
async fn fund_sdp_transaction(
|
||||
node: &NodeHttpClient,
|
||||
genesis_utxos: &[Utxo],
|
||||
funding_wallet: &WalletAccount,
|
||||
extra_op: Op,
|
||||
) -> MantleTx {
|
||||
let funding_source = current_wallet_funding_source(node, genesis_utxos, funding_wallet.clone())
|
||||
.await
|
||||
.expect("funding wallet source should sync from chain");
|
||||
|
||||
let empty_context = MantleTxGasContext::new(
|
||||
HashMap::new(),
|
||||
HashMap::new(),
|
||||
GasPrices {
|
||||
execution_base_gas_price: 0.into(),
|
||||
storage_gas_price: GENESIS_STORAGE_GAS_PRICE,
|
||||
},
|
||||
);
|
||||
let tx_context = lb_core::mantle::transactions::MantleTxContext {
|
||||
gas_context: empty_context,
|
||||
leader_reward_amount: 0,
|
||||
};
|
||||
let tx_builder = MantleTxBuilder::new()
|
||||
.push_op(extra_op)
|
||||
.expect("mixed-op helper should fit op bounds");
|
||||
|
||||
let funded_builder = fund_builder_from_wallet_source(&funding_source, &tx_builder, &tx_context)
|
||||
.expect("funding mixed-op transaction should succeed");
|
||||
|
||||
// With zero gas prices the funding step adds no input, so the built tx carries
|
||||
// no trailing transfer op — the SDP op is the only op, and the only proof.
|
||||
funded_builder
|
||||
.build()
|
||||
.expect("funded mixed-op builder should build")
|
||||
}
|
||||
|
||||
@@ -77,10 +77,10 @@ impl NodeHttpClient {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn gas_prices(&self) -> Result<GasPricesResponseBody, Error> {
|
||||
pub async fn gas_prices(&self, tip: Option<HeaderId>) -> Result<GasPricesResponseBody, Error> {
|
||||
self.with_timeout(
|
||||
"Gas prices request",
|
||||
self.http_client.gas_prices(self.base_url.clone()),
|
||||
self.http_client.gas_prices(self.base_url.clone(), tip),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -192,7 +192,7 @@ impl<'a, E: LbcScenarioEnv> Submission<'a, E> {
|
||||
|
||||
async fn execute(mut self) -> Result<(), DynError> {
|
||||
let gas_context =
|
||||
MantleTxGasContext::new(HashMap::new(), HashMap::new(), GasPrices::new(0, 0));
|
||||
MantleTxGasContext::new(HashMap::new(), HashMap::new(), GasPrices::default());
|
||||
while let Some(input) = self.plan.pop_front() {
|
||||
submit_wallet_transaction(self.ctx, &input, gas_context.clone()).await?;
|
||||
if !self.interval.is_zero() {
|
||||
|
||||
@@ -41,7 +41,9 @@ const KEY_MATERIAL_LEN: usize = 16;
|
||||
|
||||
const REGULAR_NOTE_VALUE: u64 = 100_000;
|
||||
const BLEND_NOTE_VALUE: u64 = 1;
|
||||
const SDP_NOTE_VALUE: u64 = 100;
|
||||
/// Funds SDP declare/activity transaction fees at non-zero gas prices; an
|
||||
/// activity transaction costs roughly 400-1000 at genesis prices.
|
||||
const SDP_NOTE_VALUE: u64 = 10_000;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ProviderInfo {
|
||||
|
||||
Reference in New Issue
Block a user