Skip to content

Commit

Permalink
Merge branch 'develop' of github.com:ergoplatform/sigma-rust into no_std
Browse files Browse the repository at this point in the history
  • Loading branch information
SethDusek committed Dec 16, 2024
2 parents f27bd19 + 04f3d4c commit 823c01d
Show file tree
Hide file tree
Showing 4 changed files with 258 additions and 18 deletions.
6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ ergo-lib = { version = "^0.28.0", path = "./ergo-lib" }
k256 = { version = "0.13.1", default-features = false, features = [
"arithmetic",
"ecdsa",
] } # TODO: look into precomputed_tables feature for std, could be useful for speeding up signature verification
elliptic-curve = { version = "0.12", features = ["ff"] }
] }
elliptic-curve = { version = "0.13", features = ["ff"] }
derive_more = { version = "0.99", features = [
"add",
"add_assign",
Expand All @@ -55,7 +55,7 @@ derive_more = { version = "0.99", features = [
] }
num-derive = "0.4.2"
thiserror = { version = "2.0.1", default-features = false }
bounded-vec = { version = "0.8.0", default-features = false }
bounded-vec = { version = "0.8.0", default-features = false }
bitvec = { version = "1.0.1", default-features = false, features = ["alloc"] }
blake2 = { version = "0.10.6", default-features = false }
sha2 = { version = "0.10", default-features = false }
Expand Down
53 changes: 45 additions & 8 deletions ergo-lib/src/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pub mod box_selector;
pub mod derivation_path;
mod deterministic;
pub mod ext_pub_key;
pub mod ext_secret_key;
pub mod miner_fee;
Expand All @@ -26,15 +27,12 @@ use hashbrown::HashMap;
use secret_key::SecretKey;
use thiserror::Error;

#[cfg(feature = "std")]
use crate::chain::ergo_state_context::ErgoStateContext;
#[cfg(feature = "std")]
use crate::chain::transaction::reduced::reduce_tx;
use crate::chain::transaction::reduced::ReducedTransaction;
#[cfg(feature = "std")]
use crate::chain::transaction::unsigned::UnsignedTransaction;
#[cfg(feature = "std")]
use crate::chain::transaction::Input;
#[cfg(feature = "std")]
use crate::chain::transaction::Transaction;
#[cfg(feature = "std")]
use crate::ergotree_ir::sigma_protocol::sigma_boolean::SigmaBoolean;
Expand All @@ -44,12 +42,11 @@ use crate::wallet::multi_sig::{generate_commitments, generate_commitments_for};

use self::ext_secret_key::ExtSecretKey;
use self::ext_secret_key::ExtSecretKeyError;
use self::signing::sign_reduced_transaction;
use self::signing::TransactionContext;
use self::signing::TxSigningError;
#[cfg(feature = "std")]
use self::signing::{
make_context, sign_message, sign_reduced_transaction, sign_transaction, sign_tx_input,
TransactionContext,
};
use self::signing::{make_context, sign_message, sign_transaction, sign_tx_input};

/// Wallet
pub struct Wallet {
Expand Down Expand Up @@ -160,6 +157,46 @@ impl Wallet {
Ok(tx_hints)
}

/// Generate commitments for P2PK inputs using deterministic nonces. \
/// See: [`Wallet::sign_transaction_deterministic`]
pub fn generate_deterministic_commitments(
&self,
reduced_tx: &ReducedTransaction,
aux_rand: &[u8],
) -> Result<TransactionHintsBag, TxSigningError> {
let mut tx_hints = TransactionHintsBag::empty();
let msg = reduced_tx.unsigned_tx.bytes_to_sign()?;
for (index, input) in reduced_tx.reduced_inputs().iter().enumerate() {
if let Some(bag) = self::deterministic::generate_commitments_for(
&*self.prover,
&input.sigma_prop,
&msg,
aux_rand,
) {
tx_hints.add_hints_for_input(index, bag)
};
}
Ok(tx_hints)
}

/// Generate signatures for P2PK inputs deterministically
///
/// 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
/// is not available, `sign_transaction_deterministic` can be used to generate the nonce using a hash of the private key and message. \
/// Additionally `aux_rand` can be optionally supplied with up 32 bytes of entropy.
/// # Limitations
/// 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
pub fn sign_transaction_deterministic(
&self,
tx_context: TransactionContext<UnsignedTransaction>,
state_context: &ErgoStateContext,
aux_rand: &[u8],
) -> Result<Transaction, WalletError> {
let reduced_tx = reduce_tx(tx_context, state_context)?;
let hints = self.generate_deterministic_commitments(&reduced_tx, aux_rand)?;
sign_reduced_transaction(&*self.prover, reduced_tx, Some(&hints)).map_err(From::from)
}

/// Signs a message
#[cfg(feature = "std")]
pub fn sign_message(
Expand Down
139 changes: 139 additions & 0 deletions ergo-lib/src/wallet/deterministic.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
use ergotree_interpreter::sigma_protocol::{
dlog_protocol::interactive_prover::first_message_deterministic,
private_input::PrivateInput,
prover::{
hint::{CommitmentHint, Hint, HintsBag, OwnCommitment, RealCommitment},
Prover,
},
unproven_tree::NodePosition,
FirstProverMessage,
};
use ergotree_ir::sigma_protocol::sigma_boolean::{SigmaBoolean, SigmaProofOfKnowledgeTree};

pub(super) fn generate_commitments_for<P: Prover + ?Sized>(
prover: &P,
sigma_tree: &SigmaBoolean,
msg: &[u8],
aux_rand: &[u8],
) -> Option<HintsBag> {
let position = NodePosition::crypto_tree_prefix();
match sigma_tree {
SigmaBoolean::ProofOfKnowledge(SigmaProofOfKnowledgeTree::ProveDlog(pk)) => {
let PrivateInput::DlogProverInput(sk) = prover
.secrets()
.iter()
.find(|secret| secret.public_image() == *sigma_tree)?
.clone()
else {
return None;
};
let (r, a) = first_message_deterministic(&sk, msg, aux_rand);
let mut bag = HintsBag::empty();
let own_commitment: Hint =
Hint::CommitmentHint(CommitmentHint::OwnCommitment(OwnCommitment {
image: SigmaBoolean::ProofOfKnowledge(pk.clone().into()),
secret_randomness: r,
commitment: FirstProverMessage::FirstDlogProverMessage(a.clone()),
position: position.clone(),
}));
let real_commitment: Hint =
Hint::CommitmentHint(CommitmentHint::RealCommitment(RealCommitment {
image: SigmaBoolean::ProofOfKnowledge(pk.clone().into()),
commitment: FirstProverMessage::FirstDlogProverMessage(a),
position,
}));
bag.add_hint(real_commitment);
bag.add_hint(own_commitment);
Some(bag)
}
SigmaBoolean::TrivialProp(_)
| SigmaBoolean::ProofOfKnowledge(_)
| SigmaBoolean::SigmaConjecture(_) => None,
}
}

#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::unreachable, clippy::panic)]
mod test {
use ergo_chain_types::EcPoint;
use ergotree_interpreter::sigma_protocol::dlog_protocol::interactive_prover::compute_commitment;
use ergotree_interpreter::sigma_protocol::sig_serializer::parse_sig_compute_challenges;
use ergotree_interpreter::sigma_protocol::unchecked_tree::{UncheckedLeaf, UncheckedTree};
use ergotree_interpreter::sigma_protocol::{private_input::DlogProverInput, wscalar::Wscalar};
use ergotree_ir::chain::context_extension::ContextExtension;
use ergotree_ir::chain::ergo_box::box_value::BoxValue;
use ergotree_ir::chain::ergo_box::NonMandatoryRegisters;
use ergotree_ir::chain::ergo_box::{arbitrary::ArbBoxParameters, ErgoBox};
use ergotree_ir::sigma_protocol::sigma_boolean::SigmaProofOfKnowledgeTree;
use proptest::collection::vec;
use proptest::prelude::*;
use sigma_test_util::force_any_val;

use crate::chain::ergo_box::box_builder::ErgoBoxCandidateBuilder;
use crate::chain::transaction::unsigned::UnsignedTransaction;
use crate::chain::transaction::{Input, Transaction, UnsignedInput};
use crate::wallet::secret_key::SecretKey;
use crate::wallet::signing::TransactionContext;
use crate::wallet::Wallet;
fn gen_boxes() -> impl Strategy<Value = (SecretKey, Vec<ErgoBox>)> {
any::<Wscalar>()
.prop_map(|s| SecretKey::DlogSecretKey(DlogProverInput { w: s }))
.prop_flat_map(|sk: SecretKey| {
(
Just(sk.clone()),
vec(
any_with::<ErgoBox>(ArbBoxParameters {
ergo_tree: Just(sk.get_address_from_public_image().script().unwrap())
.boxed(),
registers: Just(NonMandatoryRegisters::empty()).boxed(),
tokens: Just(None).boxed(),
..Default::default()
}),
1..10,
),
)
})
}

fn parse_sig(sk: &SecretKey, input: &Input) -> (EcPoint, Vec<u8>) {
let ergotree_ir::chain::address::Address::P2Pk(pk) = sk.get_address_from_public_image()
else {
unreachable!()
};
let UncheckedTree::UncheckedLeaf(UncheckedLeaf::UncheckedSchnorr(schnorr)) =
parse_sig_compute_challenges(
&SigmaProofOfKnowledgeTree::from(pk.clone()).into(),
input.spending_proof.proof.clone().to_bytes(),
)
.unwrap()
else {
unreachable!();
};
let commitment = compute_commitment(&pk, &schnorr.challenge, &schnorr.second_message);
(commitment, schnorr.challenge.into())
}

proptest! {
// Produce signatures for different messages and test for nonce re-use
#[test]
fn test_sign_deterministic((sk, boxes) in gen_boxes()) {
let wallet = Wallet::from_secrets(vec![sk.clone()]);
let output = ErgoBoxCandidateBuilder::new(BoxValue::SAFE_USER_MIN, sk.get_address_from_public_image().script().unwrap(), 0).build().unwrap();
let inputs: Vec<_> = boxes.iter().map(|b| UnsignedInput::new(b.box_id(), ContextExtension::empty())).collect();
let txes: Vec<Transaction> = (1..10).map(|i| {
let mut output = output.clone();
output.value = output.value.checked_mul_u32(i).unwrap();
let tx = UnsignedTransaction::new_from_vec(inputs.clone(), vec![], vec![output]).unwrap();
wallet.sign_transaction_deterministic(TransactionContext::new(tx, boxes.clone(), vec![]).unwrap(), &force_any_val(), &[]).unwrap()
}).collect();
let signatures: Vec<_> = txes.iter().flat_map(|tx| tx.inputs.iter()).map(|input| parse_sig(&sk, input)).collect();

for (i, (r, c)) in signatures.iter().enumerate() {
if let Some((_, _)) = signatures.iter().enumerate().find(|(j, (r1, c1))| i != *j && r1 == r && c != c1) {
panic!();
}
}

}
}
}
78 changes: 71 additions & 7 deletions ergotree-interpreter/src/sigma_protocol/dlog_protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,23 @@ pub struct SecondDlogProverMessage {

/// Interactive prover
pub mod interactive_prover {
use alloc::boxed::Box;
use core::ops::Mul;

use super::SecondDlogProverMessage;
use super::{FirstDlogProverMessage, SecondDlogProverMessage};
use crate::sigma_protocol::wscalar::Wscalar;
use crate::sigma_protocol::{private_input::DlogProverInput, Challenge};
use blake2::Blake2b;
use blake2::Digest;
use elliptic_curve::ops::MulByGenerator;
use ergo_chain_types::{
ec_point::{exponentiate, generator, inverse},
EcPoint,
};
use ergotree_ir::serialization::SigmaSerializable;
use ergotree_ir::sigma_protocol::sigma_boolean::ProveDlog;
use k256::Scalar;
use k256::elliptic_curve::ops::Reduce;
use k256::{ProjectivePoint, Scalar};

/// Step 5 from <https://ergoplatform.org/docs/ErgoScript.pdf>
/// For every leaf marked “simulated”, use the simulator of the sigma protocol for that leaf
Expand All @@ -60,7 +66,7 @@ pub mod interactive_prover {
pub(crate) fn simulate(
public_input: &ProveDlog,
challenge: &Challenge,
) -> (super::FirstDlogProverMessage, SecondDlogProverMessage) {
) -> (FirstDlogProverMessage, SecondDlogProverMessage) {
use ergotree_ir::sigma_protocol::dlog_group;
//SAMPLE a random z <- Zq
let z = dlog_group::random_scalar_in_group_range(
Expand All @@ -74,7 +80,7 @@ pub mod interactive_prover {
let g_to_z = exponentiate(&generator(), &z);
let a = g_to_z * &h_to_e;
(
super::FirstDlogProverMessage { a: a.into() },
FirstDlogProverMessage { a: a.into() },
SecondDlogProverMessage { z: z.into() },
)
}
Expand All @@ -83,14 +89,57 @@ pub mod interactive_prover {
/// For every leaf marked “real”, use the first prover step of the sigma protocol for
/// that leaf to compute the necessary randomness "r" and the commitment "a"
#[cfg(feature = "std")]
pub fn first_message() -> (Wscalar, super::FirstDlogProverMessage) {
pub fn first_message() -> (Wscalar, FirstDlogProverMessage) {
use ergotree_ir::sigma_protocol::dlog_group;
let r = dlog_group::random_scalar_in_group_range(
crate::sigma_protocol::crypto_utils::secure_rng(),
);
let g = generator();
let a = exponentiate(&g, &r);
(r.into(), super::FirstDlogProverMessage { a: a.into() })
(r.into(), FirstDlogProverMessage { a: a.into() })
}

/// Step 6 from <https://ergoplatform.org/docs/ErgoScript.pdf>
/// Generate first message "nonce" deterministically, optionally using auxilliary rng
/// # Safety
/// This is only intended to be used in single-signer scenarios.
/// 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
pub fn first_message_deterministic(
sk: &DlogProverInput,
msg: &[u8],
aux_rand: &[u8],
) -> (Wscalar, FirstDlogProverMessage) {
// This is based on BIP340 deterministic nonces, see: https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki#default-signing
type Blake2b256 = Blake2b<blake2::digest::typenum::U32>;
const AUX_TAG: &[u8] = b"erg/aux";
// Perform domain seperation so alternative signature schemes don't end up producing the same nonce, for example ProveDHTuple with deterministic nonces
const NONCE_TAG: &[u8] = b"ergprovedlog/nonce";

let aux_rand_hash: [u8; 32] = Blake2b256::new()
.chain_update(AUX_TAG)
.chain_update(aux_rand)
.finalize()
.into();
let mut sk_bytes = sk.w.as_scalar_ref().to_bytes();
sk_bytes
.iter_mut()
.zip(aux_rand_hash)
.for_each(|(a, b)| *a ^= b);
#[allow(clippy::unwrap_used)] // unwrap will only fail if OOM
let hash = Blake2b256::new()
.chain_update(NONCE_TAG)
.chain_update(sk_bytes)
.chain_update(sk.public_image().h.sigma_serialize_bytes().unwrap())
.chain_update(msg)
.finalize();

let r = <Scalar as Reduce<k256::U256>>::reduce_bytes(&hash);
(
r.into(),
FirstDlogProverMessage {
a: Box::new(ProjectivePoint::mul_by_generator(&r).into()),
},
)
}

/// Step 9 part 2 from <https://ergoplatform.org/docs/ErgoScript.pdf>
Expand Down Expand Up @@ -137,20 +186,35 @@ mod tests {
use super::*;
use crate::sigma_protocol::private_input::DlogProverInput;

use fiat_shamir::fiat_shamir_hash_fn;
use proptest::collection::vec;
use proptest::prelude::*;

proptest! {

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

#[test]
#[cfg(feature = "arbitrary")]
fn test_compute_commitment(secret in any::<DlogProverInput>(), challenge in any::<Challenge>()) {
let pk = secret.public_image();
let (r, commitment) = interactive_prover::first_message();
let second_message = interactive_prover::second_message(&secret, r, &challenge);
let a = interactive_prover::compute_commitment(&pk, &challenge, &second_message);
prop_assert_eq!(a, *commitment.a);
}

#[test]
fn test_deterministic_commitment(secret in any::<DlogProverInput>(), secret2 in any::<DlogProverInput>(), message in vec(any::<u8>(), 0..100000)) {
fn sign(secret: &DlogProverInput, message: &[u8]) -> EcPoint {
let pk = secret.public_image();
let challenge: Challenge = fiat_shamir_hash_fn(message).into();
let (r, _) = interactive_prover::first_message_deterministic(secret, message, &[]);
let second_message = interactive_prover::second_message(secret, r, &challenge);
interactive_prover::compute_commitment(&pk, &challenge, &second_message)
}
let a = sign(&secret, &message);
let a2 = sign(&secret2, &message);
prop_assert_ne!(a, a2);
}
}
}

0 comments on commit 823c01d

Please sign in to comment.