Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 120 additions & 3 deletions src/rpc/mining.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@

std::map<std::string, CBlock> mapProgPowTemplates;
std::map<std::string, CBlock> mapRandomXTemplates;
std::map<std::string, CBlock> mapSha256dTemplates;

unsigned int ParseConfirmTarget(const UniValue& value)
{
Expand Down Expand Up @@ -625,6 +626,8 @@ static UniValue getblocktemplate_impl(const std::string &strMode, const UniValue
mapProgPowTemplates.clear();
if constexpr (nPoWType == CBlockHeader::RANDOMX_BLOCK)
mapRandomXTemplates.clear();
if constexpr (nPoWType == CBlockHeader::SHA256D_BLOCK)
mapSha256dTemplates.clear();

// Store the pindexBest used before CreateNewBlock, to avoid races
nTransactionsUpdatedLast = mempool.GetTransactionsUpdated();
Expand Down Expand Up @@ -794,7 +797,10 @@ static UniValue getblocktemplate_impl(const std::string &strMode, const UniValue
result.pushKV("target", hashTarget.GetHex());
result.pushKV("mintime", (int64_t)pindexPrev->GetMedianTimePast()+1);
result.pushKV("mutable", aMutable);
result.pushKV("noncerange", "00000000ffffffff");
if constexpr (nPoWType == CBlockHeader::SHA256D_BLOCK)
result.pushKV("noncerange", "0000000000000000ffffffffffffffff");
else
result.pushKV("noncerange", "00000000ffffffff");
int64_t nSigOpLimit = MAX_BLOCK_SIGOPS_COST;
int64_t nSizeLimit = MAX_BLOCK_SERIALIZED_SIZE;
if (fPreSegWit) {
Expand Down Expand Up @@ -858,6 +864,26 @@ static UniValue getblocktemplate_impl(const std::string &strMode, const UniValue
lastheader = blockHeaderHex;
}
}
if constexpr (nPoWType == CBlockHeader::SHA256D_BLOCK) { // if (pblock->IsSha256D()) {
std::string address = gArgs.GetArg("-miningaddress", "");
if (IsValidDestinationString(address)) {
static std::string lastheader = "";
if (mapSha256dTemplates.count(lastheader)) {
if (pblock->nTime - 60 < mapSha256dTemplates.at(lastheader).nTime) {
result.pushKV("sharpcheader", lastheader);
return result;
}
}

pblock->nNonce64 = 0;
CDataStream ssBlockHeader(SER_NETWORK, PROTOCOL_VERSION | RPCSerializationFlags());
ssBlockHeader << CSha256dInput(*pblock, pblock->GetSha256dMidstate());
std::string blockHeaderHex = HexStr(ssBlockHeader);
result.pushKV("sharpcheader", blockHeaderHex);
mapSha256dTemplates[blockHeaderHex] = *pblock;
lastheader = blockHeaderHex;
}
}

return result;
}
Expand Down Expand Up @@ -927,7 +953,7 @@ static UniValue getblocktemplate(const JSONRPCRequest& request)
" \"value\" (string) A way the block template may be changed, e.g. 'time', 'transactions', 'prevblock'\n"
" ,...\n"
" ],\n"
" \"noncerange\" : \"00000000ffffffff\",(string) A range of valid nonces\n"
" \"noncerange\" : \"00000000ffffffff\",(string) A range of valid nonces (sha256d templates return the 64 bit range 0000000000000000ffffffffffffffff)\n"
" \"sigoplimit\" : n, (numeric) limit of sigops in blocks\n"
" \"sizelimit\" : n, (numeric) limit of block size\n"
" \"weightlimit\" : n, (numeric) limit of block weight\n"
Expand All @@ -938,6 +964,7 @@ static UniValue getblocktemplate(const JSONRPCRequest& request)
" \"pprpcepoch\" : n (numeric) The epoch of the progpow pprpcheader given to user to be used by the local GPU miner\n"
" \"rxrpcheader\" : \"xxxx\" (string) The header that can be used by the local CPU miner to mine a randomx block (using -miningaddress) as the destination for the coinbase tx\n"
" \"rxrpcseed\" : \"xxxx\" (string) The seed hash of the randomx rxrpcheader given to user to be used by the local CPU miner\n"
" \"sharpcheader\" : \"xxxx\" (string) The 80 byte header that can be used by a sha256d miner to mine a sha256d block (using -miningaddress) as the destination for the coinbase tx. The last 8 bytes are the little endian 64 bit nonce to grind; submit with sharpcsb\n"
"}\n"

"\nExamples:\n"
Expand Down Expand Up @@ -1040,8 +1067,10 @@ static UniValue getblocktemplate(const JSONRPCRequest& request)
return getblocktemplate_impl<CBlockHeader::PROGPOW_BLOCK>(strMode, lpval, setClientRules, nMaxVersionPreVB);
case MINE_SHA256D:
return getblocktemplate_impl<CBlockHeader::SHA256D_BLOCK>(strMode, lpval, setClientRules, nMaxVersionPreVB);
default:
case MINE_RANDOMX:
return getblocktemplate_impl<CBlockHeader::RANDOMX_BLOCK>(strMode, lpval, setClientRules, nMaxVersionPreVB);
default:
throw JSONRPCError(RPC_INVALID_PARAMETER, "Unknown mining algorithm");
}
}

