Skip to main content

binius_examples/circuits/
ethsign.rs

1// Copyright 2025 Irreducible Inc.
2use std::{array, iter};
3
4use anyhow::Result;
5use binius_circuits::{
6	bignum::BigUint, bytes::swap_bytes, ecdsa::ecrecover, fixed_byte_vec::ByteVec,
7	keccak::keccak256_varlen,
8};
9use binius_core::word::Word;
10use binius_frontend::{CircuitBuilder, Wire, WitnessFiller};
11use clap::Args;
12use ethsign::SecretKey;
13use rand::prelude::*;
14use sha3::{Digest, Keccak256 as Sha3Keccak256};
15
16use crate::ExampleCircuit;
17
18struct Signature {
19	r: [Wire; 4],
20	s: [Wire; 4],
21	recid_odd: Wire,
22	address: [Wire; 3],
23	/// The signed message, hashed in-circuit by [`keccak256_varlen`].
24	msg: ByteVec,
25}
26
27/// Example circuit that proves validity of Ethereum-style ECDSA signatures.
28pub struct EthSignExample {
29	max_msg_len_bytes: usize,
30	signatures: Vec<Signature>,
31}
32
33#[derive(Args, Debug, Clone)]
34pub struct Params {
35	/// Number of Ethereum-style signatures to validate
36	#[arg(short = 'n', long, default_value_t = 1)]
37	pub n_signatures: usize,
38	/// Maximum message length
39	#[arg(short = 'm', long, default_value_t = 128, value_parser = clap::value_parser!(u16).range(1..))]
40	pub max_msg_len_bytes: u16,
41}
42
43#[derive(Args, Debug, Clone)]
44pub struct Instance {}
45
46impl ExampleCircuit for EthSignExample {
47	type Params = Params;
48	type Instance = Instance;
49
50	fn build(params: Params, builder: &mut CircuitBuilder) -> Result<Self> {
51		let max_msg_len_bytes = params.max_msg_len_bytes as usize;
52		let signatures = (0..params.n_signatures)
53			.map(|_| {
54				let msg_len = builder.add_inout();
55				let message = (0..params.max_msg_len_bytes.div_ceil(8))
56					.map(|_| builder.add_inout())
57					.collect::<Vec<_>>();
58				let r = array::from_fn(|_| builder.add_inout());
59				let s = array::from_fn(|_| builder.add_inout());
60				let recid_odd = builder.add_inout();
61				let address = array::from_fn(|_| builder.add_inout());
62
63				let msg = ByteVec::new(message, msg_len);
64				let msg_digest = keccak256_varlen(builder, &msg);
65
66				// The Keccak digest is little endian encoded into 4 words, while Ethereum expects
67				// big endian
68				let z = BigUint {
69					limbs: msg_digest
70						.iter()
71						.rev()
72						.map(|&word| swap_bytes(builder, word))
73						.collect(),
74				};
75
76				// Encoding r & s in little endian
77				let public_key = ecrecover(
78					builder,
79					&z,
80					&BigUint { limbs: r.to_vec() },
81					&BigUint { limbs: s.to_vec() },
82					recid_odd,
83				);
84
85				// Check that public key is not a point-at-infinity
86				builder.assert_false("recovered_pk_not_pai", public_key.is_point_at_infinity);
87
88				// Concatenate x & y in _big_ endian, hash the result to obtain the address
89				let mut public_key_limbs = Vec::with_capacity(8);
90				public_key_limbs.extend(&public_key.y.limbs);
91				public_key_limbs.extend(&public_key.x.limbs);
92				public_key_limbs.reverse();
93
94				let public_key_message = public_key_limbs
95					.into_iter()
96					.map(|word| swap_bytes(builder, word))
97					.collect::<Vec<_>>();
98
99				// The public key is always 64 bytes; hash it with a constant-length `ByteVec`. Its
100				// data wires are derived from `ecrecover`, so nothing needs populating at witness
101				// time.
102				let address_msg = ByteVec::new_const_len(builder, public_key_message, 64);
103				let address_digest = keccak256_varlen(builder, &address_msg);
104
105				// Assert that the provided address equals digest bytes 12..32
106				assert_address_eq(builder, &address_digest, &address);
107
108				Signature {
109					r,
110					s,
111					recid_odd,
112					address,
113					msg,
114				}
115			})
116			.collect();
117
118		Ok(Self {
119			signatures,
120			max_msg_len_bytes,
121		})
122	}
123
124	fn populate_witness(&self, _instance: Instance, w: &mut WitnessFiller<'_>) -> Result<()> {
125		// Generate random initial state with fixed seed for reproducibility
126		let mut rng = StdRng::seed_from_u64(42);
127
128		for Signature {
129			r,
130			s,
131			recid_odd,
132			address,
133			msg,
134		} in &self.signatures
135		{
136			// Random private key
137			let sk_bytes: [u8; 32] = rng.random();
138			let secret_key = SecretKey::from_raw(&sk_bytes)?;
139
140			// Random message
141			let msg_len = rng.random_range(1..=self.max_msg_len_bytes);
142			let msg_bytes = (0..msg_len).map(|_| rng.random()).collect::<Vec<u8>>();
143			let msg_hash = keccak256(&msg_bytes);
144
145			// Sign the message with ECDSA
146			let mut signature = secret_key.sign(&msg_hash)?;
147			let public = secret_key.public();
148
149			// Populate the message bytes; its Keccak digest is computed in-circuit.
150			msg.populate_data(w, &msg_bytes);
151			msg.populate_len_bytes(w, msg_len);
152
153			// ethsign crate returns 0/1 recid, convert to `recid_odd` boolean
154			w[*recid_odd] = if signature.v != 0 {
155				Word::ALL_ONE
156			} else {
157				Word::ZERO
158			};
159
160			// ethsign crate returns r & s big endian, byteswap
161			signature.r.reverse();
162			w.pack_bytes_le(r, &signature.r);
163
164			signature.s.reverse();
165			w.pack_bytes_le(s, &signature.s);
166
167			// The public key (and hence the address-hash input) is derived in-circuit from the
168			// recovered signature, so only the expected address wires need populating.
169			w.pack_bytes_le(address, public.address());
170		}
171
172		Ok(())
173	}
174
175	fn param_summary(params: &Self::Params) -> Option<String> {
176		Some(format!("{}s-{}b", params.n_signatures, params.max_msg_len_bytes))
177	}
178}
179
180fn assert_address_eq(b: &CircuitBuilder, digest: &[Wire], address: &[Wire]) {
181	assert_eq!(digest.len(), 4);
182	assert_eq!(address.len(), 3);
183
184	let digest_len = b.add_constant_64(32);
185	let digest_byte_vec = ByteVec::new(digest.to_vec(), digest_len);
186	let digest_sliced = digest_byte_vec.slice_const_range(b, 12..32);
187
188	for (i, (&lhs_i, rhs_i)) in iter::zip(address, digest_sliced.data).enumerate() {
189		b.assert_eq(format!("address_word_{i}"), lhs_i, rhs_i);
190	}
191}
192
193fn keccak256(bytes: &[u8]) -> [u8; 32] {
194	let mut hasher = Sha3Keccak256::new();
195	hasher.update(bytes);
196	hasher.finalize().into()
197}