Skip to content

Commit 04f3d4c

Browse files
authored
Merge pull request #781 from ergoplatform/deterministic_nonce
Add Wallet.sign_deterministic, deterministic nonces
2 parents 61a345e + 210f360 commit 04f3d4c

File tree

4 files changed

+248
-4
lines changed

4 files changed

+248
-4
lines changed

Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ ergo-merkle-tree = { version = "^0.15.0", path = "./ergo-merkle-tree" }
3737
ergo-rest = { version = "^0.13.0", path = "./ergo-rest" }
3838
ergo-lib = { version = "^0.28.0", path = "./ergo-lib" }
3939
k256 = { version = "0.13.1", features = ["arithmetic", "ecdsa"] }
40-
elliptic-curve = { version = "0.12", features = ["ff"] }
40+
elliptic-curve = { version = "0.13", features = ["ff"] }
4141
thiserror = "1"
4242
bounded-vec = { version = "^0.7.0" }
4343
bitvec = { version = "1.0.1" }

ergo-lib/src/wallet.rs

+42
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
33
pub mod box_selector;
44
pub mod derivation_path;
5+
mod deterministic;
56
pub mod ext_pub_key;
67
pub mod ext_secret_key;
78
pub mod miner_fee;
@@ -23,6 +24,7 @@ use signing::{sign_transaction, TxSigningError};
2324
use thiserror::Error;
2425

2526
use crate::chain::ergo_state_context::ErgoStateContext;
27+
use crate::chain::transaction::reduced::reduce_tx;
2628
use crate::chain::transaction::reduced::ReducedTransaction;
2729
use crate::chain::transaction::unsigned::UnsignedTransaction;
2830
use crate::chain::transaction::Input;
@@ -146,6 +148,46 @@ impl Wallet {
146148
Ok(tx_hints)
147149
}
148150

