From 0059b3ed907d35a00afe4992762f0781f9565fb6 Mon Sep 17 00:00:00 2001 From: danda Date: Wed, 12 Jan 2022 16:25:58 -0800 Subject: [PATCH] fix: refactor to validate spent_proofs in Dbc::confirm_valid --- src/amount_secrets.rs | 4 ++ src/builder.rs | 89 +++++++++++++------------- src/dbc.rs | 29 ++++----- src/lib.rs | 9 +-- src/mint.rs | 142 ++++++++++++++++++++++++++++-------------- src/spent_proof.rs | 43 ++++++------- 6 files changed, 187 insertions(+), 129 deletions(-) diff --git a/src/amount_secrets.rs b/src/amount_secrets.rs index 4f3c8e9..68acc43 100644 --- a/src/amount_secrets.rs +++ b/src/amount_secrets.rs @@ -39,6 +39,10 @@ impl AmountSecrets { self.0.value } + pub fn blinding_factor(&self) -> Scalar { + self.0.blinding + } + /// Convert to bytes pub fn to_bytes(&self) -> Vec { self.0.to_bytes() diff --git a/src/builder.rs b/src/builder.rs index d9617cf..49fc777 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -5,7 +5,7 @@ pub use blstrs::{G1Affine, Scalar}; use blsttc::{PublicKeySet, SignatureShare}; use bulletproofs::PedersenGens; use rand_core::RngCore; -use std::collections::{BTreeMap, HashSet}; +use std::collections::{BTreeMap, BTreeSet, HashSet}; use crate::{ Amount, AmountSecrets, Dbc, DbcContent, Error, KeyImage, NodeSignature, ReissueRequest, @@ -26,6 +26,29 @@ impl TransactionBuilder { self } + pub fn add_input_by_true_input( + mut self, + true_input: TrueInput, + decoy_inputs: Vec, + mut rng: impl RngCore, + ) -> Self { + self.0 + .inputs + .push(MlsagMaterial::new(true_input, decoy_inputs, &mut rng)); + self + } + + pub fn add_inputs_by_true_inputs( + mut self, + inputs: Vec<(TrueInput, Vec)>, + mut rng: impl RngCore, + ) -> Self { + for (true_input, decoy_inputs) in inputs.into_iter() { + self = self.add_input_by_true_input(true_input, decoy_inputs, &mut rng); + } + self + } + pub fn add_input_by_secrets( mut self, secret_key: Scalar, @@ -110,15 +133,15 @@ impl ReissueRequestBuilder { } /// Add a SpentProofShare for the given key_image - pub fn add_spent_proof_share(mut self, key_image: KeyImage, share: SpentProofShare) -> Self { - let shares = self.spent_proof_shares.entry(key_image).or_default(); + pub fn add_spent_proof_share(mut self, share: SpentProofShare) -> Self { + let shares = self.spent_proof_shares.entry(share.key_image).or_default(); shares.insert(share); self } pub fn build(&self) -> Result { - let spent_proofs: BTreeMap = self + let spent_proofs: BTreeSet = self .spent_proof_shares .iter() .map(|(key_image, shares)| { @@ -153,24 +176,14 @@ impl ReissueRequestBuilder { let public_commitments: Vec = any_share.public_commitments.clone(); - let index = match self - .transaction - .mlsags - .iter() - .position(|m| m.key_image.to_compressed() == *key_image) - { - Some(idx) => idx, - None => return Err(Error::SpentProofKeyImageMismatch), - }; - let spent_proof = SpentProof { - index, + key_image: *key_image, spentbook_pub_key, spentbook_sig, public_commitments, }; - Ok((*key_image, spent_proof)) + Ok(spent_proof) }) .collect::>()?; @@ -189,19 +202,14 @@ impl ReissueRequestBuilder { /// generate the final Dbc outputs. #[derive(Debug)] pub struct DbcBuilder { - pub transaction: RingCtTransaction, pub revealed_commitments: Vec, pub reissue_shares: Vec, } impl DbcBuilder { /// Create a new DbcBuilder from a ReissueTransaction - pub fn new( - transaction: RingCtTransaction, - revealed_commitments: Vec, - ) -> Self { + pub fn new(revealed_commitments: Vec) -> Self { Self { - transaction, revealed_commitments, reissue_shares: Default::default(), } @@ -246,28 +254,21 @@ impl DbcBuilder { // add pubkeyset to HashSet, so we can verify there is only one distinct PubKeySet pk_set = &pk_set | &pub_key_sets; // union the sets together. - // Verify transaction returned to us by the Mint matches our request - - // fixme: binary operation `!=` cannot be applied to type `RingCtTransaction` - - // if self.transaction != rs.transaction { - // return Err(Error::ReissueShareDbcTransactionMismatch); - // } - // Verify that mint sig count matches input count. - if rs.mint_node_signatures.len() != self.transaction.mlsags.len() { + if rs.mint_node_signatures.len() != rs.transaction.mlsags.len() { return Err(Error::ReissueShareMintNodeSignaturesLenMismatch); } // Verify that each input has a NodeSignature - - // todo: what to replace this with? - - // for input in self.reissue_transaction.inputs.iter() { - // if rs.mint_node_signatures.get(&input.spend_key()).is_none() { - // return Err(Error::ReissueShareMintNodeSignatureNotFoundForInput); - // } - // } + for mlsag in rs.transaction.mlsags.iter() { + if rs + .mint_node_signatures + .get(&mlsag.key_image.to_compressed()) + .is_none() + { + return Err(Error::ReissueShareMintNodeSignatureNotFoundForInput); + } + } } // verify that PublicKeySet for all Dbc in all ReissueShare match. @@ -286,8 +287,9 @@ impl DbcBuilder { .collect(); // Note: we can just use the first item because we already verified that - // all the ReissueShare match for dbc_transaction + // all the ReissueShare match let transaction = &self.reissue_shares[0].transaction; + let spent_proofs = &self.reissue_shares[0].spent_proofs; // Combine signatures from all the mint nodes to obtain Mint's Signature. let mint_sig = mint_public_key_set.combine_signatures(mint_sig_shares_ref)?; @@ -300,8 +302,7 @@ impl DbcBuilder { .collect(); // Form the final output DBCs, with Mint's Signature for each. - let output_dbcs: Vec = self - .transaction + let output_dbcs: Vec = transaction .outputs .iter() .map(|proof| { @@ -318,8 +319,7 @@ impl DbcBuilder { amount_secrets_list[0].clone(), )), transaction: transaction.clone(), - transaction_sigs: self - .transaction + transaction_sigs: transaction .mlsags .iter() .map(|mlsag| { @@ -329,6 +329,7 @@ impl DbcBuilder { ) }) .collect(), + spent_proofs: spent_proofs.clone(), } }) .collect(); diff --git a/src/dbc.rs b/src/dbc.rs index 2d421ec..0535059 100644 --- a/src/dbc.rs +++ b/src/dbc.rs @@ -8,9 +8,10 @@ use crate::{dbc_content::OwnerPublicKey, DbcContent, Error, KeyManager, Result}; +use crate::{Hash, SpentProof}; use blst_ringct::ringct::RingCtTransaction; use blsttc::{PublicKey, Signature}; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use tiny_keccak::{Hasher, Sha3}; // note: typedef should be moved into blst_ringct crate @@ -22,6 +23,7 @@ pub struct Dbc { pub content: DbcContent, pub transaction: RingCtTransaction, pub transaction_sigs: BTreeMap, + pub spent_proofs: BTreeSet, } impl Dbc { @@ -52,21 +54,20 @@ impl Dbc { hash } - // todo: Do we even need this fn anymore? not called by MintNode::reissue()... - // To be correct, it would need to call RingCtTransaction::verify()... - // Check there exists a DbcTransaction with the output containing this Dbc // Check there DOES NOT exist a DbcTransaction with this Dbc as parent (already minted) - pub fn confirm_valid(&self, _verifier: &K) -> Result<(), Error> { - // for (input, (mint_key, mint_sig)) in self.transaction_sigs.iter() { - // if !self.transaction.inputs.contains(input) { - // return Err(Error::UnknownInput); - // } - - // verifier - // .verify(&self.transaction.hash(), mint_key, mint_sig) - // .map_err(|e| Error::Signing(e.to_string()))?; - // } + pub fn confirm_valid(&self, verifier: &K) -> Result<(), Error> { + for spent_proof in self.spent_proofs.iter() { + if !self + .transaction + .mlsags + .iter() + .any(|m| m.key_image.to_compressed() == spent_proof.key_image) + { + return Err(Error::UnknownInput); + } + spent_proof.validate(Hash::from(self.transaction.hash()), verifier)?; + } if self.transaction.mlsags.is_empty() { Err(Error::TransactionMustHaveAnInput) diff --git a/src/lib.rs b/src/lib.rs index 6534228..39997b2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -135,22 +135,23 @@ impl DbcHelper { } } -pub(crate) struct BlsHelper {} +// temporary: should go away once blsttc is integrated with with blstrs +pub struct BlsHelper {} impl BlsHelper { #[allow(dead_code)] - pub(crate) fn blsttc_to_blstrs_sk(sk: SecretKey) -> Scalar { + pub fn blsttc_to_blstrs_sk(sk: SecretKey) -> Scalar { let bytes = sk.to_bytes(); println!("sk bytes: {:?}", bytes); Scalar::from_bytes_be(&bytes).unwrap() } - pub(crate) fn blsttc_to_blstrs_pubkey(pk: &PublicKey) -> G1Affine { + pub fn blsttc_to_blstrs_pubkey(pk: &PublicKey) -> G1Affine { let bytes = pk.to_bytes(); G1Affine::from_compressed(&bytes).unwrap() } - pub(crate) fn blstrs_to_blsttc_pubkey(pk: &G1Affine) -> PublicKey { + pub fn blstrs_to_blsttc_pubkey(pk: &G1Affine) -> PublicKey { let bytes = pk.to_compressed(); PublicKey::from_bytes(bytes).unwrap() } diff --git a/src/mint.rs b/src/mint.rs index 18578c1..18482a3 100644 --- a/src/mint.rs +++ b/src/mint.rs @@ -15,7 +15,7 @@ use crate::{ Amount, AmountSecrets, DbcContent, Error, Hash, KeyImage, KeyManager, NodeSignature, - PublicKeySet, Result, SpentProof, + PublicKeySet, Result, SpentProof, SpentProofShare, }; use blst_ringct::mlsag::{MlsagMaterial, TrueInput}; use blst_ringct::ringct::{RingCtMaterial, RingCtTransaction}; @@ -26,7 +26,8 @@ use blstrs::{G1Affine, Scalar}; use blsttc::{poly::Poly, SecretKeySet}; use rand_core::RngCore; use serde::{Deserialize, Serialize}; -use std::{collections::BTreeMap, iter::FromIterator}; +use std::collections::{BTreeMap, BTreeSet}; +use std::iter::FromIterator; pub type MintNodeSignatures = BTreeMap; @@ -44,19 +45,21 @@ pub struct GenesisDbcShare { pub public_key_set: PublicKeySet, pub transaction_sig: NodeSignature, pub secret_key: Scalar, + pub spent_proofs: BTreeSet, } // #[derive(Eq, PartialEq, Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone)] pub struct ReissueRequest { pub transaction: RingCtTransaction, - pub spent_proofs: BTreeMap, + pub spent_proofs: BTreeSet, } // #[derive(Eq, PartialEq, Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone)] pub struct ReissueShare { pub transaction: RingCtTransaction, + pub spent_proofs: BTreeSet, pub mint_node_signatures: MintNodeSignatures, } @@ -120,6 +123,8 @@ impl MintNode { .sign(&Hash::from(transaction.hash())) .map_err(|e| Error::Signing(e.to_string()))?; + let spent_proofs: BTreeSet = Default::default(); + Ok(GenesisDbcShare { ringct_material, dbc_content, @@ -128,6 +133,7 @@ impl MintNode { public_key_set: secret_key_set_ttc.public_keys(), transaction_sig, secret_key, + spent_proofs, }) } @@ -135,29 +141,82 @@ impl MintNode { &self.key_manager } + // This API will be called multiple times, once per input dbc, per section. + // The key_image param comes from the true input, but cannot be linked by mint to true input. + pub fn spend(_key_image: KeyImage, _transaction: RingCtTransaction) -> Result { + unimplemented!() + + // note: client is writing spentbook, so needs to write: + // a: key_image --> RingCtTransaction, + // b: public_key --> key_image (for lookup by public key) + + // note: do decoys have to be from same section? (for drusu to think about) + + // 1. lookup key image in spentbook, return error if not existing. + // (client did not write spentbook entry.). + + // 2. verify that tx in spentbook matches tx received from client. + + // 3. find mlsag in transaction that corresponds to the key_image, else return error. + + // 4. for each input in mlsag + // lookup tx in spentbook whose output is equal to this input. + // (full table scan, ouch, or multiple indexes into spentbook (key_image + public_key)) + // obtain the public commitment from the output. + // verify commitment from input matches spentbook output commitment. + + // 5. verify transaction itself. tx.verify() (RingCtTransaction) + + // 6. create SpentProofShare and return it. + } + pub fn reissue(&mut self, reissue_req: ReissueRequest) -> Result { - if reissue_req.transaction.mlsags.len() != reissue_req.spent_proofs.len() { + let ReissueRequest { + transaction, + spent_proofs, + } = reissue_req; + + if transaction.mlsags.len() != spent_proofs.len() { return Err(Error::SpentProofInputMismatch); } - let mut spent_proofs: Vec<&SpentProof> = Vec::from_iter(reissue_req.spent_proofs.values()); - spent_proofs.sort_by(|a, b| a.index.cmp(&b.index)); - let public_commitments: Vec> = spent_proofs + // We must get the spent_proofs into the same order as mlsags + // so that resulting public_commitments will be in the right order. + let mut spent_proofs_found: Vec<(usize, SpentProof)> = spent_proofs + .into_iter() + .filter_map(|s| { + transaction + .mlsags + .iter() + .position(|m| m.key_image.to_compressed() == s.key_image) + .map(|idx| (idx, s)) + }) + .collect(); + if spent_proofs_found.len() != transaction.mlsags.len() { + return Err(Error::SpentProofKeyImageMismatch); + } + spent_proofs_found.sort_by_key(|s| s.0); + let spent_proofs_sorted: Vec = + spent_proofs_found.into_iter().map(|s| s.1).collect(); + + let public_commitments: Vec> = spent_proofs_sorted .iter() .map(|s| s.public_commitments.clone()) .collect(); - reissue_req.transaction.verify(&public_commitments)?; + transaction.verify(&public_commitments)?; - let transaction = reissue_req.transaction; let transaction_hash = Hash::from(transaction.hash()); // Validate that each input has not yet been spent. // iterate over mlsags. each has key_image() for mlsag in transaction.mlsags.iter() { let key_image = mlsag.key_image.to_compressed(); - match reissue_req.spent_proofs.get(&key_image) { - Some(proof) => proof.validate(key_image, transaction_hash, self.key_manager())?, + match spent_proofs_sorted + .iter() + .find(|s| s.key_image == key_image) + { + Some(proof) => proof.validate(transaction_hash, self.key_manager())?, None => return Err(Error::MissingSpentProof(key_image)), } } @@ -166,6 +225,7 @@ impl MintNode { let reissue_share = ReissueShare { transaction, + spent_proofs: BTreeSet::from_iter(spent_proofs_sorted), mint_node_signatures: transaction_sigs, }; @@ -243,6 +303,7 @@ mod tests { genesis_dbc_input(), (genesis_key, genesis_sig), )]), + spent_proofs: genesis.spent_proofs, }; let validation = genesis_dbc.confirm_valid(genesis_node.key_manager()); @@ -279,6 +340,7 @@ mod tests { genesis_dbc_input(), (genesis_key, genesis_sig), )]), + spent_proofs: genesis.spent_proofs, }; let output_owner = crate::bls_dkg_id(&mut rng); @@ -300,9 +362,7 @@ mod tests { let tx_hash = &Hash::from(reissue_tx.hash()); let spentbook_pks = genesis_node.key_manager.public_key_set()?; - let spentbook_sig_share = genesis_node - .key_manager - .sign(&SpentProof::proof_msg(&tx_hash))?; + let spentbook_sig_share = genesis_node.key_manager.sign(&tx_hash)?; // there is only one input (genesis), so no decoys are available. let public_commitments: Vec = genesis_dbc @@ -313,14 +373,12 @@ mod tests { .collect(); let rr = ReissueRequestBuilder::new(reissue_tx.clone()) - .add_spent_proof_share( - reissue_tx.mlsags[0].key_image.to_compressed(), - SpentProofShare { - spentbook_pks, - spentbook_sig_share, - public_commitments, - }, - ) + .add_spent_proof_share(SpentProofShare { + key_image: reissue_tx.mlsags[0].key_image.to_compressed(), + spentbook_pks, + spentbook_sig_share, + public_commitments, + }) .build()?; let reissue_share = match genesis_node.reissue(rr) { @@ -348,7 +406,7 @@ mod tests { }; // Aggregate ReissueShare to build output DBCs - let mut dbc_builder = DbcBuilder::new(reissue_tx, revealed_commitments); + let mut dbc_builder = DbcBuilder::new(revealed_commitments); dbc_builder = dbc_builder.add_reissue_share(reissue_share); let output_dbcs = dbc_builder.build()?; @@ -434,9 +492,7 @@ mod tests { let tx_hash = &Hash::from(reissue_tx.hash()); let spentbook_pks = genesis_node.key_manager.public_key_set()?; - let spentbook_sig_share = genesis_node - .key_manager - .sign(&SpentProof::proof_msg(&tx_hash))?; + let spentbook_sig_share = genesis_node.key_manager.sign(&tx_hash)?; // there is only one input (genesis), so no decoys are available. let public_commitments: Vec = genesis @@ -449,14 +505,12 @@ mod tests { let key_image = reissue_tx.mlsags[0].key_image.to_compressed(); let rr1 = ReissueRequestBuilder::new(reissue_tx) - .add_spent_proof_share( + .add_spent_proof_share(SpentProofShare { key_image, - SpentProofShare { - spentbook_pks, - spentbook_sig_share, - public_commitments, - }, - ) + spentbook_pks, + spentbook_sig_share, + public_commitments, + }) .build()?; let reissue_share = match genesis_node.reissue(rr1.clone()) { @@ -479,7 +533,7 @@ mod tests { }; // Aggregate ReissueShare to build output DBCs - let mut dbc_builder = DbcBuilder::new(rr1.transaction, revealed_commitments); + let mut dbc_builder = DbcBuilder::new(revealed_commitments); dbc_builder = dbc_builder.add_reissue_share(reissue_share); let input_dbcs = dbc_builder.build()?; @@ -544,23 +598,19 @@ mod tests { }; let spentbook_pks = genesis_node.key_manager.public_key_set()?; - let spentbook_sig_share = genesis_node - .key_manager - .sign(&SpentProof::proof_msg(&tx_hash))?; + let spentbook_sig_share = genesis_node.key_manager.sign(&tx_hash)?; let public_commitments = in_material.commitments(&Default::default()); // note: alternate way to obtain public_commitments without RingCtMaterial // exists as comment in commit: b1502a050967f3b04659be96ea8ba745d7b49f2b - rr2_builder = rr2_builder.add_spent_proof_share( - in_mlsag.key_image.to_compressed(), - SpentProofShare { - public_commitments, - spentbook_pks, - spentbook_sig_share, - }, - ); + rr2_builder = rr2_builder.add_spent_proof_share(SpentProofShare { + key_image: in_mlsag.key_image.to_compressed(), + public_commitments, + spentbook_pks, + spentbook_sig_share, + }); } let rr2 = rr2_builder.build()?; @@ -580,7 +630,7 @@ mod tests { ); // Aggregate ReissueShare to build output DBCs - let mut dbc_builder = DbcBuilder::new(reissue_tx2, revealed_commitments); + let mut dbc_builder = DbcBuilder::new(revealed_commitments); dbc_builder = dbc_builder.add_reissue_share(rs); let output_dbcs = dbc_builder.build()?; diff --git a/src/spent_proof.rs b/src/spent_proof.rs index 5aa8e3f..c8ce227 100644 --- a/src/spent_proof.rs +++ b/src/spent_proof.rs @@ -3,13 +3,17 @@ use crate::{ }; use blstrs::G1Affine; -use serde::{Deserialize, Serialize}; +// use serde::{Deserialize, Serialize}; +use std::cmp::Ordering; use std::hash; /// A share of a SpentProof, combine enough of these to form a /// SpentProof. -#[derive(Debug, Clone, Serialize, Deserialize)] +// #[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub struct SpentProofShare { + pub key_image: KeyImage, + /// The Spentbook who notarized that this DBC was spent. pub spentbook_pks: PublicKeySet, @@ -30,6 +34,7 @@ impl Eq for SpentProofShare {} impl hash::Hash for SpentProofShare { fn hash(&self, state: &mut H) { + self.key_image.hash(state); self.spentbook_pks.hash(state); self.spentbook_sig_share.hash(state); for pc in self.public_commitments.iter() { @@ -55,9 +60,10 @@ impl SpentProofShare { /// SpentProof's are constructed when a DBC is logged to the spentbook. // #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +// #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct SpentProof { - pub index: usize, + pub key_image: KeyImage, /// The Spentbook who notarized that this DBC was spent. pub spentbook_pub_key: PublicKey, @@ -70,27 +76,22 @@ pub struct SpentProof { } impl SpentProof { - pub fn validate( - &self, - key_image: KeyImage, - tx: Hash, - verifier: &K, - ) -> Result<()> { - let msg = Self::proof_msg(&tx); + pub fn validate(&self, tx: Hash, verifier: &K) -> Result<()> { verifier - .verify(&msg, &self.spentbook_pub_key, &self.spentbook_sig) - .map_err(|_| Error::InvalidSpentProofSignature(key_image))?; + .verify(&tx, &self.spentbook_pub_key, &self.spentbook_sig) + .map_err(|_| Error::InvalidSpentProofSignature(self.key_image))?; Ok(()) } +} - pub fn proof_msg(tx: &Hash) -> Hash { - use tiny_keccak::{Hasher, Sha3}; - let mut sha3 = Sha3::v256(); - - sha3.update(&tx.0); +impl PartialOrd for SpentProof { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} - let mut hash = [0u8; 32]; - sha3.finalize(&mut hash); - Hash(hash) +impl Ord for SpentProof { + fn cmp(&self, other: &Self) -> Ordering { + self.key_image.cmp(&other.key_image) } }