Open pool: the share's claimed key, its header's key and its coinbase reveal are one key or the share is refused before its PoW (sidechain::check_key, called in accept; known-pass make_share, known-fail the claim, the header and a foreign reveal); suite 30 of 30 on build-2

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-08 18:45:41 +00:00
parent c999eb2f0a
commit a4c9958cc8
2 changed files with 52 additions and 1 deletions

View file

@ -10,7 +10,7 @@
//! fee-neutral and the choice is about variance alone (reinvent 3.5).
use crate::pool::{Member, Pool};
use crate::sidechain::{check_pow, ChainParams, SeedsWire, Share, ShareChain, GENESIS_PARENT};
use crate::sidechain::{check_key, check_pow, ChainParams, SeedsWire, Share, ShareChain, GENESIS_PARENT};
use crate::state::{unix_ms, BlockRec, State};
use crate::verify::JobKey;
use kaspa_consensus_core::block::Block;
@ -318,6 +318,8 @@ impl Open {
if wire != seeds {
return Err(format!("the share's day {} is not the day of its timestamp ({})", wire.day, seeds.day));
}
// Igneum 2.0: the claimed key, the header's key and the reveal are one key, before the PoW
crate::sidechain::check_key(&share)?;
check_pow(&self.engine, &share)?;
let share = Arc::new(share);
let tip = {

View file

@ -436,6 +436,25 @@ impl ShareChain {
}
/// The PoW of a share: the lane hash under the share's seeds equals the claim and meets the share's target.
/// Igneum 2.0, the vote-key commitment on the open pool (`docs/design/pool-vote-key-commitment.md` 2.2 item 3 and 5.3
/// item 2): the key a share claims (`key_hash`), the key its header names (`vote_key_hash`, under proof of work) and,
/// when its coinbase carries a reveal (`IGNK`), the reveal's key are one key, or the share is refused before its PoW is
/// evaluated. A daemon that stamps another identity into a member's template fails here on every honest daemon.
pub fn check_key(s: &Share) -> Result<(), String> {
let claimed: Hash = s.key_hash.parse().map_err(|_| format!("key_hash {} is not a hash", s.key_hash))?;
let header_key = s.raw.header.vote_key_hash;
if claimed != header_key {
return Err(format!("the share claims key {} but its header names {header_key}", s.key_hash));
}
if let Some(cb) = s.raw.transactions.first()
&& let Some(reveal) = KeyReveal::find_in(&cb.payload)
&& reveal.key_hash() != header_key
{
return Err(format!("the coinbase reveals key {} but the header names {header_key}", reveal.key_hash()));
}
Ok(())
}
pub fn check_pow(engine: &IgneumEngine, s: &Share) -> Result<(), String> {
let seeds = s.seeds.to_seeds()?;
let block: Block = s.raw.clone().try_into().map_err(|e| format!("block convert: {e}"))?;
@ -583,6 +602,36 @@ pub mod tests {
}
}
/// Igneum 2.0: the key a share claims, the key its header names and the key its coinbase reveals are one key.
/// Known-pass: make_share. Known-fail: the claim swapped, the header's key swapped (the PoW still over the old
/// header, so check_key fires before check_pow would), and a foreign reveal in the coinbase.
#[test]
fn a_share_whose_keys_disagree_is_refused_before_its_pow() {
let engine = IgneumEngine::new();
let mut chain = ShareChain::new(params());
chain.genesis_target = Some(u64::MAX >> 6);
let good = make_share(&chain, &engine, GENESIS_PARENT, "ma", [0xaa; 20], 1_000_000, 0);
assert!(check_key(&good).is_ok(), "known-pass");
let mut claim = good.clone();
claim.key_hash = Hash::from_bytes([9u8; 32]).to_string();
assert!(check_key(&claim).unwrap_err().contains("claims key"), "known-fail: the claim");
let mut header = good.clone();
header.raw.header.vote_key_hash = Hash::from_bytes([9u8; 32]);
assert!(check_key(&header).unwrap_err().contains("header names"), "known-fail: the header");
let mut reveal = good.clone();
let other = kaspa_consensus_core::finality::VoteSecretKey::from_label("mb").key_reveal();
let mut payload = reveal.raw.transactions[0].payload.clone();
// the reveal in the payload is IGNK || pubkey || pop (scheme 0): overwrite it in place with the other key's
let tag = kaspa_consensus_core::finality::KEY_REVEAL_TAG;
let at = payload.windows(4).position(|w| w == tag).expect("the test share carries a reveal");
let mine = good.raw.transactions[0].payload[at..].to_vec();
let theirs = other.to_extra_data();
assert_eq!(mine.len() >= theirs.len(), true);
payload[at..at + theirs.len()].copy_from_slice(&theirs);
reveal.raw.transactions[0].payload = payload;
assert!(check_key(&reveal).unwrap_err().contains("reveals key"), "known-fail: a foreign reveal");
}
/// A chain of shares by three members: every share passes the structure check and the PoW check, the window's
/// split is the members' work plus the dev entry and sums to one, the retarget moves toward the share interval,
/// a withheld share is in nobody's window, and a share off the heavier branch is stale.