Skip to content

Commit 2bb9bb6

Browse files
committed
Merge rust-bitcoin#4563: Add Coinbase(Transaction) newtype to distinguish coinbase transactions
394fa53 Add comprehensive tests for Coinbase type and block validation (jrakibi) 0edabd4 Add coinbase validation to Block::validate() with new error variants (jrakibi) ca4d87d Change Block::coinbase() to return &Coinbase instead of Option<&Transaction> (jrakibi) 7c438b2 transaction: Implement Coinbase type with explicit API design (jrakibi) 6571307 transaction: Add Coinbase newtype to distinguish coinbase transactions (jrakibi) Pull request description: Coinbase transactions are structurally and semantically different from normal transactions. We introduce a distinct `Coinbase(Transaction)` newtype to make this distinction explicit at the type level, helping to catch logic that shouldn't apply to coinbases (e.g attempting to look up coinbase inputs in the UTXO set). This PR is split into several patches to keep changes reviewable and to document the rationale behind each step. - Closes rust-bitcoin#4437 - Closes rust-bitcoin#4543 ACKs for top commit: tcharding: ACK 394fa53 apoelstra: ACK 394fa53; successfully ran local tests Tree-SHA512: 3e7bc95e3614c725c3c3558d4989d918619e96e3d44f56c94eed93f57353c8b94eb0e159789323e36119dbb3c1014a4a879e51a3b7ea2b2e1e74823eceb0d432
2 parents 9dea259 + 394fa53 commit 2bb9bb6

File tree

2 files changed

+242
-7
lines changed

2 files changed

+242
-7
lines changed

bitcoin/src/blockdata/block.rs

Lines changed: 164 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use internals::{compact_size, ToU64};
1515
use io::{BufRead, Write};
1616
use units::BlockTime;
1717

