Merge pull request #22 from paritytech/sapling_nullifiers

Checking sapling nullifiers (double spend)
This commit is contained in:
Svyatoslav Nikolsky 2018-12-21 15:31:16 +03:00 committed by GitHub
commit 50ca1a5448
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 159 additions and 50 deletions

View File

@ -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;

View File

@ -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,25 +488,26 @@ 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() {
for nullifier in &description.nullifiers[..] {
let check = Nullifier::new(self.tag, H256::from(&nullifier[..]));
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(NullifierTag::Sprout, H256::from(&nullifier[..]));
if self.tracker.contains_nullifier(check) {
return Err(TransactionError::JoinSplitDeclared(*check.hash()))
if self.tracker.contains_nullifier(check) {
return Err(TransactionError::JoinSplitDeclared(*check.hash()))
}
}
}
}
@ -516,20 +516,69 @@ impl<'a> Nullifiers<'a> {
}
}
/// 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()))
);
}
}

View File

@ -136,5 +136,7 @@ pub enum TransactionError {
JoinSplitVersionInvalid,
/// Transaction sapling verification has failed.
InvalidSapling,
/// Sapling nullifier already revealed earlier in the chain.
SaplingDeclared(H256),
}