Skip to content

Commit 38c9956

Browse files
feat: Change proof serialization/deserialization (#1638)
## Why this should be merged Second PR for change proof FFI (builds on #1637). This PR includes `fwd_change_proof_to_bytes` and `fwd_change_proof_from_bytes` for serialization/deserization of a change proof. ## How this works Mostly follows the serialization/deserialization implementation in range proof. Main change is to support serializing/deserializing `BatchOp`s which are used for storing the difference between two revisions. ## How this was tested Basic serialization and deserialization tests in `proofs_test.go`, including a round trip test where a change proof is serialized, deserialized, and serialized again, and verifying the two serialized proofs match.
1 parent b0c0958 commit 38c9956

File tree

6 files changed

+222
-11
lines changed

6 files changed

+222
-11
lines changed

ffi/firewood.h

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ffi/proofs_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,25 @@ func newSerializedRangeProof(
9292
return proofBytes
9393
}
9494

95+
func newSerializedChangeProof(
96+
t *testing.T,
97+
db *Database,
98+
startRoot, endRoot Hash,
99+
startKey, endKey maybe,
100+
proofLen uint32,
101+
) []byte {
102+
r := require.New(t)
103+
104+
// TODO: Replace with newVerifiedChangeProof after adding verify.
105+
proof, err := db.ChangeProof(startRoot, endRoot, startKey, endKey, proofLen)
106+
r.NoError(err)
107+
108+
proofBytes, err := proof.MarshalBinary()
109+
r.NoError(err)
110+
111+
return proofBytes
112+
}
113+
95114
func TestRangeProofEmptyDB(t *testing.T) {
96115
r := require.New(t)
97116
db := newTestDatabase(t)
@@ -412,3 +431,59 @@ func TestChangeProofCreation(t *testing.T) {
412431
_, err = db.ChangeProof(root1, root2, nothing(), nothing(), changeProofLenUnbounded)
413432
r.NoError(err)
414433
}
434+
435+
func TestChangeProofDiffersAfterUpdate(t *testing.T) {
436+
r := require.New(t)
437+
db := newTestDatabase(t)
438+
439+
// Insert first half of data in the first batch
440+
_, _, batch := kvForTest(10000)
441+
root1, err := db.Update(batch[:2500])
442+
r.NoError(err)
443+
444+
// Insert the rest in the second batch
445+
root2, err := db.Update(batch[2500:5000])
446+
r.NoError(err)
447+
r.NotEqual(root1, root2)
448+
449+
// get a proof
450+
proof1 := newSerializedChangeProof(t, db, root1, root2, nothing(), nothing(), changeProofLenUnbounded)
451+
r.NoError(err)
452+
453+
// insert more data
454+
root3, err := db.Update(batch[5000:])
455+
r.NoError(err)
456+
r.NotEqual(root2, root3)
457+
458+
// get a proof again
459+
proof2 := newSerializedChangeProof(t, db, root2, root3, nothing(), nothing(), changeProofLenUnbounded)
460+
// ensure the proofs are different
461+
r.NotEqual(proof1, proof2)
462+
}
463+
464+
func TestRoundTripChangeProofSerialization(t *testing.T) {
465+
r := require.New(t)
466+
db := newTestDatabase(t)
467+
468+
// Insert some data.
469+
_, _, batch := kvForTest(10)
470+
root1, err := db.Update(batch[:5])
471+
r.NoError(err)
472+
473+
root2, err := db.Update(batch[5:])
474+
r.NoError(err)
475+
476+
// get a proof
477+
proofBytes := newSerializedChangeProof(t, db, root1, root2, nothing(), nothing(), changeProofLenUnbounded)
478+
479+
// Deserialize the proof.
480+
proof := new(ChangeProof)
481+
err = proof.UnmarshalBinary(proofBytes)
482+
r.NoError(err)
483+
t.Cleanup(func() { r.NoError(proof.Free()) })
484+
485+
// serialize the proof again
486+
serialized, err := proof.MarshalBinary()
487+
r.NoError(err)
488+
r.Equal(proofBytes, serialized)
489+
}

ffi/src/proofs/change.rs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@
33

44
use std::num::NonZeroUsize;
55

6-
#[cfg(feature = "ethhash")]
7-
use firewood::ProofError;
86
#[cfg(feature = "ethhash")]
97
use firewood_storage::TrieHash;
108
#[cfg(feature = "ethhash")]
119
use rlp::Rlp;
1210

13-
use firewood::v2::api::{self, FrozenChangeProof};
11+
use firewood::{
12+
ProofError,
13+
v2::api::{self, FrozenChangeProof},
14+
};
1415

1516
use crate::{
1617
BorrowedBytes, CResult, ChangeProofResult, DatabaseHandle, HashKey, HashResult, Maybe,
@@ -366,8 +367,12 @@ pub extern "C" fn fwd_change_proof_find_next_key(
366367
///
367368
/// The other [`ValueResult`] variants are not used.
368369
#[unsafe(no_mangle)]
369-
pub extern "C" fn fwd_change_proof_to_bytes(_proof: Option<&ChangeProofContext>) -> ValueResult {
370-
CResult::from_err("not yet implemented")
370+
pub extern "C" fn fwd_change_proof_to_bytes(proof: Option<&ChangeProofContext>) -> ValueResult {
371+
crate::invoke_with_handle(proof, |ctx| {
372+
let mut vec = Vec::new();
373+
ctx.proof.write_to_vec(&mut vec);
374+
vec
375+
})
371376
}
372377

373378
/// Deserialize a `ChangeProof` from bytes.
@@ -384,8 +389,11 @@ pub extern "C" fn fwd_change_proof_to_bytes(_proof: Option<&ChangeProofContext>)
384389
/// well-formed. The verify method must be called to ensure the proof is cryptographically valid.
385390
/// - [`ChangeProofResult::Err`] containing an error message if the proof could not be parsed.
386391
#[unsafe(no_mangle)]
387-
pub extern "C" fn fwd_change_proof_from_bytes(_bytes: BorrowedBytes) -> ChangeProofResult {
388-
CResult::from_err("not yet implemented")
392+
pub extern "C" fn fwd_change_proof_from_bytes(bytes: BorrowedBytes) -> ChangeProofResult {
393+
crate::invoke(move || {
394+
FrozenChangeProof::from_slice(&bytes)
395+
.map_err(|err| api::Error::ProofError(ProofError::Deserialization(err)))
396+
})
389397
}
390398

391399
/// Frees the memory associated with a `ChangeProofContext`.

firewood/src/proofs/de.rs

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,12 @@ use super::{
1818
reader::{ProofReader, ReadError, ReadItem, V0Reader, Version0},
1919
types::{Proof, ProofNode, ProofType},
2020
};
21-
use crate::v2::api::FrozenRangeProof;
21+
use crate::{
22+
db::BatchOp,
23+
merkle::{Key, Value},
24+
proofs::magic::{BATCH_DELETE, BATCH_DELETE_RANGE, BATCH_PUT},
25+
v2::api::{FrozenChangeProof, FrozenRangeProof},
26+
};
2227

2328
impl FrozenRangeProof {
2429
/// Parses a `FrozenRangeProof` from the given byte slice.
@@ -72,6 +77,46 @@ impl<T: Version0> Version0 for Box<[T]> {
7277
}
7378
}
7479

80+
impl FrozenChangeProof {
81+
/// Parses a `FrozenChangeProof` from the given byte slice.
82+
///
83+
/// Currently only V0 proofs are supported. See [`FrozenChangeProof::write_to_vec`]
84+
/// for the serialization format.
85+
///
86+
/// # Errors
87+
///
88+
/// Returns a [`ReadError`] if the data is invalid. See the enum variants for
89+
/// the possible reasons.
90+
pub fn from_slice(data: &[u8]) -> Result<Self, ReadError> {
91+
let mut reader = ProofReader::new(data);
92+
93+
let header = reader.read_item::<Header>()?;
94+
header
95+
.validate(Some(ProofType::Change))
96+
.map_err(ReadError::InvalidHeader)?;
97+
98+
if header.version != 0 {
99+
return Err(ReadError::InvalidHeader(
100+
InvalidHeader::UnsupportedVersion {
101+
found: header.version,
102+
},
103+
));
104+
}
105+
106+
let mut reader = V0Reader::new(reader, header);
107+
let this = reader.read_v0_item()?;
108+
if reader.remainder().is_empty() {
109+
Ok(this)
110+
} else {
111+
Err(reader.invalid_item(
112+
"trailing bytes",
113+
"no data after the proof",
114+
format!("{} bytes", reader.remainder().len()),
115+
))
116+
}
117+
}
118+
}
119+
75120
impl Version0 for FrozenRangeProof {
76121
fn read_v0_item(reader: &mut V0Reader<'_>) -> Result<Self, ReadError> {
77122
let start_proof = reader.read_v0_item()?;
@@ -86,6 +131,41 @@ impl Version0 for FrozenRangeProof {
86131
}
87132
}
88133

134+
impl Version0 for FrozenChangeProof {
135+
fn read_v0_item(reader: &mut V0Reader<'_>) -> Result<Self, ReadError> {
136+
let start_proof = reader.read_v0_item()?;
137+
let end_proof = reader.read_v0_item()?;
138+
let key_values = reader.read_v0_item()?;
139+
140+
Ok(Self::new(
141+
Proof::new(start_proof),
142+
Proof::new(end_proof),
143+
key_values,
144+
))
145+
}
146+
}
147+
148+
impl Version0 for BatchOp<Key, Value> {
149+
fn read_v0_item(reader: &mut V0Reader<'_>) -> Result<Self, ReadError> {
150+
match reader
151+
.read_item::<u8>()
152+
.map_err(|err| err.set_item("option discriminant"))?
153+
{
154+
BATCH_PUT => Ok(BatchOp::Put {
155+
key: reader.read_item()?,
156+
value: reader.read_item()?,
157+
}),
158+
BATCH_DELETE => Ok(BatchOp::Delete {
159+
key: reader.read_item()?,
160+
}),
161+
BATCH_DELETE_RANGE => Ok(BatchOp::DeleteRange {
162+
prefix: reader.read_item()?,
163+
}),
164+
found => Err(reader.invalid_item("option discriminant", "0, 1, or 2", found)),
165+
}
166+
}
167+
}
168+
89169
impl Version0 for ProofNode {
90170
fn read_v0_item(reader: &mut V0Reader<'_>) -> Result<Self, ReadError> {
91171
let key = reader.read_v0_item()?;

firewood/src/proofs/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,4 +114,9 @@ pub(super) mod magic {
114114

115115
/// Branching factor identifier for branch factor 16
116116
pub const BRANCH_FACTOR: u8 = 16;
117+
118+
/// `BatchOp` constants for serialization
119+
pub const BATCH_PUT: u8 = 0;
120+
pub const BATCH_DELETE: u8 = 1;
121+
pub const BATCH_DELETE_RANGE: u8 = 2;
117122
}

firewood/src/proofs/ser.rs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@ use super::{
1515
header::Header,
1616
types::{ProofNode, ProofType},
1717
};
18-
use crate::v2::api::FrozenRangeProof;
18+
use crate::{
19+
db::BatchOp,
20+
merkle::{Key, Value},
21+
proofs::magic::{BATCH_DELETE, BATCH_DELETE_RANGE, BATCH_PUT},
22+
v2::api::{FrozenChangeProof, FrozenRangeProof},
23+
};
1924

2025
impl FrozenRangeProof {
2126
/// Serializes this proof into the provided byte vector.
@@ -78,6 +83,13 @@ impl FrozenRangeProof {
7883
}
7984
}
8085

86+
impl FrozenChangeProof {
87+
pub fn write_to_vec(&self, out: &mut Vec<u8>) {
88+
Header::from(ProofType::Change).write_item(out);
89+
self.write_item(out);
90+
}
91+
}
92+
8193
trait PushVarInt {
8294
fn push_var_int<VI: VarInt>(&mut self, v: VI);
8395
}
@@ -103,6 +115,37 @@ impl WriteItem for FrozenRangeProof {
103115
}
104116
}
105117

118+
impl WriteItem for FrozenChangeProof {
119+
fn write_item(&self, out: &mut Vec<u8>) {
120+
self.start_proof().write_item(out);
121+
self.end_proof().write_item(out);
122+
self.batch_ops().write_item(out);
123+
}
124+
}
125+
126+
impl WriteItem for [BatchOp<Key, Value>] {
127+
fn write_item(&self, out: &mut Vec<u8>) {
128+
out.push_var_int(self.len());
129+
for item in self {
130+
match item {
131+
BatchOp::Put { key, value } => {
132+
out.push(BATCH_PUT);
133+
key.write_item(out);
134+
value.write_item(out);
135+
}
136+
BatchOp::Delete { key } => {
137+
out.push(BATCH_DELETE);
138+
key.write_item(out);
139+
}
140+
BatchOp::DeleteRange { prefix } => {
141+
out.push(BATCH_DELETE_RANGE);
142+
prefix.write_item(out);
143+
}
144+
}
145+
}
146+
}
147+
}
148+
106149
impl WriteItem for ProofNode {
107150
fn write_item(&self, out: &mut Vec<u8>) {
108151
self.key.write_item(out);

0 commit comments

Comments
 (0)