Skip to content

Commit dacd927

Browse files
1440000bytesAsh-L2L
authored andcommitted
Add test for rejecting transactions with missing authorizations
1 parent aca2a48 commit dacd927

1 file changed

Lines changed: 108 additions & 0 deletions

File tree

lib/state/mod.rs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)