Merge pull request #22 from paritytech/sapling_nullifiers
Checking sapling nullifiers (double spend)
This commit is contained in:
commit
50ca1a5448
|
@ -7,7 +7,7 @@ use bytes::Bytes;
|
|||
use primitives::compact::Compact;
|
||||
use chain::{
|
||||
IndexedBlock, IndexedBlockHeader, IndexedTransaction, BlockHeader, Block, Transaction,
|
||||
OutPoint, TransactionOutput, SAPLING_TX_VERSION_GROUP_ID,
|
||||
OutPoint, TransactionOutput,
|
||||
};
|
||||
use ser::{
|
||||
deserialize, serialize, List
|
||||
|
@ -287,15 +287,13 @@ impl<T> BlockChainDatabase<T> where T: KeyValueDatabase {
|
|||
}
|
||||
|
||||
for tx in block.transactions.iter().skip(1) {
|
||||
let is_sapling_group = tx.raw.version_group_id == SAPLING_TX_VERSION_GROUP_ID;
|
||||
|
||||
modified_meta.insert(tx.hash.clone(), TransactionMeta::new(new_best_block.number, tx.raw.outputs.len()));
|
||||
|
||||
if let Some(ref js) = tx.raw.join_split {
|
||||
for js_descriptor in js.descriptions.iter() {
|
||||
for nullifier in &js_descriptor.nullifiers[..] {
|
||||
let nullifier_key = Nullifier::new(
|
||||
if is_sapling_group { NullifierTag::Sapling } else { NullifierTag::Sprout },
|
||||
NullifierTag::Sprout,
|
||||
H256::from(&nullifier[..])
|
||||
);
|
||||
if self.contains_nullifier(nullifier_key) {
|
||||
|
@ -307,6 +305,20 @@ impl<T> BlockChainDatabase<T> where T: KeyValueDatabase {
|
|||
}
|
||||
}
|
||||
|
||||
if let Some(ref sapling) = tx.raw.sapling {
|
||||
for spend in &sapling.spends {
|
||||
let nullifier_key = Nullifier::new(
|
||||
NullifierTag::Sapling,
|
||||
H256::from(&spend.nullifier[..])
|
||||
);
|
||||
if self.contains_nullifier(nullifier_key) {
|
||||
trace!(target: "db", "Duplicate nullifer during canonization: {:?}", nullifier_key);
|
||||
return Err(Error::CannotCanonize);
|
||||
}
|
||||
update.insert(KeyValue::Nullifier(nullifier_key));
|
||||
}
|
||||
}
|
||||
|
||||
for input in &tx.raw.inputs {
|
||||
use std::collections::hash_map::Entry;
|
||||
|
||||
|
@ -363,12 +375,11 @@ impl<T> BlockChainDatabase<T> where T: KeyValueDatabase {
|
|||
|
||||
let mut modified_meta: HashMap<H256, TransactionMeta> = HashMap::new();
|
||||
for tx in block.transactions.iter().skip(1) {
|
||||
let is_sapling_group = tx.raw.version_group_id == SAPLING_TX_VERSION_GROUP_ID;
|
||||
if let Some(ref js) = tx.raw.join_split {
|
||||
for js_descriptor in js.descriptions.iter() {
|
||||
for nullifier in &js_descriptor.nullifiers[..] {
|
||||
let nullifier_key = Nullifier::new(
|
||||
if is_sapling_group { NullifierTag::Sapling } else { NullifierTag::Sprout },
|
||||
NullifierTag::Sprout,
|
||||
H256::from(&nullifier[..])
|
||||
);
|
||||
if !self.contains_nullifier(nullifier_key) {
|
||||
|
@ -380,6 +391,20 @@ impl<T> BlockChainDatabase<T> where T: KeyValueDatabase {
|
|||
}
|
||||
}
|
||||
|
||||
if let Some(ref sapling) = tx.raw.sapling {
|
||||
for spend in &sapling.spends {
|
||||
let nullifier_key = Nullifier::new(
|
||||
NullifierTag::Sapling,
|
||||
H256::from(&spend.nullifier[..])
|
||||
);
|
||||
if !self.contains_nullifier(nullifier_key) {
|
||||
warn!(target: "db", "cannot decanonize, no nullifier: {:?}", nullifier_key);
|
||||
return Err(Error::CannotDecanonize);
|
||||
}
|
||||
update.delete(Key::Nullifier(nullifier_key));
|
||||
}
|
||||
}
|
||||
|
||||
for input in &tx.raw.inputs {
|
||||
use std::collections::hash_map::Entry;
|
||||
|
||||
|
|
|
@ -11,7 +11,6 @@ use canon::CanonTransaction;
|
|||
use constants::COINBASE_MATURITY;
|
||||
use error::TransactionError;
|
||||
use primitives::hash::H256;
|
||||
use chain::SAPLING_TX_VERSION_GROUP_ID;
|
||||
use VerificationLevel;
|
||||
|
||||
pub struct TransactionAcceptor<'a> {
|
||||
|
@ -22,8 +21,8 @@ pub struct TransactionAcceptor<'a> {
|
|||
pub overspent: TransactionOverspent<'a>,
|
||||
pub double_spent: TransactionDoubleSpend<'a>,
|
||||
pub eval: TransactionEval<'a>,
|
||||
pub join_split: Option<JoinSplitVerification<'a>>,
|
||||
pub sapling_valid: TransactionSaplingValid<'a>,
|
||||
pub join_split: JoinSplitVerification<'a>,
|
||||
pub sapling: SaplingVerification<'a>,
|
||||
}
|
||||
|
||||
impl<'a> TransactionAcceptor<'a> {
|
||||
|
@ -48,17 +47,16 @@ impl<'a> TransactionAcceptor<'a> {
|
|||
bip30: TransactionBip30::new_for_sync(transaction, meta_store),
|
||||
missing_inputs: TransactionMissingInputs::new(transaction, output_store, transaction_index),
|
||||
maturity: TransactionMaturity::new(transaction, meta_store, height),
|
||||
sapling_valid: TransactionSaplingValid::new(
|
||||
overspent: TransactionOverspent::new(transaction, output_store),
|
||||
double_spent: TransactionDoubleSpend::new(transaction, output_store),
|
||||
eval: TransactionEval::new(transaction, output_store, consensus, verification_level, height, time, deployments),
|
||||
join_split: JoinSplitVerification::new(transaction, nullifier_tracker),
|
||||
sapling: SaplingVerification::new(
|
||||
nullifier_tracker,
|
||||
consensus.sapling_spend_verifying_key,
|
||||
consensus.sapling_output_verifying_key,
|
||||
transaction,
|
||||
),
|
||||
overspent: TransactionOverspent::new(transaction, output_store),
|
||||
double_spent: TransactionDoubleSpend::new(transaction, output_store),
|
||||
eval: TransactionEval::new(transaction, output_store, consensus, verification_level, height, time, deployments),
|
||||
join_split: transaction.join_split().map(|js| {
|
||||
JoinSplitVerification::new(transaction.raw.version_group_id, js, nullifier_tracker)
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -73,7 +71,8 @@ impl<'a> TransactionAcceptor<'a> {
|
|||
// to make sure we're using the sighash-cache, let's make all sighash-related
|
||||
// calls from single checker && pass sighash to other checkers
|
||||
let sighash = self.eval.check()?;
|
||||
self.sapling_valid.check(sighash)?;
|
||||
self.join_split.check()?;
|
||||
self.sapling.check(sighash)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -477,11 +476,11 @@ impl<'a> TransactionSize<'a> {
|
|||
|
||||
/// Check the joinsplit proof of the transaction
|
||||
pub struct JoinSplitProof<'a> {
|
||||
_join_split: &'a chain::JoinSplit,
|
||||
_transaction: CanonTransaction<'a>,
|
||||
}
|
||||
|
||||
impl<'a> JoinSplitProof<'a> {
|
||||
fn new(join_split: &'a chain::JoinSplit) -> Self { JoinSplitProof { _join_split: join_split }}
|
||||
fn new(transaction: CanonTransaction<'a>) -> Self { JoinSplitProof { _transaction: transaction }}
|
||||
|
||||
fn check(&self) -> Result<(), TransactionError> {
|
||||
// TODO: Zero-knowledge proof
|
||||
|
@ -489,47 +488,97 @@ impl<'a> JoinSplitProof<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Check if nullifiers are unique
|
||||
pub struct Nullifiers<'a> {
|
||||
tag: NullifierTag,
|
||||
/// Check if join split nullifiers are unique
|
||||
pub struct JoinSplitNullifiers<'a> {
|
||||
tracker: &'a NullifierTracker,
|
||||
join_split: &'a chain::JoinSplit,
|
||||
transaction: CanonTransaction<'a>,
|
||||
}
|
||||
|
||||
impl<'a> Nullifiers<'a> {
|
||||
fn new(tag: NullifierTag, tracker: &'a NullifierTracker, join_split: &'a chain::JoinSplit) -> Self {
|
||||
Nullifiers { tag: tag, tracker: tracker, join_split: join_split }
|
||||
impl<'a> JoinSplitNullifiers<'a> {
|
||||
fn new(tracker: &'a NullifierTracker, transaction: CanonTransaction<'a>) -> Self {
|
||||
JoinSplitNullifiers { tracker: tracker, transaction: transaction }
|
||||
}
|
||||
|
||||
fn check(&self) -> Result<(), TransactionError> {
|
||||
for description in self.join_split.descriptions.iter() {
|
||||
if let Some(ref join_split) = self.transaction.raw.join_split {
|
||||
for description in join_split.descriptions.iter() {
|
||||
for nullifier in &description.nullifiers[..] {
|
||||
let check = Nullifier::new(self.tag, H256::from(&nullifier[..]));
|
||||
let check = Nullifier::new(NullifierTag::Sprout, H256::from(&nullifier[..]));
|
||||
|
||||
if self.tracker.contains_nullifier(check) {
|
||||
return Err(TransactionError::JoinSplitDeclared(*check.hash()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Join split verification
|
||||
pub struct JoinSplitVerification<'a> {
|
||||
proof: JoinSplitProof<'a>,
|
||||
nullifiers: JoinSplitNullifiers<'a>,
|
||||
}
|
||||
|
||||
impl<'a> JoinSplitVerification<'a> {
|
||||
pub fn new(transaction: CanonTransaction<'a>, tracker: &'a NullifierTracker)
|
||||
-> Self
|
||||
{
|
||||
JoinSplitVerification {
|
||||
proof: JoinSplitProof::new(transaction),
|
||||
nullifiers: JoinSplitNullifiers::new(tracker, transaction),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check(&self) -> Result<(), TransactionError> {
|
||||
self.proof.check()?;
|
||||
self.nullifiers.check()
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if Sapling nullifiers are unique
|
||||
pub struct SaplingNullifiers<'a> {
|
||||
tracker: &'a NullifierTracker,
|
||||
transaction: CanonTransaction<'a>,
|
||||
}
|
||||
|
||||
impl<'a> SaplingNullifiers<'a> {
|
||||
fn new(tracker: &'a NullifierTracker, transaction: CanonTransaction<'a>) -> Self {
|
||||
SaplingNullifiers { tracker: tracker, transaction: transaction }
|
||||
}
|
||||
|
||||
fn check(&self) -> Result<(), TransactionError> {
|
||||
if let Some(ref sapling) = self.transaction.raw.sapling {
|
||||
for spend in &sapling.spends {
|
||||
let check = Nullifier::new(NullifierTag::Sapling, H256::from(&spend.nullifier[..]));
|
||||
|
||||
if self.tracker.contains_nullifier(check) {
|
||||
return Err(TransactionError::SaplingDeclared(*check.hash()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Checks that sapling signatures/proofs are valid.
|
||||
pub struct TransactionSaplingValid<'a> {
|
||||
pub struct SaplingProof<'a> {
|
||||
spend_vk: &'a Groth16VerifyingKey,
|
||||
output_vk: &'a Groth16VerifyingKey,
|
||||
transaction: CanonTransaction<'a>,
|
||||
}
|
||||
|
||||
impl<'a> TransactionSaplingValid<'a> {
|
||||
impl<'a> SaplingProof<'a> {
|
||||
fn new(
|
||||
spend_vk: &'a Groth16VerifyingKey,
|
||||
output_vk: &'a Groth16VerifyingKey,
|
||||
transaction: CanonTransaction<'a>,
|
||||
) -> Self {
|
||||
TransactionSaplingValid {
|
||||
SaplingProof {
|
||||
spend_vk,
|
||||
output_vk,
|
||||
transaction: transaction,
|
||||
|
@ -546,27 +595,28 @@ impl<'a> TransactionSaplingValid<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Join split verification
|
||||
pub struct JoinSplitVerification<'a> {
|
||||
proof: JoinSplitProof<'a>,
|
||||
nullifiers: Nullifiers<'a>,
|
||||
/// Sapling verification
|
||||
pub struct SaplingVerification<'a> {
|
||||
proof: SaplingProof<'a>,
|
||||
nullifiers: SaplingNullifiers<'a>,
|
||||
}
|
||||
|
||||
impl<'a> JoinSplitVerification<'a> {
|
||||
pub fn new(tx_version_group: u32, join_split: &'a chain::JoinSplit, tracker: &'a NullifierTracker)
|
||||
-> Self
|
||||
impl<'a> SaplingVerification<'a> {
|
||||
pub fn new(
|
||||
tracker: &'a NullifierTracker,
|
||||
spend_vk: &'a Groth16VerifyingKey,
|
||||
output_vk: &'a Groth16VerifyingKey,
|
||||
transaction: CanonTransaction<'a>
|
||||
) -> Self
|
||||
{
|
||||
let tag = if tx_version_group == SAPLING_TX_VERSION_GROUP_ID
|
||||
{ NullifierTag::Sapling } else { NullifierTag::Sprout };
|
||||
|
||||
JoinSplitVerification {
|
||||
proof: JoinSplitProof::new(join_split),
|
||||
nullifiers: Nullifiers::new(tag, tracker, join_split),
|
||||
SaplingVerification {
|
||||
proof: SaplingProof::new(spend_vk, output_vk, transaction),
|
||||
nullifiers: SaplingNullifiers::new(tracker, transaction),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check(&self) -> Result<(), TransactionError> {
|
||||
self.proof.check()?;
|
||||
pub fn check(&self, sighash: H256) -> Result<(), TransactionError> {
|
||||
self.proof.check(sighash)?;
|
||||
self.nullifiers.check()
|
||||
}
|
||||
}
|
||||
|
@ -574,8 +624,10 @@ impl<'a> JoinSplitVerification<'a> {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use chain::Transaction;
|
||||
use chain::{Transaction, Sapling};
|
||||
use db::BlockChainDatabase;
|
||||
use script::{Script, VerificationFlags, TransactionSignatureChecker, TransactionInputSigner, verify_script};
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn join_split() {
|
||||
|
@ -606,4 +658,34 @@ mod tests {
|
|||
.verify_p2sh(true);
|
||||
assert_eq!(verify_script(&input_script, &output_script, &flags, &mut checker), Ok(()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sapling_nullifiers_works() {
|
||||
let storage = BlockChainDatabase::init_test_chain(vec![test_data::genesis().into()]);
|
||||
|
||||
let tx: Transaction = test_data::TransactionBuilder::with_sapling(Sapling {
|
||||
spends: vec![Default::default()],
|
||||
..Default::default()
|
||||
}).into();
|
||||
let block = test_data::block_builder()
|
||||
.header().parent(test_data::genesis().hash()).build()
|
||||
.transaction().coinbase().build()
|
||||
.with_transaction(tx.clone())
|
||||
.build();
|
||||
let tx = tx.into();
|
||||
let block_hash = block.hash();
|
||||
|
||||
// when nullifier is not in the db
|
||||
assert_eq!(SaplingNullifiers::new(&storage, CanonTransaction::new(&tx)).check(), Ok(()));
|
||||
|
||||
// insert nullifier into db
|
||||
storage.insert(block.into()).unwrap();
|
||||
storage.canonize(&block_hash).unwrap();
|
||||
|
||||
// when nullifier is in the db
|
||||
assert_eq!(
|
||||
SaplingNullifiers::new(&storage, CanonTransaction::new(&tx)).check(),
|
||||
Err(TransactionError::SaplingDeclared(Default::default()))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -136,5 +136,7 @@ pub enum TransactionError {
|
|||
JoinSplitVersionInvalid,
|
||||
/// Transaction sapling verification has failed.
|
||||
InvalidSapling,
|
||||
/// Sapling nullifier already revealed earlier in the chain.
|
||||
SaplingDeclared(H256),
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue