Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 28 additions & 5 deletions lib/state/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,19 @@ use crate::{
AmountOverflowError, Authorization, Body, FilledOutput,
FilledOutputContent, FilledTransaction, GetAddress as _, GetValue as _,
Header, InPoint, MerkleRoot, OutPoint, OutPointKey, OutputContent,
SpentOutput, TxData, Verify as _,
SpentOutput, TxData, Verify as _, constants,
transaction::WithdrawalValueRule,
},
};

fn withdrawal_rule_for_block(header: &Header) -> WithdrawalValueRule {
if constants::LEGACY_WITHDRAWAL_ACCOUNTING_BLOCKS.contains(&header.hash()) {
WithdrawalValueRule::PayoutOnly
} else {
WithdrawalValueRule::PayoutAndMainchainFee
}
}

/// Calculate total number of inputs across all transactions in a block body
fn calculate_total_inputs(body: &Body) -> usize {
body.transactions.iter().map(|t| t.inputs.len()).sum()
Expand Down Expand Up @@ -71,17 +80,25 @@ pub fn validate(
}

// Process transactions for fee validation
let withdrawal_rule = withdrawal_rule_for_block(header);
for filled_tx in &filled_txs {
total_fees = total_fees
.checked_add(state.validate_filled_transaction(rotxn, filled_tx)?)
.checked_add(
state.validate_filled_transaction_with_withdrawal_rule(
rotxn,
filled_tx,
withdrawal_rule,
)?,
)
.ok_or(AmountOverflowError)?;
}
if coinbase_value > total_fees {
return Err(Error::NotEnoughFees);
}
let merkle_root = Body::compute_merkle_root(
let merkle_root = Body::compute_merkle_root_with_withdrawal_rule(
body.coinbase.as_slice(),
filled_txs.as_slice(),
withdrawal_rule,
)?;
if merkle_root != header.merkle_root {
let err = Error::InvalidBody {
Expand Down Expand Up @@ -175,19 +192,25 @@ pub fn prevalidate(
}

// Process transactions for fee validation
let withdrawal_rule = withdrawal_rule_for_block(header);
for filled_transaction in &filled_transactions {
total_fees = total_fees
.checked_add(
state.validate_filled_transaction(rotxn, filled_transaction)?,
state.validate_filled_transaction_with_withdrawal_rule(
rotxn,
filled_transaction,
withdrawal_rule,
)?,
)
.ok_or(AmountOverflowError)?;
}
if coinbase_value > total_fees {
return Err(Error::NotEnoughFees);
}
let computed_merkle_root = Body::compute_merkle_root(
let computed_merkle_root = Body::compute_merkle_root_with_withdrawal_rule(
body.coinbase.as_slice(),
filled_transactions.as_slice(),
withdrawal_rule,
)?;
if computed_merkle_root != header.merkle_root {
let err = Error::InvalidBody {
Expand Down
80 changes: 78 additions & 2 deletions lib/state/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use crate::{
GetValue as _, Header, InPoint, M6id, MerkleRoot, OutPoint,
OutPointKey, SpentOutput, Transaction, VERSION, Verify as _, Version,
WithdrawalBundle, WithdrawalBundleStatus, constants, hashes,
proto::mainchain::TwoWayPegData,
proto::mainchain::TwoWayPegData, transaction::WithdrawalValueRule,
},
util::Watchable,
};
Expand Down Expand Up @@ -493,6 +493,19 @@ impl State {
&self,
rotxn: &RoTxn,
tx: &FilledTransaction,
) -> Result<bitcoin::Amount, Error> {
self.validate_filled_transaction_with_withdrawal_rule(
rotxn,
tx,
WithdrawalValueRule::PayoutAndMainchainFee,
)
}

pub(crate) fn validate_filled_transaction_with_withdrawal_rule(
&self,
rotxn: &RoTxn,
tx: &FilledTransaction,
withdrawal_rule: WithdrawalValueRule,
) -> Result<bitcoin::Amount, Error> {
let () = self.validate_reservations(tx)?;
let () = self.validate_bitnames(rotxn, tx)?;
Expand All @@ -506,7 +519,7 @@ impl State {
});
}
}
let fee = tx.fee()?;
let fee = tx.fee_with_withdrawal_rule(withdrawal_rule)?;
Ok(fee)
}

Expand Down Expand Up @@ -709,6 +722,7 @@ mod test {
MutableBitNameData, OutPoint, OutPointKey, Output, OutputContent,
SpentOutput, Transaction, TxData, Txid, VerifyingKey,
WithdrawalOutputContent,
transaction::{ComputeFeeError, WithdrawalValueRule},
},
};

Expand Down Expand Up @@ -935,6 +949,68 @@ mod test {
Ok(())
}

#[test]
fn consensus_validator_reproduces_withdrawal_activation_transition()
-> anyhow::Result<()> {
let (_temp_dir, env, state) = fresh_state("withdrawal-activation")?;
let input = FilledOutput::new_bitcoin_value(
Address::ALL_ZEROS,
bitcoin::Amount::from_sat(5_000_000),
);
let withdrawal = Output::new(
Address::ALL_ZEROS,
OutputContent::Withdrawal(WithdrawalOutputContent {
value: bitcoin::Amount::from_sat(2_000_000),
main_fee: bitcoin::Amount::from_sat(10_000),
main_address: "tb1qg2muwvd42czzxnh2ewrgt67rfmudzcatz9lmh4"
.parse()?,
}),
);
let historical = FilledTransaction {
transaction: Transaction::new(
vec![OutPoint::Regular {
txid: [1; 32].into(),
vout: 0,
}],
vec![
withdrawal.clone(),
bitcoin_filled_output(Address::ALL_ZEROS, 2_999_990).into(),
],
),
spent_utxos: vec![input.clone()],
};
let rotxn = env.read_txn()?;

assert_eq!(
state.validate_filled_transaction_with_withdrawal_rule(
&rotxn,
&historical,
WithdrawalValueRule::PayoutOnly,
)?,
bitcoin::Amount::from_sat(10)
);
assert!(matches!(
state.validate_filled_transaction(&rotxn, &historical),
Err(Error::ComputeFee(ComputeFeeError::Underfunded))
));

let corrected = FilledTransaction {
transaction: Transaction::new(
historical.transaction.inputs,
vec![
withdrawal,
bitcoin_filled_output(Address::ALL_ZEROS, 2_989_990).into(),
],
),
spent_utxos: vec![input],
};
assert_eq!(
state.validate_filled_transaction(&rotxn, &corrected)?,
bitcoin::Amount::from_sat(10)
);
Ok(())
}

#[test]
fn sidechain_wealth() -> anyhow::Result<()> {
use std::str::FromStr;
Expand Down
16 changes: 15 additions & 1 deletion types/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,21 @@ use std::sync::LazyLock;
use ed25519_dalek::PUBLIC_KEY_LENGTH;
use hex_literal::hex;

use crate::VerifyingKey;
use crate::{BlockHash, VerifyingKey};

/// Drivenet blocks mined with the historical withdrawal accounting rule.
///
/// These blocks predate the rule that includes the mainchain fee in a
/// withdrawal output's sidechain value. Restricting compatibility to this
/// exact set preserves history without weakening validation for new blocks.
pub const LEGACY_WITHDRAWAL_ACCOUNTING_BLOCKS: [BlockHash; 2] = [
BlockHash(hex!(
"f9a7a9117bec4ed6c4fffbf3b651b60f9459a5fc881f267b271778874ad55d0f"
)),
BlockHash(hex!(
"208e6bb567efd46d073bab68d9cff041279faada60c7f27a011a56bd1f7f86b7"
)),
];

/// authorized pubkey that can make batch icann registration txs
const BATCH_ICANN_VERIFYING_KEY_BYTES: [u8; PUBLIC_KEY_LENGTH] =
Expand Down
33 changes: 28 additions & 5 deletions types/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,21 @@ impl Body {
coinbase: &[Output],
txs: &[FilledTx],
) -> Result<MerkleRoot, ComputeMerkleRootError>
where
FilledTx: Borrow<FilledTransaction> + Sync,
{
Self::compute_merkle_root_with_withdrawal_rule(
coinbase,
txs,
transaction::WithdrawalValueRule::PayoutAndMainchainFee,
)
}

pub fn compute_merkle_root_with_withdrawal_rule<FilledTx>(
coinbase: &[Output],
txs: &[FilledTx],
withdrawal_rule: transaction::WithdrawalValueRule,
) -> Result<MerkleRoot, ComputeMerkleRootError>
where
FilledTx: Borrow<FilledTransaction> + Sync,
{
Expand All @@ -619,12 +634,20 @@ impl Body {
.enumerate()
.map(|(idx, tx)| {
let tx = tx.borrow();
let fees = tx.get_fee().map_err(|err| {
ComputeMerkleRootError::FeeComputation {
let fees = tx
.fee_with_withdrawal_rule(withdrawal_rule)
.map_err(|err| ComputeMerkleRootError::FeeComputation {
txid: tx.transaction.txid(),
source: err,
}
})?;
source: match err {
transaction::ComputeFeeError::Underfunded => {
GetFeeError::AmountUnderflow
}
transaction::ComputeFeeError::ValueInOverflow(_)
| transaction::ComputeFeeError::ValueOutOverflow(_) => {
GetFeeError::AmountOverflow
}
},
})?;
let canonical_size = tx.transaction.canonical_size();
let leaf_pre_commitment = CbmtLeafPreCommitment {
fee: fees,
Expand Down
Loading
Loading