From c6631e555bda1476aad71bd4fc6ba50dcc49ead7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Feb 2026 02:04:33 +0000 Subject: [PATCH] feat(mining): header mining hash and nonce checking Port of C++ get_block_header_mining_hash(). Computes BlockHashingBlob with nonce=0, Keccak-256's it. CheckNonce wraps RandomX + difficulty. Co-Authored-By: Charon --- mining/hash.go | 52 +++++++++++++++++++++++ mining/hash_test.go | 101 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 153 insertions(+) create mode 100644 mining/hash.go create mode 100644 mining/hash_test.go diff --git a/mining/hash.go b/mining/hash.go new file mode 100644 index 0000000..a1ccfd3 --- /dev/null +++ b/mining/hash.go @@ -0,0 +1,52 @@ +// Copyright (c) 2017-2026 Lethean (https://lt.hn) +// +// Licensed under the European Union Public Licence (EUPL) version 1.2. +// SPDX-License-Identifier: EUPL-1.2 + +// Package mining provides a solo PoW miner that talks to a C++ daemon +// via JSON-RPC. It fetches block templates, grinds nonces with RandomX, +// and submits solutions. +package mining + +import ( + "encoding/binary" + + "forge.lthn.ai/core/go-blockchain/consensus" + "forge.lthn.ai/core/go-blockchain/crypto" + "forge.lthn.ai/core/go-blockchain/types" + "forge.lthn.ai/core/go-blockchain/wire" +) + +// RandomXKey is the cache initialisation key for RandomX hashing. +var RandomXKey = []byte("LetheanRandomXv1") + +// HeaderMiningHash computes the header hash used as input to RandomX. +// The nonce in the block is set to 0 before computing the hash, matching +// the C++ get_block_header_mining_hash() function. +// +// The result is deterministic for a given block template regardless of +// the block's current nonce value. +func HeaderMiningHash(b *types.Block) [32]byte { + // Save and zero the nonce. + savedNonce := b.Nonce + b.Nonce = 0 + blob := wire.BlockHashingBlob(b) + b.Nonce = savedNonce + + return wire.Keccak256(blob) +} + +// CheckNonce tests whether a specific nonce produces a valid PoW solution +// for the given header mining hash and difficulty. +func CheckNonce(headerHash [32]byte, nonce, difficulty uint64) (bool, error) { + var input [40]byte + copy(input[:32], headerHash[:]) + binary.LittleEndian.PutUint64(input[32:], nonce) + + powHash, err := crypto.RandomXHash(RandomXKey, input[:]) + if err != nil { + return false, err + } + + return consensus.CheckDifficulty(types.Hash(powHash), difficulty), nil +} diff --git a/mining/hash_test.go b/mining/hash_test.go new file mode 100644 index 0000000..fa02923 --- /dev/null +++ b/mining/hash_test.go @@ -0,0 +1,101 @@ +// Copyright (c) 2017-2026 Lethean (https://lt.hn) +// +// Licensed under the European Union Public Licence (EUPL) version 1.2. +// SPDX-License-Identifier: EUPL-1.2 + +package mining + +import ( + "bytes" + "encoding/binary" + "encoding/hex" + "testing" + + "forge.lthn.ai/core/go-blockchain/types" + "forge.lthn.ai/core/go-blockchain/wire" +) + +func testnetGenesisHeader() types.BlockHeader { + return types.BlockHeader{ + MajorVersion: 1, + Nonce: 101011010221, + PrevID: types.Hash{}, + MinorVersion: 0, + Timestamp: 1770897600, + Flags: 0, + } +} + +func testnetGenesisRawTx() []byte { + u64s := [25]uint64{ + 0xa080800100000101, 0x03018ae3c8e0c8cf, 0x7b0287d2a2218485, 0x720c5b385edbe3dd, + 0x178e7c64d18a598f, 0x98bb613ff63e6d03, 0x3814f971f9160500, 0x1c595f65f55d872e, + 0x835e5fd926b1f78d, 0xf597c7f5a33b6131, 0x2074496b139c8341, 0x64612073656b6174, + 0x20656761746e6176, 0x6e2065687420666f, 0x666f206572757461, 0x616d726f666e6920, + 0x696562206e6f6974, 0x207973616520676e, 0x6165727073206f74, 0x6168207475622064, + 0x7473206f74206472, 0x202d202e656c6966, 0x206968736f746153, 0x6f746f6d616b614e, + 0x0a0e0d66020b0015, + } + u8s := [2]uint8{0x00, 0x00} + buf := make([]byte, 25*8+2) + for i, v := range u64s { + binary.LittleEndian.PutUint64(buf[i*8:], v) + } + buf[200] = u8s[0] + buf[201] = u8s[1] + return buf +} + +func TestHeaderMiningHash_Good(t *testing.T) { + // Build the genesis block from the known raw coinbase transaction. + rawTx := testnetGenesisRawTx() + dec := wire.NewDecoder(bytes.NewReader(rawTx)) + minerTx := wire.DecodeTransaction(dec) + if dec.Err() != nil { + t.Fatalf("decode genesis tx: %v", dec.Err()) + } + + block := types.Block{ + BlockHeader: testnetGenesisHeader(), + MinerTx: minerTx, + } + + got := HeaderMiningHash(&block) + + // The header mining hash is computed with nonce=0, so manually compute + // it to get the expected value. + block.Nonce = 0 + blob := wire.BlockHashingBlob(&block) + want := wire.Keccak256(blob) + + if got != want { + t.Errorf("HeaderMiningHash:\n got: %s\n want: %s", + hex.EncodeToString(got[:]), hex.EncodeToString(want[:])) + } +} + +func TestHeaderMiningHash_Good_NonceIgnored(t *testing.T) { + // HeaderMiningHash must produce the same result regardless of the + // block's current nonce value. + rawTx := testnetGenesisRawTx() + dec := wire.NewDecoder(bytes.NewReader(rawTx)) + minerTx := wire.DecodeTransaction(dec) + if dec.Err() != nil { + t.Fatalf("decode genesis tx: %v", dec.Err()) + } + + block1 := types.Block{ + BlockHeader: testnetGenesisHeader(), + MinerTx: minerTx, + } + block2 := block1 + block2.Nonce = 999999 + + h1 := HeaderMiningHash(&block1) + h2 := HeaderMiningHash(&block2) + + if h1 != h2 { + t.Errorf("HeaderMiningHash changed with different nonce:\n nonce=%d: %x\n nonce=%d: %x", + block1.Nonce, h1, block2.Nonce, h2) + } +}