151+
/// Generate commitments for P2PK inputs using deterministic nonces. \
152+
/// See: [`Wallet::sign_transaction_deterministic`]
153+
pub fn generate_deterministic_commitments(
154+
&self,
155+
reduced_tx: &ReducedTransaction,
156+
aux_rand: &[u8],
157+
) -> Result<TransactionHintsBag, TxSigningError> {
158+
let mut tx_hints = TransactionHintsBag::empty();
159+
let msg = reduced_tx.unsigned_tx.bytes_to_sign()?;
160+
for (index, input) in reduced_tx.reduced_inputs().iter().enumerate() {
161+
if let Some(bag) = self::deterministic::generate_commitments_for(
162+
&*self.prover,
163+
&input.sigma_prop,
164+
&msg,
165+
aux_rand,
166+
) {
167+
tx_hints.add_hints_for_input(index, bag)
168+
};
169+
}
170+
Ok(tx_hints)
171+
}
172+
173+
/// Generate signatures for P2PK inputs deterministically
174+
///
175+
/// Schnorr signatures need an unpredictable nonce added to the signature to avoid private key leakage. Normally this is generated using 32 bytes of entropy, but on platforms where that
176+
/// is not available, `sign_transaction_deterministic` can be used to generate the nonce using a hash of the private key and message. \
177+
/// Additionally `aux_rand` can be optionally supplied with up 32 bytes of entropy.
178+
/// # Limitations
179+
/// Only inputs that reduce to a single public key can be signed. Thus proveDhTuple, n-of-n and t-of-n signatures can not be produced using this method
180+
pub fn sign_transaction_deterministic(
181+
&self,
182+
tx_context: TransactionContext<UnsignedTransaction>,
183+
state_context: &ErgoStateContext,
184+
aux_rand: &[u8],
185+
) -> Result<Transaction, WalletError> {
186+
let reduced_tx = reduce_tx(tx_context, state_context)?;
187+
let hints = self.generate_deterministic_commitments(&reduced_tx, aux_rand)?;
188+
self.sign_reduced_transaction(reduced_tx, Some(&hints))
189+
}
190+
149191
/// Signs a message
150192
pub fn sign_message(
151193
&self,

ergo-lib/src/wallet/deterministic.rs

+139
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
use ergotree_interpreter::sigma_protocol::{
2+
dlog_protocol::interactive_prover::first_message_deterministic,
3+
private_input::PrivateInput,
4+
prover::{
5+
hint::{CommitmentHint, Hint, HintsBag, OwnCommitment, RealCommitment},
6+
Prover,
7+
},
8+
unproven_tree::NodePosition,
9+
FirstProverMessage,
10+
};
11+
use ergotree_ir::sigma_protocol::sigma_boolean::{SigmaBoolean, SigmaProofOfKnowledgeTree};
12+
13+
pub(super) fn generate_commitments_for<P: Prover + ?Sized>(
14+
prover: &P,
15+
sigma_tree: &SigmaBoolean,
16+
msg: &[u8],
17+
aux_rand: &[u8],
18+
) -> Option<HintsBag> {
19+
let position = NodePosition::crypto_tree_prefix();
20+
match sigma_tree {
21+
SigmaBoolean::ProofOfKnowledge(SigmaProofOfKnowledgeTree::ProveDlog(pk)) => {
22+
let PrivateInput::DlogProverInput(sk) = prover
23+
.secrets()
24+
.iter()
25+
.find(|secret| secret.public_image() == *sigma_tree)?
26+
.clone()
27+
else {
28+
return None;
29+
};
30+
let (r, a) = first_message_deterministic(&sk, msg, aux_rand);
31+
let mut bag = HintsBag::empty();
32+
let own_commitment: Hint =
33+
Hint::CommitmentHint(CommitmentHint::OwnCommitment(OwnCommitment {
34+
image: SigmaBoolean::ProofOfKnowledge(pk.clone().into()),
35+
secret_randomness: r,
36+
commitment: FirstProverMessage::FirstDlogProverMessage(a.clone()),
37+
position: position.clone(),
38+
}));
39+
let real_commitment: Hint =
40+
Hint::CommitmentHint(CommitmentHint::RealCommitment(RealCommitment {
41+
image: SigmaBoolean::ProofOfKnowledge(pk.clone().into()),
42+
commitment: FirstProverMessage::FirstDlogProverMessage(a),
43+
position,
44+
}));
45+
bag.add_hint(real_commitment);
46+
bag.add_hint(own_commitment);
47+
Some(bag)
48+
}
49+
SigmaBoolean::TrivialProp(_)
50+
| SigmaBoolean::ProofOfKnowledge(_)
51+
| SigmaBoolean::SigmaConjecture(_) => None,
52+
}
53+
}
54+
55+
#[cfg(test)]
56+
#[allow(clippy::unwrap_used, clippy::unreachable, clippy::panic)]
57+
mod test {
58+
use ergo_chain_types::EcPoint;
59+
use ergotree_interpreter::sigma_protocol::dlog_protocol::interactive_prover::compute_commitment;
60+
use ergotree_interpreter::sigma_protocol::sig_serializer::parse_sig_compute_challenges;
61+
use ergotree_interpreter::sigma_protocol::unchecked_tree::{UncheckedLeaf, UncheckedTree};
62+
use ergotree_interpreter::sigma_protocol::{private_input::DlogProverInput, wscalar::Wscalar};
63+
use ergotree_ir::chain::context_extension::ContextExtension;
64+
use ergotree_ir::chain::ergo_box::box_value::BoxValue;
65+
use ergotree_ir::chain::ergo_box::NonMandatoryRegisters;
66+
use ergotree_ir::chain::ergo_box::{arbitrary::ArbBoxParameters, ErgoBox};
67+
use ergotree_ir::sigma_protocol::sigma_boolean::SigmaProofOfKnowledgeTree;
68+
use proptest::collection::vec;
69+
use proptest::prelude::*;
70+
use sigma_test_util::force_any_val;
71+
72+
use crate::chain::ergo_box::box_builder::ErgoBoxCandidateBuilder;
73+
use crate::chain::transaction::unsigned::UnsignedTransaction;
74+
use crate::chain::transaction::{Input, Transaction, UnsignedInput};
75+
use crate::wallet::secret_key::SecretKey;
76+
use crate::wallet::signing::TransactionContext;
77+
use crate::wallet::Wallet;
78+
fn gen_boxes() -> impl Strategy<Value = (SecretKey, Vec<ErgoBox>)> {
79+
any::<Wscalar>()
80+
.prop_map(|s| SecretKey::DlogSecretKey(DlogProverInput { w: s }))
81+
.prop_flat_map(|sk: SecretKey| {
82+
(
83+
Just(sk.clone()),
84+
vec(
85+
any_with::<ErgoBox>(ArbBoxParameters {
86+
ergo_tree: Just(sk.get_address_from_public_image().script().unwrap())
87+
.boxed(),
88+
registers: Just(NonMandatoryRegisters::empty()).boxed(),
89+
tokens: Just(None).boxed(),
90+
..Default::default()
91+
}),
92+
1..10,
93+
),
94+
)
95+
})
96+
}
97+
98+
fn parse_sig(sk: &SecretKey, input: &Input) -> (EcPoint, Vec<u8>) {
99+
let ergotree_ir::chain::address::Address::P2Pk(pk) = sk.get_address_from_public_image()
100+
else {
101+
unreachable!()
102+
};
103+
let UncheckedTree::UncheckedLeaf(UncheckedLeaf::UncheckedSchnorr(schnorr)) =
104+
parse_sig_compute_challenges(
105+
&SigmaProofOfKnowledgeTree::from(pk.clone()).into(),
106+
input.spending_proof.proof.clone().to_bytes(),
107+
)
108+
.unwrap()
109+
else {
110+
unreachable!();
111+
};
112+
let commitment = compute_commitment(&pk, &schnorr.challenge, &schnorr.second_message);
113+
(commitment, schnorr.challenge.into())
114+
}
115+
116+
proptest! {
117+
// Produce signatures for different messages and test for nonce re-use
118+
#[test]
119+
fn test_sign_deterministic((sk, boxes) in gen_boxes()) {
120+
let wallet = Wallet::from_secrets(vec![sk.clone()]);
121+
let output = ErgoBoxCandidateBuilder::new(BoxValue::SAFE_USER_MIN, sk.get_address_from_public_image().script().unwrap(), 0).build().unwrap();
122+
let inputs: Vec<_> = boxes.iter().map(|b| UnsignedInput::new(b.box_id(), ContextExtension::empty())).collect();
123+
let txes: Vec<Transaction> = (1..10).map(|i| {
124+
let mut output = output.clone();
125+
output.value = output.value.checked_mul_u32(i).unwrap();
126+
let tx = UnsignedTransaction::new_from_vec(inputs.clone(), vec![], vec![output]).unwrap();
127+
wallet.sign_transaction_deterministic(TransactionContext::new(tx, boxes.clone(), vec![]).unwrap(), &force_any_val(), &[]).unwrap()
128+
}).collect();
129+
let signatures: Vec<_> = txes.iter().flat_map(|tx| tx.inputs.iter()).map(|input| parse_sig(&sk, input)).collect();
130+
131+
for (i, (r, c)) in signatures.iter().enumerate() {
132+
if let Some((_, _)) = signatures.iter().enumerate().find(|(j, (r1, c1))| i != *j && r1 == r && c != c1) {
133+
panic!();
134+
}
135+
}
136+
137+
}
138+
}
139+
}

ergotree-interpreter/src/sigma_protocol/dlog_protocol.rs

+66-3
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,18 @@ pub mod interactive_prover {
4444
use crate::sigma_protocol::crypto_utils;
4545
use crate::sigma_protocol::wscalar::Wscalar;
4646
use crate::sigma_protocol::{private_input::DlogProverInput, Challenge};
47+
use blake2::Blake2b;
48+
use blake2::Digest;
49+
use elliptic_curve::ops::MulByGenerator;
4750
use ergo_chain_types::{
4851
ec_point::{exponentiate, generator, inverse},
4952
EcPoint,
5053
};
54+
use ergotree_ir::serialization::SigmaSerializable;
5155
use ergotree_ir::sigma_protocol::dlog_group;
5256
use ergotree_ir::sigma_protocol::sigma_boolean::ProveDlog;
53-
use k256::Scalar;
57+
use k256::elliptic_curve::ops::Reduce;
58+
use k256::{ProjectivePoint, Scalar};
5459

5560
/// Step 5 from <https://ergoplatform.org/docs/ErgoScript.pdf>
5661
/// For every leaf marked “simulated”, use the simulator of the sigma protocol for that leaf
@@ -85,6 +90,49 @@ pub mod interactive_prover {
8590
(r.into(), FirstDlogProverMessage { a: a.into() })
8691
}
8792

93+
/// Step 6 from <https://ergoplatform.org/docs/ErgoScript.pdf>
94+
/// Generate first message "nonce" deterministically, optionally using auxilliary rng
95+
/// # Safety
96+
/// This is only intended to be used in single-signer scenarios.
97+
/// Using this in multi-signature situations where other (untrusted) signers influence the signature can cause private key leakage by producing multiple signatures for the same message with the same nonce
98+
pub fn first_message_deterministic(
99+
sk: &DlogProverInput,
100+
msg: &[u8],
101+
aux_rand: &[u8],
102+
) -> (Wscalar, FirstDlogProverMessage) {
103+
// This is based on BIP340 deterministic nonces, see: https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki#default-signing
104+
type Blake2b256 = Blake2b<blake2::digest::typenum::U32>;
105+
const AUX_TAG: &[u8] = b"erg/aux";
106+
// Perform domain seperation so alternative signature schemes don't end up producing the same nonce, for example ProveDHTuple with deterministic nonces
107+
const NONCE_TAG: &[u8] = b"ergprovedlog/nonce";
108+
109+
let aux_rand_hash: [u8; 32] = Blake2b256::new()
110+
.chain_update(AUX_TAG)
111+
.chain_update(aux_rand)
112+
.finalize()
113+
.into();
114+
let mut sk_bytes = sk.w.as_scalar_ref().to_bytes();
115+
sk_bytes
116+
.iter_mut()
117+
.zip(aux_rand_hash)
118+
.for_each(|(a, b)| *a ^= b);
119+
#[allow(clippy::unwrap_used)] // unwrap will only fail if OOM
120+
let hash = Blake2b256::new()
121+
.chain_update(NONCE_TAG)
122+
.chain_update(sk_bytes)
123+
.chain_update(sk.public_image().h.sigma_serialize_bytes().unwrap())
124+
.chain_update(msg)
125+
.finalize();
126+
127+
let r = <Scalar as Reduce<k256::U256>>::reduce_bytes(&hash);
128+
(
129+
r.into(),
130+
FirstDlogProverMessage {
131+
a: Box::new(ProjectivePoint::mul_by_generator(&r).into()),
132+
},
133+
)
134+
}
135+
88136
/// Step 9 part 2 from <https://ergoplatform.org/docs/ErgoScript.pdf>
89137
/// compute its response "z" according to the second prover step(step 5 in whitepaper)
90138
/// of the sigma protocol given the randomness "r"(rnd) used for the commitment "a",
@@ -105,7 +153,7 @@ pub mod interactive_prover {
105153
/// The function computes initial prover's commitment to randomness
106154
/// ("a" message of the sigma-protocol) based on the verifier's challenge ("e")
107155
/// and prover's response ("z")
108-
///
156+
///
109157
/// g^z = a*h^e => a = g^z/h^e
110158
pub fn compute_commitment(
111159
proposition: &ProveDlog,
@@ -129,20 +177,35 @@ mod tests {
129177
use super::*;
130178
use crate::sigma_protocol::private_input::DlogProverInput;
131179

180+
use fiat_shamir::fiat_shamir_hash_fn;
181+
use proptest::collection::vec;
132182
use proptest::prelude::*;
133183

134184
proptest! {
135185

136186
#![proptest_config(ProptestConfig::with_cases(16))]
137187

138188
#[test]
139-
#[cfg(feature = "arbitrary")]
140189
fn test_compute_commitment(secret in any::<DlogProverInput>(), challenge in any::<Challenge>()) {
141190
let pk = secret.public_image();
142191
let (r, commitment) = interactive_prover::first_message();
143192
let second_message = interactive_prover::second_message(&secret, r, &challenge);
144193
let a = interactive_prover::compute_commitment(&pk, &challenge, &second_message);
145194
prop_assert_eq!(a, *commitment.a);
146195
}
196+
197+
#[test]
198+
fn test_deterministic_commitment(secret in any::<DlogProverInput>(), secret2 in any::<DlogProverInput>(), message in vec(any::<u8>(), 0..100000)) {
199+
fn sign(secret: &DlogProverInput, message: &[u8]) -> EcPoint {
200+
let pk = secret.public_image();
201+
let challenge: Challenge = fiat_shamir_hash_fn(message).into();
202+
let (r, _) = interactive_prover::first_message_deterministic(secret, message, &[]);
203+
let second_message = interactive_prover::second_message(secret, r, &challenge);
204+
interactive_prover::compute_commitment(&pk, &challenge, &second_message)
205+
}
206+
let a = sign(&secret, &message);
207+
let a2 = sign(&secret2, &message);
208+
prop_assert_ne!(a, a2);
209+
}
147210
}
148211
}

0 commit comments

Comments
 (0)