@@ -686,20 +686,25 @@ impl Watchable<()> for State {
686686mod tests {
687687 use ed25519_dalek:: SigningKey ;
688688
689- use super :: { Error , State } ;
690689 use crate :: {
691690 authorization,
691+ state:: { Error , State , error} ,
692692 types:: {
693- Address , AuthorizedTransaction , BitcoinOutputContent , FilledOutput ,
694- FilledOutputContent , OutPoint , OutPointKey , Output , OutputContent ,
695- Transaction , VerifyingKey ,
693+ Address , AuthorizedTransaction , BitName , BitcoinOutputContent ,
694+ FilledOutput , FilledOutputContent , FilledTransaction , Hash ,
695+ MutableBitNameData , OutPoint , OutPointKey , Output , OutputContent ,
696+ Transaction , TxData , Txid , VerifyingKey ,
696697 } ,
697698 } ;
698699
699700 /// Open a fresh state in a unique temporary directory.
700- fn new_state ( ) -> ( sneed:: Env , State ) {
701- let path = std:: env:: temp_dir ( )
702- . join ( format ! ( "plain_bitnames_auth_count_{}" , std:: process:: id( ) ) ) ;
701+ fn new_state ( dir_name_suffix : & str ) -> ( sneed:: Env , State ) {
702+ let dir_name = if dir_name_suffix. is_empty ( ) {
703+ format ! ( "plain_bitnames_{}" , std:: process:: id( ) )
704+ } else {
705+ format ! ( "plain_bitnames_{dir_name_suffix}_{}" , std:: process:: id( ) )
706+ } ;
707+ let path = std:: env:: temp_dir ( ) . join ( dir_name) ;
703708 let _remove_result = std:: fs:: remove_dir_all ( & path) ;
704709 std:: fs:: create_dir_all ( & path) . unwrap ( ) ;
705710 let env = {
@@ -738,13 +743,47 @@ mod tests {
738743 outpoint
739744 }
740745
746+ /// Build a BitName registration that registers `name_hash` with
747+ /// `revealed_nonce`, while spending a single reservation that commits to
748+ /// `reservation_commitment`.
749+ fn registration_tx (
750+ name_hash : BitName ,
751+ revealed_nonce : Hash ,
752+ reservation_commitment : Hash ,
753+ ) -> FilledTransaction {
754+ let address = Address ( [ 0 ; 20 ] ) ;
755+ let mut transaction = Transaction :: new (
756+ vec ! [ OutPoint :: Regular {
757+ txid: Txid ( [ 0 ; 32 ] ) ,
758+ vout: 0 ,
759+ } ] ,
760+ vec ! [ Output :: new( address, OutputContent :: BitName ) ] ,
761+ ) ;
762+ transaction. data = Some ( TxData :: BitNameRegistration {
763+ name_hash,
764+ revealed_nonce,
765+ bitname_data : Box :: new ( MutableBitNameData :: default ( ) ) ,
766+ } ) ;
767+ let reservation = FilledOutput :: new (
768+ address,
769+ FilledOutputContent :: BitNameReservation (
770+ Txid ( [ 0 ; 32 ] ) ,
771+ reservation_commitment,
772+ ) ,
773+ ) ;
774+ FilledTransaction {
775+ transaction,
776+ spent_utxos : vec ! [ reservation] ,
777+ }
778+ }
779+
741780 /// A transaction that spends an input without supplying an authorization
742781 /// for it must be rejected. Otherwise the `zip` of authorizations and
743782 /// spent UTXOs silently skips the unauthorized input, allowing any UTXO to
744783 /// be spent without a signature.
745784 #[ test]
746785 fn validate_transaction_rejects_missing_authorization ( ) {
747- let ( env, state) = new_state ( ) ;
786+ let ( env, state) = new_state ( "auth_count" ) ;
748787 let signing_key = SigningKey :: from_bytes ( & [ 1u8 ; 32 ] ) ;
749788 let verifying_key: VerifyingKey = signing_key. verifying_key ( ) . into ( ) ;
750789 let address = authorization:: get_address ( & verifying_key) ;
@@ -788,4 +827,42 @@ mod tests {
788827 . validate_transaction ( & rotxn, & authorized)
789828 . expect ( "correctly authorized tx should validate" ) ;
790829 }
830+
831+ /// A registration whose spent reservation does not commit to the
832+ /// registered name must be rejected. Otherwise it passes validation and
833+ /// later panics in `apply_registration`, which fails to find the
834+ /// reservation to burn.
835+ #[ test]
836+ fn validate_bitnames_rejects_registration_without_matching_reservation ( ) {
837+ let ( env, state) = new_state ( "registration" ) ;
838+ let rotxn = env. read_txn ( ) . unwrap ( ) ;
839+ let name_hash = BitName ( [ 7 ; 32 ] ) ;
840+ let revealed_nonce: Hash = [ 3 ; 32 ] ;
841+ let implied_commitment: Hash =
842+ blake3:: keyed_hash ( & revealed_nonce, & name_hash. 0 ) . into ( ) ;
843+
844+ // The reservation commits to something other than the registered name.
845+ let mismatched_commitment: Hash = [ 0 ; 32 ] ;
846+ assert_ne ! ( mismatched_commitment, implied_commitment) ;
847+ let tx =
848+ registration_tx ( name_hash, revealed_nonce, mismatched_commitment) ;
849+ let err = state. validate_bitnames ( & rotxn, & tx) . expect_err (
850+ "registration without a matching reservation must be rejected" ,
851+ ) ;
852+ assert ! (
853+ matches!(
854+ err,
855+ Error :: BitName (
856+ error:: BitName :: NoReservationForRegistration { bitname }
857+ ) if bitname == name_hash
858+ ) ,
859+ "unexpected error: {err:?}"
860+ ) ;
861+
862+ // The same registration burning the matching reservation is accepted.
863+ let tx = registration_tx ( name_hash, revealed_nonce, implied_commitment) ;
864+ state. validate_bitnames ( & rotxn, & tx) . expect (
865+ "registration burning the matching reservation should validate" ,
866+ ) ;
867+ }
791868}
0 commit comments