Skip to content

Commit

Permalink
feat: add APIs that enable recipient to verify AmountSecrets match co…
Browse files Browse the repository at this point in the history
…mmitted amount

test: add test_mismatched_amount_and_commitment() that tests the APIs and mis-match behavior
  • Loading branch information
dan-da committed Aug 13, 2021
1 parent ee2623e commit 54776ee
Show file tree
Hide file tree
Showing 2 changed files with 333 additions and 0 deletions.
65 changes: 65 additions & 0 deletions src/dbc_content.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,26 @@ impl DbcContent {
)?)
}

/// Checks if the secret (encrypted) amount matches the amount commitment.
/// returns true if they match, false if not, or an error if decryption fails.
pub fn confirm_amount_matches_commitment(
&self,
public_key_set: &PublicKeySet,
decryption_shares: &BTreeMap<usize, DecryptionShare>,
) -> Result<bool, Error> {
let secrets =
self.amount_secrets_by_decryption_shares(public_key_set, decryption_shares)?;
Ok(self.confirm_provided_amount_matches_commitment(&secrets))
}

/// Checks if the provided AmountSecrets matches the amount commitment.
/// note that both the amount and blinding_factor must be correct.
pub fn confirm_provided_amount_matches_commitment(&self, amount: &AmountSecrets) -> bool {
let commitment =
PedersenGens::default().commit(Scalar::from(amount.amount), amount.blinding_factor);
self.commitment == commitment.compress()
}

/// Calculates the blinding factor for the next output, typically used inside a loop.
///
/// is_last: must be true if this is the last output, else false.
Expand All @@ -268,3 +288,48 @@ impl DbcContent {
}
}
}