Expand Down Expand Up @@ -1243,6 +1272,93 @@ static UniValue rxrpcsb(const JSONRPCRequest& request) {
}
}

static UniValue sharpcsb(const JSONRPCRequest& request) {
if (request.fHelp || request.params.size() != 3) {
throw std::runtime_error(
"sharpcsb \"header\" \"sha_hash\" \"nonce\"\n"
"\nAttempts to submit new block to network mined by sha256d miner via rpc.\n"

"\nArguments\n"
"1. \"header\" (string, required) the sha256d header that was given to the miner from this rpc client\n"
"2. \"sha_hash\" (string, required) the sha256d hash that was mined by the miner via rpc\n"
"3. \"nonce\" (string, required) the 64 bit hex nonce of the block that hashed the valid block\n"
"\nResult:\n"
"\nExamples:\n"
+ HelpExampleCli("sharpcsb", "\"header\" \"sha_hash\" 100000")
+ HelpExampleRpc("sharpcsb", "\"header\" \"sha_hash\" 100000")
);
}

std::string header = request.params[0].get_str();
std::string str_sha_hash = request.params[1].get_str();
std::string str_nonce = request.params[2].get_str();

uint256 sha_hash = uint256S(str_sha_hash);
(void) sha_hash; // Currently unused here but the parameter is left there
// to keep the rpc method signature the same as pprpcsb

uint64_t nonce;
if (!ParseUInt64(str_nonce, &nonce, 16))
throw JSONRPCError(RPC_INVALID_PARAMS, "Invalid hex nonce");

if (!mapSha256dTemplates.count(header))
throw JSONRPCError(RPC_INVALID_PARAMS, "Block header not found in block data");

std::shared_ptr<CBlock> blockptr = std::make_shared<CBlock>();
*blockptr = mapSha256dTemplates.at(header);

blockptr->nNonce64 = nonce;

if (blockptr->vtx.empty() || !blockptr->vtx[0]->IsCoinBase()) {
throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "Block does not start with a coinbase");
}

if (!CheckProofOfWork(blockptr->GetSha256DPoWHash(), blockptr->nBits, Params().GetConsensus(),
CBlockHeader::SHA256D_BLOCK)) {
throw JSONRPCError(RPC_INVALID_REQUEST, "Block does not solve the boundary");
}

uint256 hash = blockptr->GetHash();
const CBlockIndex* pindex = LookupBlockIndex(hash);
if (pindex) {
if (pindex->IsValid(BLOCK_VALID_SCRIPTS)) {
return "duplicate";
}
if (pindex->nStatus & BLOCK_FAILED_MASK) {
return "duplicate-invalid";
}
}

pindex = LookupBlockIndex(blockptr->hashPrevBlock);
if (pindex) {
UpdateUncommittedBlockStructures(*blockptr, pindex, Params().GetConsensus());
}

bool new_block;
submitblock_StateCatcher sc(blockptr->GetHash());
RegisterValidationInterface(&sc);
bool accepted = ProcessNewBlock(Params(), blockptr, /* fForceProcessing */ true, /* fNewBlock */ &new_block);
UnregisterValidationInterface(&sc);
if (!new_block) {
if (!accepted) {
// TODO Maybe pass down fNewBlock to AcceptBlockHeader, so it is properly set to true in this case?
return "invalid";
}
return "duplicate";
}
if (!sc.found) {
return "inconclusive";
}
UniValue ret = BIP22ValidationResult(sc.state);

// BIP22ValidationResult set the return to null when the state is valid
if (ret.isNull()) {
return true;
} else {
return ret;
}
}

