|
@@ -30,7 +30,7 @@ use darkfi_money_contract::{
|
|
|
use darkfi_sdk::{
|
|
use darkfi_sdk::{
|
|
|
blockchain::expected_reward,
|
|
blockchain::expected_reward,
|
|
|
crypto::{contract_id::MONEY_CONTRACT_ID, SecretKey},
|
|
crypto::{contract_id::MONEY_CONTRACT_ID, SecretKey},
|
|
|
- fee::{burn_fee, minimum_fee},
|
|
|
|
|
|
|
+ fee::{burn_fee, fee_call_overhead, minimum_fee},
|
|
|
ContractCall,
|
|
ContractCall,
|
|
|
};
|
|
};
|
|
|
use darkfi_serial::AsyncEncodable;
|
|
use darkfi_serial::AsyncEncodable;
|
|
@@ -127,13 +127,10 @@ async fn measure_required_fee(
|
|
|
Ok(minimum_fee(gas_used)?)
|
|
Ok(minimum_fee(gas_used)?)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/// Gas usage varies between builds of the same transaction in fixed
|
|
|
|
|
-/// quantized steps, because per-build random values (blinds, proofs)
|
|
|
|
|
-/// feed shape- and size-metered storage operations, so fee values
|
|
|
|
|
-/// derived from one build do not necessarily hold for the next. This
|
|
|
|
|
-/// builds transactions until one declares fee values that are
|
|
|
|
|
-/// self-consistent with its own measured gas: `paid = required + tip`
|
|
|
|
|
-/// and `burned = mandatory + burn_delta`.
|
|
|
|
|
|
|
+/// The fee call is charged a fixed amount of gas, so estimation is
|
|
|
|
|
+/// deterministic. It builds a transaction declaring values relative
|
|
|
|
|
+/// to its own measured gas: `paid = required + tip` and
|
|
|
|
|
+/// `burned = mandatory + burn_delta`.
|
|
|
async fn converge_fee_tx(
|
|
async fn converge_fee_tx(
|
|
|
th: &mut TestHarness,
|
|
th: &mut TestHarness,
|
|
|
from: &Holder,
|
|
from: &Holder,
|
|
@@ -237,6 +234,69 @@ fn fees_reward_claim() -> Result<()> {
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/// The wallet-side estimation formula must bound validator gas
|
|
|
|
|
+/// accounting from above, closely: the fee call is charged a fixed
|
|
|
|
|
+/// amount of gas, so the measured fee-less gas plus
|
|
|
|
|
+/// `fee_call_overhead()` is an upper bound on the final gas of the
|
|
|
|
|
+/// assembled transaction, and a transaction built from the formula
|
|
|
|
|
+/// passes verification. The bound must be tight (a small overestimate
|
|
|
|
|
+/// only, becoming an implicit miner tip under the burn floor).
|
|
|
|
|
+///
|
|
|
|
|
+/// This test pins the overhead constants; if a serialized structure
|
|
|
|
|
+/// changes size, it fails and the constants have to be updated.
|
|
|
|
|
+#[test]
|
|
|
|
|
+fn fees_overhead_formula_bounds() -> Result<()> {
|
|
|
|
|
+ smol::block_on(async {
|
|
|
|
|
+ init_logger();
|
|
|
|
|
+
|
|
|
|
|
+ use Holder::{Alice, Bob};
|
|
|
|
|
+
|
|
|
|
|
+ let holders = vec![Alice, Bob];
|
|
|
|
|
+ let mut th = TestHarness::new(&[Alice, Bob], true).await?;
|
|
|
|
|
+
|
|
|
|
|
+ // Alice mines two blocks so she holds coins to pay fees with
|
|
|
|
|
+ th.generate_block_all(&Alice).await?;
|
|
|
|
|
+ th.generate_block_all(&Alice).await?;
|
|
|
|
|
+ let height = 3;
|
|
|
|
|
+
|
|
|
|
|
+ // Build the fee-less transaction and measure its gas
|
|
|
|
|
+ let coin = th.coins(&Alice).last().unwrap().clone();
|
|
|
|
|
+ let amount = coin.note.value / 8;
|
|
|
|
|
+ let (leaf, _, signature_secrets, _) =
|
|
|
|
|
+ transfer_call_parts(&th, &Alice, &Bob, amount, &coin).await?;
|
|
|
|
|
+ let mut tx_builder = TransactionBuilder::new(leaf, vec![])?;
|
|
|
|
|
+ let mut base_tx = tx_builder.build()?;
|
|
|
|
|
+ let sigs = base_tx.create_sigs(&signature_secrets)?;
|
|
|
|
|
+ base_tx.signatures = vec![sigs];
|
|
|
|
|
+ let base_fee = measure_required_fee(&th, &Alice, &base_tx, height).await?;
|
|
|
|
|
+
|
|
|
|
|
+ // Price the fee call with the formula
|
|
|
|
|
+ let overhead_fee = minimum_fee(fee_call_overhead(1)?)?;
|
|
|
|
|
+ let required = base_fee.checked_add(overhead_fee).ok_or(darkfi::Error::AdditionOverflow)?;
|
|
|
|
|
+ let mandatory_burn = burn_fee(required)?;
|
|
|
|
|
+
|
|
|
|
|
+ // Build the full transaction with those exact values. The
|
|
|
|
|
+ // transfer is rebuilt with fresh randomness, which does not
|
|
|
|
|
+ // affect its gas, and the fee call is charged a fixed amount.
|
|
|
|
|
+ let (tx, params, fee_params) =
|
|
|
|
|
+ fee_tx(&mut th, &Alice, &Bob, &coin, required, mandatory_burn).await?;
|
|
|
|
|
+
|
|
|
|
|
+ // The assembled transaction's measured gas must be covered by
|
|
|
|
|
+ // the formula, tightly: the formula is an upper bound and the
|
|
|
|
|
+ // overestimate must stay small.
|
|
|
|
|
+ let measured = measure_required_fee(&th, &Alice, &tx, height).await?;
|
|
|
|
|
+ assert!(measured <= required, "formula underestimates: {measured} > {required}");
|
|
|
|
|
+ let overestimate = required - measured;
|
|
|
|
|
+ assert!(overestimate <= minimum_fee(1_024)?, "formula too loose: +{overestimate}");
|
|
|
|
|
+
|
|
|
|
|
+ // And it must verify paying exactly the minimum fee
|
|
|
|
|
+ execute_on_all(&mut th, &holders, &tx, ¶ms, &fee_params, height).await?;
|
|
|
|
|
+
|
|
|
|
|
+ // Thanks for reading
|
|
|
|
|
+ Ok(())
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
#[test]
|
|
#[test]
|
|
|
fn fees() -> Result<()> {
|
|
fn fees() -> Result<()> {
|
|
|
smol::block_on(async {
|
|
smol::block_on(async {
|