Skip to content

Commit 5155f23

Browse files
AIQnetLabclaude
andcommitted
Genesis registry_root determinism: single-source registration writer
The block creator wrote its genesis registry rows without vrf_pk_sha3 while peers wrote it (vrf was bound into registry_root in 06b2076), so the creator's registry_root diverged and it rejected every macroblock proposal on the registry_root field (the chain survived via 2f+1 + per-macroblock self-heal). Unify the inline registration writers behind one canonical write_registration_row helper (vrf+burn+height derivation; vrf for super/genesis only). Route producer-inline and genesis through it, and replace the height-only stamp_genesis_registration_heights backfill with apply_genesis_registrations, which writes the full row (vrf co-resident) byte-identical to the peer-apply path. Add a determinism test pinning creator == peer == vrf-bound. Requires a fresh genesis (changes genesis registry content). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 9b78435 commit 5155f23

1 file changed

Lines changed: 73 additions & 40 deletions

File tree

  • development/qnet-integration/src

development/qnet-integration/src/node.rs

Lines changed: 73 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -5299,25 +5299,40 @@ impl BlockchainNode {
52995299
}
53005300
}
53015301

5302-
/// Stamp `reg_height = 0` for every genesis NodeRegistration TX in block 0.
5303-
/// `super_registrations_sorted` (the reward roster) only counts entries with a stamped
5304-
/// reg_height. Synced peers stamp all five genesis nodes via the block-apply path
5305-
/// (deferred_registrations → save_node_registration_at_height, height 0, reputation 1.0).
5306-
/// The block CREATOR, however, applies block 0 through cache_node_registrations_from_transactions
5307-
/// → save_node_registration (NO height), so its roster would hold only its own self-registered
5308-
/// genesis id (count=1) while peers compute count=5 — a persistent per-epoch reward divergence.
5309-
/// This backfill makes the creator/restarted node byte-identical to synced peers. Idempotent.
5310-
fn stamp_genesis_registration_heights(storage: &crate::storage::Storage, transactions: &[qnet_state::Transaction]) {
5302+
/// Single canonical writer of ONE chain-confirmed NodeRegistration's registry row: stamps
5303+
/// reg_height + burn + vrf_pk co-resident (save_node_registration_at_height_burn_vrf) plus the
5304+
/// backing cbw binding. The ONE place producer-inline and genesis derive the row, so registry_root
5305+
/// is byte-identical on every node. vrf bound for consensus signers (super/genesis) only; light None.
5306+
fn write_registration_row(
5307+
storage: &crate::storage::Storage,
5308+
node_id: &str,
5309+
node_type: &qnet_state::NodeType,
5310+
wallet: &str,
5311+
burn_tx: &str,
5312+
dilithium_pk: Option<&String>,
5313+
height: u64,
5314+
) {
5315+
let type_str = match node_type {
5316+
qnet_state::NodeType::Super => "super",
5317+
qnet_state::NodeType::Light => "light",
5318+
};
5319+
let vrf = if matches!(node_type, qnet_state::NodeType::Super) {
5320+
dilithium_pk.and_then(|s| hex::decode(s).ok())
5321+
} else { None };
5322+
let _ = storage.save_node_registration_at_height_burn_vrf(node_id, type_str, wallet, 1.0, height, burn_tx, vrf.as_deref());
5323+
if !burn_tx.is_empty() {
5324+
let _ = storage.committed_burn_wallet_put(burn_tx, wallet);
5325+
}
5326+
}
5327+
5328+
/// Apply block-0 genesis NodeRegistration TXs through the SAME canonical row writer as a peer-
5329+
/// applying validator (reg_height 0, burn, vrf co-resident), so the creator's registry_root and
5330+
/// reward roster are byte-identical to synced peers. Replaces the old height-only backfill that
5331+
/// dropped vrf_pk and diverged the creator's registry_root. Idempotent (immutable once stamped).
5332+
fn apply_genesis_registrations(storage: &crate::storage::Storage, transactions: &[qnet_state::Transaction]) {
53115333
for tx in transactions {
5312-
if let qnet_state::TransactionType::NodeRegistration { node_id, node_type, wallet_address, .. } = &tx.tx_type {
5313-
let type_str = match node_type {
5314-
qnet_state::NodeType::Super => "super",
5315-
qnet_state::NodeType::Light => "light",
5316-
};
5317-
// height 0 + reputation 1.0 == the values synced peers write via deferred_registrations.
5318-
if let Err(e) = storage.save_node_registration_at_height(node_id, type_str, wallet_address, 1.0, 0) {
5319-
eprintln!("[WARN][REG] genesis_reg_height_stamp_fail node={} err={}", node_id, e);
5320-
}
5334+
if let qnet_state::TransactionType::NodeRegistration { node_id, node_type, wallet_address, burn_tx, .. } = &tx.tx_type {
5335+
Self::write_registration_row(storage, node_id, node_type, wallet_address, burn_tx, tx.dilithium_public_key.as_ref(), 0);
53215336
}
53225337
}
53235338
}
@@ -7900,10 +7915,10 @@ impl BlockchainNode {
79007915
let genesis_timestamp = match storage.load_microblock_auto_format(0) {
79017916
Ok(Some(genesis_block)) => {
79027917
if is_info() { println!("[INFO][GEN] loaded_ts={}", genesis_block.timestamp); }
7903-
// Self-heal the reward roster on restart: stamp reg_height=0 for the genesis
7904-
// registrations so a former creator (which only cached them without a height)
7905-
// matches synced peers. Idempotent for peers that already stamped them.
7906-
Self::stamp_genesis_registration_heights(&storage, &genesis_block.transactions);
7918+
// Apply genesis registrations canonically (reg_height 0 + vrf co-resident) so a former
7919+
// creator which only cached them without height/vrf — is byte-identical to synced peers.
7920+
// Idempotent (immutable once stamped).
7921+
Self::apply_genesis_registrations(&storage, &genesis_block.transactions);
79077922
genesis_block.timestamp
79087923
}
79097924
Ok(None) => {
@@ -11890,8 +11905,8 @@ impl BlockchainNode {
1189011905
// CRITICAL FIX v3.2: Cache NodeRegistration TXs from genesis block
1189111906
// Without this, genesis creator can't find wallet addresses for rewards!
1189211907
Self::cache_node_registrations_from_transactions(&storage, &genesis_microblock.transactions);
11893-
// Stamp reg_height=0 so the creator's reward roster matches synced peers (count=5, not 1).
11894-
Self::stamp_genesis_registration_heights(&storage, &genesis_microblock.transactions);
11908+
// Apply genesis registrations canonically (reg_height 0 + vrf) so the creator is byte-identical to synced peers.
11909+
Self::apply_genesis_registrations(&storage, &genesis_microblock.transactions);
1189511910
println!("[INFO][GEN] Cached {} NodeRegistration TXs from genesis block",
1189611911
genesis_microblock.transactions.iter()
1189711912
.filter(|tx| matches!(tx.tx_type, qnet_state::TransactionType::NodeRegistration { .. }))
@@ -16784,22 +16799,9 @@ impl BlockchainNode {
1678416799
qnet_state::TransactionType::NodeRegistration {
1678516800
node_id: rid, node_type: rtype, wallet_address: rwallet, burn_tx: rburn, ..
1678616801
} => {
16787-
let rtype_str = match rtype {
16788-
qnet_state::NodeType::Super => "super",
16789-
qnet_state::NodeType::Light => "light",
16790-
};
16791-
// vrf bound for consensus participants ONLY (super/genesis); light = None,
16792-
// byte-identical to the peer-apply deferred path (else the producer's own
16793-
// registry_root row diverges from synced validators → fork).
16794-
let rvrf = if matches!(rtype, qnet_state::NodeType::Super) { tx.dilithium_public_key.as_ref().and_then(|s| hex::decode(s).ok()) } else { None };
16795-
let _ = storage.save_node_registration_at_height_burn_vrf(
16796-
rid, rtype_str, rwallet, 1.0, next_block_height, rburn, rvrf.as_deref());
16797-
// Incremental cbw binding for the producer's own block (before save below),
16798-
// mirroring peer-apply. Scope = any non-empty burn (super + LIGHT), MATCHING
16799-
// rebuild_committed_burn_wallet (srtr_+lrtr_). Empty-burn regs auto-skip.
16800-
if !rburn.is_empty() {
16801-
let _ = storage.committed_burn_wallet_put(rburn, rwallet);
16802-
}
16802+
// Single canonical derivation (vrf super-only + cbw), byte-identical to the
16803+
// peer-apply path — else the producer's own registry_root row diverges → fork.
16804+
Self::write_registration_row(&storage, rid, rtype, rwallet, rburn, tx.dilithium_public_key.as_ref(), next_block_height);
1680316805
}
1680416806
qnet_state::TransactionType::NodeActivation { node_type: ntype, phase, .. } => {
1680516807
// Mirror apply_block_to_state EXACTLY, including its FIRST-MATCH-WINS arm
@@ -27571,6 +27573,37 @@ mod tests {
2757127573
assert!(BlockchainNode::committee_for_height(&storage, 300).is_none(), "absent N-2 ⇒ None (no guess)");
2757227574
}
2757327575

27576+
// Regression guard for the genesis registry_root fork: the block CREATOR (apply_genesis_registrations)
27577+
// must write vrf_pk co-resident, byte-identical to the peer-apply path — else its registry_root
27578+
// diverges from synced peers and proposal_content never reaches 2f+1. Pins creator == peer == vrf-bound.
27579+
#[test]
27580+
fn genesis_apply_writes_vrf_byte_identical_to_peer() {
27581+
let pk_hex = "a1b2c3d4e5f60718"; // genesis consensus key bytes (any valid hex)
27582+
let mut gtx = BlockchainNode::create_node_registration_tx_with_timestamp(
27583+
"genesis_node_001", qnet_state::NodeType::Super, "walletG", "genesis", "", Some(0));
27584+
gtx.dilithium_public_key = Some(pk_hex.to_string());
27585+
27586+
// Creator path (the fix): reg_height 0 + vrf via the shared canonical writer.
27587+
let _dc = tempfile::TempDir::new().expect("tempdir");
27588+
let creator = crate::storage::Storage::new(_dc.path().to_str().unwrap()).expect("storage");
27589+
BlockchainNode::apply_genesis_registrations(&creator, std::slice::from_ref(&gtx));
27590+
27591+
// Peer-apply path: the deferred consumer writes the same row (super ⇒ vrf from the TX pubkey).
27592+
let _dp = tempfile::TempDir::new().expect("tempdir");
27593+
let peer = crate::storage::Storage::new(_dp.path().to_str().unwrap()).expect("storage");
27594+
let vrf = hex::decode(pk_hex).unwrap();
27595+
peer.save_node_registration_at_height_burn_vrf("genesis_node_001", "super", "walletG", 1.0, 0, "", Some(vrf.as_slice())).unwrap();
27596+
assert_eq!(creator.compute_registry_root(0), peer.compute_registry_root(0),
27597+
"genesis creator path must be byte-identical to the peer-apply path");
27598+
27599+
// vrf is actually bound: a vrf-less write yields a DIFFERENT root (proves the regression is closed).
27600+
let _dn = tempfile::TempDir::new().expect("tempdir");
27601+
let novrf = crate::storage::Storage::new(_dn.path().to_str().unwrap()).expect("storage");
27602+
novrf.save_node_registration_at_height_burn("genesis_node_001", "super", "walletG", 1.0, 0, "").unwrap();
27603+
assert_ne!(creator.compute_registry_root(0), novrf.compute_registry_root(0),
27604+
"vrf_pk must be bound into registry_root (creator != vrf-less)");
27605+
}
27606+
2757427607
// ── B cutover: merkle reward-claim determinism + security ──
2757527608
// The producer's committed root and every node's apply-time recompute call the SAME
2757627609
// distribute_equal_rewards over the SAME consensus inputs. These pin the three properties

0 commit comments

Comments
 (0)