Skip to content

Commit 4ec9b15

Browse files
Eligioojsdanielh
authored andcommitted
ChainInfo: reject blocks with overflowing transaction fee sums
1 parent 3bc449a commit 4ec9b15

9 files changed

Lines changed: 167 additions & 15 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

blockchain-interface/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,6 @@ nimiq-primitives = { workspace = true, features = ["coin", "key-nibbles", "polic
3737
nimiq-serde = { workspace = true }
3838
nimiq-transaction = { workspace = true }
3939
nimiq-vrf = { workspace = true }
40+
41+
[dev-dependencies]
42+
nimiq-keys = { workspace = true }

blockchain-interface/src/chain_info.rs

Lines changed: 80 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use std::{fmt::Formatter, ops::RangeFrom};
22

33
use nimiq_block::{
4-
Block, BlockType, MacroBlock, MacroHeader, MicroBlock, MicroHeader, MicroJustification,
5-
TendermintProof,
4+
Block, BlockError, BlockType, MacroBlock, MacroHeader, MicroBlock, MicroHeader,
5+
MicroJustification, TendermintProof,
66
};
77
use nimiq_database_value_derive::DbSerializable;
88
use nimiq_hash::Blake2bHash;
@@ -58,24 +58,30 @@ impl ChainInfo {
5858
}
5959

6060
/// Creates a new ChainInfo for a block given its predecessor.
61+
///
62+
/// Returns an error if the transaction fee sum overflows.
6163
pub fn from_block(
6264
block: Block,
6365
prev_info: &ChainInfo,
6466
prev_missing_range: Option<RangeFrom<KeyNibbles>>,
65-
) -> Self {
67+
) -> Result<Self, BlockError> {
6668
assert_eq!(prev_info.head.block_number(), block.block_number() - 1);
6769

6870
// Reset the transaction fee accumulator if this is the first block of a batch. Otherwise,
6971
// just add the transactions fees of this block to the accumulator.
72+
let block_fees = block.sum_transaction_fees()?;
7073
let cum_tx_fees = if Policy::is_macro_block_at(prev_info.head.block_number()) {
71-
block.sum_transaction_fees()
74+
block_fees
7275
} else {
73-
prev_info.cum_tx_fees + block.sum_transaction_fees()
76+
prev_info
77+
.cum_tx_fees
78+
.checked_add(block_fees)
79+
.ok_or(BlockError::TransactionFeeOverflow)?
7480
};
7581

7682
let prunable = !block.is_election();
7783

78-
ChainInfo {
84+
Ok(ChainInfo {
7985
on_main_chain: false,
8086
main_chain_successor: None,
8187
head: block,
@@ -84,7 +90,7 @@ impl ChainInfo {
8490
history_tree_len: 0,
8591
prunable,
8692
prev_missing_range,
87-
}
93+
})
8894
}
8995

9096
/// Sets the value for the cumulative historic transaction size and the prunable flag
@@ -188,3 +194,70 @@ impl<'de> Visitor<'de> for ChainHeadVisitor {
188194
Ok(block)
189195
}
190196
}
197+
198+
#[cfg(test)]
199+
mod tests {
200+
use nimiq_block::{BlockError, MicroBlock, MicroBody, MicroHeader};
201+
use nimiq_keys::Address;
202+
use nimiq_primitives::{coin::Coin, networks::NetworkId, policy::Policy};
203+
use nimiq_transaction::ExecutedTransaction;
204+
205+
use super::*;
206+
207+
fn tx_with_fee(fee: Coin) -> ExecutedTransaction {
208+
ExecutedTransaction::Ok(nimiq_transaction::Transaction::new_basic(
209+
Address::default(),
210+
Address::from([1u8; 20]),
211+
Coin::ZERO,
212+
fee,
213+
1,
214+
NetworkId::UnitAlbatross,
215+
))
216+
}
217+
218+
#[test]
219+
fn test_chain_info_cumulative_fee_overflow() {
220+
// Use block_number 2 so it's NOT a macro block predecessor, triggering the
221+
// cumulative addition path.
222+
let prev_block = Block::Micro(MicroBlock {
223+
header: MicroHeader {
224+
network: NetworkId::UnitAlbatross,
225+
version: Policy::max_supported_version(),
226+
block_number: 2,
227+
timestamp: 0,
228+
..Default::default()
229+
},
230+
justification: None,
231+
body: None,
232+
});
233+
234+
let mut prev_info = ChainInfo::new(prev_block, true);
235+
prev_info.cum_tx_fees = Coin::MAX;
236+
237+
// Create a block with fee = 1. Adding to MAX should overflow.
238+
let transactions = vec![tx_with_fee(Coin::from_u64_unchecked(1))];
239+
let micro_body = MicroBody {
240+
equivocation_proofs: vec![],
241+
transactions,
242+
};
243+
let block = Block::Micro(MicroBlock {
244+
header: MicroHeader {
245+
network: NetworkId::UnitAlbatross,
246+
version: Policy::max_supported_version(),
247+
block_number: 3,
248+
timestamp: 1,
249+
..Default::default()
250+
},
251+
justification: None,
252+
body: Some(micro_body),
253+
});
254+
255+
assert!(
256+
matches!(
257+
ChainInfo::from_block(block, &prev_info, None),
258+
Err(BlockError::TransactionFeeOverflow)
259+
),
260+
"Expected TransactionFeeOverflow for cumulative fee overflow",
261+
);
262+
}
263+
}

blockchain/src/blockchain/push.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ impl Blockchain {
122122

123123
read_txn.close();
124124

125-
let chain_info = ChainInfo::from_block(block, &prev_info, prev_missing_range);
125+
let chain_info = ChainInfo::from_block(block, &prev_info, prev_missing_range)?;
126126

127127
// Extend, rebranch or just store the block depending on the chain ordering.
128128
let result = match chain_order {

blockchain/src/blockchain/verify.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -495,7 +495,7 @@ impl Blockchain {
495495
}
496496

497497
// Create a ChainInfo for the proposed block.
498-
let chain_info = ChainInfo::from_block(block.clone(), &prev_info, prev_missing_range);
498+
let chain_info = ChainInfo::from_block(block.clone(), &prev_info, prev_missing_range)?;
499499

500500
let read_txn = self.read_transaction();
501501
// First the common ancestor of the two chains needs to be found.

light-blockchain/src/push.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ impl LightBlockchain {
114114
}
115115

116116
// Create the chain info for the new block.
117-
let chain_info = ChainInfo::from_block(block, &prev_info, None);
117+
let chain_info = ChainInfo::from_block(block, &prev_info, None)?;
118118

119119
// More chain ordering.
120120
let result = match chain_order {

primitives/block/src/block.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -234,19 +234,24 @@ impl Block {
234234

235235
/// Returns the sum of the fees of all of the transactions in the block. If the block is a Macro
236236
/// block it just returns zero, since Macro blocks don't contain any transactions.
237-
pub fn sum_transaction_fees(&self) -> Coin {
237+
///
238+
/// Returns an error if the sum of fees would overflow `Coin::MAX`.
239+
pub fn sum_transaction_fees(&self) -> Result<Coin, BlockError> {
238240
match self {
239-
Block::Macro(_) => Coin::ZERO,
241+
Block::Macro(_) => Ok(Coin::ZERO),
240242
Block::Micro(block) => block
241243
.body
242244
.as_ref()
243245
.map(|ex| {
244246
ex.transactions
245247
.iter()
246248
.map(|tx| tx.get_raw_transaction().fee)
247-
.sum()
249+
.try_fold(Coin::ZERO, |acc, fee| {
250+
acc.checked_add(fee)
251+
.ok_or(BlockError::TransactionFeeOverflow)
252+
})
248253
})
249-
.unwrap_or(Coin::ZERO),
254+
.unwrap_or(Ok(Coin::ZERO)),
250255
}
251256
}
252257

primitives/block/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,4 +89,7 @@ pub enum BlockError {
8989

9090
#[error("Skip block contains a non empty body")]
9191
InvalidSkipBlockBody,
92+
93+
#[error("Transaction fee sum overflow")]
94+
TransactionFeeOverflow,
9295
}

primitives/block/tests/verify.rs

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@ use nimiq_collections::BitSet;
88
use nimiq_hash::Hash;
99
use nimiq_keys::{Address, Ed25519PublicKey as SchnorrPublicKey, Ed25519Signature, KeyPair};
1010
use nimiq_primitives::{
11+
coin::Coin,
1112
networks::NetworkId,
1213
policy::Policy,
1314
slots_allocation::{Validator, Validators, ValidatorsBuilder},
1415
};
1516
use nimiq_test_log::test;
1617
use nimiq_test_utils::blockchain::{generate_transactions, validator_address};
17-
use nimiq_transaction::ExecutedTransaction;
18+
use nimiq_transaction::{ExecutedTransaction, Transaction};
1819

1920
/// Test blocks use timestamp 0; this allows up to 1 second into the future.
2021
const TEST_MAX_TIMESTAMP: u64 = 1000;
@@ -636,6 +637,72 @@ fn test_verify_micro_block_body_fork_proofs() {
636637
);
637638
}
638639

640+
/// Helper to create a transaction with a specific fee and a unique recipient.
641+
/// The transaction is not signed and won't pass signature verification, but is
642+
/// sufficient for fee sum tests.
643+
fn tx_with_fee(fee: Coin, unique_byte: u8) -> ExecutedTransaction {
644+
ExecutedTransaction::Ok(Transaction::new_basic(
645+
Address::default(),
646+
Address::from([unique_byte; 20]),
647+
Coin::from_u64_unchecked(1),
648+
fee,
649+
1,
650+
NetworkId::UnitAlbatross,
651+
))
652+
}
653+
654+
#[test]
655+
fn test_sum_transaction_fees_overflow() {
656+
let high_fee = Coin::from_u64_unchecked(Policy::TOTAL_SUPPLY);
657+
658+
// 5 transactions each with fee = TOTAL_SUPPLY should overflow Coin::MAX
659+
let transactions: Vec<ExecutedTransaction> =
660+
(0..5).map(|i| tx_with_fee(high_fee, i + 1)).collect();
661+
662+
let micro_body = MicroBody {
663+
equivocation_proofs: vec![],
664+
transactions,
665+
};
666+
667+
let block = Block::Micro(MicroBlock {
668+
header: MicroHeader {
669+
network: NetworkId::UnitAlbatross,
670+
version: Policy::max_supported_version(),
671+
block_number: 1,
672+
timestamp: 0,
673+
body_root: micro_body.hash(),
674+
..Default::default()
675+
},
676+
justification: Some(MicroJustification::Micro(Ed25519Signature::default())),
677+
body: Some(micro_body),
678+
});
679+
680+
assert_eq!(
681+
block.sum_transaction_fees(),
682+
Err(BlockError::TransactionFeeOverflow),
683+
);
684+
}
685+
686+
#[test]
687+
fn test_sum_transaction_fees_normal() {
688+
let fee = Coin::from_u64_unchecked(1000);
689+
let transactions: Vec<ExecutedTransaction> = (0..3).map(|i| tx_with_fee(fee, i + 1)).collect();
690+
691+
let block = Block::Micro(MicroBlock {
692+
header: MicroHeader::default(),
693+
justification: None,
694+
body: Some(MicroBody {
695+
equivocation_proofs: vec![],
696+
transactions,
697+
}),
698+
});
699+
700+
assert_eq!(
701+
block.sum_transaction_fees(),
702+
Ok(Coin::from_u64_unchecked(3000)),
703+
);
704+
}
705+
639706
#[test]
640707
fn test_verify_election_macro_body() {
641708
let mut macro_header = MacroHeader {

0 commit comments

Comments
 (0)