@@ -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(>x));
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