@@ -619,3 +619,159 @@ pub fn disconnect_tip(
619619 . map_err ( DbError :: from) ?;
620620 Ok ( ( ) )
621621}
622+
623+ #[ cfg( test) ]
624+ mod test {
625+ use crate :: state:: test:: { fresh_state, value_output} ;
626+
627+ #[ test]
628+ fn validation_rejects_outpoint_utxo_hash_mismatch ( ) -> anyhow:: Result < ( ) > {
629+ use bitcoin:: hashes:: Hash as _;
630+ use rustreexo:: accumulator:: node_hash:: BitcoinNodeHash ;
631+
632+ use crate :: {
633+ authorization:: { SigningKey , authorize, get_address} ,
634+ types:: {
635+ Accumulator , AccumulatorDiff , Body , Header , OutPoint ,
636+ OutPointKey , PointedOutput , Transaction , hash,
637+ } ,
638+ } ;
639+ let ( env, state) =
640+ fresh_state ( "validation_rejects_outpoint_utxo_hash_mismatch" ) ?;
641+
642+ // Attacker key (owns A). Victim key (owns B).
643+ let attacker = SigningKey :: from_seeds (
644+ & [ 0x11 ; fips205:: slh_dsa_shake_256s:: N ] ,
645+ & [ 0x11 ; fips205:: slh_dsa_shake_256s:: N ] ,
646+ & [ 0x11 ; fips205:: slh_dsa_shake_256s:: N ] ,
647+ ) ;
648+ let attacker_addr = get_address ( & attacker. verifying_key ( ) ) ;
649+ let victim = SigningKey :: from_seeds (
650+ & [ 0x22 ; fips205:: slh_dsa_shake_256s:: N ] ,
651+ & [ 0x22 ; fips205:: slh_dsa_shake_256s:: N ] ,
652+ & [ 0x22 ; fips205:: slh_dsa_shake_256s:: N ] ,
653+ ) ;
654+ let victim_addr = get_address ( & victim. verifying_key ( ) ) ;
655+
656+ // UTXO A (attacker, 10_000) and victim UTXO B (20_000).
657+ let outpoint_a = OutPoint :: Deposit ( bitcoin:: OutPoint {
658+ txid : bitcoin:: Txid :: from_byte_array ( [ 0xAA ; 32 ] ) ,
659+ vout : 0 ,
660+ } ) ;
661+ let output_a = value_output ( attacker_addr, 10_000 ) ;
662+ let outpoint_b = OutPoint :: Deposit ( bitcoin:: OutPoint {
663+ txid : bitcoin:: Txid :: from_byte_array ( [ 0xBB ; 32 ] ) ,
664+ vout : 0 ,
665+ } ) ;
666+ let output_b = value_output ( victim_addr, 20_000 ) ;
667+
668+ // Leaf hashes for A and B (the Utreexo commitments).
669+ let pointed_a = PointedOutput {
670+ outpoint : outpoint_a,
671+ output : output_a. clone ( ) ,
672+ } ;
673+ let pointed_b = PointedOutput {
674+ outpoint : outpoint_b,
675+ output : output_b. clone ( ) ,
676+ } ;
677+ let leaf_a: BitcoinNodeHash = ( & pointed_a) . into ( ) ;
678+ let leaf_b: BitcoinNodeHash = ( & pointed_b) . into ( ) ;
679+ let hash_b: crate :: types:: Hash = hash ( & pointed_b) ; // input's utxo_hash
680+
681+ // Helper: build a fresh accumulator seeded with leaves A and B
682+ // (Accumulator is not Clone, so re-seed when a fresh copy is needed).
683+ let seeded_accumulator = || -> anyhow:: Result < _ > {
684+ let mut acc = Accumulator :: default ( ) ;
685+ let mut diff = AccumulatorDiff :: default ( ) ;
686+ diff. insert ( leaf_a) ;
687+ diff. insert ( leaf_b) ;
688+ acc. apply_diff ( diff) ?;
689+ Ok ( acc)
690+ } ;
691+
692+ // Seed UTXO DB with A and B; seed the accumulator with both leaves.
693+ let pre_accumulator = seeded_accumulator ( ) ?;
694+ {
695+ let mut rwtxn = env. write_txn ( ) ?;
696+ state. utxos . put (
697+ & mut rwtxn,
698+ & OutPointKey :: from ( & outpoint_a) ,
699+ & output_a,
700+ ) ?;
701+ state. utxos . put (
702+ & mut rwtxn,
703+ & OutPointKey :: from ( & outpoint_b) ,
704+ & output_b,
705+ ) ?;
706+ state
707+ . utreexo_accumulator
708+ . put ( & mut rwtxn, & ( ) , & pre_accumulator) ?;
709+ // tip stays unset (None) so validate's prev_side_hash check
710+ // expects header.prev_side_hash == None.
711+ rwtxn. commit ( ) ?;
712+ }
713+
714+ // Build the malicious tx: input = (outpoint_A, hash_B) + proof for B;
715+ // output C = 9_000 to the attacker.
716+ let proof_for_b = pre_accumulator. prove ( & [ leaf_b] ) ?;
717+ let output_c = value_output ( attacker_addr, 9_000 ) ;
718+ let tx = Transaction {
719+ inputs : vec ! [ ( outpoint_a, hash_b) ] ,
720+ proof : proof_for_b,
721+ outputs : vec ! [ output_c. clone( ) ] ,
722+ } ;
723+ // Sign with A's key (the spender of outpoint A authorizes the tx).
724+ let authorized = authorize (
725+ & mut rand:: thread_rng ( ) ,
726+ & [ ( attacker_addr, & attacker) ] ,
727+ tx,
728+ ) ?;
729+
730+ // Assemble body.
731+ let body = Body :: new ( vec ! [ authorized] , Vec :: new ( ) ) ;
732+
733+ // Compute the header the validator expects:
734+ // merkle_root from the filled tx, roots = post-block accumulator
735+ // (B's leaf removed, C's leaf inserted) -- exactly the diff validate
736+ // builds from the SUPPLIED utxo_hash.
737+ let filled = {
738+ let rotxn = env. read_txn ( ) ?;
739+ state. fill_transaction ( & rotxn, & body. transactions [ 0 ] ) ?
740+ } ;
741+
742+ // tx validation REJECTS the outpoint/utxo_hash mismatch.
743+ anyhow:: ensure!( state. validate_filled_transaction( & filled) . is_err( ) ) ;
744+ let merkle_root =
745+ Body :: compute_merkle_root ( body. coinbase . as_slice ( ) , & [ filled] ) ?;
746+ let mut post_accumulator = seeded_accumulator ( ) ?;
747+ {
748+ let mut diff = AccumulatorDiff :: default ( ) ;
749+ // validate removes the SUPPLIED utxo_hash (B), inserts output C.
750+ diff. remove ( leaf_b) ;
751+ let txid = body. transactions [ 0 ] . txid ( ) ;
752+ let pointed_c = PointedOutput {
753+ outpoint : OutPoint :: Regular { txid, vout : 0 } ,
754+ output : output_c. clone ( ) ,
755+ } ;
756+ diff. insert ( ( & pointed_c) . into ( ) ) ;
757+ post_accumulator. apply_diff ( diff) ?;
758+ }
759+ let header = Header {
760+ merkle_root,
761+ prev_side_hash : None ,
762+ prev_main_hash : bitcoin:: BlockHash :: from_byte_array ( [ 0u8 ; 32 ] ) ,
763+ roots : post_accumulator. get_roots ( ) ,
764+ } ;
765+
766+ // block validation REJECTS the outpoint/utxo_hash mismatch.
767+ {
768+ let rotxn = env. read_txn ( ) ?;
769+ anyhow:: ensure!(
770+ state. validate_block( & rotxn, & header, & body) . is_err( ) ,
771+ "BUG: real validate_block accepts an input whose outpoint (A) \
772+ and utxo_hash (B) refer to different UTXOs",
773+ ) ;
774+ }
775+ Ok ( ( ) )
776+ }
777+ }
0 commit comments