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
30 changes: 29 additions & 1 deletion relay/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,16 @@ pub fn zone_hash(zone_data: &[u8]) -> Vec<u8> {
sha2::Sha256::digest(zone_data).to_vec()
}

/// Finality ordering for comparing zones — higher is more final.
fn sovereignty_rank(s: libveritas::SovereigntyState) -> u8 {
use libveritas::SovereigntyState::*;
match s {
Sovereign => 2,
Pending => 1,
Dependent => 0,
}
}

/// SQLite-backed store for handles.
pub struct SqliteStore {
conn: Mutex<Connection>,
Expand Down Expand Up @@ -363,7 +373,25 @@ impl SqliteStore {
let existing = match existing_zones.get(e.handle.as_str()) {
None => return Some(e), // new handle, nothing to preserve
Some(existing) => {
if !update.zone.is_better_than(existing).unwrap_or(false) {
// A temp->final finalization (or a re-proof after a newer
// commitment) carries a fresher anchor but often EMPTY owner
// records. For a sub-handle both zones have Unknown
// commitment, so is_better_than falls to the records check
// and rejects the empty-records final BEFORE the anchor
// tiebreaker — leaving the relay serving a stale temp that no
// longer verifies against the tip. Accept a strictly fresher
// proof from the same key as long as it doesn't downgrade
// finality; the owner's records are preserved by the merge
// below. Incoming certs are already verified against current
// anchors, so a higher anchor is genuine fresher chain truth.
let fresher_proof = update.zone.script_pubkey
== existing.script_pubkey
&& update.zone.anchor > existing.anchor
&& sovereignty_rank(update.zone.sovereignty)
>= sovereignty_rank(existing.sovereignty);
if !fresher_proof
&& !update.zone.is_better_than(existing).unwrap_or(false)
{
return None; // stored zone is as good or better
}
existing
Expand Down
64 changes: 64 additions & 0 deletions relay/tests/integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,70 @@ fn test_finality_upgrade_preserves_owner_records() {
);
}

/// The harder finalize: a sub-handle temp->final where the commitment stays
/// Unknown (finality comes from the parent, not the sub-handle's own on-chain
/// commitment) and the final carries EMPTY owner records but a fresher anchor.
/// `is_better_than` sees equal commitments and rejects the empty-records final at
/// the records comparison, BEFORE the anchor tiebreaker — so without the
/// fresher-proof acceptance the relay keeps serving a stale temp that no longer
/// verifies against the tip. Regression for the `nzjsk@test10000` case.
#[test]
fn test_finalize_without_commitment_upgrade_replaces_temp() {
use relay::store::HandleRecord;

let mut state = ChainState::new();
let mut runner = FixtureRunner::new(&mut state, single_commit_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 alice = handler
.store
.get_handle("alice@sovereign")
.unwrap()
.unwrap();
assert!(!alice.zone.records.is_empty(), "precondition: alice has records");
let alice_seq = alice.zone.records.seq();

// Finalize WITHOUT upgrading the commitment — only a fresher anchor + empty
// records, same controlling key (mirrors a sub-handle finalize on the wire).
let mut zone = alice.zone.clone();
zone.anchor = alice.zone.anchor + 1;
zone.records = Default::default();
let alice_final = HandleRecord {
cert: alice.cert.clone(),
zone,
epoch_height: alice.epoch_height + 1,
offchain_seq: 0,
delegate_offchain_seq: alice.delegate_offchain_seq,
};

// The exact trap: is_better_than rejects this update (empty records lose
// before the anchor tiebreaker), so plain is_better_than gating would drop it.
assert!(
!alice_final.zone.is_better_than(&alice.zone).unwrap(),
"precondition: is_better_than rejects the empty-records fresher proof"
);

handler.store.update_handles(&[alice_final]).unwrap();
let after = handler
.store
.get_handle("alice@sovereign")
.unwrap()
.unwrap();
assert_eq!(
after.zone.anchor,
alice.zone.anchor + 1,
"the fresher-anchor finalize must replace the stale temp"
);
assert!(
!after.zone.records.is_empty() && after.zone.records.seq() == alice_seq,
"the owner's records must be preserved across the finalize"
);
}

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