Confidential Transaction: A Mutual Cooperation Procedure - garyyu/rust-secp256k1-zkp GitHub Wiki
As pointed in Confidential Transaction second demo, the confidential transaction must be a mutual cooperation procedure. In this page, let's have a close view on this mutual procedure.
As a good start, let's reuse an existing transaction workflow as the reference procedure, and then here we can mainly focus on a working demo for this workflow, step by step. The unrelated steps will be skipped.
On sender side:
- Create Transaction UUID (for reference and maintaining correct state)
- Set lock_height for transaction kernel (current chain height)
- Select inputs using desired selection strategy
- Create change_output
- Select blinding factor for change_output
- Create lock function sF that locks inputs and stores change_output in wallet and identifying wallet transaction log entry TS linking inputs + outputs (Not executed at this point)
- Calculate tx_weight: MAX(-1 * num_inputs + 4 * num_change_outputs + 1, 1) (+1 covers a single output on the receiver's side)
- Calculate fee: tx_weight * 1,000,000 nG. Note: 1 nG = 10^-9 G.
- Calculate total blinding excess xS1 (private scalar), sum for all inputs(-) and outputs(+)
- Select a random nonce kS (private scalar)
- Subtract random kernel offset oS from xS1 to create xS. Calculate xS = xS1 - oS
- Multiply xS and kS by generator G to create public curve points xSG and kSG
- Add values to Slate for passing to other participants: UUID, inputs, change_outputs, fee, amount, lock_height, kSG, xSG, oS
On receiver side:
- Check fee against number of inputs, change_outputs +1 * receiver_output)
- Create receiver_output
- Choose random blinding factor for receiver_output xR (private scalar)
- Calculate message M = fee | lock_height
- Choose random nonce kR (private scalar)
- Multiply xR and kR by generator G to create public curve points xRG and kRG
- Compute Schnorr challenge e = SHA256(M | kRG + kSG)
- Compute Recipient Schnorr signature sR = kR + e * xR
- Add sR, xRG, kRG to Slate
- Create wallet output function rF that stores receiver_output in wallet with status "Unconfirmed" and identifying transaction log entry TR linking receiver_output with transaction.
On sender side:
- Calculate message M = fee | lock_height
- Compute Schnorr challenge e = SHA256(M | kRG + kSG)
- Verify sR by verifying kRG + e * xRG = sRG
- Compute Sender Schnorr signature sS = kS + e * xS
- Calculate final signature s = (sS+sR, kSG+kRG)
- Calculate public key for s: xG = xRG + xSG
- Verify s against excess values in final transaction using xG
- Create Transaction Kernel Containing:
- Signature s
- Public excess(also as public key), converted from xG
- fee
- lock_height
- Offset value oS
Let's check details step by step, by run the following Rust test.
Environment preparation:
$ git clone --recursive https://github.com/garyyu/rust-secp256k1-zkp.git
$ cd rust-secp256k1-zkp
$ cargo build --release
Source Code (Click to expand)
#[test]
#[allow(non_snake_case)]
fn demo_mutual_procedure() {
fn commit(value: u64, blinding: SecretKey) -> Commitment {
let secp = Secp256k1::with_caps(ContextFlag::Commit);
secp.commit(value, blinding).unwrap()
}
println!("\nCT Mutual Procedure: Round 1 (on sender side)");
let sender_sk; // private key of sender
let sender_kS; // secretnonce of sender
let (input,change_output,fee,amount,lock_height,kSG,xSG,oS) = {
let secp = Secp256k1::with_caps(ContextFlag::Full);
let in_amount: u64 = 10 * 1_000_000_000;
let out_amount: u64 = 8 * 1_000_000_000;
//--- step 2. Set lock_height for transaction kernel (current chain height)
let lock_height: u64 = 10_000; // just for example
//--- step 3. Select inputs using desired selection strategy
//simulate an UTXO as the input
let blinding_input = SecretKey::new(&secp, &mut OsRng::new().unwrap());
let input = commit(in_amount, blinding_input);
//--- step 7. Skipped.
//--- step 8. Calculate fee: tx_weight * 1_000_000 nG
let fee: u64 = 8 * 1_000_000;
//--- step 4. Create change_output
//--- step 5. Select blinding factor for change_output
let blinding_change_output = SecretKey::new(&secp, &mut OsRng::new().unwrap());
let change_output = commit(in_amount-out_amount-fee, blinding_change_output);
//--- step 9. Calculate total blinding excess sum xS1 (private scalar), for all inputs(-) and outputs(+)
let xS1 = secp.blind_sum(vec![blinding_change_output], vec![blinding_input]).unwrap();
//--- step 10. Select a random nonce kS (private scalar)
let kS = SecretKey::new(&secp, &mut OsRng::new().unwrap());
//--- step 11. Subtract random value oS (kernel offset) from xS1. Calculate xS = xS1 - oS
let oS = SecretKey::new(&secp, &mut OsRng::new().unwrap());
let xS = secp.blind_sum(vec![xS1], vec![oS]).unwrap();
sender_sk = xS; // save for final round
sender_kS = kS; // save for final round
//--- step 12. Multiply xS and kS by generator G to create public curve points xSG and kSG
let xSG = PublicKey::from_secret_key(&secp, &xS).unwrap();
let kSG = PublicKey::from_secret_key(&secp, &kS).unwrap();
//--- step 13. Add values to Slate for passing to other participants: UUID, inputs, change_outputs, fee, amount, lock_height, kSG, xSG, oS
(input,change_output,fee,out_amount,lock_height,kSG,xSG,oS)
};
println!("\nCT Mutual Procedure: Round 1 Done. Sender post to Receiver: inputs, change_outputs, fee, amount, lock_height, kSG, xSG, oS");
println!("\nCT Mutual Procedure: Round 2 (on receiver side)");
let (sR, xRG, kRG, receiver_output) = {
let secp = Secp256k1::with_caps(ContextFlag::Full);
//--- step 1. Check fee against number of inputs, change_outputs +1 * receiver_output)
//skipped.
//--- step 2. Create receiver_output
//--- step 3. Choose random blinding factor for receiver_output xR (private scalar)
let xR = SecretKey::new(&secp, &mut OsRng::new().unwrap());
let output = commit(amount, xR);
//--- step 4. Calculate message M = fee | lock_height
let msg = Message::from_slice(&kernel_sig_msg(fee, lock_height)).unwrap();
//--- step 5. Choose random nonce kR (private scalar)
let kR = SecretKey::new(&secp, &mut OsRng::new().unwrap());
//--- step 6. Multiply xR and kR by generator G to create public curve points xRG and kRG
let xRG = PublicKey::from_secret_key(&secp, &xR).unwrap();
let kRG = PublicKey::from_secret_key(&secp, &kR).unwrap();
let xG = PublicKey::from_combination(&secp, vec![&xRG, &xSG]).unwrap();
let excess_commit = Commitment::from_pubkey(&secp, &xG).unwrap();
if true==secp.verify_commit_sum(
vec![output, change_output,
commit(fee, secp.blind_sum(vec![], vec![oS]).unwrap())],
vec![input, excess_commit],
){
println!("\ntotal sum balance OK:\toutput + change_output + (-offset*G + fee*H) = input + excess");
}else{
println!("\ntotal sum balance NOK:\toutput + change_output + (-offset*G + fee*H) != input + excess");
}
//--- step 7. Compute Schnorr challenge e = SHA256(M | kRG + kSG)
//--- step 8. Compute Recipient Schnorr signature sR = kR + e * xR
let nonce_sum = PublicKey::from_combination(&secp, vec![&kRG, &kSG]).unwrap();
let sR = sign_single(&secp, &msg, &xR, Some(&kR), Some(&nonce_sum), Some(&nonce_sum)).unwrap();
//--- step 9. Add sR, xRG, kRG to Slate
//--- step 10. Create wallet output function rF that stores receiver_output in wallet
// with status "Unconfirmed" and identifying transaction log entry TR linking
// receiver_output with transaction.
(sR, xRG, kRG, output)
};
println!("\nCT Mutual Procedure: Round 2 Done. Receiver post to Sender: sR, xRG, kRG, receiver_output");
println!("\nCT Mutual Procedure: Final Round (on sender side)");
let (s, excess_commit, fee, lock_height, oS) = {
let secp = Secp256k1::with_caps(ContextFlag::Full);
//--- step 1. Calculate message M = fee | lock_height
let msg = Message::from_slice(&kernel_sig_msg(fee, lock_height)).unwrap();
//--- step 2. Compute Schnorr challenge e = SHA256(M | kRG + kSG)
//--- step 3. Verify sR by verifying kRG = sRG - e * xRG
let nonce_sum = PublicKey::from_combination(&secp, vec![&kRG, &kSG]).unwrap();
let result = verify_single(&secp, &sR, &msg, Some(&nonce_sum), &xRG, true);
if true==result {
println!("Signature 'sR' Verification:\tOK");
}else{
println!("Signature 'sR' Verification:\tNOK");
}
//--- step 4. Compute Sender Schnorr signature sS = kS + e * xS
let xS = sender_sk; // load sender's private key , which is saved in 1st round
let kS = sender_kS; // load sender's secret nonce, which is saved in 1st round
let sS = sign_single(&secp, &msg, &xS, Some(&kS), Some(&nonce_sum), Some(&nonce_sum)).unwrap();
//--- step 5. Calculate final signature s = (sS+sR, kSG+kRG)
let sig_vec = vec![&sR, &sS];
let s = add_signatures_single(&secp, sig_vec, &nonce_sum).unwrap();
//--- step 6. Calculate public key for s: xG = xRG + xSG
let xG = PublicKey::from_combination(&secp, vec![&xRG, &xSG]).unwrap();
let excess_commit = Commitment::from_pubkey(&secp, &xG).unwrap();
if true==secp.verify_commit_sum(
vec![receiver_output, change_output,
commit(fee, secp.blind_sum(vec![], vec![oS]).unwrap())],
vec![input, excess_commit],
){
println!("\ntotal sum balance OK:\toutput + change_output + (-offset*G + fee*H) = input + excess");
}else{
println!("\ntotal sum balance NOK:\toutput + change_output + (-offset*G + fee*H) != input + excess");
}
//--- step 7. Verify s against excess values in final transaction using xG
let result = verify_single(&secp, &s, &msg, Some(&nonce_sum), &xG, false);
if true==result {
println!("Signature 's' Verification:\tOK");
}else{
println!("Signature 's' Verification:\tNOK");
}
//--- step 8. Create Transaction Kernel Containing:
// Signature: s, Public key: xG, fee, lock_height, excess value: oS
(s, excess_commit, fee, lock_height, oS)
};
println!("\nCT Mutual Procedure: Final Round Done. Sender post to mempool: s, 'public excess', fee, lock_height, oS, and input,outputs");
println!("\ns:\t\t{:?}\nxG 2 commit:\t{:?}\nfee:\t\t{:?}\nlock_height:\t{:?}\noS:\t\t{:?}\n",
s, excess_commit, fee, lock_height, oS);
}
And here is the running result:
running 1 test
CT Mutual Procedure: Round 1 (on sender side)
CT Mutual Procedure: Round 1 Done. Sender post to Receiver: inputs, change_outputs, fee, amount, lock_height, kSG, xSG, oS
CT Mutual Procedure: Round 2 (on receiver side)
total sum balance OK: output + change_output + (-offset*G + fee*H) = input + excess
CT Mutual Procedure: Round 2 Done. Receiver post to Sender: sR, xRG, kRG, receiver_output
CT Mutual Procedure: Final Round (on sender side)
Signature 'sR' Verification: OK
total sum balance OK: output + change_output + (-offset*G + fee*H) = input + excess
Signature 's' Verification: OK
CT Mutual Procedure: Final Round Done. Sender post to mempool: s, 'public excess', fee, lock_height, oS, and input,outputs
s: Signature(1e325e06b47adac7ed50c5a70f7c325ca1fbc625a0fc87ab454c1beb6748fde903bc21a4fb65191fdc215bd8c86a933f1c218083ba0b4b8229b209980927af4f)
xG 2 commit: Commitment(09fa79bbe28cf054b56c88aaeb16774279d2a13bf8bc2893b33215e5cd624fdde1)
fee: 8000000
lock_height: 10000
oS: SecretKey(9b36cb945c4e0c1c4c67697aeb709fb07464b0f77ec5683ac883d5d127fa7ade)
Explain:
-
xS
andxR
are private keys of sender and receiver, which are used for Schnorr Signature. -
xG = xSG+xRG
is used as the final signature's public key, this is a typical shared signature. -
xS
comes from a special calculation:xS = roc - ri - offset
, refer to detail here. The receiver getxSG
only after 1st round cooperation. -
xR
is the blinding factor of receiver output. The sender getxRG
only after 2nd round cooperation. - The receiver need check the Pedersen Commitment balance, by
change_output + receiver_output + (-offset*G + fee*H) = input + public_excess
, wherepublic_excess = xRG + xSG
. Refer to detail here. -
kR
andkS
are secret nonces of receiver and sender.nonce_sum = kRG + kSG
is used for aggregated Schnorr Signature. - In final round, after sender receive
xRG
and receiver output, the sender also need check the Pedersen Commitment balance. - The sender's Schnorr Signature
sS
don't need send to receiver. - The final signature's done by sender! and the final transaction is published by sender!
Compare to transaction design of original Mimblewimble protocol, above procedure has a big improvement:
- Instead of receiver do the final signature, this improved procedure let sender do that.
- Instead of receiver publish the final transaction, this improved procedure let sender do that!
Thanks to Schnorr Signature, this improved transaction procedure is more reasonable and theoretically more safe than the original protocol.
In this transaction procedure, if receiver stop at Round 2, and directly complete a single signature then publish to mempool, what will happen?
Answer: With the same public excess, that's impossible, because neither the receiver nor the sender has the private key of this public excess. Remember this public excess = xRG + xSG
, and sender hold the private of xSG
, but receiver hold the private of xRG
. So, receiver can't complete a signature with this public excess as public key.
Answer: With the equation change_output + receiver_output + (-offset*G + fee*H) = input + excess'
(refer to detail here), if want to change excess'
, to make it still balance, we need change offset
also, looks like it's possible. But even receiver changed this excess'
with adjusted offset
value, it's still a public key! without private key, receiver still can't complete a signature with it as public key.
Answer: Yes. But that's no any benefit for him/her, a transaction with wrong fee
will be rejected by miner.
Note: For a simple demo, the test routine doesn't do the range proof for the output. To add it, it's quite simple, just refer to Range Proof page.