@@ -661,3 +661,111 @@ impl Watchable<()> for State {
661661 tokio_stream:: wrappers:: WatchStream :: new ( self . tip . watch ( ) . clone ( ) )
662662 }
663663}
664+
665+ #[ cfg( test) ]
666+ mod tests {
667+ use ed25519_dalek:: SigningKey ;
668+
669+ use super :: { Error , State } ;
670+ use crate :: {
671+ authorization,
672+ types:: {
673+ Address , AuthorizedTransaction , BitcoinOutputContent , FilledOutput ,
674+ FilledOutputContent , OutPoint , OutPointKey , Output , OutputContent ,
675+ Transaction , VerifyingKey ,
676+ } ,
677+ } ;
678+
679+ /// Open a fresh state in a unique temporary directory.
680+ fn new_state ( ) -> ( sneed:: Env , State ) {
681+ let path = std:: env:: temp_dir ( )
682+ . join ( format ! ( "plain_bitnames_auth_count_{}" , std:: process:: id( ) ) ) ;
683+ let _remove_result = std:: fs:: remove_dir_all ( & path) ;
684+ std:: fs:: create_dir_all ( & path) . unwrap ( ) ;
685+ let env = {
686+ let mut opts = heed:: EnvOpenOptions :: new ( ) ;
687+ opts. map_size ( 10 * 1024 * 1024 ) . max_dbs ( State :: NUM_DBS ) ;
688+ unsafe { sneed:: Env :: open ( & opts, & path) } . unwrap ( )
689+ } ;
690+ let state = State :: new ( & env) . unwrap ( ) ;
691+ ( env, state)
692+ }
693+
694+ /// Fund `address` with a single bitcoin UTXO of `value` sats, returning its
695+ /// outpoint.
696+ fn fund (
697+ env : & sneed:: Env ,
698+ state : & State ,
699+ address : Address ,
700+ value : u64 ,
701+ ) -> OutPoint {
702+ let outpoint = OutPoint :: Regular {
703+ txid : Default :: default ( ) ,
704+ vout : 0 ,
705+ } ;
706+ let output = FilledOutput :: new (
707+ address,
708+ FilledOutputContent :: Bitcoin ( BitcoinOutputContent (
709+ bitcoin:: Amount :: from_sat ( value) ,
710+ ) ) ,
711+ ) ;
712+ let mut rwtxn = env. write_txn ( ) . unwrap ( ) ;
713+ state
714+ . utxos
715+ . put ( & mut rwtxn, & OutPointKey :: from ( & outpoint) , & output)
716+ . unwrap ( ) ;
717+ rwtxn. commit ( ) . unwrap ( ) ;
718+ outpoint
719+ }
720+
721+ /// A transaction that spends an input without supplying an authorization
722+ /// for it must be rejected. Otherwise the `zip` of authorizations and
723+ /// spent UTXOs silently skips the unauthorized input, allowing any UTXO to
724+ /// be spent without a signature.
725+ #[ test]
726+ fn validate_transaction_rejects_missing_authorization ( ) {
727+ let ( env, state) = new_state ( ) ;
728+ let signing_key = SigningKey :: from_bytes ( & [ 1u8 ; 32 ] ) ;
729+ let verifying_key: VerifyingKey = signing_key. verifying_key ( ) . into ( ) ;
730+ let address = authorization:: get_address ( & verifying_key) ;
731+ let outpoint = fund ( & env, & state, address, 1000 ) ;
732+
733+ let transaction = Transaction :: new (
734+ vec ! [ outpoint] ,
735+ vec ! [ Output :: new(
736+ address,
737+ OutputContent :: Bitcoin ( BitcoinOutputContent (
738+ bitcoin:: Amount :: from_sat( 900 ) ,
739+ ) ) ,
740+ ) ] ,
741+ ) ;
742+
743+ // The attack: spend the input while providing no authorization for it.
744+ let unauthorized = AuthorizedTransaction {
745+ transaction : transaction. clone ( ) ,
746+ authorizations : Vec :: new ( ) ,
747+ } ;
748+ let rotxn = env. read_txn ( ) . unwrap ( ) ;
749+ let err = state
750+ . validate_transaction ( & rotxn, & unauthorized)
751+ . expect_err ( "tx with no authorizations must be rejected" ) ;
752+ assert ! (
753+ matches!(
754+ err,
755+ Error :: WrongNumberOfAuthorizations {
756+ expected: 1 ,
757+ received: 0
758+ }
759+ ) ,
760+ "unexpected error: {err:?}"
761+ ) ;
762+
763+ // The same transaction with a valid authorization is accepted.
764+ let authorized =
765+ authorization:: authorize ( & [ ( address, & signing_key) ] , transaction)
766+ . unwrap ( ) ;
767+ state
768+ . validate_transaction ( & rotxn, & authorized)
769+ . expect ( "correctly authorized tx should validate" ) ;
770+ }
771+ }
0 commit comments