Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F14864542
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
37 KB
Subscribers
None
View Options
diff --git a/chronik/chronik-cpp/chronik.cpp b/chronik/chronik-cpp/chronik.cpp
index 3f9f3856f..934131387 100644
--- a/chronik/chronik-cpp/chronik.cpp
+++ b/chronik/chronik-cpp/chronik.cpp
@@ -1,126 +1,137 @@
// Copyright (c) 2022 The Bitcoin developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include <chainparams.h>
#include <chainparamsbase.h>
#include <chrono>
#include <common/args.h>
#include <config.h>
#include <logging.h>
#include <node/context.h>
#include <node/ui_interface.h>
#include <util/result.h>
#include <util/time.h>
#include <util/translation.h>
#include <chronik-cpp/chronik.h>
#include <chronik-cpp/chronik_validationinterface.h>
#include <chronik_lib/src/ffi.rs.h>
+#include <tinyformat.h>
+
namespace chronik {
// Duration between WebSocket pings initiated by Chronik.
// 45s has been empirically established as a reliable duration for both browser
// and NodeJS WebSockets.
static constexpr std::chrono::seconds WS_PING_INTERVAL_DEFAULT{45s};
// Ping duration is just 5s on regtest to speed up ping tests and make
// functional tests more reliable.
static constexpr std::chrono::seconds WS_PING_INTERVAL_REGTEST{5s};
template <typename T, typename C> rust::Vec<T> ToRustVec(const C &container) {
rust::Vec<T> vec;
vec.reserve(container.size());
std::copy(container.begin(), container.end(), std::back_inserter(vec));
return vec;
}
chronik_bridge::Net ParseNet(const std::string &net_str) {
if (net_str == CBaseChainParams::MAIN) {
return chronik_bridge::Net::Mainnet;
} else if (net_str == CBaseChainParams::TESTNET) {
return chronik_bridge::Net::Testnet;
} else if (net_str == CBaseChainParams::REGTEST) {
return chronik_bridge::Net::Regtest;
}
throw std::runtime_error("Unknown net string");
}
util::Result<chronik_bridge::SetupParams>
ParseChronikParams(const ArgsManager &args, const Config &config, bool fWipe) {
const bool is_pause_allowed = args.GetBoolArg("-chronikallowpause", false);
const CChainParams ¶ms = config.GetChainParams();
if (is_pause_allowed && !params.IsTestChain()) {
return {{_("Using -chronikallowpause on a mainnet chain is not allowed "
"for security reasons.")}};
}
const auto electrum_hosts = args.GetArgs("-chronikelectrumbind");
const bool is_scripthashindex_enabled =
args.GetBoolArg("-chronikscripthashindex", false);
if (!electrum_hosts.empty()) {
if (args.IsArgSet("-chronikelectrumcert") ^
args.IsArgSet("-chronikelectrumprivkey")) {
return {{_("The -chronikelectrumcert and -chronikelectrumprivkey "
"options should both be set or unset.")}};
}
if (!is_scripthashindex_enabled) {
return {{_("The -chronikelectrumbind option requires "
"-chronikscripthashindex to be true.")}};
}
}
+ const int64_t electrum_max_history = args.GetIntArg(
+ "-chronikelectrummaxhistory", DEFAULT_ELECTRUM_MAX_HISTORY);
+ if (electrum_max_history < 1 ||
+ electrum_max_history > std::numeric_limits<uint32_t>::max()) {
+ return {{_(strprintf("The -chronikelectrummaxhistory value should be "
+ "withing the range [1, %d].",
+ std::numeric_limits<uint32_t>::max())
+ .c_str())}};
+ }
+
return {{
.net = ParseNet(params.NetworkIDString()),
.datadir = args.GetDataDirBase().u8string(),
.datadir_net = args.GetDataDirNet().u8string(),
.hosts = ToRustVec<rust::String>(args.IsArgSet("-chronikbind")
? args.GetArgs("-chronikbind")
: DEFAULT_BINDS),
.default_port = BaseParams().ChronikPort(),
.wipe_db = fWipe,
.enable_token_index = args.GetBoolArg("-chroniktokenindex", true),
.enable_lokad_id_index = args.GetBoolArg("-chroniklokadidindex", true),
.enable_scripthash_index = is_scripthashindex_enabled,
.is_pause_allowed = is_pause_allowed,
.enable_perf_stats = args.GetBoolArg("-chronikperfstats", false),
.ws_ping_interval_secs =
params.NetworkIDString() == CBaseChainParams::REGTEST
? uint64_t(count_seconds(WS_PING_INTERVAL_REGTEST))
: uint64_t(count_seconds(WS_PING_INTERVAL_DEFAULT)),
.enable_cors = args.GetBoolArg("-chronikcors", false),
.tx_num_cache =
{
.num_buckets = (size_t)args.GetIntArg(
"-chroniktxnumcachebuckets", DEFAULT_TX_NUM_CACHE_BUCKETS),
.bucket_size =
(size_t)args.GetIntArg("-chroniktxnumcachebucketsize",
DEFAULT_TX_NUM_CACHE_BUCKET_SIZE),
},
.electrum_hosts = ToRustVec<rust::String>(electrum_hosts),
.electrum_default_port = BaseParams().ChronikElectrumPort(),
.electrum_default_protocol = 't',
.electrum_cert_path = args.GetArg("-chronikelectrumcert", ""),
.electrum_privkey_path = args.GetArg("-chronikelectrumprivkey", ""),
- .electrum_max_history = args.GetIntArg("-chronikelectrummaxhistory",
- DEFAULT_ELECTRUM_MAX_HISTORY),
+ .electrum_max_history = static_cast<uint32_t>(electrum_max_history),
}};
}
bool Start(const ArgsManager &args, const Config &config,
const node::NodeContext &node, bool fWipe) {
util::Result<chronik_bridge::SetupParams> params =
ParseChronikParams(args, config, fWipe);
if (!params) {
return InitError(ErrorString(params));
}
return chronik_bridge::setup_chronik(*params, node);
}
void Stop() {
LogPrintf("Stopping Chronik...\n");
StopChronikValidationInterface();
}
} // namespace chronik
diff --git a/test/functional/chronik_electrum_blockchain.py b/test/functional/chronik_electrum_blockchain.py
index ac372f9e0..294531809 100644
--- a/test/functional/chronik_electrum_blockchain.py
+++ b/test/functional/chronik_electrum_blockchain.py
@@ -1,814 +1,832 @@
# Copyright (c) 2024-present The Bitcoin developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""
Test Chronik's electrum interface: blockchain.* methods
"""
from test_framework.address import ADDRESS_ECREG_UNSPENDABLE
from test_framework.blocktools import (
GENESIS_BLOCK_HASH,
GENESIS_CB_SCRIPT_PUBKEY,
GENESIS_CB_SCRIPT_SIG,
GENESIS_CB_TXID,
TIME_GENESIS_BLOCK,
)
from test_framework.hash import hex_be_sha256
from test_framework.merkle import merkle_root_and_branch
from test_framework.messages import (
XEC,
COutPoint,
CTransaction,
CTxIn,
CTxOut,
FromHex,
ToHex,
)
from test_framework.script import OP_RETURN, OP_TRUE, CScript
from test_framework.test_framework import BitcoinTestFramework
from test_framework.txtools import pad_tx
from test_framework.util import assert_equal, chronikelectrum_port, hex_to_be_bytes
from test_framework.wallet import MiniWallet
COINBASE_TX_HEX = (
"01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d"
+ GENESIS_CB_SCRIPT_SIG.hex()
+ "ffffffff0100f2052a0100000043"
+ GENESIS_CB_SCRIPT_PUBKEY.hex()
+ "00000000"
)
max_int32 = 2**31 - 1
max_int64 = 2**63 - 1
class ChronikElectrumBlockchain(BitcoinTestFramework):
def set_test_params(self):
self.num_nodes = 1
self.extra_args = [
[
"-chronik",
f"-chronikelectrumbind=127.0.0.1:{chronikelectrum_port(0)}",
"-chronikscripthashindex=1",
]
]
def skip_test_if_missing_module(self):
self.skip_if_no_chronik()
def run_test(self):
self.client = self.nodes[0].get_chronik_electrum_client()
self.node = self.nodes[0]
self.wallet = MiniWallet(self.node)
self.test_invalid_params()
self.test_transaction_get()
self.test_transaction_get_height()
self.test_transaction_broadcast()
self.test_transaction_get_merkle()
self.test_block_header()
self.test_scripthash()
def test_invalid_params(self):
# Invalid params type
for response in (
self.client.synchronous_request("blockchain.transaction.get", params="foo"),
self.client.synchronous_request("blockchain.transaction.get", params=42),
):
assert_equal(
response.error,
{"code": -32602, "message": "'params' must be an array or an object"},
)
assert_equal(
self.client.synchronous_request(
"blockchain.transaction.get", params=None
).error,
{"code": -32602, "message": "Missing required params"},
)
# Too many params
for response in (
self.client.blockchain.transaction.get(1, 2, 3),
self.client.blockchain.transaction.get(txid=1, verbose=2, blockhash=3),
):
assert_equal(
response.error,
{"code": -32602, "message": "Expected at most 2 parameters"},
)
assert_equal(
self.client.blockchain.transaction.get_height(1, 2).error,
{"code": -32602, "message": "Expected at most 1 parameter"},
)
# Missing mandatory argument in otherwise valid params
for response in (
self.client.synchronous_request("blockchain.transaction.get", params=[]),
self.client.synchronous_request("blockchain.transaction.get", params={}),
self.client.synchronous_request(
"blockchain.transaction.get",
params={"nottxid": 32 * "ff", "verbose": False},
),
self.client.blockchain.transaction.get(verbose=True),
):
assert_equal(
response.error,
{"code": -32602, "message": "Missing mandatory 'txid' parameter"},
)
# Non-string json type for txid
assert_equal(
self.client.blockchain.transaction.get(txid=int(32 * "ff", 16)).error,
{"code": 1, "message": "Invalid tx hash"},
)
for response in (
# non-hex characters
self.client.blockchain.transaction.get("les sanglots longs"),
# odd number of hex chars
self.client.blockchain.transaction.get(GENESIS_CB_TXID[:-1]),
# valid hex but invalid length for a txid
self.client.blockchain.transaction.get(GENESIS_CB_TXID[:-2]),
):
assert_equal(
response.error,
{"code": 1, "message": "Invalid tx hash"},
)
# Invalid type for boolean argument
assert_equal(
self.client.blockchain.transaction.get(
txid=32 * "ff", verbose="true"
).error,
{
"code": 1,
"message": "Invalid verbose argument; expected boolean",
},
)
# Valid txid, but no such transaction was found
assert_equal(
self.client.blockchain.transaction.get(txid=32 * "ff").error,
{
"code": 1,
"message": "No transaction matching the requested hash was found",
},
)
def test_transaction_get(self):
for response in (
self.client.blockchain.transaction.get(GENESIS_CB_TXID),
self.client.blockchain.transaction.get(GENESIS_CB_TXID, False),
self.client.blockchain.transaction.get(txid=GENESIS_CB_TXID),
self.client.blockchain.transaction.get(txid=GENESIS_CB_TXID, verbose=False),
):
assert_equal(response.result, COINBASE_TX_HEX)
for response in (
self.client.blockchain.transaction.get(GENESIS_CB_TXID, True),
self.client.blockchain.transaction.get(txid=GENESIS_CB_TXID, verbose=True),
):
assert_equal(
response.result,
{
"blockhash": GENESIS_BLOCK_HASH,
"blocktime": TIME_GENESIS_BLOCK,
"confirmations": 201,
"hash": GENESIS_CB_TXID,
"hex": COINBASE_TX_HEX,
"time": 0,
},
)
self.generate(self.wallet, 2)
assert_equal(
self.client.blockchain.transaction.get(
txid=GENESIS_CB_TXID, verbose=True
).result["confirmations"],
203,
)
def test_transaction_get_height(self):
response = self.client.blockchain.transaction.get_height(GENESIS_CB_TXID)
assert_equal(response.result, 0)
self.wallet.rescan_utxos()
tx = self.wallet.create_self_transfer()
response = self.client.blockchain.transaction.broadcast(tx["hex"])
assert_equal(response.result, tx["txid"])
self.node.syncwithvalidationinterfacequeue()
response = self.client.blockchain.transaction.get(tx["txid"])
assert_equal(response.result, tx["hex"])
# A mempool transaction has height 0
response = self.client.blockchain.transaction.get_height(tx["txid"])
assert_equal(response.result, 0)
# Mine the tx
self.generate(self.wallet, 1)
response = self.client.blockchain.transaction.get_height(tx["txid"])
assert_equal(response.result, 203)
response = self.client.blockchain.transaction.get_height(32 * "ff")
assert_equal(response.error, {"code": -32600, "message": "Unknown txid"})
def test_transaction_broadcast(self):
tx = self.wallet.create_self_transfer()
for _ in range(3):
response = self.client.blockchain.transaction.broadcast(tx["hex"])
assert_equal(response.result, tx["txid"])
self.generate(self.wallet, 1)
response = self.client.blockchain.transaction.broadcast(tx["hex"])
assert_equal(
response.error, {"code": 1, "message": "Transaction already in block chain"}
)
spent_utxo = tx["tx"].vin[0]
tx_obj = self.wallet.create_self_transfer()["tx"]
tx_obj.vin[0] = spent_utxo
response = self.client.blockchain.transaction.broadcast(ToHex(tx_obj))
assert_equal(
response.error,
{"code": 1, "message": "Missing inputs: bad-txns-inputs-missingorspent"},
)
raw_tx_reference = self.wallet.create_self_transfer()["hex"]
tx_obj = FromHex(CTransaction(), raw_tx_reference)
tx_obj.vin[0].scriptSig = b"aaaaaaaaaaaaaaa"
response = self.client.blockchain.transaction.broadcast(ToHex(tx_obj))
assert_equal(
response.error,
{
"code": 1,
"message": "Transaction rejected by mempool: scriptsig-not-pushonly",
},
)
tx_obj = FromHex(CTransaction(), raw_tx_reference)
tx_obj.vout[0].scriptPubKey = CScript([OP_RETURN, b"\xff"])
tx_obj.vout = [tx_obj.vout[0]] * 2
response = self.client.blockchain.transaction.broadcast(ToHex(tx_obj))
assert_equal(
response.error,
{"code": 1, "message": "Transaction rejected by mempool: multi-op-return"},
)
tx_obj = FromHex(CTransaction(), raw_tx_reference)
tx_obj.vin[0].nSequence = 0xFFFFFFFE
tx_obj.nLockTime = self.node.getblockcount() + 1
response = self.client.blockchain.transaction.broadcast(ToHex(tx_obj))
assert_equal(
response.error,
{
"code": 1,
"message": "Transaction rejected by mempool: bad-txns-nonfinal, non-final transaction",
},
)
tx_obj = FromHex(CTransaction(), raw_tx_reference)
tx_obj.vout = []
response = self.client.blockchain.transaction.broadcast(ToHex(tx_obj))
assert_equal(
response.error,
{
"code": 1,
"message": "Transaction rejected by mempool: bad-txns-vout-empty",
},
)
# Non-standard script
tx_obj.vout.append(CTxOut(0, CScript([OP_TRUE])))
response = self.client.blockchain.transaction.broadcast(ToHex(tx_obj))
assert_equal(
response.error,
{"code": 1, "message": "Transaction rejected by mempool: scriptpubkey"},
)
tx_obj.vout[0] = CTxOut(0, CScript([OP_RETURN, b"\xff"]))
assert len(ToHex(tx_obj)) // 2 < 100
response = self.client.blockchain.transaction.broadcast(ToHex(tx_obj))
assert_equal(
response.error,
{
"code": 1,
"message": "Transaction rejected by mempool: bad-txns-undersize",
},
)
tx_obj = self.wallet.create_self_transfer()["tx"]
pad_tx(tx_obj, 100_001)
response = self.client.blockchain.transaction.broadcast(ToHex(tx_obj))
assert_equal(
response.error,
{"code": 1, "message": "Transaction rejected by mempool: tx-size"},
)
tx_obj = FromHex(CTransaction(), raw_tx_reference)
tx_obj.vin.append(tx_obj.vin[0])
response = self.client.blockchain.transaction.broadcast(ToHex(tx_obj))
assert_equal(
response.error,
{
"code": 1,
"message": "Transaction rejected by mempool: bad-txns-inputs-duplicate",
},
)
tx_obj.vin = []
response = self.client.blockchain.transaction.broadcast(ToHex(tx_obj))
assert_equal(
response.error,
{
"code": 1,
"message": "Transaction rejected by mempool: bad-txns-vin-empty",
},
)
tx_obj = FromHex(CTransaction(), raw_tx_reference)
tx_obj.nVersion = 1337
response = self.client.blockchain.transaction.broadcast(ToHex(tx_obj))
assert_equal(
response.error,
{"code": 1, "message": "Transaction rejected by mempool: version"},
)
# Coinbase input in first position
tx_obj = FromHex(CTransaction(), raw_tx_reference)
tx_obj.vin[0] = CTxIn(COutPoint(txid=0, n=0xFFFFFFFF))
response = self.client.blockchain.transaction.broadcast(ToHex(tx_obj))
assert_equal(
response.error,
{"code": 1, "message": "Transaction rejected by mempool: bad-tx-coinbase"},
)
# Coinbase input in second position
tx_obj = FromHex(CTransaction(), raw_tx_reference)
tx_obj.vin.append(CTxIn(COutPoint(txid=0, n=0xFFFFFFFF)))
response = self.client.blockchain.transaction.broadcast(ToHex(tx_obj))
assert_equal(
response.error,
{
"code": 1,
"message": "Transaction rejected by mempool: bad-txns-prevout-null",
},
)
tx = self.wallet.create_self_transfer(fee_rate=0, fee=0)
response = self.client.blockchain.transaction.broadcast(tx["hex"])
assert_equal(
response.error,
{
"code": 1,
"message": "Transaction rejected by mempool: min relay fee not met, 0 < 100",
},
)
tx = self.wallet.create_self_transfer(fee_rate=10_000_000, fee=0)
response = self.client.blockchain.transaction.broadcast(tx["hex"])
assert_equal(
response.error,
{
"code": 1,
"message": "Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)",
},
)
# Mine enough blocks to ensure that the following test does not try to spend
# a utxo already spent in a previous test.
# Invalidate two blocks, so that miniwallet has access to a coin that
# will mature in the next block.
self.generate(self.wallet, 100)
chain_height = self.node.getblockcount() - 3
block_to_invalidate = self.node.getblockhash(chain_height + 1)
self.node.invalidateblock(block_to_invalidate)
immature_txid = self.nodes[0].getblock(
self.nodes[0].getblockhash(chain_height - 100 + 2)
)["tx"][0]
immature_utxo = self.wallet.get_utxo(txid=immature_txid)
tx = self.wallet.create_self_transfer(utxo_to_spend=immature_utxo)
response = self.client.blockchain.transaction.broadcast(tx["hex"])
assert_equal(
response.error,
{
"code": 1,
"message": "Transaction rejected by mempool: bad-txns-premature-spend-of-coinbase, tried to spend coinbase at depth 99",
},
)
def test_transaction_get_merkle(self):
for _ in range(42):
self.wallet.send_self_transfer(from_node=self.node)
block_hash = self.generate(self.node, 1)[0]
block_info = self.node.getblock(block_hash)
height = block_info["height"]
txids_hex = block_info["tx"]
txids = [hex_to_be_bytes(txid) for txid in txids_hex]
for i in range(len(txids)):
_root, branch = merkle_root_and_branch(txids, i)
assert_equal(
self.client.blockchain.transaction.get_merkle(txids_hex[i]).result,
{
"block_height": height,
"merkle": [h[::-1].hex() for h in branch],
"pos": i,
},
)
# We can optionally specify the correct block height as 2nd argument
assert_equal(
self.client.blockchain.transaction.get_merkle(
txid=txids_hex[-1], height=height
).result,
{
"block_height": height,
"merkle": [h[::-1].hex() for h in branch],
"pos": len(txids) - 1,
},
)
assert_equal(
self.client.blockchain.transaction.get_merkle(32 * "ff").error,
{
"code": 1,
"message": "No confirmed transaction matching the requested hash was found",
},
)
for wrong_height in (1, height - 1, height + 1, max_int32):
assert_equal(
self.client.blockchain.transaction.get_merkle(
txids_hex[-1], wrong_height
).error,
{
"code": 1,
"message": f"No transaction matching the requested hash found at height {wrong_height}",
},
)
for invalid_height in (-1, max_int32 + 1, max_int64, max_int64 + 1):
assert_equal(
self.client.blockchain.transaction.get_merkle(
txids_hex[-1], invalid_height
).error,
{
"code": 1,
"message": "Invalid height argument; expected non-negative numeric value",
},
)
def test_block_header(self):
block_hashes = [
self.node.getblockhash(i) for i in range(self.node.getblockcount() + 1)
]
block_hashes_bytes = [hex_to_be_bytes(bh) for bh in block_hashes]
headers = [self.node.getblockheader(bh, False) for bh in block_hashes]
tip_height = len(headers) - 1
self.log.info("Testing the blockchain.block.header RPC")
response = self.client.blockchain.block.header(0)
assert_equal(response.result, headers[0])
response = self.client.blockchain.block.header(
len(block_hashes) // 2, tip_height
)
root, branch = merkle_root_and_branch(
block_hashes_bytes, len(block_hashes) // 2
)
assert_equal(
response.result,
{
"branch": [h[::-1].hex() for h in branch],
"header": headers[len(block_hashes) // 2],
"root": root[::-1].hex(),
},
)
for bh in ("toto", -1, max_int32 + 1, max_int64, max_int64 + 1):
for rpc_call in (
lambda h: self.client.blockchain.block.header(h),
lambda h: self.client.blockchain.block.headers(start_height=h, count=0),
):
assert_equal(
rpc_call(bh).error,
{
"code": 1,
"message": "Invalid height",
},
)
for cp_height in ("toto", -1, max_int32 + 1, max_int64, max_int64 + 1):
for rpc_call in (
lambda h: self.client.blockchain.block.header(0, h),
lambda h: self.client.blockchain.block.headers(0, 10, h),
):
assert_equal(
rpc_call(cp_height).error,
{
"code": 1,
"message": "Invalid cp_height",
},
)
for bh in (max_int32, tip_height + 1):
assert_equal(
self.client.blockchain.block.header(bh).error,
{
"code": 1,
"message": f"Height {bh} is out of range",
},
)
assert_equal(
self.client.blockchain.block.header(2, 1).error,
{
"code": 1,
"message": f"header height 2 must be <= cp_height 1 which must be <= chain height {tip_height}",
},
)
self.log.info("Testing the blockchain.block.headers RPC")
# Fulcrum basically just ignores the other parameters when count = 0,
# unless they reach a much higher limit than tip_height.
for start_height in (0, 5, max_int32):
# Note that Fulcrum has a lower hard limit than max int32 start_height
# before returning a RPC error: Storage::MAX_HEADERS = 100'000'000.
# So it is a minor difference in behavior to not error in such a case for
# 100'000'000 < start_height <= 2**31
count = 0
assert_equal(
self.client.blockchain.block.headers(start_height, count).result,
{"count": 0, "hex": "", "max": 2016},
)
for bh in range(0, tip_height + 1):
assert_equal(
self.client.blockchain.block.headers(start_height=bh, count=1).result,
{"count": 1, "hex": headers[bh], "max": 2016},
)
start_height = 5
count = 6
assert_equal(
self.client.blockchain.block.headers(start_height, count).result,
{
"count": 6,
"hex": "".join(headers[start_height : start_height + count]),
"max": 2016,
},
)
cp_height = 21
root, branch = merkle_root_and_branch(
block_hashes_bytes[: cp_height + 1], start_height + count - 1
)
assert_equal(
self.client.blockchain.block.headers(start_height, count, cp_height).result,
{
"branch": [h[::-1].hex() for h in branch],
"count": 6,
"hex": "".join(headers[start_height : start_height + count]),
"max": 2016,
"root": root[::-1].hex(),
},
)
# The RPC may return less than {count} headers if the chain is not long enough
start_height = 4
for excessive_count in (tip_height - start_height + 2, max_int32):
response = self.client.blockchain.block.headers(
start_height, excessive_count
)
assert_equal(
response.result,
{
"count": tip_height - start_height + 1,
"hex": "".join(headers[start_height:]),
"max": 2016,
},
)
for count in ("toto", -1, max_int32 + 1, max_int64, max_int64 + 1):
assert_equal(
self.client.blockchain.block.headers(0, count).error,
{
"code": 1,
"message": "Invalid count",
},
)
for cp_height in (1, 8, tip_height + 1, max_int32):
assert_equal(
self.client.blockchain.block.headers(0, 10, cp_height).error,
{
"code": 1,
"message": (
f"header height + (count - 1) 9 must be <= cp_height {cp_height} "
f"which must be <= chain height {tip_height}"
),
},
)
def test_scripthash(self):
for invalid_scripthash in (31 * "ff", 31 * "ff" + "f", 42, False, "spam"):
assert_equal(
self.client.blockchain.scripthash.get_balance(invalid_scripthash).error,
{
"code": 1,
"message": "Invalid scripthash",
},
)
assert_equal(
self.client.blockchain.scripthash.get_history(invalid_scripthash).error,
{
"code": 1,
"message": "Invalid scripthash",
},
)
assert_equal(
self.client.blockchain.scripthash.listunspent(invalid_scripthash).error,
{
"code": 1,
"message": "Invalid scripthash",
},
)
# valid hash, but not associated with any known script
assert_equal(
self.client.blockchain.scripthash.get_balance(32 * "ff").result,
{
"confirmed": 0,
"unconfirmed": 0,
},
)
assert_equal(
self.client.blockchain.scripthash.get_history(32 * "ff").result,
[],
)
assert_equal(
self.client.blockchain.scripthash.listunspent(32 * "ff").result,
[],
)
# Mine a block just to be sure all the utxos are confirmed
self.generate(self.wallet, 1)
value = sum(
[
utxo["value"]
for utxo in self.wallet.get_utxos(
include_immature_coinbase=True, mark_as_spent=False
)
]
)
scripthash = hex_be_sha256(self.wallet.get_scriptPubKey())
assert_equal(
self.client.blockchain.scripthash.get_balance(scripthash).result,
{
"confirmed": value * XEC,
"unconfirmed": 0,
},
)
tx = self.wallet.send_self_transfer(from_node=self.node)
assert_equal(
self.client.blockchain.scripthash.get_balance(scripthash).result,
{"confirmed": value * XEC, "unconfirmed": -tx["fee"] * XEC},
)
self.generatetoaddress(self.node, 1, ADDRESS_ECREG_UNSPENDABLE)
assert_equal(
self.client.blockchain.scripthash.get_balance(scripthash).result,
{
"confirmed": (value - tx["fee"]) * XEC,
"unconfirmed": 0,
},
)
# Send transactions to a previously unused script
script = CScript(
bytes.fromhex("76a91462e907b15cbf27d5425399ebf6f0fb50ebb88f1888ac")
)
scripthash = hex_be_sha256(script)
confirmed = 0
unconfirmed = 0
history = []
utxos = []
def utxo_sorting_key(utxo):
return utxo["tx_hash"], utxo["tx_pos"]
def assert_scripthash_balance_and_history(check_sorting=True):
assert_equal(
self.client.blockchain.scripthash.get_balance(scripthash).result,
{
"confirmed": confirmed,
"unconfirmed": unconfirmed,
},
)
actual_history = self.client.blockchain.scripthash.get_history(
scripthash
).result
expected_history = history
actual_utxos = self.client.blockchain.scripthash.listunspent(
scripthash
).result
expected_utxos = utxos
if not check_sorting:
# Enforce any unique arbitrary sorting so we can compare equality
# between the two lists.
def sorting_key(hist_item):
return hist_item["tx_hash"]
actual_history = sorted(actual_history, key=sorting_key)
expected_history = sorted(expected_history, key=sorting_key)
actual_utxos = sorted(actual_utxos, key=utxo_sorting_key)
expected_utxos = sorted(expected_utxos, key=utxo_sorting_key)
assert_equal(actual_history, expected_history)
assert_equal(actual_utxos, expected_utxos)
assert_scripthash_balance_and_history()
def add_unconfirmed_transaction(amount: int, fee: int) -> tuple[str, int]:
nonlocal unconfirmed
nonlocal history
nonlocal utxos
txid, n = self.wallet.send_to(
from_node=self.node, scriptPubKey=script, amount=amount, fee=fee
)
unconfirmed += amount
history.append({"fee": fee, "height": 0, "tx_hash": txid})
utxos.append({"height": 0, "tx_hash": txid, "tx_pos": n, "value": amount})
return txid, n
for _ in range(4):
txid, n = add_unconfirmed_transaction(amount=1337, fee=1000)
assert_scripthash_balance_and_history()
# Confirm the transaction
self.generatetoaddress(self.node, 1, ADDRESS_ECREG_UNSPENDABLE)
confirmed += 1337
unconfirmed -= 1337
h = self.node.getblockcount()
history.pop()
history.append({"height": h, "tx_hash": txid})
utxos.pop()
utxos.append({"height": h, "tx_hash": txid, "tx_pos": n, "value": 1337})
assert_scripthash_balance_and_history()
# History with multiple unconfirmed transactions
for _ in range(3):
add_unconfirmed_transaction(amount=888, fee=999)
# We cannot guarantee the sorting of unconfirmed transactions
assert_scripthash_balance_and_history(check_sorting=False)
# Test an excessive transaction history
history_len = len(
self.client.blockchain.scripthash.get_history(scripthash).result
)
- self.restart_node(
+
+ self.stop_node(0)
+
+ self.nodes[0].assert_start_raises_init_error(
+ extra_args=self.extra_args[0] + ["-chronikelectrummaxhistory=-1"],
+ expected_msg="Error: The -chronikelectrummaxhistory value should be withing the range [1, 4294967295].",
+ )
+
+ self.nodes[0].assert_start_raises_init_error(
+ extra_args=self.extra_args[0] + ["-chronikelectrummaxhistory=0"],
+ expected_msg="Error: The -chronikelectrummaxhistory value should be withing the range [1, 4294967295].",
+ )
+
+ self.nodes[0].assert_start_raises_init_error(
+ extra_args=self.extra_args[0] + ["-chronikelectrummaxhistory=4294967296"],
+ expected_msg="Error: The -chronikelectrummaxhistory value should be withing the range [1, 4294967295].",
+ )
+
+ self.start_node(
0,
extra_args=self.extra_args[0]
+ [f"-chronikelectrummaxhistory={history_len + 1}"],
)
self.client = self.nodes[0].get_chronik_electrum_client()
# We can add one more transaction
add_unconfirmed_transaction(amount=777, fee=998)
assert_scripthash_balance_and_history(check_sorting=False)
# The next transaction makes the tx history too long.
add_unconfirmed_transaction(amount=777, fee=998)
msg = f"transaction history for scripthash {scripthash} exceeds limit ({history_len + 1})"
assert_equal(
self.client.blockchain.scripthash.get_history(scripthash).error,
{
"code": 1,
"message": msg,
},
)
# We compute the balance on demand, so this RPC is also limited by the max
# history parameter.
assert_equal(
self.client.blockchain.scripthash.get_balance(scripthash).error,
{
"code": 1,
"message": msg,
},
)
# But the listunspent RPC is unaffected.
assert_equal(
sorted(
self.client.blockchain.scripthash.listunspent(scripthash).result,
key=utxo_sorting_key,
),
sorted(utxos, key=utxo_sorting_key),
)
if __name__ == "__main__":
ChronikElectrumBlockchain().main()
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Wed, May 21, 20:27 (1 d, 9 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5865876
Default Alt Text
(37 KB)
Attached To
rABC Bitcoin ABC
Event Timeline
Log In to Comment