@@ -15,6 +15,7 @@ use internals::{compact_size, ToU64};
1515use io:: { BufRead , Write } ;
1616use units:: BlockTime ;
1717
18+ use super :: transaction:: Coinbase ;
1819use super :: Weight ;
1920use crate :: consensus:: encode:: WriteExt as _;
2021use 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
404422impl 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) ]
0 commit comments