Преглед изворни кода

sdk: add checked fee-burning helpers

brid пре 1 недеља
родитељ
комит
a995baa8a7
3 измењених фајлова са 167 додато и 1 уклоњено
  1. 23 0
      src/sdk/src/error.rs
  2. 140 0
      src/sdk/src/fee.rs
  3. 4 1
      src/sdk/src/lib.rs

+ 23 - 0
src/sdk/src/error.rs

@@ -21,6 +21,29 @@ use std::result::Result as ResultGeneric;
 pub type GenericResult<T> = ResultGeneric<T, ContractError>;
 pub type ContractResult = ResultGeneric<(), ContractError>;
 
+/// Result type for checked consensus fee arithmetic.
+pub type FeeResult<T> = ResultGeneric<T, FeeError>;
+
+/// Error returned by checked consensus fee arithmetic.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
+pub enum FeeError {
+    /// Fee arithmetic overflowed and must fail closed.
+    #[error("Fee arithmetic overflow")]
+    ArithmeticOverflow,
+
+    /// Paid fee cannot cover the requested fee split.
+    #[error("Insufficient fee payment")]
+    InsufficientFee,
+
+    /// Consensus fee constants are invalid.
+    #[error("Invalid fee constants")]
+    InvalidFeeConstants,
+
+    /// Fee call data is missing, malformed, or targets the wrong call type.
+    #[error("Invalid fee call")]
+    InvalidFeeCall,
+}
+
 /// Error codes available in the contract.
 #[derive(Debug, Clone, thiserror::Error)]
 pub enum ContractError {

+ 140 - 0
src/sdk/src/fee.rs

@@ -0,0 +1,140 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2026 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use crate::error::{FeeError, FeeResult};
+
+/// Fixed consensus fee charged per gas unit for the initial fee-burning testnet.
+pub const FEE_PER_GAS: u64 = 5;
+
+/// Numerator for the mandatory burn ratio applied to the minimum fee.
+pub const BURN_NUM: u64 = 3;
+
+/// Denominator for the mandatory burn ratio applied to the minimum fee.
+pub const BURN_DEN: u64 = 4;
+
+fn validate_fee_constants(fee_per_gas: u64, burn_num: u64, burn_den: u64) -> FeeResult<()> {
+    if fee_per_gas == 0 || burn_num == 0 || burn_den == 0 || burn_num >= burn_den {
+        return Err(FeeError::InvalidFeeConstants)
+    }
+
+    Ok(())
+}
+
+fn minimum_fee_with_constants(gas: u64, fee_per_gas: u64) -> FeeResult<u64> {
+    validate_fee_constants(fee_per_gas, BURN_NUM, BURN_DEN)?;
+    gas.checked_mul(fee_per_gas).ok_or(FeeError::ArithmeticOverflow)
+}
+
+fn burn_fee_with_constants(minimum_fee: u64, burn_num: u64, burn_den: u64) -> FeeResult<u64> {
+    validate_fee_constants(FEE_PER_GAS, burn_num, burn_den)?;
+
+    let burned =
+        (minimum_fee as u128).checked_mul(burn_num as u128).ok_or(FeeError::ArithmeticOverflow)? /
+            burn_den as u128;
+
+    u64::try_from(burned).map_err(|_| FeeError::ArithmeticOverflow)
+}
+
+/// Compute the minimum fee required for final measured gas.
+pub fn minimum_fee(gas: u64) -> FeeResult<u64> {
+    minimum_fee_with_constants(gas, FEE_PER_GAS)
+}
+
+/// Compute the mandatory burn for a minimum fee.
+pub fn burn_fee(minimum_fee: u64) -> FeeResult<u64> {
+    burn_fee_with_constants(minimum_fee, BURN_NUM, BURN_DEN)
+}
+
+/// Compute the miner-claimable fee from the paid fee and mandatory burn.
+pub fn miner_claimable_fee(paid_fee: u64, burned_fee: u64) -> FeeResult<u64> {
+    paid_fee.checked_sub(burned_fee).ok_or(FeeError::InsufficientFee)
+}
+
+/// Checked addition for accumulated fee values.
+pub fn accumulate_fee(total: u64, fee: u64) -> FeeResult<u64> {
+    total.checked_add(fee).ok_or(FeeError::ArithmeticOverflow)
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn minimum_fee_uses_fixed_fee_per_gas() {
+        assert_eq!(minimum_fee(0).unwrap(), 0);
+        assert_eq!(minimum_fee(1).unwrap(), 5);
+        assert_eq!(minimum_fee(42).unwrap(), 210);
+    }
+
+    #[test]
+    fn fee_constant_validation_rejects_invalid_constants() {
+        assert_eq!(minimum_fee_with_constants(1, 0), Err(FeeError::InvalidFeeConstants));
+        assert_eq!(burn_fee_with_constants(1, 0, BURN_DEN), Err(FeeError::InvalidFeeConstants));
+        assert_eq!(burn_fee_with_constants(1, BURN_NUM, 0), Err(FeeError::InvalidFeeConstants));
+        assert_eq!(
+            burn_fee_with_constants(1, BURN_DEN, BURN_DEN),
+            Err(FeeError::InvalidFeeConstants)
+        );
+        assert_eq!(
+            burn_fee_with_constants(1, BURN_DEN + 1, BURN_DEN),
+            Err(FeeError::InvalidFeeConstants)
+        );
+    }
+
+    #[test]
+    fn minimum_fee_rejects_multiplication_overflow() {
+        assert_eq!(minimum_fee(u64::MAX), Err(FeeError::ArithmeticOverflow));
+        assert_eq!(minimum_fee(u64::MAX / FEE_PER_GAS + 1), Err(FeeError::ArithmeticOverflow));
+        assert_eq!(
+            minimum_fee(u64::MAX / FEE_PER_GAS).unwrap(),
+            u64::MAX / FEE_PER_GAS * FEE_PER_GAS
+        );
+    }
+
+    #[test]
+    fn burn_fee_rounds_down() {
+        assert_eq!(burn_fee(0).unwrap(), 0);
+        assert_eq!(burn_fee(1).unwrap(), 0);
+        assert_eq!(burn_fee(2).unwrap(), 1);
+        assert_eq!(burn_fee(3).unwrap(), 2);
+        assert_eq!(burn_fee(4).unwrap(), 3);
+        assert_eq!(burn_fee(5).unwrap(), 3);
+    }
+
+    #[test]
+    fn miner_claimable_includes_overpayment_tip() {
+        let minimum = minimum_fee(10).unwrap();
+        let burned = burn_fee(minimum).unwrap();
+
+        assert_eq!(minimum, 50);
+        assert_eq!(burned, 37);
+        assert_eq!(miner_claimable_fee(minimum, burned).unwrap(), 13);
+        assert_eq!(miner_claimable_fee(minimum + 7, burned).unwrap(), 20);
+    }
+
+    #[test]
+    fn miner_claimable_rejects_underflow() {
+        assert_eq!(miner_claimable_fee(2, 3), Err(FeeError::InsufficientFee));
+    }
+
+    #[test]
+    fn accumulated_fee_rejects_overflow() {
+        assert_eq!(accumulate_fee(7, 11).unwrap(), 18);
+        assert_eq!(accumulate_fee(u64::MAX, 1), Err(FeeError::ArithmeticOverflow));
+    }
+}

+ 4 - 1
src/sdk/src/lib.rs

@@ -36,7 +36,10 @@ pub mod deploy;
 
 /// Error handling
 pub mod error;
-pub use error::{ContractError, ContractResult, GenericResult};
+pub use error::{ContractError, ContractResult, FeeError, FeeResult, GenericResult};
+
+/// Fee calculation helpers
+pub mod fee;
 
 /// Hex encoding/decoding from bytes
 pub mod hex;