static UniValue submitblock(const JSONRPCRequest& request)
{
// We allow 2 arguments for compliance with BIP22. Argument 2 is ignored.
Expand Down Expand Up @@ -1484,6 +1600,7 @@ static const CRPCCommand commands[] =
{ "mining", "prioritisetransaction", &prioritisetransaction, {"txid","dummy","fee_delta"} },
{ "mining", "pprpcsb", &pprpcsb, {"header_hash", "mix_hash", "nonce"} },
{ "mining", "rxrpcsb", &rxrpcsb, {"header", "rx_hash", "nonce"} },
{ "mining", "sharpcsb", &sharpcsb, {"header", "sha_hash", "nonce"} },
{ "mining", "setminingalgo", &setminingalgo, {"algo"} },
{ "mining", "submitblock", &submitblock, {"hexdata","dummy"} },

Expand Down
90 changes: 90 additions & 0 deletions test/functional/mining_sha256d_rpc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/usr/bin/env python3
# Copyright (c) 2026 The Veil developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Test sha256d external mining via rpc

- getblocktemplate with algo sha256d returns sharpcheader work
- sharpcsb submits a solved nonce and the block is accepted and relayed
"""

from test_framework.messages import hash256, uint256_from_str
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import assert_equal, assert_raises_rpc_error, connect_nodes_bi

SHA256D_VERSION_BIT = 1 << 24


class Sha256dMiningRPCTest(BitcoinTestFramework):
def set_test_params(self):
self.num_nodes = 2
self.setup_clean_chain = True

def run_test(self):
node = self.nodes[0]

self.log.info("Restart node0 with -miningaddress so getblocktemplate hands out sha256d work")
addr = node.getnewbasecoinaddress()
self.restart_node(0, extra_args=["-miningaddress=" + addr])
connect_nodes_bi(self.nodes, 0, 1)

# Leave initial block download so getblocktemplate is willing to serve work
node.generate(1)
self.sync_all()

self.log.info("getblocktemplate algo=sha256d returns an 80 byte header and a 64 bit noncerange")
tmpl = node.getblocktemplate({"algo": "sha256d", "rules": ["segwit"]})
assert "sharpcheader" in tmpl
header_hex = tmpl["sharpcheader"]
assert_equal(len(header_hex), 160)
assert_equal(tmpl["noncerange"], "0000000000000000ffffffffffffffff")

header = bytes.fromhex(header_hex)
# The template is handed out with the 64 bit nonce zeroed
assert_equal(header[72:80], bytes(8))

self.log.info("Grind the 64 bit nonce over the header blob")
target = int(tmpl["target"], 16)
good_nonce = None
good_hash = None
bad_nonce = None
nonce = 0
while good_nonce is None or bad_nonce is None:
powhash = hash256(header[:72] + nonce.to_bytes(8, "little"))
if uint256_from_str(powhash) < target:
if good_nonce is None:
good_nonce = nonce
good_hash = powhash
elif bad_nonce is None:
bad_nonce = nonce
nonce += 1

self.log.info("sharpcsb rejects a nonce that does not solve the boundary")
assert_raises_rpc_error(-32600, "Block does not solve the boundary",
node.sharpcsb, header_hex, "00" * 32, "%016x" % bad_nonce)

self.log.info("sharpcsb rejects an unknown header")
assert_raises_rpc_error(-32602, "Block header not found in block data",
node.sharpcsb, "11" * 80, "00" * 32, "%016x" % good_nonce)

self.log.info("sharpcsb accepts the solved block")
height = node.getblockcount()
# sha_hash is signature parity with pprpcsb/rxrpcsb; the node ignores
# it, so a bogus value must still be accepted
result = node.sharpcsb(header_hex, "ff" * 32, "%016x" % good_nonce)
assert_equal(result, True)
assert_equal(node.getblockcount(), height + 1)

self.log.info("The sha256d block relays and carries the sha256d version bit")
self.sync_all()
tip = node.getbestblockhash()
assert_equal(self.nodes[1].getbestblockhash(), tip)
header_json = node.getblockheader(tip)
assert header_json["version"] & SHA256D_VERSION_BIT

self.log.info("A resubmit of the same block reports duplicate")
assert_equal(node.sharpcsb(header_hex, good_hash[::-1].hex(), "%016x" % good_nonce), "duplicate")


if __name__ == '__main__':
Sha256dMiningRPCTest().main()
8 changes: 4 additions & 4 deletions test/functional/test_framework/test_framework.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,8 @@ def main(self):

config = configparser.ConfigParser()
config.read_file(open(self.options.configfile))
self.options.bitcoind = os.getenv("BITCOIND", default=config["environment"]["BUILDDIR"] + '/src/bitcoind' + config["environment"]["EXEEXT"])
self.options.bitcoincli = os.getenv("BITCOINCLI", default=config["environment"]["BUILDDIR"] + '/src/bitcoin-cli' + config["environment"]["EXEEXT"])
self.options.bitcoind = os.getenv("BITCOIND", default=config["environment"]["BUILDDIR"] + '/src/veild' + config["environment"]["EXEEXT"])
self.options.bitcoincli = os.getenv("BITCOINCLI", default=config["environment"]["BUILDDIR"] + '/src/veil-cli' + config["environment"]["EXEEXT"])

os.environ['PATH'] = os.pathsep.join([
os.path.join(config['environment']['BUILDDIR'], 'src'),
Expand Down Expand Up @@ -267,7 +267,7 @@ def add_nodes(self, num_nodes, extra_args=None, *, rpchost=None, binary=None):
assert_equal(len(extra_args), num_nodes)
assert_equal(len(binary), num_nodes)
for i in range(num_nodes):
self.nodes.append(TestNode(i, get_datadir_path(self.options.tmpdir, i), rpchost=rpchost, timewait=self.rpc_timewait, bitcoind=binary[i], bitcoin_cli=self.options.bitcoincli, mocktime=self.mocktime, coverage_dir=self.options.coveragedir, extra_conf=extra_confs[i], extra_args=extra_args[i], use_cli=self.options.usecli))
self.nodes.append(TestNode(i, get_datadir_path(self.options.tmpdir, i), rpchost=rpchost, timewait=self.rpc_timewait, veild=binary[i], veil_cli=self.options.bitcoincli, mocktime=self.mocktime, coverage_dir=self.options.coveragedir, extra_conf=extra_confs[i], extra_args=extra_args[i], use_cli=self.options.usecli))

def start_node(self, i, *args, **kwargs):
"""Start a bitcoind"""
Expand Down Expand Up @@ -418,7 +418,7 @@ def _initialize_chain(self):
args = [self.options.bitcoind, "-datadir=" + datadir]
if i > 0:
args.append("-connect=127.0.0.1:" + str(p2p_port(0)))
self.nodes.append(TestNode(i, get_datadir_path(self.options.cachedir, i), extra_conf=["bind=127.0.0.1"], extra_args=[], rpchost=None, timewait=self.rpc_timewait, bitcoind=self.options.bitcoind, bitcoin_cli=self.options.bitcoincli, mocktime=self.mocktime, coverage_dir=None))
self.nodes.append(TestNode(i, get_datadir_path(self.options.cachedir, i), extra_conf=["bind=127.0.0.1"], extra_args=[], rpchost=None, timewait=self.rpc_timewait, veild=self.options.bitcoind, veil_cli=self.options.bitcoincli, mocktime=self.mocktime, coverage_dir=None))
self.nodes[i].args = args
self.start_node(i)

Expand Down
8 changes: 7 additions & 1 deletion test/functional/test_framework/test_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,9 @@ def __init__(self, i, datadir, *, rpchost, timewait, veild, veil_cli, mocktime,
"-debugexclude=libevent",
"-debugexclude=leveldb",
"-mocktime=" + str(mocktime),
"-uacomment=testnode%d" % i
"-uacomment=testnode%d" % i,
# Veil requires an explicit seed decision on new wallet creation
"-generateseed=1"
]

self.cli = TestNodeCLI(veil_cli, self.datadir)
Expand Down Expand Up @@ -197,6 +199,10 @@ def stop_node(self, expected_stderr=''):
# Check that stderr is as expected
self.stderr.seek(0)
stderr = self.stderr.read().decode('utf-8').strip()
# Veil prints a seed backup warning on first wallet creation
# (-generateseed=1); it is expected and not an error.
stderr = '\n'.join(l for l in stderr.splitlines()
if not l.startswith('WARNING BACKUP THESE WORDS')).strip()
if stderr != expected_stderr:
raise AssertionError("Unexpected stderr {} != {}".format(stderr, expected_stderr))

Expand Down
1 change: 1 addition & 0 deletions test/functional/test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@
'rpc_bind.py --ipv6',
'rpc_bind.py --nonloopback',
'mining_basic.py',
'mining_sha256d_rpc.py',
'wallet_bumpfee.py',
'rpc_named_arguments.py',
'wallet_listsinceblock.py',
Expand Down