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 <charon@lethean.io>
This commit is contained in:
parent
8735e535d5
commit
c6631e555b
2 changed files with 153 additions and 0 deletions
52
mining/hash.go
Normal file
52
mining/hash.go
Normal file
|
|
@ -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
|
||||
}
|
||||
101
mining/hash_test.go
Normal file
101
mining/hash_test.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue