Skip to content

Commit 2ae00a1

Browse files
author
¨Jeff
committed
Add accumulation mode
1 parent eb6c4b2 commit 2ae00a1

File tree

3 files changed

+159
-35
lines changed

3 files changed

+159
-35
lines changed

ark-transcript/README.md

+17-3
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ We achieve this by doing basic domain seperation using postfix writes
88
of the length of written data, as opposed to the prefix writes done
99
by merlin, which break arkworks.
1010

11-
## Why not merlin?
11+
### Why not merlin?
1212

1313
A trascript flavored hash like [merlin](https://merlin.cool/)
1414
([docs](https://docs.rs/merlin/latest/merlin/)) simplifies protocol
@@ -41,13 +41,27 @@ As a minor cost for us, any users whose hashing involves multiple
4141
code paths should ensure they invoke label between arkworks types
4242
and user data with possibly zero length.
4343

44-
Aside postfix lengths..
44+
Aside from postfix lengths..
4545

46-
there do exist people who feel STROBE maybe overkill, unfamiliar,
46+
There do exist people who feel STROBE maybe overkill, unfamiliar,
4747
or not widely available. Almost all sha3 implementations provide
4848
shake128, making this transcript simple, portable, etc.
4949

5050
We also "correct" merlin's excessively opinionated requirement of
5151
`&'static [u8]`s for labels, which complicates some key management
5252
practices.
5353

54+
### Accumulation
55+
56+
We support accumulating hashed data in a `Vec<u8>`, which users could
57+
then transport to a remote signer for actual signing or proving.
58+
Although possible in principle, we do not reparse this accumulated data,
59+
but regardless the accumulation should not break any domain seperation
60+
that occurs inside the remore signer.
61+
62+
Ideally, accumulations should've applications specific labels both
63+
prefixed inside the accumulations and postfixed by the remote signer.
64+
A remote signer could check this prefix of course, but applying a
65+
postfix label should suffice given we use Shake128.
66+
67+
We have a `debug-transcript` feature similar to merlin as well.

ark-transcript/src/lib.rs

+114-31
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use ark_std::{
1414
UniformRand,
1515
borrow::{Borrow,BorrowMut},
1616
io::{self, Read, Write}, // Result
17+
vec::Vec,
1718
};
1819
use ark_serialize::{CanonicalSerialize};
1920
use ark_ff::{Field};
@@ -96,31 +97,58 @@ impl<'a, const N: usize> IntoTranscript for &'a [u8; N] {
9697
}
9798
}
9899

100+
/// Inner hasher or accumulator object.
101+
///
102+
/// We make this distinction at runtime instead of at compile-time
103+
/// for simplicity elsewhere.
104+
#[derive(Clone)]
105+
enum Mode {
106+
/// Actual Shake128 hasher being written to.
107+
Hash(Shake128),
108+
/// Accumulate bytes instead of hashing them.
109+
Accumulate(Vec<u8>),
110+
}
111+
112+
impl Mode {
113+
/// Abstracts over the writing modes
114+
fn raw_write(&mut self, bytes: &[u8]) {
115+
match self {
116+
Mode::Hash(hasher) => hasher.update(bytes),
117+
Mode::Accumulate(acc) => acc.extend_from_slice(bytes),
118+
}
119+
}
120+
121+
/// Switch from writing to reading
122+
///
123+
/// Panics if called in accumulation mode
124+
fn raw_reader(self) -> Reader {
125+
#[cfg(feature = "debug-transcript")]
126+
println!("Shake128 {}transcript XoF reader",self.debug_name);
127+
match self {
128+
Mode::Hash(hasher) => Reader(hasher.clone().finalize_xof()),
129+
Mode::Accumulate(_) => panic!("Attempt to read from accumulating Transcript"),
130+
}
131+
}
132+
}
133+
99134
/// Shake128 transcript style hasher.
100135
#[derive(Clone)]
101136
pub struct Transcript {
102137
/// Length writen between `seperate()` calls. Always less than 2^31.
103138
/// `None` means `write` was not yet invoked, so seperate() does nothing.
104139
/// We need this to distinguish zero length write calls.
105140
length: Option<u32>,
141+
/// Actual Shake128 hasher being written to, or maybe an accumulator
142+
mode: Mode,
106143
/// Is this a witness transcript?
107144
#[cfg(feature = "debug-transcript")]
108145
debug_name: &'static str,
109-
/// Actual Shake128 hasher being written to.
110-
h: Shake128,
111146
}
112147

113148
impl Default for Transcript {
114149
/// Create a fresh empty `Transcript`.
115150
fn default() -> Transcript {
116-
#[cfg(feature = "debug-transcript")]
117-
println!("Initial Shake128 transcript..");
118-
Transcript {
119-
length: None,
120-
#[cfg(feature = "debug-transcript")]
121-
debug_name: "",
122-
h: Shake128::default(),
123-
}
151+
Transcript::new_blank()
124152
}
125153
}
126154

@@ -145,6 +173,78 @@ impl Write for Transcript {
145173

146174

147175
impl Transcript {
176+
/// Create a `Transcript` from `Shake128`.
177+
pub fn from_shake128(hasher: Shake128) -> Transcript {
178+
Transcript {
179+
length: None,
180+
mode: Mode::Hash(hasher),
181+
#[cfg(feature = "debug-transcript")]
182+
debug_name: "",
183+
}
184+
}
185+
186+
/// Create a `Transcript` from previously accumulated bytes.
187+
///
188+
/// We do not domain seperate these initial bytes, but we domain
189+
/// seperate everything after this, making this safe.
190+
pub fn from_accumulation(acc: impl AsRef<[u8]>) -> Transcript {
191+
let mut hasher = Shake128::default();
192+
hasher.update(acc.as_ref());
193+
Transcript::from_shake128(hasher)
194+
}
195+
196+
/// Create an empty `Transcript`.
197+
pub fn new_blank() -> Transcript {
198+
#[cfg(feature = "debug-transcript")]
199+
println!("Initial Shake128 transcript..");
200+
Transcript::from_accumulation(&[])
201+
}
202+
203+
/// Create a fresh `Transcript` with an initial domain label.
204+
///
205+
/// We implicitly have an initial zero length user data write
206+
/// preceeding this first label.
207+
pub fn new(label: impl AsLabel) -> Transcript {
208+
let mut t = Transcript::new_blank();
209+
t.label(label);
210+
t
211+
}
212+
213+
/// Create an empty `Transcript` in bytes accumulation mode.
214+
///
215+
/// You cannot create `Reader`s in accumulation mode, but
216+
/// `accumulator_finalize` exports the accumulated `Vec<u8>`.
217+
/// You could then transport this elsewhere and start a
218+
/// real hasher using `from_accumulation`.
219+
pub fn new_blank_accumulator() -> Transcript {
220+
#[cfg(feature = "debug-transcript")]
221+
println!("Initial Shake128 transcript..");
222+
Transcript {
223+
length: None,
224+
mode: Mode::Accumulate(Vec::new()),
225+
#[cfg(feature = "debug-transcript")]
226+
debug_name: "",
227+
}
228+
}
229+
230+
/// Avoid repeated allocations by reserving additional space when in accumulation mode.
231+
pub fn accumulator_reserve(&mut self, additional: usize) {
232+
match &mut self.mode {
233+
Mode::Accumulate(acc) => acc.reserve(additional),
234+
_ => {},
235+
}
236+
}
237+
238+
/// Invokes `seperate` and exports the accumulated transcript bytes,
239+
/// which you later pass into `Transcript::from_accumulation`.
240+
pub fn accumulator_finalize(mut self) -> Vec<u8> {
241+
self.seperate();
242+
match self.mode {
243+
Mode::Hash(_) => panic!("Attempte to accumulator_finalize a hashing Transcript"),
244+
Mode::Accumulate(acc) => acc,
245+
}
246+
}
247+
148248
/// Write basic unlabeled domain seperator into the hasher.
149249
///
150250
/// Implemented by writing in big endian the number of bytes
@@ -160,7 +260,7 @@ impl Transcript {
160260
#[cfg(feature = "debug-transcript")]
161261
println!("Shake128 {}transcript seperator: {}",self.debug_name, self.length);
162262
if let Some(l) = self.length {
163-
self.h.update( & l.to_be_bytes() );
263+
self.mode.raw_write( & l.to_be_bytes() );
164264
}
165265
self.length = None;
166266
}
@@ -183,7 +283,7 @@ impl Transcript {
183283
println!("Shake128 {}transcript write of {} bytes out of {}", self.debug_name, l, bytes.len());
184284
}
185285
}
186-
self.h.update( &bytes[0..l] );
286+
self.mode.raw_write( &bytes[0..l] );
187287
bytes = &bytes[l..];
188288
if bytes.len() == 0 {
189289
*length += u32::try_from(l).unwrap();
@@ -252,23 +352,6 @@ impl Transcript {
252352
self.seperate();
253353
}
254354

255-
/// Create a fresh `Transcript` with an initial domain label.
256-
///
257-
/// We implicitly have an initial zero length user data write
258-
/// preceeding this first label.
259-
pub fn new(label: impl AsLabel) -> Transcript {
260-
let mut t = Transcript::default();
261-
t.label(label);
262-
t
263-
}
264-
265-
/// Switch from writing to reading
266-
fn raw_reader(self) -> Reader {
267-
#[cfg(feature = "debug-transcript")]
268-
println!("Shake128 {}transcript XoF reader",self.debug_name);
269-
Reader(self.h.clone().finalize_xof())
270-
}
271-
272355
/// Create a challenge reader.
273356
///
274357
/// Invoking `self.label(label)` has the same effect upon `self`,
@@ -278,7 +361,7 @@ impl Transcript {
278361
println!("Shake128 {}transcript challenge",self.debug_name);
279362
self.seperate();
280363
self.write_bytes(label.as_label());
281-
let reader = self.clone().raw_reader();
364+
let reader = self.mode.clone().raw_reader();
282365
self.seperate();
283366
reader
284367
}
@@ -328,7 +411,7 @@ impl Transcript {
328411
let mut rand = [0u8; 32];
329412
rng.fill_bytes(&mut rand);
330413
self.write_bytes(&rand);
331-
self.raw_reader()
414+
self.mode.raw_reader()
332415
}
333416
}
334417

ark-transcript/src/tests.rs

+28-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ fn transcript_v_witnesses() {
99
// transcripts that diverge at different points and checking
1010
// that they produce different challenges.
1111

12-
let protocol_label = b"test TranscriptRng collisions";
12+
let protocol_label = b"test collisions";
1313
let commitment1 = b"commitment data 1";
1414
let commitment2 = b"commitment data 2";
1515
let witness1 = b"witness data 1";
@@ -53,3 +53,30 @@ fn transcript_v_witnesses() {
5353
// above aren't because the RNG is accidentally different.
5454
assert_eq!(s3, s4);
5555
}
56+
57+
58+
#[test]
59+
fn accumulation() {
60+
let protocol_label = b"test collisions";
61+
62+
let mut t1 = Transcript::new(protocol_label);
63+
let mut t2 = Transcript::new_blank_accumulator();
64+
t2.label(protocol_label);
65+
66+
let commitment1 = b"commitment data 1";
67+
let commitment2 = b"commitment data 2";
68+
69+
t1.write_bytes(commitment1);
70+
t2.write_bytes(commitment1);
71+
72+
t1.seperate();
73+
let v = t2.accumulator_finalize();
74+
let mut t3 = Transcript::from_accumulation(v);
75+
76+
t1.write_bytes(commitment2);
77+
t3.write_bytes(commitment2);
78+
79+
let c1: [u8; 32] = t1.challenge(b"challenge").read_byte_array();
80+
let c2: [u8; 32] = t3.challenge(b"challenge").read_byte_array();
81+
assert_eq!(c1,c2);
82+
}

0 commit comments

Comments
 (0)