18+
use super::transaction::Coinbase;
1819
use super::Weight;
1920
use crate::consensus::encode::WriteExt as _;
2021
use crate::consensus::{encode, Decodable, Encodable};
@@ -117,6 +118,14 @@ impl BlockUncheckedExt for Block<Unchecked> {
117118
fn validate(self) -> Result<Block<Checked>, InvalidBlockError> {
118119
let (header, transactions) = self.into_parts();
119120

121+
if transactions.is_empty() {
122+
return Err(InvalidBlockError::NoTransactions);
123+
}
124+
125+
if !transactions[0].is_coinbase() {
126+
return Err(InvalidBlockError::InvalidCoinbase);
127+
}
128+
120129
if !check_merkle_root(&header, &transactions) {
121130
return Err(InvalidBlockError::InvalidMerkleRoot);
122131
}
@@ -258,8 +267,11 @@ pub trait BlockCheckedExt: sealed::Sealed {
258267
/// > including base data and witness data.
259268
fn total_size(&self) -> usize;
260269

261-
/// Returns the coinbase transaction, if one is present.
262-
fn coinbase(&self) -> Option<&Transaction>;
270+
/// Returns the coinbase transaction.
271+
///
272+
/// This method is infallible for checked blocks because validation ensures
273+
/// that a valid coinbase transaction is always present.
274+
fn coinbase(&self) -> &Coinbase;
263275

264276
/// Returns the block height, as encoded in the coinbase transaction according to BIP34.
265277
fn bip34_block_height(&self) -> Result<u64, Bip34Error>;
@@ -293,8 +305,10 @@ impl BlockCheckedExt for Block<Checked> {
293305
size
294306
}
295307

296-
/// Returns the coinbase transaction, if one is present.
297-
fn coinbase(&self) -> Option<&Transaction> { self.transactions().first() }
308+
fn coinbase(&self) -> &Coinbase {
309+
let first_tx = &self.transactions()[0];
310+
Coinbase::assume_coinbase_ref(first_tx)
311+
}
298312

299313
/// Returns the block height, as encoded in the coinbase transaction according to BIP34.
300314
fn bip34_block_height(&self) -> Result<u64, Bip34Error> {
@@ -311,8 +325,8 @@ impl BlockCheckedExt for Block<Checked> {
311325
return Err(Bip34Error::Unsupported);
312326
}
313327

314-
let cb = self.coinbase().ok_or(Bip34Error::NotPresent)?;
315-
let input = cb.input.first().ok_or(Bip34Error::NotPresent)?;
328+
let cb = self.coinbase();
329+
let input = cb.first_input();
316330
let push = input
317331
.script_sig
318332
.instructions_minimal()
@@ -399,6 +413,10 @@ pub enum InvalidBlockError {
399413
InvalidMerkleRoot,
400414
/// The witness commitment in coinbase transaction does not match the calculated witness_root.
401415
InvalidWitnessCommitment,
416+
/// Block has no transactions (missing coinbase).
417+
NoTransactions,
418+
/// The first transaction is not a valid coinbase transaction.
419+
InvalidCoinbase,
402420
}
403421

404422
impl From<Infallible> for InvalidBlockError {
@@ -412,6 +430,8 @@ impl fmt::Display for InvalidBlockError {
412430
match *self {
413431
InvalidMerkleRoot => write!(f, "header Merkle root does not match the calculated Merkle root"),
414432
InvalidWitnessCommitment => write!(f, "the witness commitment in coinbase transaction does not match the calculated witness_root"),
433+
NoTransactions => write!(f, "block has no transactions (missing coinbase)"),
434+
InvalidCoinbase => write!(f, "the first transaction is not a valid coinbase transaction"),
415435
}
416436
}
417437
}
@@ -513,7 +533,10 @@ mod tests {
513533
use super::*;
514534
use crate::consensus::encode::{deserialize, serialize};
515535
use crate::pow::test_utils::{u128_to_work, u64_to_work};
536+
use crate::script::ScriptBuf;
537+
use crate::transaction::{OutPoint, Transaction, TxIn, TxOut, Txid};
516538
use crate::{block, CompactTarget, Network, TestnetVersion};
539+
use crate::{Amount, Sequence, Witness};
517540

518541
#[test]
519542
fn static_vector() {
@@ -539,7 +562,7 @@ mod tests {
539562
let block = block.assume_checked(None);
540563

541564
let cb_txid = "d574f343976d8e70d91cb278d21044dd8a396019e6db70755a0a50e4783dba38";
542-
assert_eq!(block.coinbase().unwrap().compute_txid().to_string(), cb_txid);
565+
assert_eq!(block.coinbase().compute_txid().to_string(), cb_txid);
543566

544567
assert_eq!(block.bip34_block_height(), Ok(100_000));
545568

@@ -761,6 +784,140 @@ mod tests {
761784
assert!(segwit_signal.is_signalling_soft_fork(1));
762785
assert!(!segwit_signal.is_signalling_soft_fork(2));
763786
}
787+
788+
#[test]
789+
fn block_validation_no_transactions() {
790+
let header = header();
791+
let transactions = Vec::new(); // Empty transactions
792+
793+
let block = Block::new_unchecked(header, transactions);
794+
match block.validate() {
795+
Err(InvalidBlockError::NoTransactions) => (),
796+
other => panic!("Expected NoTransactions error, got: {:?}", other),
797+
}
798+
}
799+
800+
#[test]
801+
fn block_validation_invalid_coinbase() {
802+
let header = header();
803+
804+
// Create a non-coinbase transaction (has a real previous output, not all zeros)
805+
let non_coinbase_tx = Transaction {
806+
version: primitives::transaction::Version::TWO,
807+
lock_time: crate::absolute::LockTime::ZERO,
808+
input: vec![TxIn {
809+
previous_output: OutPoint {
810+
txid: Txid::from_byte_array([1; 32]), // Not all zeros
811+
vout: 0,
812+
},
813+
script_sig: ScriptBuf::new(),
814+
sequence: Sequence::ENABLE_LOCKTIME_AND_RBF,
815+
witness: Witness::new(),
816+
}],
817+
output: vec![TxOut { value: Amount::ONE_BTC, script_pubkey: ScriptBuf::new() }],
818+
};
819+
820+
let transactions = vec![non_coinbase_tx];
821+
let block = Block::new_unchecked(header, transactions);
822+
823+
match block.validate() {
824+
Err(InvalidBlockError::InvalidCoinbase) => (),
825+
other => panic!("Expected InvalidCoinbase error, got: {:?}", other),
826+
}
827+
}
828+
829+
#[test]
830+
fn block_validation_success_with_coinbase() {
831+
use crate::constants;
832+
833+
// Use the genesis block which has a valid coinbase
834+
let genesis = constants::genesis_block(Network::Bitcoin);
835+
836+
let header = *genesis.header();
837+
let transactions = genesis.transactions().to_vec();
838+
839+
let unchecked_block = Block::new_unchecked(header, transactions);
840+
let validated_block = unchecked_block.validate();
841+
842+
assert!(validated_block.is_ok(), "Genesis block should validate successfully");
843+
}
844+
845+
#[test]
846+
fn checked_block_coinbase_method() {
847+
use crate::constants;
848+
849+
let genesis = constants::genesis_block(Network::Bitcoin);
850+
let coinbase = genesis.coinbase();
851+
852+
// Test that coinbase method returns the expected transaction
853+
let expected_txid = genesis.transactions()[0].compute_txid();
854+
assert_eq!(coinbase.compute_txid(), expected_txid);
855+
assert_eq!(coinbase.wtxid(), Wtxid::COINBASE);
856+
857+
// Test that as_inner() returns the correct transaction
858+
assert_eq!(coinbase.as_transaction(), &genesis.transactions()[0]);
859+
}
860+
861+
#[test]
862+
fn block_new_checked_validation() {
863+
use crate::constants;
864+
865+
// Test successful validation with genesis block
866+
let genesis = constants::genesis_block(Network::Bitcoin);
867+
let header = *genesis.header();
868+
let transactions = genesis.transactions().to_vec();
869+
870+
let checked_block = Block::new_checked(header, transactions.clone());
871+
assert!(checked_block.is_ok(), "Genesis block should validate via new_checked");
872+
873+
// Test validation failure with empty transactions
874+
let empty_result = Block::new_checked(header, Vec::new());
875+
match empty_result {
876+
Err(InvalidBlockError::NoTransactions) => (),
877+
other => panic!("Expected NoTransactions error, got: {:?}", other),
878+
}
879+
880+
// Test validation failure with invalid coinbase
881+
let non_coinbase_tx = Transaction {
882+
version: primitives::transaction::Version::TWO,
883+
lock_time: crate::absolute::LockTime::ZERO,
884+
input: vec![TxIn {
885+
previous_output: OutPoint {
886+
txid: Txid::from_byte_array([1; 32]), // Not all zeros
887+
vout: 0,
888+
},
889+
script_sig: ScriptBuf::new(),
890+
sequence: Sequence::ENABLE_LOCKTIME_AND_RBF,
891+
witness: Witness::new(),
892+
}],
893+
output: vec![TxOut { value: Amount::ONE_BTC, script_pubkey: ScriptBuf::new() }],
894+
};
895+
896+
let invalid_coinbase_result = Block::new_checked(header, vec![non_coinbase_tx]);
897+
match invalid_coinbase_result {
898+
Err(InvalidBlockError::InvalidCoinbase) => (),
899+
other => panic!("Expected InvalidCoinbase error, got: {:?}", other),
900+
}
901+
}
902+
903+
#[test]
904+
fn coinbase_bip34_height_with_coinbase_type() {
905+
// testnet block 100,000
906+
const BLOCK_HEX: &str = "0200000035ab154183570282ce9afc0b494c9fc6a3cfea05aa8c1add2ecc56490000000038ba3d78e4500a5a7570dbe61960398add4410d278b21cd9708e6d9743f374d544fc055227f1001c29c1ea3b0101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff3703a08601000427f1001c046a510100522cfabe6d6d0000000000000000000068692066726f6d20706f6f6c7365727665726aac1eeeed88ffffffff0100f2052a010000001976a914912e2b234f941f30b18afbb4fa46171214bf66c888ac00000000";
907+
let block: Block = deserialize(&hex!(BLOCK_HEX)).unwrap();
908+
let block = block.assume_checked(None);
909+
910+
// Test that BIP34 height extraction works with the Coinbase type
911+
assert_eq!(block.bip34_block_height(), Ok(100_000));
912+
913+
// Test that coinbase method returns a Coinbase type
914+
let coinbase = block.coinbase();
915+
assert!(coinbase.as_transaction().is_coinbase());
916+
917+
// Test that the coinbase transaction ID matches expected
918+
let cb_txid = "d574f343976d8e70d91cb278d21044dd8a396019e6db70755a0a50e4783dba38";
919+
assert_eq!(coinbase.compute_txid().to_string(), cb_txid);
920+
}
764921
}
765922

766923
#[cfg(bench)]

bitcoin/src/blockdata/transaction.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1154,6 +1154,65 @@ impl InputWeightPrediction {
11541154
}
11551155
}
11561156

1157+
internals::transparent_newtype! {
1158+
/// A wrapper type for the coinbase transaction of a block.
1159+
///
1160+
/// This type exists to distinguish coinbase transactions from regular ones at the type level.
1161+
#[derive(Clone, PartialEq, Eq, Debug, Hash)]
1162+
pub struct Coinbase(Transaction);
1163+
1164+
impl Coinbase {
1165+
/// Creates a reference to `Coinbase` from a reference to the inner `Transaction`.
1166+
///
1167+
/// This method does not validate that the transaction is actually a coinbase transaction.
1168+
/// The caller must ensure that the transaction is indeed a valid coinbase transaction
1169+
pub fn assume_coinbase_ref(inner: &_) -> &Self;
1170+
}
1171+
}
1172+
1173+
impl Coinbase {
1174+
/// Creates a `Coinbase` wrapper assuming this transaction is a coinbase transaction.
1175+
///
1176+
/// This method does not validate that the transaction is actually a coinbase transaction.
1177+
/// The caller must ensure that this transaction is indeed a valid coinbase transaction.
1178+
pub fn assume_coinbase(tx: Transaction) -> Self {
1179+
Self(tx)
1180+
}
1181+
1182+
/// Returns the first input of this coinbase transaction.
1183+
///
1184+
/// This method is infallible because a valid coinbase transaction is guaranteed
1185+
/// to have exactly one input.
1186+
pub fn first_input(&self) -> &TxIn {
1187+
&self.0.input[0]
1188+
}
1189+
1190+
/// Returns a reference to the underlying transaction.
1191+
///
1192+
/// Warning: The coinbase input contains dummy prevouts that should not be treated as real prevouts.
1193+
#[doc(alias = "as_inner")]
1194+
pub fn as_transaction(&self) -> &Transaction { &self.0 }
1195+
1196+
/// Returns the underlying transaction.
1197+
///
1198+
/// Warning: The coinbase input contains dummy prevouts that should not be treated as real prevouts.
1199+
#[doc(alias = "into_inner")]
1200+
pub fn into_transaction(self) -> Transaction { self.0 }
1201+
1202+
/// Computes the [`Txid`] of this coinbase transaction.
1203+
pub fn compute_txid(&self) -> Txid {
1204+
self.0.compute_txid()
1205+
}
1206+
1207+
/// Returns the wtxid of this coinbase transaction.
1208+
///
1209+
/// For coinbase transactions, this is always `Wtxid::COINBASE`.
1210+
#[doc(alias = "compute_wtxid")]
1211+
pub const fn wtxid(&self) -> Wtxid {
1212+
Wtxid::COINBASE
1213+
}
1214+
}
1215+
11571216
mod sealed {
11581217
pub trait Sealed {}
11591218
impl Sealed for super::Transaction {}
@@ -2068,6 +2127,25 @@ mod tests {
20682127
let pretty_txid = "0x0000000000000000000000000000000000000000000000000000000000000000";
20692128
assert_eq!(pretty_txid, format!("{:#}", &outpoint.txid));
20702129
}
2130+
2131+
#[test]
2132+
fn coinbase_assume_methods() {
2133+
use crate::constants;
2134+
use crate::network::Network;
2135+
2136+
let genesis = constants::genesis_block(Network::Bitcoin);
2137+
let coinbase_tx = &genesis.transactions()[0];
2138+
2139+
// Test that we can create a Coinbase reference using assume_coinbase_ref
2140+
let coinbase_ref = Coinbase::assume_coinbase_ref(coinbase_tx);
2141+
assert_eq!(coinbase_ref.compute_txid(), coinbase_tx.compute_txid());
2142+
assert_eq!(coinbase_ref.wtxid(), Wtxid::COINBASE);
2143+
2144+
// Test that we can create a Coinbase using assume_coinbase
2145+
let coinbase_owned = Coinbase::assume_coinbase(coinbase_tx.clone());
2146+
assert_eq!(coinbase_owned.compute_txid(), coinbase_tx.compute_txid());
2147+
assert_eq!(coinbase_owned.wtxid(), Wtxid::COINBASE);
2148+
}
20712149
}
20722150

20732151
#[cfg(bench)]

0 commit comments

Comments
 (0)