#[cfg(test)]
pub(crate) mod tests {
use super::*;

/// Generates a DbcContent where the committed amount may be different
/// from the secret (encrypted) amount. for testing only.
pub(crate) fn dbc_new_mismatched(
parents: BTreeSet<DbcContentHash>,
amount_committed: u64,
amount_secret: u64,
output_number: u32,
owner_key: PublicKey,
blinding_factor: Scalar,
) -> Result<DbcContent, Error> {
let owner = BlindedOwner::new(&owner_key, &parents, output_number);

let pc_gens = PedersenGens::default();
let bullet_gens = BulletproofGens::new(RANGE_PROOF_BITS, RANGE_PROOF_PARTIES);
let mut prover_ts = Transcript::new(MERLIN_TRANSCRIPT_LABEL);
let (proof, commitment) = RangeProof::prove_single(
&bullet_gens,
&pc_gens,
&mut prover_ts,
amount_committed,
&blinding_factor,
RANGE_PROOF_BITS,
)?;

let amount_secrets = AmountSecrets {
amount: amount_secret,
blinding_factor,
};
let amount_secrets_cipher = owner_key.encrypt(amount_secrets.to_bytes().as_slice());

Ok(DbcContent {
parents,
amount_secrets_cipher,
output_number,
owner,
commitment,
range_proof_bytes: proof.to_bytes(),
})
}
}
268 changes: 268 additions & 0 deletions src/mint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,7 @@ impl<K: KeyManager, S: SpendBook> Mint<K, S> {
#[cfg(test)]
mod tests {
use super::*;
use blsttc::{Ciphertext, DecryptionShare, SecretKeyShare};
use curve25519_dalek_ng::scalar::Scalar;
use quickcheck_macros::quickcheck;

Expand Down Expand Up @@ -1078,4 +1079,271 @@ mod tests {

Ok(())
}

/// This tests how the system handles a mis-match between the
/// committed amount and amount encrypted in AmountSecrets.
/// Normally these should be the same, however a malicious user or buggy
/// implementation could produce different values. The mint cannot detect
/// this situation and prevent it as the secret amount is encrypted. So it
/// is up to the recipient to check that the amounts match upon receipt. If they
/// do not match and the recipient cannot learn (or guess) the committed value then
/// the DBC will be unspendable. If they do learn the committed amount then it
/// can still be spent. So herein we do the following to test:
///
/// 1. produce a standard genesis DBC with value 1000
/// 2. reissue genesis DBC to an output with mis-matched amounts where the
/// committed amount is 1000 (required to match input) but the secret
/// amount is 2000.
/// 3. Check if the amounts match, using the two provided APIs.
/// assert that APIs report they do not match.
/// 4. Attempt to reissue the mis-matched output using the amount from
/// AmountSecrets. Verify that this fails with error DbcReissueRequestDoesNotBalance
/// 5. Attempt to reissue using the correct amount that was committed to.
/// Verify that this reissue succeeds.
#[test]
fn test_mismatched_amount_and_commitment() -> Result<(), Error> {
use crate::dbc_content::tests::dbc_new_mismatched;

// ----------
// Phase 1. Creation of Genesis DBC
// ----------
let genesis_owner = crate::bls_dkg_id();
let genesis_key = genesis_owner.public_key_set.public_key();

let key_manager = SimpleKeyManager::new(
SimpleSigner::new(
genesis_owner.public_key_set.clone(),
(0, genesis_owner.secret_key_share.clone()),
),
genesis_owner.public_key_set.public_key(),
);
let mut genesis_node = Mint::new(key_manager.clone(), SimpleSpendBook::new());

let (gen_dbc_content, gen_dbc_trans, (gen_key_set, gen_node_sig)) =
genesis_node.issue_genesis_dbc(1000)?;
let genesis_sig = gen_key_set.combine_signatures(vec![gen_node_sig.threshold_crypto()])?;

let genesis_dbc = Dbc {
content: gen_dbc_content,
transaction: gen_dbc_trans,
transaction_sigs: BTreeMap::from_iter(vec![(
GENESIS_DBC_INPUT,
(genesis_key, genesis_sig),
)]),
};

let inputs = HashSet::from_iter(vec![genesis_dbc.clone()]);
let input_hashes = BTreeSet::from_iter(vec![genesis_dbc.name()]);

let genesis_secrets =
DbcHelper::decrypt_amount_secrets(&genesis_owner, &genesis_dbc.content)?;
let outputs_owner = crate::bls_dkg_id();
let output_amount = 1000;

// ----------
// Phase 2. Creation of mis-matched output
// ----------

// Here we create an output that has a different committed amount than secret amount.
// DbcContent::new() does not allow this, so we use a replacement fn dbc_new_mismatched().
let transaction = ReissueTransaction {
inputs,
outputs: HashSet::from_iter(vec![dbc_new_mismatched(
input_hashes.clone(),
output_amount, // committed amount
output_amount * 2, // secret amount
0,
outputs_owner.public_key_set.public_key(),
genesis_secrets.blinding_factor,
)
.unwrap()]),
};

let sig_share = genesis_node
.key_manager
.sign(&transaction.blinded().hash())?;

let sig = genesis_node
.key_manager
.public_key_set()?
.combine_signatures(vec![sig_share.threshold_crypto()])?;

let reissue_req = ReissueRequest {
transaction,
input_ownership_proofs: HashMap::from_iter(vec![(
genesis_dbc.name(),
(genesis_node.key_manager.public_key_set()?.public_key(), sig),
)]),
};

// The mint should reissue this without error because the output commitment sum matches the
// input commitment sum. However the recipient will be unable to spend it using the received
// secret amount. The only way to spend it would be receive the true amount from the sender,
// or guess it. And that's assuming the secret blinding_factor is correct, which it is in this
// case, but might not be in the wild. So the output DBC could be considered to be in a
// semi-unspendable state.
let (transaction, transaction_sigs) =
genesis_node.reissue(reissue_req.clone(), input_hashes)?;

// Verify transaction returned to us by the Mint matches our request
assert_eq!(reissue_req.transaction.blinded(), transaction);

// Verify signatures corespond to each input
let (pub_key_set, sig) = transaction_sigs.values().cloned().next().unwrap();
for input in reissue_req.transaction.inputs.iter() {
assert_eq!(
transaction_sigs.get(&input.name()),
Some(&(pub_key_set.clone(), sig.clone()))
);
}
assert_eq!(transaction_sigs.len(), transaction.inputs.len());

let mint_sig = genesis_owner
.public_key_set
.combine_signatures(vec![sig.threshold_crypto()])?;

let output_dbcs =
Vec::from_iter(reissue_req.transaction.outputs.into_iter().map(|content| {
Dbc {
content,
transaction: transaction.clone(),
transaction_sigs: BTreeMap::from_iter(
transaction_sigs
.iter()
.map(|(input, _)| (*input, (genesis_key, mint_sig.clone()))),
),
}
}));
let output_dbc = &output_dbcs[0];

// obtain decryption shares so we can call confirm_amount_matches_commitment()
let mut sk_shares: BTreeMap<usize, SecretKeyShare> = Default::default();
sk_shares.insert(0, outputs_owner.secret_key_share.clone());
let decrypt_shares =
gen_decryption_shares(&output_dbc.content.amount_secrets_cipher, &sk_shares);

// obtain amount secrets
let secrets = DbcHelper::decrypt_amount_secrets(&outputs_owner, &output_dbc.content)?;

// confirm the secret amount is 2000.
assert_eq!(secrets.amount, 1000 * 2);
// confirm the dbc is considered valid using the mint-accessible api.
assert!(output_dbc.confirm_valid(&key_manager).is_ok());
// confirm the mis-match is detectable by the user who has the key to access the secrets.
assert!(!output_dbc
.content
.confirm_provided_amount_matches_commitment(&secrets));
assert!(!output_dbc
.content
.confirm_amount_matches_commitment(&outputs_owner.public_key_set, &decrypt_shares)?);

// confirm that the sum of output secrets does not match the committed amount.
assert_ne!(
output_dbcs
.iter()
.map(|dbc| { DbcHelper::decrypt_amount(&outputs_owner, &dbc.content) })
.sum::<Result<u64, _>>()?,
output_amount
);

// ----------
// Phase 3. Attempt reissue of mis-matched DBC using provided AmountSecrets
// ----------

// Next: attempt reissuing the output DBC:
// a) with provided secret amount (in band for recipient). (should fail)
// b) with true committed amount (out of band for recipient). (should succeeed)

let input_dbc = output_dbc;
let inputs = HashSet::from_iter(vec![input_dbc.clone()]);
let input_hashes = BTreeSet::from_iter(vec![input_dbc.name()]);

let input_secrets = DbcHelper::decrypt_amount_secrets(&outputs_owner, &input_dbc.content)?;

let transaction = ReissueTransaction {
inputs: inputs.clone(),
outputs: HashSet::from_iter(vec![DbcContent::new(
input_hashes.clone(),
input_secrets.amount, // secret amount
0,
outputs_owner.public_key_set.public_key(),
input_secrets.blinding_factor,
)
.unwrap()]),
};

let sig_share = outputs_owner
.secret_key_share
.sign(&transaction.blinded().hash());

let sig = outputs_owner
.public_key_set
.combine_signatures(vec![(0, &sig_share)])?;

let reissue_req = ReissueRequest {
transaction,
input_ownership_proofs: HashMap::from_iter(vec![(
input_dbc.name(),
(outputs_owner.public_key_set.public_key(), sig),
)]),
};

// The mint should give an error on reissue because the sum(inputs) does not equal sum(outputs)
let result = genesis_node.reissue(reissue_req, input_hashes.clone());
match result {
Err(Error::DbcReissueRequestDoesNotBalance) => {}
_ => panic!("Expecting Error::DbcReissueRequestDoesNotBalance"),
}

// ----------
// Phase 4. Successful reissue of mis-matched DBC using true committed amount.
// ----------

let transaction = ReissueTransaction {
inputs,
outputs: HashSet::from_iter(vec![DbcContent::new(
input_hashes.clone(),
output_amount, // the true (committed) amount from previous reissue.
0,
outputs_owner.public_key_set.public_key(),
input_secrets.blinding_factor,
)
.unwrap()]),
};

let sig_share = outputs_owner
.secret_key_share
.sign(&transaction.blinded().hash());

let sig = outputs_owner
.public_key_set
.combine_signatures(vec![(0, &sig_share)])?;

let reissue_req = ReissueRequest {
transaction,
input_ownership_proofs: HashMap::from_iter(vec![(
input_dbc.name(),
(outputs_owner.public_key_set.public_key(), sig),
)]),
};

// The mint should reissue without error because the sum(inputs) does equal sum(outputs)
let result = genesis_node.reissue(reissue_req, input_hashes);
assert!(result.is_ok());

Ok(())
}

/// helper fn to generate DecryptionShares from SecretKeyShare(s) and a Ciphertext
fn gen_decryption_shares(
cipher: &Ciphertext,
secret_key_shares: &BTreeMap<usize, SecretKeyShare>,
) -> BTreeMap<usize, DecryptionShare> {
let mut decryption_shares: BTreeMap<usize, DecryptionShare> = Default::default();
for (idx, sec_share) in secret_key_shares.iter() {
let share = sec_share.decrypt_share_no_verify(cipher);
decryption_shares.insert(*idx, share);
}
decryption_shares
}
}

0 comments on commit 54776ee

Please sign in to comment.