Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 53 additions & 4 deletions relay/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ use std::path::Path;
use std::sync::Mutex;

use anyhow::anyhow;
use libveritas::ProvableOption;
use libveritas::Zone;
use libveritas::cert::Certificate;
use libveritas::cert::{Certificate, Witness};
use resolver::ReverseRecord;
use rusqlite::{Connection, OptionalExtension, params};
use spaces_protocol::slabel::SLabel;
Expand Down Expand Up @@ -173,6 +174,18 @@ fn sovereignty_rank(s: libveritas::SovereigntyState) -> u8 {
}
}

/// Whether two zones carry the same on-chain commitment (Exists, equal state
/// root) — so a receipt verified for one still proves the other.
fn same_commitment_root(a: &Zone, b: &Zone) -> bool {
matches!(
(&a.commitment, &b.commitment),
(
ProvableOption::Exists { value: x },
ProvableOption::Exists { value: y },
) if x.onchain.state_root == y.onchain.state_root
)
}

/// SQLite-backed store for handles.
pub struct SqliteStore {
conn: Mutex<Connection>,
Expand Down Expand Up @@ -316,6 +329,9 @@ impl SqliteStore {
epoch_height: u32,
offchain_seq: u64,
delegate_offchain_seq: u64,
/// Keep the existing stored cert_data instead of this update's cert
/// (its receipt was omitted; see the preservation note below).
preserve_cert: bool,
}

let mut entries = Vec::with_capacity(updates.len());
Expand All @@ -339,6 +355,7 @@ impl SqliteStore {
epoch_height: update.epoch_height,
offchain_seq: update.offchain_seq,
delegate_offchain_seq: update.delegate_offchain_seq,
preserve_cert: false,
});
}

Expand Down Expand Up @@ -427,6 +444,21 @@ impl SqliteStore {
}
}

// Preserve the ZK receipt: a root cert re-sent for the same
// commitment often omits it (the sender assumes we still hold
// it), which would leave us serving a receipt-less cert that no
// sub-handle can verify against the tip. Keep our existing
// cert_data (the zone still updates to the fresher anchor above;
// only the cert, and its receipt, is retained). Same commitment
// root is the safe condition: a receipt-less cert only reaches
// here if we already verified that commitment. The insert reads
// the existing cert_data in-place, so no extra fetch is needed.
if matches!(&update.cert.witness, Witness::Root { receipt: None })
&& same_commitment_root(&update.zone, existing)
{
e.preserve_cert = true;
}

Some(e)
})
.collect();
Expand All @@ -453,10 +485,21 @@ impl SqliteStore {
params![seq_base + to_store.len() as i64],
)?;

// Bulk INSERT
// Bulk INSERT. For preserve_cert rows the cert_data column reads the
// existing stored value in-place (keeping its ZK receipt) rather than
// taking this update's receipt-less cert; the zone still updates. The
// subquery runs before REPLACE deletes the old row, and since the value
// is unchanged the storage-byte trigger nets out on cert_data.
let placeholders: Vec<String> = to_store
.iter()
.map(|_| "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)".to_string())
.map(|e| {
if e.preserve_cert {
"(?, ?, (SELECT cert_data FROM handles WHERE handle = ?), ?, ?, ?, ?, ?, ?, ?)"
.to_string()
} else {
"(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)".to_string()
}
})
.collect();
let query = format!(
"INSERT OR REPLACE INTO handles (handle, space, cert_data, zone_data, epoch_height, offchain_seq, delegate_offchain_seq, sync_seq, zone_hash, updated_at) VALUES {}",
Expand All @@ -467,7 +510,13 @@ impl SqliteStore {
for (i, e) in to_store.iter().enumerate() {
params.push(Box::new(e.handle.clone()));
params.push(Box::new(e.space.clone()));
params.push(Box::new(e.cert_data.clone()));
// cert_data: bind the handle for the SELECT subquery when preserving,
// otherwise this update's cert bytes.
if e.preserve_cert {
params.push(Box::new(e.handle.clone()));
} else {
params.push(Box::new(e.cert_data.clone()));
}
params.push(Box::new(e.zone_data.clone()));
params.push(Box::new(e.epoch_height));
params.push(Box::new(e.offchain_seq as i64));
Expand Down
59 changes: 59 additions & 0 deletions relay/tests/integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,65 @@ fn test_finalize_without_commitment_upgrade_replaces_temp() {
);
}

/// A root cert re-sent for the same commitment often omits its ZK receipt (the
/// sender assumes the relay still holds it). `is_better_than` accepts the
/// fresher-anchor cert, and without preservation the receipt-less cert would
/// overwrite the receipt-bearing one — leaving the relay unable to prove the
/// commitment to any fresh client, so sub-handles fail with "receipt required".
/// The store must keep the existing receipt. Regression for the `@test10000` case.
#[test]
fn test_receiptless_root_update_preserves_receipt() {
use libveritas::cert::Witness;
use relay::store::HandleRecord;

// A second commitment proves a transition from the first, so its root cert
// carries a ZK receipt (a first commit has nothing to prove and omits it).
let mut state = ChainState::new();
let mut runner = FixtureRunner::new(&mut state, two_commits_both_finalized());
runner.run(&mut state);
let handler = setup_handler(&state);
let bundle = runner.build_bundle();
let msg = state.message(vec![bundle]);
handler.handle_message(msg).unwrap();

let root = handler.store.get_handle("@two-finalized").unwrap().unwrap();
assert!(
matches!(root.cert.witness, Witness::Root { receipt: Some(_) }),
"precondition: the stored root cert carries a receipt"
);

// Re-send the root proven against a fresher anchor but with the receipt
// omitted, same commitment and key.
let mut cert = root.cert.clone();
if let Witness::Root { receipt } = &mut cert.witness {
*receipt = None;
}
let mut zone = root.zone.clone();
zone.anchor += 1;
let update = HandleRecord {
cert,
zone,
epoch_height: root.epoch_height,
offchain_seq: root.offchain_seq,
delegate_offchain_seq: root.delegate_offchain_seq,
};
// The fresher-anchor cert IS accepted by is_better_than — that's the trap.
assert!(update.zone.is_better_than(&root.zone).unwrap());

handler.store.update_handles(&[update]).unwrap();

let after = handler.store.get_handle("@two-finalized").unwrap().unwrap();
assert!(
matches!(after.cert.witness, Witness::Root { receipt: Some(_) }),
"the receipt must survive a receipt-less re-send for the same commitment"
);
assert_eq!(
after.zone.anchor,
root.zone.anchor + 1,
"the zone still updates to the fresher anchor"
);
}

#[test]
fn test_all_fixtures() {
let fixtures: Vec<(&str, Fixture, Vec<&str>)> = vec![
Expand Down
Loading