Page MenuHomePhabricator

No OneTemporary

diff --git a/chronik/chronik-cpp/chronik.cpp b/chronik/chronik-cpp/chronik.cpp
index 8ec774683..3f9f3856f 100644
--- a/chronik/chronik-cpp/chronik.cpp
+++ b/chronik/chronik-cpp/chronik.cpp
@@ -1,124 +1,126 @@
// 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>
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 &params = 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.")}};
}
}
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),
}};
}
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/chronik/chronik-cpp/chronik.h b/chronik/chronik-cpp/chronik.h
index 1dcb02422..24c9355ed 100644
--- a/chronik/chronik-cpp/chronik.h
+++ b/chronik/chronik-cpp/chronik.h
@@ -1,39 +1,41 @@
// 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.
#ifndef BITCOIN_CHRONIK_CPP_CHRONIK_H
#define BITCOIN_CHRONIK_CPP_CHRONIK_H
#include <string>
#include <vector>
class Config;
namespace node {
struct NodeContext;
} // namespace node
namespace chronik {
static const std::vector<std::string> DEFAULT_BINDS = {"127.0.0.1", "::1"};
// How many buckets of txid -> tx num maps are cached in-memory.
// Don't set this too high, Chronik will do a linear scan over all buckets.
static const size_t DEFAULT_TX_NUM_CACHE_BUCKETS = 10;
// Size of each bucket in the in-memory cache of tx nums.
// Unlike the number of buckets, this may be increased without much danger of
// slowing the indexer down. The total cache size will be
// `num_buckets * bucket_size * 40B`, so by default the cache will require 40MB
// of memory.
static const size_t DEFAULT_TX_NUM_CACHE_BUCKET_SIZE = 100'000;
+static const uint32_t DEFAULT_ELECTRUM_MAX_HISTORY{200'000};
+
// Registers Chronik indexer as ValidationInterface, listens to HTTP queries
bool Start(const ArgsManager &args, const Config &config,
const node::NodeContext &node, bool fWipe);
// Unregisters Chronik indexer as ValidationInterface, stops the HTTP server
void Stop();
} // namespace chronik
#endif // BITCOIN_CHRONIK_CPP_CHRONIK_H
diff --git a/chronik/chronik-http/src/electrum.rs b/chronik/chronik-http/src/electrum.rs
index cd08d14f4..1a7005bcb 100644
--- a/chronik/chronik-http/src/electrum.rs
+++ b/chronik/chronik-http/src/electrum.rs
@@ -1,1013 +1,1015 @@
// Copyright (c) 2024 The Bitcoin developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
//! Module for [`ChronikElectrumServer`].
use std::{cmp, net::SocketAddr, sync::Arc};
use abc_rust_error::Result;
use bitcoinsuite_core::{
hash::{Hashed, Sha256, Sha256d},
tx::TxId,
};
use bytes::Bytes;
use chronik_bridge::ffi;
use chronik_db::group::GroupMember;
use chronik_indexer::{merkle::MerkleTree, query::MAX_HISTORY_PAGE_SIZE};
use chronik_proto::proto::{Tx, TxHistoryPage};
use futures::future;
use itertools::izip;
use karyon_jsonrpc::{
error::RPCError,
net::{Addr, Endpoint},
rpc_impl, rpc_method,
server::ServerBuilder,
};
use rustls::pki_types::{
pem::PemObject,
{CertificateDer, PrivateKeyDer},
};
use serde_json::{json, Value};
use thiserror::Error;
use versions::Versioning;
use crate::{
server::{ChronikIndexerRef, NodeRef},
{electrum::ChronikElectrumServerError::*, electrum_codec::ElectrumCodec},
};
-/// Largest tx history we a willing to serve.
-/// Electrum responses are not paginated, so the protocol is limited when
-/// it comes to scripts with an unreasonable number of transactions.
-/// TODO: make this an init param
-pub const MAX_HISTORY: usize = 200_000;
-
/// Protocol version implemented by this server
pub const ELECTRUM_PROTOCOL_VERSION: &str = "1.4";
/// Chronik Electrum protocol
#[derive(Clone, Copy, Debug)]
pub enum ChronikElectrumProtocol {
/// TCP
Tcp,
/// TLS
Tls,
}
/// Params defining what and where to serve for [`ChronikElectrumServer`].
#[derive(Clone, Debug)]
pub struct ChronikElectrumServerParams {
/// Host address (port + IP) where to serve the electrum server at.
pub hosts: Vec<(SocketAddr, ChronikElectrumProtocol)>,
/// Indexer to read data from
pub indexer: ChronikIndexerRef,
/// Access to the bitcoind node
pub node: NodeRef,
/// The TLS certificate chain file
pub tls_cert_path: String,
/// The TLS private key file
pub tls_privkey_path: String,
+ /// Maximum transaction history length
+ pub max_history: u32,
}
/// Chronik Electrum server, holding all the data/handles required to serve an
/// instance.
#[derive(Debug)]
pub struct ChronikElectrumServer {
hosts: Vec<(SocketAddr, ChronikElectrumProtocol)>,
indexer: ChronikIndexerRef,
node: NodeRef,
tls_cert_path: String,
tls_privkey_path: String,
+ max_history: u32,
}
/// Errors for [`ChronikElectrumServer`].
#[derive(Debug, Eq, Error, PartialEq)]
pub enum ChronikElectrumServerError {
/// Binding to host address failed
#[error("Chronik Electrum failed binding to {0}: {1}")]
FailedBindingAddress(SocketAddr, String),
/// Serving Electrum failed
#[error("Chronik Electrum failed serving: {0}")]
ServingFailed(String),
/// Chronik Electrum TLS invalid configuration
#[error("Chronik Electrum TLS configuration is invalid: {0}")]
InvalidTlsConfiguration(String),
/// Chronik Electrum TLS configuration failed
#[error("Chronik Electrum TLS configuration failed: {0}")]
TlsConfigurationFailed(String),
/// Missing certificate chain file
#[error(
"Chronik Electrum TLS configuration requires a certificate chain file \
(see -chronikelectrumcert)"
)]
MissingCertificateFile,
/// Certificate chain file not found
#[error(
"Chronik Electrum TLS configuration failed to open the certificate \
chain file {0}: {1}"
)]
CertificateFileNotFound(String, String),
/// Missing private key file
#[error(
"Chronik Electrum TLS configuration requires a private key file (see \
-chronikelectrumprivkey)"
)]
MissingPrivateKeyFile,
/// Private key file not found
#[error(
"Chronik Electrum TLS configuration failed to open the private key \
file {0}, {1}"
)]
PrivateKeyFileNotFound(String, String),
}
impl ChronikElectrumServer {
/// Binds the Chronik server on the given hosts
pub fn setup(params: ChronikElectrumServerParams) -> Result<Self> {
Ok(ChronikElectrumServer {
hosts: params.hosts,
indexer: params.indexer,
node: params.node,
tls_cert_path: params.tls_cert_path,
tls_privkey_path: params.tls_privkey_path,
+ max_history: params.max_history,
})
}
/// Start the Chronik electrum server
pub async fn serve(self) -> Result<()> {
// The behavior is to bind the endpoint name to its method name like so:
// endpoint.method as the name of the RPC
let server_endpoint = Arc::new(ChronikElectrumRPCServerEndpoint {});
let blockchain_endpoint =
Arc::new(ChronikElectrumRPCBlockchainEndpoint {
indexer: self.indexer,
node: self.node,
+ max_history: self.max_history,
});
let tls_cert_path = self.tls_cert_path.clone();
let tls_privkey_path = self.tls_privkey_path.clone();
let servers = izip!(
self.hosts,
std::iter::repeat(server_endpoint),
std::iter::repeat(blockchain_endpoint),
std::iter::repeat(tls_cert_path),
std::iter::repeat(tls_privkey_path)
)
.map(
|(
(host, protocol),
server_endpoint,
blockchain_endpoint,
tls_cert_path,
tls_privkey_path,
)| {
Box::pin(async move {
let mut require_tls_config = false;
// Don't use the karyon Endpoint parsing as it doesn't
// appear to support IPv6.
let endpoint = match protocol {
ChronikElectrumProtocol::Tcp => {
Endpoint::Tcp(Addr::Ip(host.ip()), host.port())
}
ChronikElectrumProtocol::Tls => {
require_tls_config = true;
Endpoint::Tls(Addr::Ip(host.ip()), host.port())
}
};
let mut builder = ServerBuilder::new_with_codec(
endpoint,
ElectrumCodec {},
)
.map_err(|err| {
FailedBindingAddress(host, err.to_string())
})?;
if require_tls_config {
if tls_cert_path.is_empty() {
return Err(MissingCertificateFile);
}
if tls_privkey_path.is_empty() {
return Err(MissingPrivateKeyFile);
}
let certs: Vec<_> = CertificateDer::pem_file_iter(
tls_cert_path.as_str(),
)
.map_err(|err| {
CertificateFileNotFound(
tls_cert_path,
err.to_string(),
)
})?
.map(|cert| cert.unwrap())
.collect();
let private_key = PrivateKeyDer::from_pem_file(
tls_privkey_path.as_str(),
)
.map_err(|err| {
PrivateKeyFileNotFound(
tls_privkey_path,
err.to_string(),
)
})?;
let tls_config = rustls::ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(certs, private_key)
.map_err(|err| {
InvalidTlsConfiguration(err.to_string())
})?;
builder =
builder.tls_config(tls_config).map_err(|err| {
TlsConfigurationFailed(err.to_string())
})?;
}
let server = builder
.service(server_endpoint)
.service(blockchain_endpoint)
.build()
.await
.map_err(|err| ServingFailed(err.to_string()))?;
server.start();
let () = future::pending().await;
Ok::<(), ChronikElectrumServerError>(())
})
},
);
let (result, _, _) = futures::future::select_all(servers).await;
result?;
Ok(())
}
}
/// Enforce maximum number of parameters for a JSONRPC method
macro_rules! check_max_number_of_params {
($params:expr, $max_num_params:expr) => {
let mut err_max =
format!("Expected at most {} parameter", $max_num_params);
if $max_num_params != 1 {
err_max.push_str("s");
}
match $params {
Value::Array(ref arr) => {
if arr.len() > $max_num_params {
return Err(RPCError::InvalidParams(err_max));
}
}
Value::Object(ref obj) => {
if obj.len() > $max_num_params {
return Err(RPCError::InvalidParams(err_max));
}
}
Value::Null => {
if $max_num_params != 0 {
return Err(RPCError::InvalidParams(
"Missing required params".to_string(),
));
}
}
_ => {
return Err(RPCError::InvalidParams(
"'params' must be an array or an object".to_string(),
))
}
};
};
}
/// Get a mandatory JSONRPC param by index or by name.
macro_rules! get_param {
($params:expr, $index:expr, $key:expr) => {{
match $params {
Value::Array(ref arr) => Ok(arr
.get($index)
.ok_or(RPCError::InvalidParams(format!(
"Missing mandatory '{}' parameter",
$key
)))?
.clone()),
Value::Object(ref obj) => match obj.get($key) {
Some(value) => Ok(value.clone()),
None => Err(RPCError::InvalidParams(format!(
"Missing mandatory '{}' parameter",
$key
))),
},
_ => Err(RPCError::InvalidParams(
"'params' must be an array or an object".to_string(),
)),
}
}};
}
/// Get an optional JSONRPC param by index or by name, return the
/// provided default if the param not specified.
macro_rules! get_optional_param {
($params:expr, $index:expr, $key:expr, $default:expr) => {{
match $params {
Value::Array(ref arr) => match arr.get($index) {
Some(val) => Ok(val.clone()),
None => Ok($default),
},
Value::Object(ref obj) => match obj.get($key) {
Some(value) => Ok(value.clone()),
None => Ok($default),
},
_ => Err(RPCError::InvalidParams(
"'params' must be an array or an object".to_string(),
)),
}
}};
}
struct ChronikElectrumRPCServerEndpoint {}
struct ChronikElectrumRPCBlockchainEndpoint {
indexer: ChronikIndexerRef,
node: NodeRef,
+ max_history: u32,
}
#[rpc_impl(name = "server")]
impl ChronikElectrumRPCServerEndpoint {
async fn ping(&self, params: Value) -> Result<Value, RPCError> {
check_max_number_of_params!(params, 0);
Ok(Value::Null)
}
async fn version(&self, params: Value) -> Result<Value, RPCError> {
check_max_number_of_params!(params, 2);
let _client_name =
get_optional_param!(params, 0, "client_name", json!(""))?;
let client_protocol_versions =
get_optional_param!(params, 1, "protocol_version", json!("1.4"))?;
let unsup_version_err = RPCError::CustomError(
1,
"Unsupported protocol version".to_string(),
);
match client_protocol_versions {
Value::String(version_string) => {
if version_string != ELECTRUM_PROTOCOL_VERSION {
return Err(unsup_version_err);
}
}
Value::Array(arr) => {
if arr.len() != 2 {
return Err(unsup_version_err);
}
let bad_version_err = || {
RPCError::CustomError(
1,
format!("Bad version tuple: {arr:?}"),
)
};
let min_version = Versioning::new(
arr[0].as_str().ok_or_else(bad_version_err)?,
)
.ok_or_else(bad_version_err)?;
let max_version = Versioning::new(
arr[1].as_str().ok_or_else(bad_version_err)?,
)
.ok_or_else(bad_version_err)?;
// Only allow versions in the correct order
if min_version > max_version {
return Err(bad_version_err());
}
let target_version =
Versioning::new(ELECTRUM_PROTOCOL_VERSION).unwrap();
if target_version < min_version || target_version > max_version
{
return Err(unsup_version_err);
}
}
_ => {
return Err(unsup_version_err);
}
};
let version_number = ffi::format_full_version();
let server_version = format!("Bitcoin ABC {version_number}");
Ok(json!([server_version, ELECTRUM_PROTOCOL_VERSION]))
}
}
fn json_to_u31(num: Value, err_msg: &str) -> Result<i32, RPCError> {
match num {
Value::Number(n) => match n.as_i64() {
Some(n) if n >= 0 => i32::try_from(n)
.map_err(|_| RPCError::CustomError(1, err_msg.to_string())),
_ => Err(RPCError::CustomError(1, err_msg.to_string())),
},
_ => Err(RPCError::CustomError(1, err_msg.to_string())),
}
}
fn be_bytes_to_le_hex(hash: &[u8]) -> String {
hex::encode(Sha256::from_be_slice(hash).unwrap().as_le_bytes())
}
fn get_scripthash_balance(
history: TxHistoryPage,
scripthash: Sha256,
) -> (i64, i64) {
let mut confirmed: i64 = 0;
let mut unconfirmed: i64 = 0;
for tx in history.txs.iter() {
let is_mempool = tx.block.is_none();
for outp in tx.outputs.iter() {
if Sha256::digest(&outp.output_script) != scripthash {
continue;
}
if is_mempool {
unconfirmed += outp.value;
} else {
confirmed += outp.value;
}
}
for inp in tx.inputs.iter() {
if Sha256::digest(&inp.output_script) != scripthash {
continue;
}
if is_mempool {
unconfirmed -= inp.value;
} else {
confirmed -= inp.value;
}
}
}
(confirmed, unconfirmed)
}
fn get_tx_fee(tx: &Tx) -> i64 {
let mut fee: i64 = 0;
for inp in tx.inputs.iter() {
fee += inp.value;
}
for outp in tx.outputs.iter() {
fee -= outp.value;
}
fee
}
#[rpc_impl(name = "blockchain")]
impl ChronikElectrumRPCBlockchainEndpoint {
#[rpc_method(name = "block.header")]
async fn block_header(&self, params: Value) -> Result<Value, RPCError> {
check_max_number_of_params!(params, 2);
let height =
json_to_u31(get_param!(params, 0, "height")?, "Invalid height")?;
let checkpoint_height = json_to_u31(
get_optional_param!(params, 1, "cp_height", json!(0))?,
"Invalid cp_height",
)?;
let indexer = self.indexer.read().await;
let blocks = indexer.blocks(&self.node);
if checkpoint_height > 0 && height > checkpoint_height {
let tip_height = blocks
.blockchain_info()
.map_err(|_| RPCError::InternalError)?
.tip_height;
return Err(RPCError::CustomError(
1,
format!(
"header height {height} must be <= cp_height \
{checkpoint_height} which must be <= chain height \
{tip_height}"
),
));
}
let proto_header = blocks
.header(height.to_string(), checkpoint_height)
.await
.map_err(|_| {
RPCError::CustomError(
1,
format!("Height {height} is out of range"),
)
})?;
if checkpoint_height == 0 {
Ok(json!(hex::encode(proto_header.raw_header)))
} else {
let branch: Vec<String> = proto_header
.branch
.iter()
.map(|h| be_bytes_to_le_hex(h))
.collect();
Ok(json!({
"branch": branch,
"header": hex::encode(proto_header.raw_header) ,
"root": be_bytes_to_le_hex(&proto_header.root),
}))
}
}
#[rpc_method(name = "block.headers")]
async fn block_headers(&self, params: Value) -> Result<Value, RPCError> {
check_max_number_of_params!(params, 3);
let start_height = json_to_u31(
get_param!(params, 0, "start_height")?,
"Invalid height",
)?;
let max_count = 2016;
let mut count =
json_to_u31(get_param!(params, 1, "count")?, "Invalid count")?;
count = cmp::min(count, max_count);
let checkpoint_height = json_to_u31(
get_optional_param!(params, 2, "cp_height", json!(0))?,
"Invalid cp_height",
)?;
let indexer = self.indexer.read().await;
let blocks = indexer.blocks(&self.node);
let end_height = start_height + count - 1;
let tip_height = blocks
.blockchain_info()
.map_err(|_| RPCError::InternalError)?
.tip_height;
if checkpoint_height > 0
&& (end_height > checkpoint_height
|| checkpoint_height > tip_height)
{
return Err(RPCError::CustomError(
1,
format!(
"header height + (count - 1) {end_height} must be <= \
cp_height {checkpoint_height} which must be <= chain \
height {tip_height}"
),
));
}
if count == 0 {
return Ok(json!({
"count": 0,
"hex": "",
"max": max_count,
}));
}
// The RPC may return less headers than requested when there aren't
// enough blocks in the chain.
count = cmp::min(count, tip_height - start_height + 1);
let proto_headers = blocks
.headers_by_range(
start_height,
start_height + count - 1,
checkpoint_height,
)
.await
.map_err(|_| RPCError::InternalError)?;
let headers_hex = proto_headers
.headers
.iter()
.map(|proto_header| hex::encode(&proto_header.raw_header))
.collect::<String>();
if checkpoint_height == 0 {
Ok(json!({
"count": count,
"hex": headers_hex,
"max": max_count,
}))
} else {
let last_header = proto_headers
.headers
.last()
.ok_or(RPCError::InternalError)?;
let branch: Vec<String> = last_header
.branch
.iter()
.map(|h| be_bytes_to_le_hex(h))
.collect();
Ok(json!({
"branch": branch,
"count": count,
"hex": headers_hex,
"max": max_count,
"root": be_bytes_to_le_hex(&last_header.root),
}))
}
}
#[rpc_method(name = "transaction.broadcast")]
async fn transaction_broadcast(
&self,
params: Value,
) -> Result<Value, RPCError> {
check_max_number_of_params!(params, 1);
let raw_tx = match get_param!(params, 0, "raw_tx")? {
Value::String(raw_tx) => Ok(raw_tx),
_ => Err(RPCError::CustomError(
1,
"Invalid raw_tx argument; expected hex string".to_string(),
)),
}?;
let raw_tx = Bytes::from(hex::decode(raw_tx).map_err(|_err| {
RPCError::CustomError(
1,
"Failed to decode raw_tx as a hex string".to_string(),
)
})?);
let max_fee = ffi::calc_fee(
raw_tx.len(),
ffi::default_max_raw_tx_fee_rate_per_kb(),
);
let txid = match self.node.bridge.broadcast_tx(&raw_tx, max_fee) {
Ok(txid) => Ok(TxId::from(txid)),
Err(err) => Err(RPCError::CustomError(1, err.what().to_string())),
}?;
Ok(Value::String(txid.to_string()))
}
#[rpc_method(name = "transaction.get")]
async fn transaction_get(&self, params: Value) -> Result<Value, RPCError> {
check_max_number_of_params!(params, 2);
let txid_hex = get_param!(params, 0, "txid")?;
let txid = TxId::try_from(&txid_hex)
.map_err(|err| RPCError::CustomError(1, err.to_string()))?;
let verbose =
get_optional_param!(params, 1, "verbose", Value::Bool(false))?;
let verbose = match verbose {
Value::Bool(v) => Ok(v),
_ => Err(RPCError::CustomError(
1,
"Invalid verbose argument; expected boolean".to_string(),
)),
}?;
let indexer = self.indexer.read().await;
let query_tx = indexer.txs(&self.node);
let unknown_txid_msg =
"No transaction matching the requested hash was found".to_string();
let raw_tx = hex::encode(
query_tx
.raw_tx_by_id(&txid)
.or(Err(RPCError::CustomError(1, unknown_txid_msg.clone())))?
.raw_tx,
);
if !verbose {
return Ok(Value::String(raw_tx));
}
let tx = query_tx
.tx_by_id(txid)
// The following error should be unreachable, unless raw_tx_by_id
// and tx_by_id are inconsistent
.or(Err(RPCError::CustomError(1, unknown_txid_msg)))?;
let blockchaininfo = indexer.blocks(&self.node).blockchain_info();
if blockchaininfo.is_err() {
return Err(RPCError::InternalError);
}
if tx.block.is_none() {
// mempool transaction
return Ok(json!({
"confirmations": 0,
"hash": txid.to_string(),
"hex": raw_tx,
"time": tx.time_first_seen,
}));
}
let block = tx.block.unwrap();
let blockhash = Sha256::from_le_slice(block.hash.as_ref()).unwrap();
let confirmations =
blockchaininfo.ok().unwrap().tip_height - block.height + 1;
// TODO: more verbose fields, inputs, outputs
// (but for now only "confirmations" is used in Electrum ABC)
Ok(json!({
"blockhash": blockhash.hex_be(),
"blocktime": block.timestamp,
"confirmations": confirmations,
"hash": txid.to_string(),
"hex": raw_tx,
"time": tx.time_first_seen,
}))
}
#[rpc_method(name = "transaction.get_height")]
async fn transaction_get_height(
&self,
params: Value,
) -> Result<Value, RPCError> {
check_max_number_of_params!(params, 1);
let txid_hex = get_param!(params, 0, "txid")?;
let txid = TxId::try_from(&txid_hex)
.map_err(|err| RPCError::CustomError(1, err.to_string()))?;
let indexer = self.indexer.read().await;
let query_tx = indexer.txs(&self.node);
let tx = query_tx
.tx_by_id(txid)
.or(Err(RPCError::InvalidRequest("Unknown txid".to_string())))?;
match tx.block {
Some(block) => Ok(json!(block.height)),
None => Ok(json!(0)), // mempool transaction
}
}
#[rpc_method(name = "transaction.get_merkle")]
async fn transaction_get_merkle(
&self,
params: Value,
) -> Result<Value, RPCError> {
check_max_number_of_params!(params, 2);
let txid = TxId::try_from(&get_param!(params, 0, "txid")?)
.map_err(|err| RPCError::CustomError(1, err.to_string()))?;
let mut block_height = json_to_u31(
get_optional_param!(params, 1, "height", json!(0))?,
"Invalid height argument; expected non-negative numeric value",
)?;
let indexer = self.indexer.read().await;
let query_tx = indexer.txs(&self.node);
let conf_tx_not_found_err =
"No confirmed transaction matching the requested hash was found";
let tx = query_tx.tx_by_id(txid).or(Err(RPCError::CustomError(
1,
conf_tx_not_found_err.to_string(),
)))?;
let block_hash = match tx.block {
Some(b) => {
// We don't actually need the block height param. In Fulcrum
// it is optional and saves a database access when provided.
// Let's just make sure we get the same error message when an
// incorrect value is provided.
if block_height != 0 && block_height != b.height {
return Err(RPCError::CustomError(
1,
format!(
"No transaction matching the requested hash found \
at height {block_height}"
),
));
}
block_height = b.height;
Ok(b.hash)
}
None => {
Err(RPCError::CustomError(1, conf_tx_not_found_err.to_string()))
}
}?;
let bridge = &self.node.bridge;
let bindex = bridge
.lookup_block_index(
block_hash.try_into().map_err(|_| RPCError::InternalError)?,
)
.map_err(|_| RPCError::InternalError)?;
let block = indexer
.load_chronik_block(bridge, bindex)
.map_err(|_| RPCError::InternalError)?;
let txids: Vec<Sha256d> = block
.block_txs
.txs
.iter()
.map(|txentry| Sha256d(txentry.txid.to_bytes()))
.collect();
let index_in_block = txids
.iter()
.position(|&id| id == Sha256d(txid.to_bytes()))
.ok_or(RPCError::InternalError)?;
let mut merkle_tree = MerkleTree::new();
let (_root, branch) =
merkle_tree.merkle_root_and_branch(&txids, index_in_block);
let branch: Vec<String> = branch
.iter()
.map(|h| hex::encode(h.to_be_bytes()))
.collect();
Ok(json!({
"merkle": branch,
"block_height": block_height,
"pos": index_in_block,
}))
}
#[rpc_method(name = "scripthash.get_balance")]
async fn scripthash_get_balance(
&self,
params: Value,
) -> Result<Value, RPCError> {
check_max_number_of_params!(params, 1);
let script_hash_hex = match get_param!(params, 0, "scripthash")? {
Value::String(v) => Ok(v),
_ => {
Err(RPCError::CustomError(1, "Invalid scripthash".to_string()))
}
}?;
let script_hash =
Sha256::from_be_hex(&script_hash_hex).map_err(|_| {
RPCError::CustomError(1, "Invalid scripthash".to_string())
})?;
let indexer = self.indexer.read().await;
let script_history = indexer
.script_history(&self.node)
.map_err(|_| RPCError::InternalError)?;
let history = script_history
.rev_history(
GroupMember::MemberHash(script_hash).as_ref(),
0,
MAX_HISTORY_PAGE_SIZE,
)
.map_err(|_| RPCError::InternalError)?;
if history.num_txs == 0 {
return Ok(json!({
"confirmed": 0,
"unconfirmed": 0
}));
}
- if history.num_txs > MAX_HISTORY as u32 {
+ if history.num_txs > self.max_history {
return Err(RPCError::CustomError(
1,
format!(
"transaction history for scripthash {script_hash_hex} \
- exceeds limit ({MAX_HISTORY})"
+ exceeds limit ({0})",
+ self.max_history
),
));
}
let num_pages = history.num_pages;
let (mut confirmed, mut unconfirmed) =
get_scripthash_balance(history, script_hash);
for page in 1..num_pages {
let history = script_history
.rev_history(
GroupMember::MemberHash(script_hash).as_ref(),
page as usize,
MAX_HISTORY_PAGE_SIZE,
)
.map_err(|_| RPCError::InternalError)?;
let (page_conf, page_unconf) =
get_scripthash_balance(history, script_hash);
confirmed += page_conf;
unconfirmed += page_unconf;
}
Ok(json!({
"confirmed": confirmed,
"unconfirmed": unconfirmed,
}))
}
#[rpc_method(name = "scripthash.get_history")]
async fn scripthash_get_history(
&self,
params: Value,
) -> Result<Value, RPCError> {
check_max_number_of_params!(params, 1);
let script_hash_hex = match get_param!(params, 0, "scripthash")? {
Value::String(v) => Ok(v),
_ => {
Err(RPCError::CustomError(1, "Invalid scripthash".to_string()))
}
}?;
let script_hash =
Sha256::from_be_hex(&script_hash_hex).map_err(|_| {
RPCError::CustomError(1, "Invalid scripthash".to_string())
})?;
let indexer = self.indexer.read().await;
let script_history = indexer
.script_history(&self.node)
.map_err(|_| RPCError::InternalError)?;
let history = script_history
.rev_history(
GroupMember::MemberHash(script_hash).as_ref(),
0,
MAX_HISTORY_PAGE_SIZE,
)
.map_err(|_| RPCError::InternalError)?;
- if history.num_txs > MAX_HISTORY as u32 {
+ if history.num_txs > self.max_history {
// Note that Fulcrum would return an empty history in this case
return Err(RPCError::CustomError(
1,
format!(
"transaction history for scripthash {script_hash_hex} \
- exceeds limit ({MAX_HISTORY})"
+ exceeds limit ({0})",
+ self.max_history
),
));
}
if history.num_txs == 0 {
return Ok(json!([]));
}
let mut json_history: Vec<Value> = vec![];
let num_pages = history.num_pages;
// Return the history in ascending block height order
for page in (0..num_pages).rev() {
let history = script_history
.rev_history(
GroupMember::MemberHash(script_hash).as_ref(),
page as usize,
MAX_HISTORY_PAGE_SIZE,
)
.map_err(|_| RPCError::InternalError)?;
for tx in history.txs.iter().rev() {
let height = match &tx.block {
Some(block) => block.height,
// Here we differ from Fulcrum because we don't discriminate
// between unconfirmed transactions and
// transactions with unconfirmed parents
// (height -1)
None => 0,
};
let be_txid: Vec<u8> = tx.txid.iter().copied().rev().collect();
if height > 0 {
json_history.push(json!({
"height": height,
"tx_hash": hex::encode(be_txid)
}));
} else {
let fee = get_tx_fee(tx);
json_history.push(json!({
"fee": fee,
"height": height,
"tx_hash": hex::encode(be_txid)
}));
}
}
}
Ok(json!(json_history))
}
#[rpc_method(name = "scripthash.listunspent")]
async fn scripthash_listunspent(
&self,
params: Value,
) -> Result<Value, RPCError> {
check_max_number_of_params!(params, 1);
let script_hash_hex = match get_param!(params, 0, "scripthash")? {
Value::String(v) => Ok(v),
_ => {
Err(RPCError::CustomError(1, "Invalid scripthash".to_string()))
}
}?;
let script_hash =
Sha256::from_be_hex(&script_hash_hex).map_err(|_| {
RPCError::CustomError(1, "Invalid scripthash".to_string())
})?;
let indexer = self.indexer.read().await;
let script_utxos = indexer
.script_utxos()
.map_err(|_| RPCError::InternalError)?;
let script = match script_utxos.script(
GroupMember::MemberHash(script_hash),
indexer.decompress_script_fn,
) {
Ok(script) => script,
Err(_) => return Ok(json!([])),
};
let utxos = script_utxos.utxos(&script).ok().unwrap_or_default();
let mut json_utxos: Vec<Value> = vec![];
for utxo in utxos.iter() {
let outpoint =
utxo.outpoint.as_ref().ok_or(RPCError::InternalError)?;
let be_txid: Vec<u8> =
outpoint.txid.iter().copied().rev().collect();
// The electrum spec says mempool utxos have a block height of 0
let height: i32 = match utxo.block_height {
-1 => 0,
i => i,
};
json_utxos.push(json!({
"height": height,
"tx_hash": hex::encode(be_txid),
"tx_pos": outpoint.out_idx,
"value": &utxo.value,
}));
}
Ok(json!(json_utxos))
}
}
diff --git a/chronik/chronik-lib/src/bridge.rs b/chronik/chronik-lib/src/bridge.rs
index fd0ba866e..a4bb9ab04 100644
--- a/chronik/chronik-lib/src/bridge.rs
+++ b/chronik/chronik-lib/src/bridge.rs
@@ -1,460 +1,461 @@
// 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.
//! Rust side of the bridge; these structs and functions are exposed to C++.
use std::{
net::{AddrParseError, IpAddr, SocketAddr},
path::PathBuf,
sync::Arc,
time::Duration,
};
use abc_rust_error::Result;
use bitcoinsuite_core::{
net::Net,
tx::{Tx, TxId},
};
use chronik_bridge::{ffi::init_error, util::expect_unique_ptr};
use chronik_db::{
index_tx::TxNumCacheSettings, io::GroupHistorySettings, mem::MempoolTx,
};
use chronik_http::electrum::{
ChronikElectrumProtocol, ChronikElectrumServer, ChronikElectrumServerParams,
};
use chronik_http::server::{
ChronikServer, ChronikServerParams, ChronikSettings,
};
use chronik_indexer::{
indexer::{ChronikIndexer, ChronikIndexerParams, Node},
pause::Pause,
};
use chronik_plugin::{context::PluginContext, params::PluginParams};
use chronik_util::{log, log_chronik, mount_loggers, Loggers};
use thiserror::Error;
use tokio::sync::RwLock;
use crate::ffi::{self, StartChronikValidationInterface};
/// Errors for [`Chronik`] and [`setup_chronik`].
#[derive(Debug, Eq, Error, PartialEq)]
pub enum ChronikError {
/// Chronik host address failed to parse
#[error("Invalid Chronik host address {0:?}: {1}")]
InvalidChronikHost(String, AddrParseError),
/// Chronik Electrum host address failed to parse
#[error("Invalid Chronik Electrum host address {0:?}: {1}")]
InvalidChronikElectrumHostAddr(String, AddrParseError),
/// Chronik electrum host address failed to parse
#[error("Invalid Chronik Electrum host address format {0:?}")]
InvalidChronikElectrumHostFormat(String),
/// Chronik electrum host address failed to parse
#[error("Invalid Chronik Electrum host protocol {0:?}")]
InvalidChronikElectrumHostProtocol(u8),
/// Unknown net repr
#[error("Unknown net repr {0}")]
UnknownNetRepr(u8),
}
use self::ChronikError::*;
/// Setup the Chronik bridge. Returns a ChronikIndexer object.
pub fn setup_chronik(
params: ffi::SetupParams,
node: &ffi::NodeContext,
) -> bool {
match try_setup_chronik(params, node) {
Ok(()) => true,
Err(report) => {
log_chronik!("{report:?}\n");
init_error(&report.to_string())
}
}
}
fn try_setup_chronik(
params: ffi::SetupParams,
node_context: &ffi::NodeContext,
) -> Result<()> {
abc_rust_error::install();
mount_loggers(Loggers {
log: chronik_bridge::ffi::log_print,
log_chronik: chronik_bridge::ffi::log_print_chronik,
});
let hosts = params
.hosts
.into_iter()
.map(|host| parse_socket_addr(host, params.default_port))
.collect::<Result<Vec<_>>>()?;
let datadir: PathBuf = params.datadir.into();
let net = match params.net {
ffi::Net::Mainnet => Net::Mainnet,
ffi::Net::Testnet => Net::Testnet,
ffi::Net::Regtest => Net::Regtest,
_ => return Err(UnknownNetRepr(params.net.repr).into()),
};
let plugin_ctx = PluginContext::setup(PluginParams {
net,
plugins_dir: datadir.join("plugins"),
plugins_conf: datadir.join("plugins.toml"),
})?;
log!("Starting Chronik bound to {:?}\n", hosts);
let bridge = chronik_bridge::ffi::make_bridge(node_context);
let bridge_ref = expect_unique_ptr("make_bridge", &bridge);
let (pause, pause_notify) = Pause::new_pair(params.is_pause_allowed);
let mut indexer = ChronikIndexer::setup(
ChronikIndexerParams {
datadir_net: params.datadir_net.into(),
wipe_db: params.wipe_db,
enable_token_index: params.enable_token_index,
enable_lokad_id_index: params.enable_lokad_id_index,
enable_scripthash_index: params.enable_scripthash_index,
enable_perf_stats: params.enable_perf_stats,
tx_num_cache: TxNumCacheSettings {
bucket_size: params.tx_num_cache.bucket_size,
num_buckets: params.tx_num_cache.num_buckets,
},
plugin_ctx: Arc::new(plugin_ctx),
script_history: GroupHistorySettings {
is_member_hash_index_enabled: params.enable_scripthash_index,
},
decompress_script_fn: decompress_script,
},
|file_num, data_pos, undo_pos| {
Ok(Tx::from(bridge_ref.load_tx(file_num, data_pos, undo_pos)?))
},
|| bridge_ref.shutdown_requested(),
)?;
indexer.resync_indexer(bridge_ref)?;
if bridge.shutdown_requested() {
// Don't setup Chronik if the user requested shutdown during resync
return Ok(());
}
let indexer = Arc::new(RwLock::new(indexer));
let node = Arc::new(Node { bridge });
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?;
let server = runtime.block_on({
let indexer = Arc::clone(&indexer);
let node = Arc::clone(&node);
async move {
// try_bind requires a Runtime
ChronikServer::setup(ChronikServerParams {
hosts,
indexer,
node,
pause_notify: Arc::new(pause_notify),
settings: ChronikSettings {
ws_ping_interval: Duration::from_secs(
params.ws_ping_interval_secs,
),
enable_cors: params.enable_cors,
},
})
}
})?;
runtime.spawn({
let node = Arc::clone(&node);
async move {
node.ok_or_abort("ChronikServer::serve", server.serve().await);
}
});
if !params.electrum_hosts.is_empty() {
let electrum_hosts = params
.electrum_hosts
.into_iter()
.map(|host| {
parse_socket_addr_protocol(
host,
params.electrum_default_port,
params.electrum_default_protocol,
)
})
.collect::<Result<Vec<_>>>()?;
log!(
"Starting Chronik Electrum interface bound to {:?}\n",
electrum_hosts
);
let electrum_server =
ChronikElectrumServer::setup(ChronikElectrumServerParams {
hosts: electrum_hosts,
indexer: indexer.clone(),
node: node.clone(),
tls_cert_path: params.electrum_cert_path,
tls_privkey_path: params.electrum_privkey_path,
+ max_history: params.electrum_max_history,
})?;
runtime.spawn({
let node = Arc::clone(&node);
async move {
node.ok_or_abort(
"ChronikElectrumServer::serve",
electrum_server.serve().await,
);
}
});
}
let chronik = Box::new(Chronik {
node: Arc::clone(&node),
indexer,
pause,
runtime,
});
StartChronikValidationInterface(node_context, chronik);
Ok(())
}
fn parse_socket_addr(host: String, default_port: u16) -> Result<SocketAddr> {
if let Ok(addr) = host.parse::<SocketAddr>() {
return Ok(addr);
}
let ip_addr = host
.parse::<IpAddr>()
.map_err(|err| InvalidChronikHost(host, err))?;
Ok(SocketAddr::new(ip_addr, default_port))
}
fn parse_socket_addr_protocol(
host: String,
default_port: u16,
default_protocol: u8,
) -> Result<(SocketAddr, ChronikElectrumProtocol)> {
let split_host = host.split(':').collect::<Vec<_>>();
let split_host_len = split_host.len();
if split_host_len > 3 {
return Err(InvalidChronikElectrumHostFormat(host).into());
}
let addr_port = split_host[..split_host_len.min(2)].join(":");
let mut protocol_letter = default_protocol;
if split_host_len > 2 {
protocol_letter = split_host[2]
.bytes()
.next()
.ok_or(InvalidChronikElectrumHostFormat(host.clone()))?;
}
let protocol = match protocol_letter {
b't' => ChronikElectrumProtocol::Tcp,
b's' => ChronikElectrumProtocol::Tls,
_ => {
return Err(
InvalidChronikElectrumHostProtocol(protocol_letter).into()
)
}
};
if let Ok(addr) = addr_port.parse::<SocketAddr>() {
return Ok((addr, protocol));
}
let ip_addr = host
.parse::<IpAddr>()
.map_err(|err| InvalidChronikElectrumHostAddr(host, err))?;
Ok((SocketAddr::new(ip_addr, default_port), protocol))
}
fn decompress_script(script: &[u8]) -> Result<Vec<u8>> {
Ok(chronik_bridge::ffi::decompress_script(script)?)
}
/// Contains all db, runtime, tpc, etc. handles needed by Chronik.
/// This makes it so when this struct is dropped, all handles are relased
/// cleanly.
pub struct Chronik {
node: Arc<Node>,
indexer: Arc<RwLock<ChronikIndexer>>,
pause: Pause,
// Having this here ensures HTTP server, outstanding requests etc. will get
// stopped when `Chronik` is dropped.
runtime: tokio::runtime::Runtime,
}
impl Chronik {
/// Tx added to the bitcoind mempool
pub fn handle_tx_added_to_mempool(
&self,
ptx: &ffi::CTransaction,
spent_coins: &cxx::CxxVector<ffi::CCoin>,
time_first_seen: i64,
) {
self.block_if_paused();
self.node.ok_or_abort(
"handle_tx_added_to_mempool",
self.add_tx_to_mempool(ptx, spent_coins, time_first_seen),
);
}
/// Tx removed from the bitcoind mempool
pub fn handle_tx_removed_from_mempool(&self, txid: [u8; 32]) {
self.block_if_paused();
let mut indexer = self.indexer.blocking_write();
let txid = TxId::from(txid);
self.node.ok_or_abort(
"handle_tx_removed_from_mempool",
indexer.handle_tx_removed_from_mempool(txid),
);
log_chronik!("Chronik: transaction {} removed from mempool\n", txid);
}
/// Block connected to the longest chain
pub fn handle_block_connected(
&self,
block: &ffi::CBlock,
bindex: &ffi::CBlockIndex,
) {
self.block_if_paused();
self.node.ok_or_abort(
"handle_block_connected",
self.connect_block(block, bindex),
);
}
/// Block disconnected from the longest chain
pub fn handle_block_disconnected(
&self,
block: &ffi::CBlock,
bindex: &ffi::CBlockIndex,
) {
self.block_if_paused();
self.node.ok_or_abort(
"handle_block_disconnected",
self.disconnect_block(block, bindex),
);
}
/// Block finalized with Avalanche
pub fn handle_block_finalized(&self, bindex: &ffi::CBlockIndex) {
self.block_if_paused();
self.node
.ok_or_abort("handle_block_finalized", self.finalize_block(bindex));
}
/// Block invalidated with Avalanche
pub fn handle_block_invalidated(
&self,
block: &ffi::CBlock,
bindex: &ffi::CBlockIndex,
) {
self.block_if_paused();
self.node.ok_or_abort(
"handle_block_invalidated",
self.invalidate_block(block, bindex),
);
}
fn add_tx_to_mempool(
&self,
ptx: &ffi::CTransaction,
spent_coins: &cxx::CxxVector<ffi::CCoin>,
time_first_seen: i64,
) -> Result<()> {
let mut indexer = self.indexer.blocking_write();
let tx = chronik_bridge::ffi::bridge_tx(ptx, spent_coins)?;
let txid = TxId::from(tx.txid);
indexer.handle_tx_added_to_mempool(MempoolTx {
tx: Tx::from(tx),
time_first_seen,
})?;
log_chronik!("Chronik: transaction {} added to mempool\n", txid);
Ok(())
}
fn connect_block(
&self,
block: &ffi::CBlock,
bindex: &ffi::CBlockIndex,
) -> Result<()> {
let block_undo = self.node.bridge.load_block_undo(bindex)?;
let block =
chronik_bridge::ffi::bridge_block(block, &block_undo, bindex)?;
let mut indexer = self.indexer.blocking_write();
let block = indexer.make_chronik_block(block);
let block_hash = block.db_block.hash.clone();
let num_txs = block.block_txs.txs.len();
indexer.handle_block_connected(block)?;
log_chronik!(
"Chronik: block {} connected with {} txs\n",
block_hash,
num_txs,
);
Ok(())
}
fn disconnect_block(
&self,
block: &ffi::CBlock,
bindex: &ffi::CBlockIndex,
) -> Result<()> {
let block_undo = self.node.bridge.load_block_undo(bindex)?;
let block =
chronik_bridge::ffi::bridge_block(block, &block_undo, bindex)?;
let mut indexer = self.indexer.blocking_write();
let block = indexer.make_chronik_block(block);
let block_hash = block.db_block.hash.clone();
let num_txs = block.block_txs.txs.len();
indexer.handle_block_disconnected(block)?;
log_chronik!(
"Chronik: block {} disconnected with {} txs\n",
block_hash,
num_txs,
);
Ok(())
}
fn finalize_block(&self, bindex: &ffi::CBlockIndex) -> Result<()> {
let mut indexer = self.indexer.blocking_write();
let block = indexer.load_chronik_block(&self.node.bridge, bindex)?;
let block_hash = block.db_block.hash.clone();
let num_txs = block.block_txs.txs.len();
indexer.handle_block_finalized(block)?;
log_chronik!(
"Chronik: block {} finalized with {} txs\n",
block_hash,
num_txs,
);
Ok(())
}
fn invalidate_block(
&self,
block: &ffi::CBlock,
bindex: &ffi::CBlockIndex,
) -> Result<()> {
// If there is no block undo for this block, skip the processing.
// This behavior can only occur for blocks building on a parked chain,
// and we don't have any interest for these blocks. This might as well
// be another chain.
let Ok(block_undo) = self.node.bridge.load_block_undo(bindex) else {
return Ok(());
};
let block =
chronik_bridge::ffi::bridge_block(block, &block_undo, bindex)?;
let mut indexer = self.indexer.blocking_write();
let block = indexer.make_chronik_block(block);
let block_hash = block.db_block.hash.clone();
let num_txs = block.block_txs.txs.len();
indexer.handle_block_invalidated(block)?;
log_chronik!(
"Chronik: block {block_hash} invalidated with {num_txs} txs\n",
);
Ok(())
}
fn block_if_paused(&self) {
self.pause.block_if_paused(&self.runtime);
}
}
impl std::fmt::Debug for Chronik {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Chronik {{ .. }}")
}
}
diff --git a/chronik/chronik-lib/src/ffi.rs b/chronik/chronik-lib/src/ffi.rs
index 1c7ef0fbe..a1a7318de 100644
--- a/chronik/chronik-lib/src/ffi.rs
+++ b/chronik/chronik-lib/src/ffi.rs
@@ -1,148 +1,150 @@
// 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.
//! Module containing the cxx definitions for the bridge from Rust to C++.
pub use self::ffi_inner::*;
use crate::bridge::{setup_chronik, Chronik};
#[allow(unsafe_code)]
#[cxx::bridge(namespace = "chronik_bridge")]
mod ffi_inner {
/// Params for setting up Chronik
#[derive(Debug)]
pub struct SetupParams {
/// Which net this Chronik instance is running on
pub net: Net,
/// Where the data of the blockchain is stored, independent of network
pub datadir: String,
/// Where the data of the blockchain is stored, dependent on network
/// (mainnet, testnet, regtest)
pub datadir_net: String,
/// Host addresses where the Chronik HTTP endpoint will be served
pub hosts: Vec<String>,
/// Default port for `hosts` if only an IP address is given
pub default_port: u16,
/// Whether to clear the DB before proceeding, e.g. when reindexing
pub wipe_db: bool,
/// Whether Chronik should index SLP/ALP token transactions
pub enable_token_index: bool,
/// Whether Chronik should index transactions by LOKAD ID
pub enable_lokad_id_index: bool,
/// Whether Chronik should index scripts by script hash
pub enable_scripthash_index: bool,
/// Whether pausing Chronik indexing is allowed
pub is_pause_allowed: bool,
/// Whether to output Chronik performance statistics into a perf/
/// folder
pub enable_perf_stats: bool,
/// Duration between WebSocket pings initiated by Chronik.
pub ws_ping_interval_secs: u64,
/// Enable permissive CORS on Chronik's HTTP endpoint
pub enable_cors: bool,
/// Tuning settings for the TxNumCache.
pub tx_num_cache: TxNumCacheSettings,
/// Electrum host ip:port param, or empty if disabled
pub electrum_hosts: Vec<String>,
/// Electrum default port
pub electrum_default_port: u16,
/// Electrum default protocol
pub electrum_default_protocol: u8,
/// Electrum certificate chain file path
pub electrum_cert_path: String,
/// Electrum private key file path
pub electrum_privkey_path: String,
+ /// Maximum transaction history length for an Electrum request
+ pub electrum_max_history: u32,
}
/// Settings for tuning the TxNumCache.
#[derive(Debug)]
pub struct TxNumCacheSettings {
/// How many buckets are on the belt
pub num_buckets: usize,
/// How many txs are cached in each bucket
pub bucket_size: usize,
}
/// Net we're running on
#[derive(Clone, Copy, Debug)]
pub enum Net {
/// Mainnet
Mainnet,
/// Testnet
Testnet,
/// Regtest
Regtest,
}
extern "Rust" {
type Chronik;
fn setup_chronik(params: SetupParams, node: &NodeContext) -> bool;
fn handle_tx_added_to_mempool(
&self,
ptx: &CTransaction,
spent_coins: &CxxVector<CCoin>,
time_first_seen: i64,
);
fn handle_tx_removed_from_mempool(&self, txid: [u8; 32]);
fn handle_block_connected(&self, block: &CBlock, bindex: &CBlockIndex);
fn handle_block_disconnected(
&self,
block: &CBlock,
bindex: &CBlockIndex,
);
fn handle_block_finalized(&self, bindex: &CBlockIndex);
fn handle_block_invalidated(
&self,
block: &CBlock,
bindex: &CBlockIndex,
);
}
unsafe extern "C++" {
include!("blockindex.h");
include!("chronik-cpp/chronik_validationinterface.h");
include!("coins.h");
include!("config.h");
include!("node/context.h");
include!("primitives/block.h");
include!("primitives/transaction.h");
/// CBlockIndex from blockindex.h
#[namespace = ""]
type CBlockIndex = chronik_bridge::ffi::CBlockIndex;
/// ::CBlock from primitives/block.h
#[namespace = ""]
type CBlock = chronik_bridge::ffi::CBlock;
/// ::Coin from coins.h (renamed to CCoin to prevent a name clash)
#[namespace = ""]
#[cxx_name = "Coin"]
type CCoin = chronik_bridge::ffi::CCoin;
/// ::Config from config.h
#[namespace = ""]
type Config = chronik_bridge::ffi::Config;
/// ::CTransaction from primitives/transaction.h
#[namespace = ""]
type CTransaction = chronik_bridge::ffi::CTransaction;
/// NodeContext from node/context.h
#[namespace = "node"]
type NodeContext = chronik_bridge::ffi::NodeContext;
/// Bridge to bitcoind to access the node
type ChronikBridge = chronik_bridge::ffi::ChronikBridge;
/// Register the Chronik instance as CValidationInterface to receive
/// chain updates from the node.
#[namespace = "chronik"]
fn StartChronikValidationInterface(
node: &NodeContext,
chronik: Box<Chronik>,
);
}
}
diff --git a/src/init.cpp b/src/init.cpp
index 679a2919d..3ec009ec1 100644
--- a/src/init.cpp
+++ b/src/init.cpp
@@ -1,3010 +1,3015 @@
// Copyright (c) 2009-2010 Satoshi Nakamoto
// Copyright (c) 2009-2018 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#if defined(HAVE_CONFIG_H)
#include <config/bitcoin-config.h>
#endif
#include <init.h>
#include <kernel/mempool_persist.h>
#include <kernel/validation_cache_sizes.h>
#include <addrman.h>
#include <avalanche/avalanche.h>
#include <avalanche/processor.h>
#include <avalanche/proof.h> // For AVALANCHE_LEGACY_PROOF_DEFAULT
#include <avalanche/validation.h>
#include <avalanche/voterecord.h> // For AVALANCHE_VOTE_STALE_*
#include <banman.h>
#include <blockfilter.h>
#include <chain.h>
#include <chainparams.h>
#include <common/args.h>
#include <compat/sanity.h>
#include <config.h>
#include <consensus/amount.h>
#include <currencyunit.h>
#include <flatfile.h>
#include <hash.h>
#include <httprpc.h>
#include <httpserver.h>
#include <index/blockfilterindex.h>
#include <index/coinstatsindex.h>
#include <index/txindex.h>
#include <init/common.h>
#include <interfaces/chain.h>
#include <interfaces/node.h>
#include <mapport.h>
#include <mempool_args.h>
#include <net.h>
#include <net_permissions.h>
#include <net_processing.h>
#include <netbase.h>
#include <node/blockmanager_args.h>
#include <node/blockstorage.h>
#include <node/caches.h>
#include <node/chainstate.h>
#include <node/chainstatemanager_args.h>
#include <node/context.h>
#include <node/kernel_notifications.h>
#include <node/mempool_persist_args.h>
#include <node/miner.h>
#include <node/peerman_args.h>
#include <node/ui_interface.h>
#include <node/validation_cache_args.h>
#include <policy/block/rtt.h>
#include <policy/policy.h>
#include <policy/settings.h>
#include <rpc/blockchain.h>
#include <rpc/register.h>
#include <rpc/server.h>
#include <rpc/util.h>
#include <scheduler.h>
#include <script/scriptcache.h>
#include <script/sigcache.h>
#include <script/standard.h>
#include <shutdown.h>
#include <sync.h>
#include <timedata.h>
#include <torcontrol.h>
#include <txdb.h>
#include <txmempool.h>
#include <util/asmap.h>
#include <util/check.h>
#include <util/fs.h>
#include <util/fs_helpers.h>
#include <util/moneystr.h>
#include <util/string.h>
#include <util/syserror.h>
#include <util/thread.h>
#include <util/threadnames.h>
#include <util/translation.h>
#include <validation.h>
#include <validationinterface.h>
#include <walletinitinterface.h>
#include <boost/signals2/signal.hpp>
#if ENABLE_CHRONIK
#include <chronik-cpp/chronik.h>
#endif
#if ENABLE_ZMQ
#include <zmq/zmqabstractnotifier.h>
#include <zmq/zmqnotificationinterface.h>
#include <zmq/zmqrpc.h>
#endif
#ifndef WIN32
#include <cerrno>
#include <csignal>
#include <sys/stat.h>
#endif
#include <algorithm>
#include <condition_variable>
#include <cstdint>
#include <cstdio>
#include <fstream>
#include <functional>
#include <set>
#include <string>
#include <thread>
#include <vector>
using kernel::DEFAULT_STOPAFTERBLOCKIMPORT;
using kernel::DumpMempool;
using kernel::ValidationCacheSizes;
using node::ApplyArgsManOptions;
using node::BlockManager;
using node::CacheSizes;
using node::CalculateCacheSizes;
using node::DEFAULT_PERSIST_MEMPOOL;
using node::fReindex;
using node::KernelNotifications;
using node::LoadChainstate;
using node::MempoolPath;
using node::NodeContext;
using node::ShouldPersistMempool;
using node::ThreadImport;
using node::VerifyLoadedChainstate;
static const bool DEFAULT_PROXYRANDOMIZE = true;
static const bool DEFAULT_REST_ENABLE = false;
static constexpr bool DEFAULT_CHRONIK = false;
#ifdef WIN32
// Win32 LevelDB doesn't use filedescriptors, and the ones used for accessing
// block files don't count towards the fd_set size limit anyway.
#define MIN_CORE_FILEDESCRIPTORS 0
#else
#define MIN_CORE_FILEDESCRIPTORS 150
#endif
static const char *DEFAULT_ASMAP_FILENAME = "ip_asn.map";
static const std::string HEADERS_TIME_FILE_NAME{"headerstime.dat"};
/**
* The PID file facilities.
*/
static const char *BITCOIN_PID_FILENAME = "bitcoind.pid";
static fs::path GetPidFile(const ArgsManager &args) {
return AbsPathForConfigVal(args,
args.GetPathArg("-pid", BITCOIN_PID_FILENAME));
}
[[nodiscard]] static bool CreatePidFile(const ArgsManager &args) {
std::ofstream file{GetPidFile(args)};
if (file) {
#ifdef WIN32
tfm::format(file, "%d\n", GetCurrentProcessId());
#else
tfm::format(file, "%d\n", getpid());
#endif
return true;
} else {
return InitError(strprintf(_("Unable to create the PID file '%s': %s"),
fs::PathToString(GetPidFile(args)),
SysErrorString(errno)));
}
}
//////////////////////////////////////////////////////////////////////////////
//
// Shutdown
//
//
// Thread management and startup/shutdown:
//
// The network-processing threads are all part of a thread group created by
// AppInit() or the Qt main() function.
//
// A clean exit happens when StartShutdown() or the SIGTERM signal handler sets
// fRequestShutdown, which makes main thread's WaitForShutdown() interrupts the
// thread group.
// And then, WaitForShutdown() makes all other on-going threads in the thread
// group join the main thread.
// Shutdown() is then called to clean up database connections, and stop other
// threads that should only be stopped after the main network-processing threads
// have exited.
//
// Shutdown for Qt is very similar, only it uses a QTimer to detect
// ShutdownRequested() getting set, and then does the normal Qt shutdown thing.
//
void Interrupt(NodeContext &node) {
InterruptHTTPServer();
InterruptHTTPRPC();
InterruptRPC();
InterruptREST();
InterruptTorControl();
InterruptMapPort();
if (node.avalanche) {
// Avalanche needs to be stopped before we interrupt the thread group as
// the scheduler will stop working then.
node.avalanche->stopEventLoop();
}
if (node.connman) {
node.connman->Interrupt();
}
if (g_txindex) {
g_txindex->Interrupt();
}
ForEachBlockFilterIndex([](BlockFilterIndex &index) { index.Interrupt(); });
if (g_coin_stats_index) {
g_coin_stats_index->Interrupt();
}
}
void Shutdown(NodeContext &node) {
static Mutex g_shutdown_mutex;
TRY_LOCK(g_shutdown_mutex, lock_shutdown);
if (!lock_shutdown) {
return;
}
LogPrintf("%s: In progress...\n", __func__);
Assert(node.args);
/// Note: Shutdown() must be able to handle cases in which initialization
/// failed part of the way, for example if the data directory was found to
/// be locked. Be sure that anything that writes files or flushes caches
/// only does this if the respective module was initialized.
util::ThreadRename("shutoff");
if (node.mempool) {
node.mempool->AddTransactionsUpdated(1);
}
StopHTTPRPC();
StopREST();
StopRPC();
StopHTTPServer();
for (const auto &client : node.chain_clients) {
client->flush();
}
StopMapPort();
// Because avalanche and the network depend on each other, it is important
// to shut them down in this order:
// 1. Stop avalanche event loop.
// 2. Shutdown network processing.
// 3. Destroy avalanche::Processor.
// 4. Destroy CConnman
if (node.avalanche) {
node.avalanche->stopEventLoop();
}
// Because these depend on each-other, we make sure that neither can be
// using the other before destroying them.
if (node.peerman) {
UnregisterValidationInterface(node.peerman.get());
}
if (node.connman) {
node.connman->Stop();
}
StopTorControl();
// After everything has been shut down, but before things get flushed, stop
// the CScheduler/checkqueue, scheduler and load block thread.
if (node.scheduler) {
node.scheduler->stop();
}
if (node.chainman && node.chainman->m_load_block.joinable()) {
node.chainman->m_load_block.join();
}
StopScriptCheckWorkerThreads();
// After the threads that potentially access these pointers have been
// stopped, destruct and reset all to nullptr.
node.peerman.reset();
// Destroy various global instances
node.avalanche.reset();
node.connman.reset();
node.banman.reset();
node.addrman.reset();
if (node.mempool && node.mempool->GetLoadTried() &&
ShouldPersistMempool(*node.args)) {
DumpMempool(*node.mempool, MempoolPath(*node.args));
}
// FlushStateToDisk generates a ChainStateFlushed callback, which we should
// avoid missing
if (node.chainman) {
LOCK(cs_main);
for (Chainstate *chainstate : node.chainman->GetAll()) {
if (chainstate->CanFlushToDisk()) {
chainstate->ForceFlushStateToDisk();
}
}
}
// After there are no more peers/RPC left to give us new data which may
// generate CValidationInterface callbacks, flush them...
GetMainSignals().FlushBackgroundCallbacks();
#if ENABLE_CHRONIK
if (node.args->GetBoolArg("-chronik", DEFAULT_CHRONIK)) {
chronik::Stop();
}
#endif
// Stop and delete all indexes only after flushing background callbacks.
if (g_txindex) {
g_txindex->Stop();
g_txindex.reset();
}
if (g_coin_stats_index) {
g_coin_stats_index->Stop();
g_coin_stats_index.reset();
}
ForEachBlockFilterIndex([](BlockFilterIndex &index) { index.Stop(); });
DestroyAllBlockFilterIndexes();
// Any future callbacks will be dropped. This should absolutely be safe - if
// missing a callback results in an unrecoverable situation, unclean
// shutdown would too. The only reason to do the above flushes is to let the
// wallet catch up with our current chain to avoid any strange pruning edge
// cases and make next startup faster by avoiding rescan.
if (node.chainman) {
LOCK(cs_main);
for (Chainstate *chainstate : node.chainman->GetAll()) {
if (chainstate->CanFlushToDisk()) {
chainstate->ForceFlushStateToDisk();
chainstate->ResetCoinsViews();
}
}
node.chainman->DumpRecentHeadersTime(node.chainman->m_options.datadir /
HEADERS_TIME_FILE_NAME);
}
for (const auto &client : node.chain_clients) {
client->stop();
}
#if ENABLE_ZMQ
if (g_zmq_notification_interface) {
UnregisterValidationInterface(g_zmq_notification_interface.get());
g_zmq_notification_interface.reset();
}
#endif
node.chain_clients.clear();
UnregisterAllValidationInterfaces();
GetMainSignals().UnregisterBackgroundSignalScheduler();
init::UnsetGlobals();
node.mempool.reset();
node.chainman.reset();
node.scheduler.reset();
try {
if (!fs::remove(GetPidFile(*node.args))) {
LogPrintf("%s: Unable to remove PID file: File does not exist\n",
__func__);
}
} catch (const fs::filesystem_error &e) {
LogPrintf("%s: Unable to remove PID file: %s\n", __func__,
fsbridge::get_filesystem_error_message(e));
}
LogPrintf("%s: done\n", __func__);
}
/**
* Signal handlers are very limited in what they are allowed to do.
* The execution context the handler is invoked in is not guaranteed,
* so we restrict handler operations to just touching variables:
*/
#ifndef WIN32
static void HandleSIGTERM(int) {
StartShutdown();
}
static void HandleSIGHUP(int) {
LogInstance().m_reopen_file = true;
}
#else
static BOOL WINAPI consoleCtrlHandler(DWORD dwCtrlType) {
StartShutdown();
Sleep(INFINITE);
return true;
}
#endif
#ifndef WIN32
static void registerSignalHandler(int signal, void (*handler)(int)) {
struct sigaction sa;
sa.sa_handler = handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(signal, &sa, NULL);
}
#endif
static boost::signals2::connection rpc_notify_block_change_connection;
static void OnRPCStarted() {
rpc_notify_block_change_connection = uiInterface.NotifyBlockTip_connect(
std::bind(RPCNotifyBlockChange, std::placeholders::_2));
}
static void OnRPCStopped() {
rpc_notify_block_change_connection.disconnect();
RPCNotifyBlockChange(nullptr);
g_best_block_cv.notify_all();
LogPrint(BCLog::RPC, "RPC stopped.\n");
}
void SetupServerArgs(NodeContext &node) {
assert(!node.args);
node.args = &gArgs;
ArgsManager &argsman = *node.args;
SetupHelpOptions(argsman);
SetupCurrencyUnitOptions(argsman);
// server-only for now
argsman.AddArg("-help-debug",
"Print help message with debugging options and exit", false,
OptionsCategory::DEBUG_TEST);
init::AddLoggingArgs(argsman);
const auto defaultBaseParams =
CreateBaseChainParams(CBaseChainParams::MAIN);
const auto testnetBaseParams =
CreateBaseChainParams(CBaseChainParams::TESTNET);
const auto regtestBaseParams =
CreateBaseChainParams(CBaseChainParams::REGTEST);
const auto defaultChainParams =
CreateChainParams(argsman, CBaseChainParams::MAIN);
const auto testnetChainParams =
CreateChainParams(argsman, CBaseChainParams::TESTNET);
const auto regtestChainParams =
CreateChainParams(argsman, CBaseChainParams::REGTEST);
// Hidden Options
std::vector<std::string> hidden_args = {
"-dbcrashratio",
"-forcecompactdb",
"-maxaddrtosend",
"-parkdeepreorg",
"-automaticunparking",
"-replayprotectionactivationtime",
"-enableminerfund",
"-chronikallowpause",
"-chronikcors",
// GUI args. These will be overwritten by SetupUIArgs for the GUI
"-allowselfsignedrootcertificates",
"-choosedatadir",
"-lang=<lang>",
"-min",
"-resetguisettings",
"-rootcertificates=<file>",
"-splash",
"-uiplatform",
// TODO remove after the May. 2025 upgrade
"-schumpeteractivationtime",
};
// Set all of the args and their help
// When adding new options to the categories, please keep and ensure
// alphabetical ordering. Do not translate _(...) -help-debug options, Many
// technical terms, and only a very small audience, so is unnecessary stress
// to translators.
argsman.AddArg("-version", "Print version and exit", ArgsManager::ALLOW_ANY,
OptionsCategory::OPTIONS);
#if defined(HAVE_SYSTEM)
argsman.AddArg(
"-alertnotify=<cmd>",
"Execute command when a relevant alert is received or we see "
"a really long fork (%s in cmd is replaced by message)",
ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
#endif
argsman.AddArg(
"-assumevalid=<hex>",
strprintf(
"If this block is in the chain assume that it and its ancestors "
"are valid and potentially skip their script verification (0 to "
"verify all, default: %s, testnet: %s)",
defaultChainParams->GetConsensus().defaultAssumeValid.GetHex(),
testnetChainParams->GetConsensus().defaultAssumeValid.GetHex()),
ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-blocksdir=<dir>",
"Specify directory to hold blocks subdirectory for *.dat "
"files (default: <datadir>)",
ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-fastprune",
"Use smaller block files and lower minimum prune height for "
"testing purposes",
ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY,
OptionsCategory::DEBUG_TEST);
#if defined(HAVE_SYSTEM)
argsman.AddArg("-blocknotify=<cmd>",
"Execute command when the best block changes (%s in cmd is "
"replaced by block hash)",
ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
#endif
argsman.AddArg("-blockreconstructionextratxn=<n>",
strprintf("Extra transactions to keep in memory for compact "
"block reconstructions (default: %u)",
DEFAULT_BLOCK_RECONSTRUCTION_EXTRA_TXN),
ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg(
"-blocksonly",
strprintf("Whether to reject transactions from network peers. "
"Disables automatic broadcast and rebroadcast of "
"transactions, unless the source peer has the "
"'forcerelay' permission. RPC transactions are"
" not affected. (default: %u)",
DEFAULT_BLOCKSONLY),
ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-coinstatsindex",
strprintf("Maintain coinstats index used by the "
"gettxoutsetinfo RPC (default: %u)",
DEFAULT_COINSTATSINDEX),
ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg(
"-conf=<file>",
strprintf("Specify path to read-only configuration file. Relative "
"paths will be prefixed by datadir location. (default: %s)",
BITCOIN_CONF_FILENAME),
ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-datadir=<dir>", "Specify data directory",
ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg(
"-dbbatchsize",
strprintf("Maximum database write batch size in bytes (default: %u)",
DEFAULT_DB_BATCH_SIZE),
ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY,
OptionsCategory::OPTIONS);
argsman.AddArg(
"-dbcache=<n>",
strprintf("Set database cache size in MiB (%d to %d, default: %d)",
MIN_DB_CACHE_MB, MAX_DB_CACHE_MB, DEFAULT_DB_CACHE_MB),
ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg(
"-includeconf=<file>",
"Specify additional configuration file, relative to the -datadir path "
"(only useable from configuration file, not command line)",
ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-loadblock=<file>",
"Imports blocks from external file on startup",
ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-maxmempool=<n>",
strprintf("Keep the transaction memory pool below <n> "
"megabytes (default: %u)",
DEFAULT_MAX_MEMPOOL_SIZE_MB),
ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-maxorphantx=<n>",
strprintf("Keep at most <n> unconnectable transactions in "
"memory (default: %u)",
DEFAULT_MAX_ORPHAN_TRANSACTIONS),
ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-mempoolexpiry=<n>",
strprintf("Do not keep transactions in the mempool longer "
"than <n> hours (default: %u)",
DEFAULT_MEMPOOL_EXPIRY_HOURS),
ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg(
"-minimumchainwork=<hex>",
strprintf(
"Minimum work assumed to exist on a valid chain in hex "
"(default: %s, testnet: %s)",
defaultChainParams->GetConsensus().nMinimumChainWork.GetHex(),
testnetChainParams->GetConsensus().nMinimumChainWork.GetHex()),
ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY,
OptionsCategory::OPTIONS);
argsman.AddArg(
"-par=<n>",
strprintf("Set the number of script verification threads (%u to %d, 0 "
"= auto, <0 = leave that many cores free, default: %d)",
-GetNumCores(), MAX_SCRIPTCHECK_THREADS,
DEFAULT_SCRIPTCHECK_THREADS),
ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-persistmempool",
strprintf("Whether to save the mempool on shutdown and load "
"on restart (default: %u)",
DEFAULT_PERSIST_MEMPOOL),
ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg(
"-persistrecentheaderstime",
strprintf(
"Whether the node stores the recent headers reception time to a "
"file and load it upon startup. This is intended for mining nodes "
"to overestimate the real time target upon restart (default: %u)",
DEFAULT_STORE_RECENT_HEADERS_TIME),
ArgsManager::ALLOW_BOOL, OptionsCategory::OPTIONS);
argsman.AddArg(
"-pid=<file>",
strprintf("Specify pid file. Relative paths will be prefixed "
"by a net-specific datadir location. (default: %s)",
BITCOIN_PID_FILENAME),
ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg(
"-prune=<n>",
strprintf("Reduce storage requirements by enabling pruning (deleting) "
"of old blocks. This allows the pruneblockchain RPC to be "
"called to delete specific blocks and enables automatic "
"pruning of old blocks if a target size in MiB is provided. "
"This mode is incompatible with -txindex and -rescan. "
"Warning: Reverting this setting requires re-downloading the "
"entire blockchain. (default: 0 = disable pruning blocks, "
"1 = allow manual pruning via RPC, >=%u = automatically "
"prune block files to stay under the specified target size "
"in MiB)",
MIN_DISK_SPACE_FOR_BLOCK_FILES / 1024 / 1024),
ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg(
"-reindex-chainstate",
"Rebuild chain state from the currently indexed blocks. When "
"in pruning mode or if blocks on disk might be corrupted, use "
"full -reindex instead.",
ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg(
"-reindex",
"Rebuild chain state and block index from the blk*.dat files on disk",
ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg(
"-settings=<file>",
strprintf(
"Specify path to dynamic settings data file. Can be disabled with "
"-nosettings. File is written at runtime and not meant to be "
"edited by users (use %s instead for custom settings). Relative "
"paths will be prefixed by datadir location. (default: %s)",
BITCOIN_CONF_FILENAME, BITCOIN_SETTINGS_FILENAME),
ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
#if HAVE_SYSTEM
argsman.AddArg("-startupnotify=<cmd>", "Execute command on startup.",
ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
#endif
#ifndef WIN32
argsman.AddArg(
"-sysperms",
"Create new files with system default permissions, instead of umask "
"077 (only effective with disabled wallet functionality)",
ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
#else
hidden_args.emplace_back("-sysperms");
#endif
argsman.AddArg("-txindex",
strprintf("Maintain a full transaction index, used by the "
"getrawtransaction rpc call (default: %d)",
DEFAULT_TXINDEX),
ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
#if ENABLE_CHRONIK
argsman.AddArg(
"-chronik",
strprintf("Enable the Chronik indexer, which can be read via a "
"dedicated HTTP/Protobuf interface (default: %d)",
DEFAULT_CHRONIK),
ArgsManager::ALLOW_BOOL, OptionsCategory::CHRONIK);
argsman.AddArg(
"-chronikbind=<addr>[:port]",
strprintf(
"Bind the Chronik indexer to the given address to listen for "
"HTTP/Protobuf connections to access the index. Unlike the "
"JSON-RPC, it's ok to have this publicly exposed on the internet. "
"This option can be specified multiple times (default: %s; default "
"port: %u, testnet: %u, regtest: %u)",
Join(chronik::DEFAULT_BINDS, ", "),
defaultBaseParams->ChronikPort(), testnetBaseParams->ChronikPort(),
regtestBaseParams->ChronikPort()),
ArgsManager::ALLOW_STRING | ArgsManager::NETWORK_ONLY,
OptionsCategory::CHRONIK);
argsman.AddArg("-chroniktokenindex",
"Enable token indexing in Chronik (default: 1)",
ArgsManager::ALLOW_BOOL, OptionsCategory::CHRONIK);
argsman.AddArg("-chroniklokadidindex",
"Enable LOKAD ID indexing in Chronik (default: 1)",
ArgsManager::ALLOW_BOOL, OptionsCategory::CHRONIK);
argsman.AddArg("-chronikreindex",
"Reindex the Chronik indexer from genesis, but leave the "
"other indexes untouched",
ArgsManager::ALLOW_BOOL, OptionsCategory::CHRONIK);
argsman.AddArg(
"-chroniktxnumcachebuckets",
strprintf(
"Tuning param of the TxNumCache, specifies how many buckets "
"to use on the belt. Caution against setting this too high, "
"it may slow down indexing. Set to 0 to disable. (default: %d)",
chronik::DEFAULT_TX_NUM_CACHE_BUCKETS),
ArgsManager::ALLOW_INT, OptionsCategory::CHRONIK);
argsman.AddArg(
"-chroniktxnumcachebucketsize",
strprintf(
"Tuning param of the TxNumCache, specifies the size of each bucket "
"on the belt. Unlike the number of buckets, this may be increased "
"without much danger of slowing the indexer down. The total cache "
"size will be `num_buckets * bucket_size * 40B`, so by default the "
"cache will require %dkB of memory. (default: %d)",
chronik::DEFAULT_TX_NUM_CACHE_BUCKETS *
chronik::DEFAULT_TX_NUM_CACHE_BUCKET_SIZE * 40 / 1000,
chronik::DEFAULT_TX_NUM_CACHE_BUCKET_SIZE),
ArgsManager::ALLOW_INT, OptionsCategory::CHRONIK);
argsman.AddArg("-chronikperfstats",
"Output some performance statistics (e.g. num cache hits, "
"seconds spent) into a <datadir>/perf folder. (default: 0)",
ArgsManager::ALLOW_BOOL, OptionsCategory::CHRONIK);
argsman.AddArg(
"-chronikscripthashindex",
"Enable the scripthash index for the Chronik indexer (default: 0) ",
ArgsManager::ALLOW_BOOL, OptionsCategory::CHRONIK);
argsman.AddArg(
"-chronikelectrumbind=<addr>[:port][:t|s]",
strprintf(
"Bind the Chronik Electrum interface to the given "
"address:port:protocol. If not set, the Electrum interface will "
"not start. This option can be specified multiple times. The "
"protocol is selected by a single letter, where 't' means TCP and "
"'s' means TLS. If TLS is selected, the certificate chain and "
"private key must both be passed (see -chronikelectrumcert and "
"-chronikelectrumprivkey (default: disabled; default port: %u, "
"testnet: %u, regtest: %u; default protocol: TCP)",
defaultBaseParams->ChronikElectrumPort(),
testnetBaseParams->ChronikElectrumPort(),
regtestBaseParams->ChronikElectrumPort()),
ArgsManager::ALLOW_STRING | ArgsManager::NETWORK_ONLY,
OptionsCategory::HIDDEN);
argsman.AddArg(
"-chronikelectrumcert",
"Path to the certificate file to be used by the Chronik Electrum "
"server when the TLS protocol is selected. The file should contain "
"the whole certificate chain (typically a .pem file). If used the "
"-chronikelectrumprivkey must be set as well.",
ArgsManager::ALLOW_STRING | ArgsManager::NETWORK_ONLY,
OptionsCategory::HIDDEN);
argsman.AddArg(
"-chronikelectrumprivkey",
"Path to the private key file to be used by the Chronik Electrum "
"server when the TLS protocol is selected. If used the "
"-chronikelectrumcert must be set as well.",
ArgsManager::ALLOW_STRING | ArgsManager::NETWORK_ONLY,
OptionsCategory::HIDDEN);
+ argsman.AddArg(
+ "-chronikelectrummaxhistory",
+ strprintf("Largest tx history we are willing to serve. (default: %u)",
+ chronik::DEFAULT_ELECTRUM_MAX_HISTORY),
+ ArgsManager::ALLOW_INT, OptionsCategory::HIDDEN);
#endif
argsman.AddArg(
"-blockfilterindex=<type>",
strprintf("Maintain an index of compact filters by block "
"(default: %s, values: %s).",
DEFAULT_BLOCKFILTERINDEX, ListBlockFilterTypes()) +
" If <type> is not supplied or if <type> = 1, indexes for "
"all known types are enabled.",
ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg(
"-usecashaddr",
"Use Cash Address for destination encoding instead of base58 "
"(activate by default on Jan, 14)",
ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg(
"-addnode=<ip>",
"Add a node to connect to and attempt to keep the connection "
"open (see the `addnode` RPC command help for more info)",
ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY,
OptionsCategory::CONNECTION);
argsman.AddArg("-asmap=<file>",
strprintf("Specify asn mapping used for bucketing of the "
"peers (default: %s). Relative paths will be "
"prefixed by the net-specific datadir location.",
DEFAULT_ASMAP_FILENAME),
ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
argsman.AddArg("-bantime=<n>",
strprintf("Default duration (in seconds) of manually "
"configured bans (default: %u)",
DEFAULT_MISBEHAVING_BANTIME),
ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
argsman.AddArg(
"-bind=<addr>[:<port>][=onion]",
strprintf("Bind to given address and always listen on it (default: "
"0.0.0.0). Use [host]:port notation for IPv6. Append =onion "
"to tag any incoming connections to that address and port as "
"incoming Tor connections (default: 127.0.0.1:%u=onion, "
"testnet: 127.0.0.1:%u=onion, regtest: 127.0.0.1:%u=onion)",
defaultBaseParams->OnionServiceTargetPort(),
testnetBaseParams->OnionServiceTargetPort(),
regtestBaseParams->OnionServiceTargetPort()),
ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY,
OptionsCategory::CONNECTION);
argsman.AddArg(
"-connect=<ip>",
"Connect only to the specified node(s); -connect=0 disables automatic "
"connections (the rules for this peer are the same as for -addnode)",
ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY,
OptionsCategory::CONNECTION);
argsman.AddArg(
"-discover",
"Discover own IP addresses (default: 1 when listening and no "
"-externalip or -proxy)",
ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
argsman.AddArg("-dns",
strprintf("Allow DNS lookups for -addnode, -seednode and "
"-connect (default: %d)",
DEFAULT_NAME_LOOKUP),
ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
argsman.AddArg(
"-dnsseed",
strprintf(
"Query for peer addresses via DNS lookup, if low on addresses "
"(default: %u unless -connect used)",
DEFAULT_DNSSEED),
ArgsManager::ALLOW_BOOL, OptionsCategory::CONNECTION);
argsman.AddArg("-externalip=<ip>", "Specify your own public address",
ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
argsman.AddArg(
"-fixedseeds",
strprintf(
"Allow fixed seeds if DNS seeds don't provide peers (default: %u)",
DEFAULT_FIXEDSEEDS),
ArgsManager::ALLOW_BOOL, OptionsCategory::CONNECTION);
argsman.AddArg(
"-forcednsseed",
strprintf(
"Always query for peer addresses via DNS lookup (default: %d)",
DEFAULT_FORCEDNSSEED),
ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
argsman.AddArg("-overridednsseed",
"If set, only use the specified DNS seed when "
"querying for peer addresses via DNS lookup.",
ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
argsman.AddArg(
"-listen",
"Accept connections from outside (default: 1 if no -proxy or -connect)",
ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
argsman.AddArg(
"-listenonion",
strprintf("Automatically create Tor onion service (default: %d)",
DEFAULT_LISTEN_ONION),
ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
argsman.AddArg(
"-maxconnections=<n>",
strprintf("Maintain at most <n> connections to peers. The effective "
"limit depends on system limitations and might be lower than "
"the specified value (default: %u)",
DEFAULT_MAX_PEER_CONNECTIONS),
ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
argsman.AddArg("-maxreceivebuffer=<n>",
strprintf("Maximum per-connection receive buffer, <n>*1000 "
"bytes (default: %u)",
DEFAULT_MAXRECEIVEBUFFER),
ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
argsman.AddArg(
"-maxsendbuffer=<n>",
strprintf(
"Maximum per-connection send buffer, <n>*1000 bytes (default: %u)",
DEFAULT_MAXSENDBUFFER),
ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
argsman.AddArg(
"-maxtimeadjustment",
strprintf("Maximum allowed median peer time offset adjustment. Local "
"perspective of time may be influenced by peers forward or "
"backward by this amount. (default: %u seconds)",
DEFAULT_MAX_TIME_ADJUSTMENT),
ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
argsman.AddArg("-onion=<ip:port>",
strprintf("Use separate SOCKS5 proxy to reach peers via Tor "
"onion services (default: %s)",
"-proxy"),
ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
argsman.AddArg("-i2psam=<ip:port>",
"I2P SAM proxy to reach I2P peers and accept I2P "
"connections (default: none)",
ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
argsman.AddArg(
"-i2pacceptincoming",
"If set and -i2psam is also set then incoming I2P connections are "
"accepted via the SAM proxy. If this is not set but -i2psam is set "
"then only outgoing connections will be made to the I2P network. "
"Ignored if -i2psam is not set. Listening for incoming I2P connections "
"is done through the SAM proxy, not by binding to a local address and "
"port (default: 1)",
ArgsManager::ALLOW_BOOL, OptionsCategory::CONNECTION);
argsman.AddArg(
"-onlynet=<net>",
"Make outgoing connections only through network <net> (" +
Join(GetNetworkNames(), ", ") +
"). Incoming connections are not affected by this option. This "
"option can be specified multiple times to allow multiple "
"networks. Warning: if it is used with non-onion networks "
"and the -onion or -proxy option is set, then outbound onion "
"connections will still be made; use -noonion or -onion=0 to "
"disable outbound onion connections in this case",
ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
argsman.AddArg("-peerbloomfilters",
strprintf("Support filtering of blocks and transaction with "
"bloom filters (default: %d)",
DEFAULT_PEERBLOOMFILTERS),
ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
argsman.AddArg(
"-peerblockfilters",
strprintf(
"Serve compact block filters to peers per BIP 157 (default: %u)",
DEFAULT_PEERBLOCKFILTERS),
ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
argsman.AddArg("-permitbaremultisig",
strprintf("Relay non-P2SH multisig (default: %d)",
DEFAULT_PERMIT_BAREMULTISIG),
ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
// TODO: remove the sentence "Nodes not using ... incoming connections."
// once the changes from https://github.com/bitcoin/bitcoin/pull/23542 have
// become widespread.
argsman.AddArg("-port=<port>",
strprintf("Listen for connections on <port>. Nodes not "
"using the default ports (default: %u, "
"testnet: %u, regtest: %u) are unlikely to get "
"incoming connections. Not relevant for I2P (see "
"doc/i2p.md).",
defaultChainParams->GetDefaultPort(),
testnetChainParams->GetDefaultPort(),
regtestChainParams->GetDefaultPort()),
ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY,
OptionsCategory::CONNECTION);
argsman.AddArg("-proxy=<ip:port>", "Connect through SOCKS5 proxy",
ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
argsman.AddArg(
"-proxyrandomize",
strprintf("Randomize credentials for every proxy connection. "
"This enables Tor stream isolation (default: %d)",
DEFAULT_PROXYRANDOMIZE),
ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
argsman.AddArg(
"-seednode=<ip>",
"Connect to a node to retrieve peer addresses, and disconnect",
ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
argsman.AddArg(
"-networkactive",
"Enable all P2P network activity (default: 1). Can be changed "
"by the setnetworkactive RPC command",
ArgsManager::ALLOW_BOOL, OptionsCategory::CONNECTION);
argsman.AddArg("-timeout=<n>",
strprintf("Specify connection timeout in milliseconds "
"(minimum: 1, default: %d)",
DEFAULT_CONNECT_TIMEOUT),
ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
argsman.AddArg(
"-peertimeout=<n>",
strprintf("Specify p2p connection timeout in seconds. This option "
"determines the amount of time a peer may be inactive before "
"the connection to it is dropped. (minimum: 1, default: %d)",
DEFAULT_PEER_CONNECT_TIMEOUT),
true, OptionsCategory::CONNECTION);
argsman.AddArg(
"-torcontrol=<ip>:<port>",
strprintf(
"Tor control port to use if onion listening enabled (default: %s)",
DEFAULT_TOR_CONTROL),
ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
argsman.AddArg("-torpassword=<pass>",
"Tor control port password (default: empty)",
ArgsManager::ALLOW_ANY | ArgsManager::SENSITIVE,
OptionsCategory::CONNECTION);
#ifdef USE_UPNP
#if USE_UPNP
argsman.AddArg("-upnp",
"Use UPnP to map the listening port (default: 1 when "
"listening and no -proxy)",
ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
#else
argsman.AddArg(
"-upnp",
strprintf("Use UPnP to map the listening port (default: %u)", 0),
ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
#endif
#else
hidden_args.emplace_back("-upnp");
#endif
#ifdef USE_NATPMP
argsman.AddArg(
"-natpmp",
strprintf("Use NAT-PMP to map the listening port (default: %s)",
DEFAULT_NATPMP ? "1 when listening and no -proxy" : "0"),
ArgsManager::ALLOW_BOOL, OptionsCategory::CONNECTION);
#else
hidden_args.emplace_back("-natpmp");
#endif // USE_NATPMP
argsman.AddArg(
"-whitebind=<[permissions@]addr>",
"Bind to the given address and add permission flags to the peers "
"connecting to it."
"Use [host]:port notation for IPv6. Allowed permissions: " +
Join(NET_PERMISSIONS_DOC, ", ") +
". "
"Specify multiple permissions separated by commas (default: "
"download,noban,mempool,relay). Can be specified multiple times.",
ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
argsman.AddArg("-whitelist=<[permissions@]IP address or network>",
"Add permission flags to the peers using the given "
"IP address (e.g. 1.2.3.4) or CIDR-notated network "
"(e.g. 1.2.3.0/24). "
"Uses the same permissions as -whitebind. "
"Additional flags \"in\" and \"out\" control whether "
"permissions apply to incoming connections and/or manual "
"(default: incoming only). "
"Can be specified multiple times.",
ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
argsman.AddArg(
"-maxuploadtarget=<n>",
strprintf("Tries to keep outbound traffic under the given target (in "
"MiB per 24h). Limit does not apply to peers with 'download' "
"permission. 0 = no limit (default: %d)",
DEFAULT_MAX_UPLOAD_TARGET),
ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
g_wallet_init_interface.AddWalletOptions(argsman);
#if ENABLE_ZMQ
argsman.AddArg("-zmqpubhashblock=<address>",
"Enable publish hash block in <address>",
ArgsManager::ALLOW_ANY, OptionsCategory::ZMQ);
argsman.AddArg("-zmqpubhashtx=<address>",
"Enable publish hash transaction in <address>",
ArgsManager::ALLOW_ANY, OptionsCategory::ZMQ);
argsman.AddArg("-zmqpubrawblock=<address>",
"Enable publish raw block in <address>",
ArgsManager::ALLOW_ANY, OptionsCategory::ZMQ);
argsman.AddArg("-zmqpubrawtx=<address>",
"Enable publish raw transaction in <address>",
ArgsManager::ALLOW_ANY, OptionsCategory::ZMQ);
argsman.AddArg("-zmqpubsequence=<address>",
"Enable publish hash block and tx sequence in <address>",
ArgsManager::ALLOW_ANY, OptionsCategory::ZMQ);
argsman.AddArg(
"-zmqpubhashblockhwm=<n>",
strprintf("Set publish hash block outbound message high water "
"mark (default: %d)",
CZMQAbstractNotifier::DEFAULT_ZMQ_SNDHWM),
ArgsManager::ALLOW_ANY, OptionsCategory::ZMQ);
argsman.AddArg(
"-zmqpubhashtxhwm=<n>",
strprintf("Set publish hash transaction outbound message high "
"water mark (default: %d)",
CZMQAbstractNotifier::DEFAULT_ZMQ_SNDHWM),
false, OptionsCategory::ZMQ);
argsman.AddArg(
"-zmqpubrawblockhwm=<n>",
strprintf("Set publish raw block outbound message high water "
"mark (default: %d)",
CZMQAbstractNotifier::DEFAULT_ZMQ_SNDHWM),
ArgsManager::ALLOW_ANY, OptionsCategory::ZMQ);
argsman.AddArg(
"-zmqpubrawtxhwm=<n>",
strprintf("Set publish raw transaction outbound message high "
"water mark (default: %d)",
CZMQAbstractNotifier::DEFAULT_ZMQ_SNDHWM),
ArgsManager::ALLOW_ANY, OptionsCategory::ZMQ);
argsman.AddArg("-zmqpubsequencehwm=<n>",
strprintf("Set publish hash sequence message high water mark"
" (default: %d)",
CZMQAbstractNotifier::DEFAULT_ZMQ_SNDHWM),
ArgsManager::ALLOW_ANY, OptionsCategory::ZMQ);
#else
hidden_args.emplace_back("-zmqpubhashblock=<address>");
hidden_args.emplace_back("-zmqpubhashtx=<address>");
hidden_args.emplace_back("-zmqpubrawblock=<address>");
hidden_args.emplace_back("-zmqpubrawtx=<address>");
hidden_args.emplace_back("-zmqpubsequence=<n>");
hidden_args.emplace_back("-zmqpubhashblockhwm=<n>");
hidden_args.emplace_back("-zmqpubhashtxhwm=<n>");
hidden_args.emplace_back("-zmqpubrawblockhwm=<n>");
hidden_args.emplace_back("-zmqpubrawtxhwm=<n>");
hidden_args.emplace_back("-zmqpubsequencehwm=<n>");
#endif
argsman.AddArg(
"-checkblocks=<n>",
strprintf("How many blocks to check at startup (default: %u, 0 = all)",
DEFAULT_CHECKBLOCKS),
ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY,
OptionsCategory::DEBUG_TEST);
argsman.AddArg("-checklevel=<n>",
strprintf("How thorough the block verification of "
"-checkblocks is: %s (0-4, default: %u)",
Join(CHECKLEVEL_DOC, ", "), DEFAULT_CHECKLEVEL),
ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY,
OptionsCategory::DEBUG_TEST);
argsman.AddArg("-checkblockindex",
strprintf("Do a consistency check for the block tree, "
"chainstate, and other validation data structures "
"occasionally. (default: %u, regtest: %u)",
defaultChainParams->DefaultConsistencyChecks(),
regtestChainParams->DefaultConsistencyChecks()),
ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY,
OptionsCategory::DEBUG_TEST);
argsman.AddArg("-checkaddrman=<n>",
strprintf("Run addrman consistency checks every <n> "
"operations. Use 0 to disable. (default: %u)",
DEFAULT_ADDRMAN_CONSISTENCY_CHECKS),
ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY,
OptionsCategory::DEBUG_TEST);
argsman.AddArg(
"-checkmempool=<n>",
strprintf("Run mempool consistency checks every <n> transactions. Use "
"0 to disable. (default: %u, regtest: %u)",
defaultChainParams->DefaultConsistencyChecks(),
regtestChainParams->DefaultConsistencyChecks()),
ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY,
OptionsCategory::DEBUG_TEST);
argsman.AddArg("-checkpoints",
strprintf("Only accept block chain matching built-in "
"checkpoints (default: %d)",
DEFAULT_CHECKPOINTS_ENABLED),
ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY,
OptionsCategory::DEBUG_TEST);
argsman.AddArg("-deprecatedrpc=<method>",
"Allows deprecated RPC method(s) to be used",
ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY,
OptionsCategory::DEBUG_TEST);
argsman.AddArg(
"-stopafterblockimport",
strprintf("Stop running after importing blocks from disk (default: %d)",
DEFAULT_STOPAFTERBLOCKIMPORT),
ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY,
OptionsCategory::DEBUG_TEST);
argsman.AddArg("-stopatheight",
strprintf("Stop running after reaching the given height in "
"the main chain (default: %u)",
DEFAULT_STOPATHEIGHT),
ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY,
OptionsCategory::DEBUG_TEST);
argsman.AddArg("-addrmantest", "Allows to test address relay on localhost",
ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY,
OptionsCategory::DEBUG_TEST);
argsman.AddArg("-capturemessages", "Capture all P2P messages to disk",
ArgsManager::ALLOW_BOOL | ArgsManager::DEBUG_ONLY,
OptionsCategory::DEBUG_TEST);
argsman.AddArg("-mocktime=<n>",
"Replace actual time with " + UNIX_EPOCH_TIME +
" (default: 0)",
ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY,
OptionsCategory::DEBUG_TEST);
argsman.AddArg(
"-maxsigcachesize=<n>",
strprintf("Limit size of signature cache to <n> MiB (default: %u)",
DEFAULT_MAX_SIG_CACHE_BYTES >> 20),
ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY,
OptionsCategory::DEBUG_TEST);
argsman.AddArg(
"-maxscriptcachesize=<n>",
strprintf("Limit size of script cache to <n> MiB (default: %u)",
DEFAULT_MAX_SCRIPT_CACHE_BYTES >> 20),
ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY,
OptionsCategory::DEBUG_TEST);
argsman.AddArg("-maxtipage=<n>",
strprintf("Maximum tip age in seconds to consider node in "
"initial block download (default: %u)",
Ticks<std::chrono::seconds>(DEFAULT_MAX_TIP_AGE)),
ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY,
OptionsCategory::DEBUG_TEST);
argsman.AddArg("-uacomment=<cmt>",
"Append comment to the user agent string",
ArgsManager::ALLOW_ANY, OptionsCategory::DEBUG_TEST);
argsman.AddArg("-uaclientname=<clientname>", "Set user agent client name",
ArgsManager::ALLOW_ANY, OptionsCategory::DEBUG_TEST);
argsman.AddArg("-uaclientversion=<clientversion>",
"Set user agent client version", ArgsManager::ALLOW_ANY,
OptionsCategory::DEBUG_TEST);
SetupChainParamsBaseOptions(argsman);
argsman.AddArg(
"-acceptnonstdtxn",
strprintf(
"Relay and mine \"non-standard\" transactions (%sdefault: %u)",
"testnet/regtest only; ", defaultChainParams->RequireStandard()),
ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY,
OptionsCategory::NODE_RELAY);
argsman.AddArg("-excessiveblocksize=<n>",
strprintf("Do not accept blocks larger than this limit, in "
"bytes (default: %d)",
DEFAULT_MAX_BLOCK_SIZE),
ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY,
OptionsCategory::NODE_RELAY);
const auto &ticker = Currency::get().ticker;
argsman.AddArg(
"-dustrelayfee=<amt>",
strprintf("Fee rate (in %s/kB) used to define dust, the value of an "
"output such that it will cost about 1/3 of its value in "
"fees at this fee rate to spend it. (default: %s)",
ticker, FormatMoney(DUST_RELAY_TX_FEE)),
ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY,
OptionsCategory::NODE_RELAY);
argsman.AddArg(
"-bytespersigcheck",
strprintf("Equivalent bytes per sigCheck in transactions for relay and "
"mining (default: %u).",
DEFAULT_BYTES_PER_SIGCHECK),
ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY);
argsman.AddArg(
"-bytespersigop",
strprintf("DEPRECATED: Equivalent bytes per sigCheck in transactions "
"for relay and mining (default: %u). This has been "
"deprecated since v0.26.8 and will be removed in the future, "
"please use -bytespersigcheck instead.",
DEFAULT_BYTES_PER_SIGCHECK),
ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY);
argsman.AddArg(
"-datacarrier",
strprintf("Relay and mine data carrier transactions (default: %d)",
DEFAULT_ACCEPT_DATACARRIER),
ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY);
argsman.AddArg(
"-datacarriersize",
strprintf("Maximum size of data in data carrier transactions "
"we relay and mine (default: %u)",
MAX_OP_RETURN_RELAY),
ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY);
argsman.AddArg(
"-minrelaytxfee=<amt>",
strprintf("Fees (in %s/kB) smaller than this are rejected for "
"relaying, mining and transaction creation (default: %s)",
ticker, FormatMoney(DEFAULT_MIN_RELAY_TX_FEE_PER_KB)),
ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY);
argsman.AddArg(
"-whitelistrelay",
strprintf("Add 'relay' permission to whitelisted peers "
"with default permissions. This will accept relayed "
"transactions even when not relaying transactions "
"(default: %d)",
DEFAULT_WHITELISTRELAY),
ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY);
argsman.AddArg(
"-whitelistforcerelay",
strprintf("Add 'forcerelay' permission to whitelisted peers "
"with default permissions. This will relay transactions "
"even if the transactions were already in the mempool "
"(default: %d)",
DEFAULT_WHITELISTFORCERELAY),
ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY);
argsman.AddArg("-blockmaxsize=<n>",
strprintf("Set maximum block size in bytes (default: %d)",
DEFAULT_MAX_GENERATED_BLOCK_SIZE),
ArgsManager::ALLOW_ANY, OptionsCategory::BLOCK_CREATION);
argsman.AddArg(
"-blockmintxfee=<amt>",
strprintf("Set lowest fee rate (in %s/kB) for transactions to "
"be included in block creation. (default: %s)",
ticker, FormatMoney(DEFAULT_BLOCK_MIN_TX_FEE_PER_KB)),
ArgsManager::ALLOW_ANY, OptionsCategory::BLOCK_CREATION);
argsman.AddArg("-simplegbt",
"Use a simplified getblocktemplate output (default: 0)",
ArgsManager::ALLOW_BOOL, OptionsCategory::BLOCK_CREATION);
argsman.AddArg("-blockversion=<n>",
"Override block version to test forking scenarios",
ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY,
OptionsCategory::BLOCK_CREATION);
argsman.AddArg("-server", "Accept command line and JSON-RPC commands",
ArgsManager::ALLOW_ANY, OptionsCategory::RPC);
argsman.AddArg("-rest",
strprintf("Accept public REST requests (default: %d)",
DEFAULT_REST_ENABLE),
ArgsManager::ALLOW_ANY, OptionsCategory::RPC);
argsman.AddArg(
"-rpcbind=<addr>[:port]",
"Bind to given address to listen for JSON-RPC connections. Do not "
"expose the RPC server to untrusted networks such as the public "
"internet! This option is ignored unless -rpcallowip is also passed. "
"Port is optional and overrides -rpcport. Use [host]:port notation "
"for IPv6. This option can be specified multiple times (default: "
"127.0.0.1 and ::1 i.e., localhost)",
ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY |
ArgsManager::SENSITIVE,
OptionsCategory::RPC);
argsman.AddArg(
"-rpcdoccheck",
strprintf("Throw a non-fatal error at runtime if the documentation for "
"an RPC is incorrect (default: %u)",
DEFAULT_RPC_DOC_CHECK),
ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::RPC);
argsman.AddArg(
"-rpccookiefile=<loc>",
"Location of the auth cookie. Relative paths will be prefixed "
"by a net-specific datadir location. (default: data dir)",
ArgsManager::ALLOW_ANY, OptionsCategory::RPC);
argsman.AddArg("-rpcuser=<user>", "Username for JSON-RPC connections",
ArgsManager::ALLOW_ANY | ArgsManager::SENSITIVE,
OptionsCategory::RPC);
argsman.AddArg("-rpcpassword=<pw>", "Password for JSON-RPC connections",
ArgsManager::ALLOW_ANY | ArgsManager::SENSITIVE,
OptionsCategory::RPC);
argsman.AddArg(
"-rpcwhitelist=<whitelist>",
"Set a whitelist to filter incoming RPC calls for a specific user. The "
"field <whitelist> comes in the format: <USERNAME>:<rpc 1>,<rpc "
"2>,...,<rpc n>. If multiple whitelists are set for a given user, they "
"are set-intersected. See -rpcwhitelistdefault documentation for "
"information on default whitelist behavior.",
ArgsManager::ALLOW_ANY, OptionsCategory::RPC);
argsman.AddArg(
"-rpcwhitelistdefault",
"Sets default behavior for rpc whitelisting. Unless "
"rpcwhitelistdefault is set to 0, if any -rpcwhitelist is set, the rpc "
"server acts as if all rpc users are subject to "
"empty-unless-otherwise-specified whitelists. If rpcwhitelistdefault "
"is set to 1 and no -rpcwhitelist is set, rpc server acts as if all "
"rpc users are subject to empty whitelists.",
ArgsManager::ALLOW_BOOL, OptionsCategory::RPC);
argsman.AddArg(
"-rpcauth=<userpw>",
"Username and HMAC-SHA-256 hashed password for JSON-RPC connections. "
"The field <userpw> comes in the format: <USERNAME>:<SALT>$<HASH>. A "
"canonical python script is included in share/rpcauth. The client then "
"connects normally using the rpcuser=<USERNAME>/rpcpassword=<PASSWORD> "
"pair of arguments. This option can be specified multiple times",
ArgsManager::ALLOW_ANY | ArgsManager::SENSITIVE, OptionsCategory::RPC);
argsman.AddArg("-rpcport=<port>",
strprintf("Listen for JSON-RPC connections on <port> "
"(default: %u, testnet: %u, regtest: %u)",
defaultBaseParams->RPCPort(),
testnetBaseParams->RPCPort(),
regtestBaseParams->RPCPort()),
ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY,
OptionsCategory::RPC);
argsman.AddArg(
"-rpcallowip=<ip>",
"Allow JSON-RPC connections from specified source. Valid for "
"<ip> are a single IP (e.g. 1.2.3.4), a network/netmask (e.g. "
"1.2.3.4/255.255.255.0) or a network/CIDR (e.g. 1.2.3.4/24). "
"This option can be specified multiple times",
ArgsManager::ALLOW_ANY, OptionsCategory::RPC);
argsman.AddArg(
"-rpcthreads=<n>",
strprintf(
"Set the number of threads to service RPC calls (default: %d)",
DEFAULT_HTTP_THREADS),
ArgsManager::ALLOW_ANY, OptionsCategory::RPC);
argsman.AddArg(
"-rpccorsdomain=value",
"Domain from which to accept cross origin requests (browser enforced)",
ArgsManager::ALLOW_ANY, OptionsCategory::RPC);
argsman.AddArg("-rpcworkqueue=<n>",
strprintf("Set the depth of the work queue to service RPC "
"calls (default: %d)",
DEFAULT_HTTP_WORKQUEUE),
ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY,
OptionsCategory::RPC);
argsman.AddArg("-rpcservertimeout=<n>",
strprintf("Timeout during HTTP requests (default: %d)",
DEFAULT_HTTP_SERVER_TIMEOUT),
ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY,
OptionsCategory::RPC);
#if HAVE_DECL_FORK
argsman.AddArg("-daemon",
strprintf("Run in the background as a daemon and accept "
"commands (default: %d)",
DEFAULT_DAEMON),
ArgsManager::ALLOW_BOOL, OptionsCategory::OPTIONS);
argsman.AddArg("-daemonwait",
strprintf("Wait for initialization to be finished before "
"exiting. This implies -daemon (default: %d)",
DEFAULT_DAEMONWAIT),
ArgsManager::ALLOW_BOOL, OptionsCategory::OPTIONS);
#else
hidden_args.emplace_back("-daemon");
hidden_args.emplace_back("-daemonwait");
#endif
// Avalanche options.
argsman.AddArg("-avalanche",
strprintf("Enable the avalanche feature (default: %u)",
AVALANCHE_DEFAULT_ENABLED),
ArgsManager::ALLOW_ANY, OptionsCategory::AVALANCHE);
argsman.AddArg(
"-avalanchestakingrewards",
strprintf("Enable the avalanche staking rewards feature (default: %u, "
"testnet: %u, regtest: %u)",
defaultChainParams->GetConsensus().enableStakingRewards,
testnetChainParams->GetConsensus().enableStakingRewards,
regtestChainParams->GetConsensus().enableStakingRewards),
ArgsManager::ALLOW_BOOL, OptionsCategory::AVALANCHE);
argsman.AddArg("-avalancheconflictingproofcooldown",
strprintf("Mandatory cooldown before a proof conflicting "
"with an already registered one can be considered "
"in seconds (default: %u)",
AVALANCHE_DEFAULT_CONFLICTING_PROOF_COOLDOWN),
ArgsManager::ALLOW_INT, OptionsCategory::AVALANCHE);
argsman.AddArg("-avalanchepeerreplacementcooldown",
strprintf("Mandatory cooldown before a peer can be replaced "
"in seconds (default: %u)",
AVALANCHE_DEFAULT_PEER_REPLACEMENT_COOLDOWN),
ArgsManager::ALLOW_INT, OptionsCategory::AVALANCHE);
argsman.AddArg(
"-avaminquorumstake",
strprintf(
"Minimum amount of known stake for a usable quorum (default: %s)",
FormatMoney(AVALANCHE_DEFAULT_MIN_QUORUM_STAKE)),
ArgsManager::ALLOW_ANY, OptionsCategory::AVALANCHE);
argsman.AddArg(
"-avaminquorumconnectedstakeratio",
strprintf("Minimum proportion of known stake we"
" need nodes for to have a usable quorum (default: %s). "
"This parameter is parsed with a maximum precision of "
"0.000001.",
AVALANCHE_DEFAULT_MIN_QUORUM_CONNECTED_STAKE_RATIO),
ArgsManager::ALLOW_STRING, OptionsCategory::AVALANCHE);
argsman.AddArg(
"-avaminavaproofsnodecount",
strprintf("Minimum number of node that needs to send us an avaproofs"
" message before we consider we have a usable quorum"
" (default: %s)",
AVALANCHE_DEFAULT_MIN_AVAPROOFS_NODE_COUNT),
ArgsManager::ALLOW_INT, OptionsCategory::AVALANCHE);
argsman.AddArg(
"-avastalevotethreshold",
strprintf("Number of avalanche votes before a voted item goes stale "
"when voting confidence is low (default: %u)",
AVALANCHE_VOTE_STALE_THRESHOLD),
ArgsManager::ALLOW_INT, OptionsCategory::AVALANCHE);
argsman.AddArg(
"-avastalevotefactor",
strprintf(
"Factor affecting the number of avalanche votes before a voted "
"item goes stale when voting confidence is high (default: %u)",
AVALANCHE_VOTE_STALE_FACTOR),
ArgsManager::ALLOW_INT, OptionsCategory::AVALANCHE);
argsman.AddArg("-avacooldown",
strprintf("Mandatory cooldown between two avapoll in "
"milliseconds (default: %u)",
AVALANCHE_DEFAULT_COOLDOWN),
ArgsManager::ALLOW_ANY, OptionsCategory::AVALANCHE);
argsman.AddArg(
"-avatimeout",
strprintf("Avalanche query timeout in milliseconds (default: %u)",
AVALANCHE_DEFAULT_QUERY_TIMEOUT.count()),
ArgsManager::ALLOW_ANY, OptionsCategory::AVALANCHE);
argsman.AddArg(
"-avadelegation",
"Avalanche proof delegation to the master key used by this node "
"(default: none). Should be used in conjunction with -avaproof and "
"-avamasterkey",
ArgsManager::ALLOW_ANY, OptionsCategory::AVALANCHE);
argsman.AddArg("-avaproof",
"Avalanche proof to be used by this node (default: none)",
ArgsManager::ALLOW_ANY, OptionsCategory::AVALANCHE);
argsman.AddArg(
"-avaproofstakeutxoconfirmations",
strprintf(
"Minimum number of confirmations before a stake utxo is mature"
" enough to be included into a proof. Utxos in the mempool are not "
"accepted (i.e this value must be greater than 0) (default: %s)",
AVALANCHE_DEFAULT_STAKE_UTXO_CONFIRMATIONS),
ArgsManager::ALLOW_INT, OptionsCategory::HIDDEN);
argsman.AddArg("-avaproofstakeutxodustthreshold",
strprintf("Minimum value each stake utxo must have to be "
"considered valid (default: %s)",
avalanche::PROOF_DUST_THRESHOLD),
ArgsManager::ALLOW_ANY, OptionsCategory::HIDDEN);
argsman.AddArg("-avamasterkey",
"Master key associated with the proof. If a proof is "
"required, this is mandatory.",
ArgsManager::ALLOW_ANY | ArgsManager::SENSITIVE,
OptionsCategory::AVALANCHE);
argsman.AddArg("-avasessionkey", "Avalanche session key (default: random)",
ArgsManager::ALLOW_ANY | ArgsManager::SENSITIVE,
OptionsCategory::HIDDEN);
argsman.AddArg("-enablertt",
strprintf("Whether to enforce Real Time Targeting via "
"Avalanche, default (%u)",
DEFAULT_ENABLE_RTT),
ArgsManager::ALLOW_BOOL, OptionsCategory::AVALANCHE);
argsman.AddArg(
"-maxavalancheoutbound",
strprintf(
"Set the maximum number of avalanche outbound peers to connect to. "
"Note that this option takes precedence over the -maxconnections "
"option (default: %u).",
DEFAULT_MAX_AVALANCHE_OUTBOUND_CONNECTIONS),
ArgsManager::ALLOW_INT, OptionsCategory::AVALANCHE);
argsman.AddArg(
"-persistavapeers",
strprintf("Whether to save the avalanche peers upon shutdown and load "
"them upon startup (default: %u).",
DEFAULT_PERSIST_AVAPEERS),
ArgsManager::ALLOW_BOOL, OptionsCategory::AVALANCHE);
hidden_args.emplace_back("-avalanchepreconsensus");
hidden_args.emplace_back("-avalanchestakingpreconsensus");
// Add the hidden options
argsman.AddHiddenArgs(hidden_args);
}
static bool fHaveGenesis = false;
static GlobalMutex g_genesis_wait_mutex;
static std::condition_variable g_genesis_wait_cv;
static void BlockNotifyGenesisWait(const CBlockIndex *pBlockIndex) {
if (pBlockIndex != nullptr) {
{
LOCK(g_genesis_wait_mutex);
fHaveGenesis = true;
}
g_genesis_wait_cv.notify_all();
}
}
#if HAVE_SYSTEM
static void StartupNotify(const ArgsManager &args) {
std::string cmd = args.GetArg("-startupnotify", "");
if (!cmd.empty()) {
std::thread t(runCommand, cmd);
// thread runs free
t.detach();
}
}
#endif
static bool AppInitServers(Config &config,
HTTPRPCRequestProcessor &httpRPCRequestProcessor,
NodeContext &node) {
const ArgsManager &args = *Assert(node.args);
RPCServerSignals::OnStarted(&OnRPCStarted);
RPCServerSignals::OnStopped(&OnRPCStopped);
if (!InitHTTPServer(config)) {
return false;
}
StartRPC();
node.rpc_interruption_point = RpcInterruptionPoint;
if (!StartHTTPRPC(httpRPCRequestProcessor)) {
return false;
}
if (args.GetBoolArg("-rest", DEFAULT_REST_ENABLE)) {
StartREST(&node);
}
StartHTTPServer();
return true;
}
// Parameter interaction based on rules
void InitParameterInteraction(ArgsManager &args) {
// when specifying an explicit binding address, you want to listen on it
// even when -connect or -proxy is specified.
if (args.IsArgSet("-bind")) {
if (args.SoftSetBoolArg("-listen", true)) {
LogPrintf(
"%s: parameter interaction: -bind set -> setting -listen=1\n",
__func__);
}
}
if (args.IsArgSet("-whitebind")) {
if (args.SoftSetBoolArg("-listen", true)) {
LogPrintf("%s: parameter interaction: -whitebind set -> setting "
"-listen=1\n",
__func__);
}
}
if (args.IsArgSet("-connect")) {
// when only connecting to trusted nodes, do not seed via DNS, or listen
// by default.
if (args.SoftSetBoolArg("-dnsseed", false)) {
LogPrintf("%s: parameter interaction: -connect set -> setting "
"-dnsseed=0\n",
__func__);
}
if (args.SoftSetBoolArg("-listen", false)) {
LogPrintf("%s: parameter interaction: -connect set -> setting "
"-listen=0\n",
__func__);
}
}
if (args.IsArgSet("-proxy")) {
// to protect privacy, do not listen by default if a default proxy
// server is specified.
if (args.SoftSetBoolArg("-listen", false)) {
LogPrintf(
"%s: parameter interaction: -proxy set -> setting -listen=0\n",
__func__);
}
// to protect privacy, do not map ports when a proxy is set. The user
// may still specify -listen=1 to listen locally, so don't rely on this
// happening through -listen below.
if (args.SoftSetBoolArg("-upnp", false)) {
LogPrintf(
"%s: parameter interaction: -proxy set -> setting -upnp=0\n",
__func__);
}
if (args.SoftSetBoolArg("-natpmp", false)) {
LogPrintf(
"%s: parameter interaction: -proxy set -> setting -natpmp=0\n",
__func__);
}
// to protect privacy, do not discover addresses by default
if (args.SoftSetBoolArg("-discover", false)) {
LogPrintf("%s: parameter interaction: -proxy set -> setting "
"-discover=0\n",
__func__);
}
}
if (!args.GetBoolArg("-listen", DEFAULT_LISTEN)) {
// do not map ports or try to retrieve public IP when not listening
// (pointless)
if (args.SoftSetBoolArg("-upnp", false)) {
LogPrintf(
"%s: parameter interaction: -listen=0 -> setting -upnp=0\n",
__func__);
}
if (args.SoftSetBoolArg("-natpmp", false)) {
LogPrintf(
"%s: parameter interaction: -listen=0 -> setting -natpmp=0\n",
__func__);
}
if (args.SoftSetBoolArg("-discover", false)) {
LogPrintf(
"%s: parameter interaction: -listen=0 -> setting -discover=0\n",
__func__);
}
if (args.SoftSetBoolArg("-listenonion", false)) {
LogPrintf("%s: parameter interaction: -listen=0 -> setting "
"-listenonion=0\n",
__func__);
}
if (args.SoftSetBoolArg("-i2pacceptincoming", false)) {
LogPrintf("%s: parameter interaction: -listen=0 -> setting "
"-i2pacceptincoming=0\n",
__func__);
}
}
if (args.IsArgSet("-externalip")) {
// if an explicit public IP is specified, do not try to find others
if (args.SoftSetBoolArg("-discover", false)) {
LogPrintf("%s: parameter interaction: -externalip set -> setting "
"-discover=0\n",
__func__);
}
}
// disable whitelistrelay in blocksonly mode
if (args.GetBoolArg("-blocksonly", DEFAULT_BLOCKSONLY)) {
if (args.SoftSetBoolArg("-whitelistrelay", false)) {
LogPrintf("%s: parameter interaction: -blocksonly=1 -> setting "
"-whitelistrelay=0\n",
__func__);
}
}
// Forcing relay from whitelisted hosts implies we will accept relays from
// them in the first place.
if (args.GetBoolArg("-whitelistforcerelay", DEFAULT_WHITELISTFORCERELAY)) {
if (args.SoftSetBoolArg("-whitelistrelay", true)) {
LogPrintf("%s: parameter interaction: -whitelistforcerelay=1 -> "
"setting -whitelistrelay=1\n",
__func__);
}
}
// If avalanche is set, soft set all the feature flags accordingly.
if (args.IsArgSet("-avalanche")) {
const bool fAvalanche =
args.GetBoolArg("-avalanche", AVALANCHE_DEFAULT_ENABLED);
args.SoftSetBoolArg("-automaticunparking", !fAvalanche);
}
}
/**
* Initialize global loggers.
*
* Note that this is called very early in the process lifetime, so you should be
* careful about what global state you rely on here.
*/
void InitLogging(const ArgsManager &args) {
init::SetLoggingOptions(args);
init::LogPackageVersion();
}
namespace { // Variables internal to initialization process only
int nMaxConnections;
int nUserMaxConnections;
int nFD;
ServiceFlags nLocalServices = ServiceFlags(NODE_NETWORK | NODE_NETWORK_LIMITED);
int64_t peer_connect_timeout;
std::set<BlockFilterType> g_enabled_filter_types;
} // namespace
[[noreturn]] static void new_handler_terminate() {
// Rather than throwing std::bad-alloc if allocation fails, terminate
// immediately to (try to) avoid chain corruption. Since LogPrintf may
// itself allocate memory, set the handler directly to terminate first.
std::set_new_handler(std::terminate);
LogPrintf("Error: Out of memory. Terminating.\n");
// The log was successful, terminate now.
std::terminate();
};
bool AppInitBasicSetup(const ArgsManager &args) {
// Step 1: setup
#ifdef _MSC_VER
// Turn off Microsoft heap dump noise
_CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_FILE);
_CrtSetReportFile(_CRT_WARN, CreateFileA("NUL", GENERIC_WRITE, 0, nullptr,
OPEN_EXISTING, 0, 0));
// Disable confusing "helpful" text message on abort, Ctrl-C
_set_abort_behavior(0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT);
#endif
#ifdef WIN32
// Enable Data Execution Prevention (DEP)
SetProcessDEPPolicy(PROCESS_DEP_ENABLE);
#endif
if (!InitShutdownState()) {
return InitError(
Untranslated("Initializing wait-for-shutdown state failed."));
}
if (!SetupNetworking()) {
return InitError(Untranslated("Initializing networking failed"));
}
#ifndef WIN32
if (!args.GetBoolArg("-sysperms", false)) {
umask(077);
}
// Clean shutdown on SIGTERM
registerSignalHandler(SIGTERM, HandleSIGTERM);
registerSignalHandler(SIGINT, HandleSIGTERM);
// Reopen debug.log on SIGHUP
registerSignalHandler(SIGHUP, HandleSIGHUP);
// Ignore SIGPIPE, otherwise it will bring the daemon down if the client
// closes unexpectedly
signal(SIGPIPE, SIG_IGN);
#else
SetConsoleCtrlHandler(consoleCtrlHandler, true);
#endif
std::set_new_handler(new_handler_terminate);
return true;
}
bool AppInitParameterInteraction(Config &config, const ArgsManager &args) {
const CChainParams &chainparams = config.GetChainParams();
// Step 2: parameter interactions
// also see: InitParameterInteraction()
// Error if network-specific options (-addnode, -connect, etc) are
// specified in default section of config file, but not overridden
// on the command line or in this network's section of the config file.
std::string network = args.GetChainName();
bilingual_str errors;
for (const auto &arg : args.GetUnsuitableSectionOnlyArgs()) {
errors += strprintf(_("Config setting for %s only applied on %s "
"network when in [%s] section.") +
Untranslated("\n"),
arg, network, network);
}
if (!errors.empty()) {
return InitError(errors);
}
// Warn if unrecognized section name are present in the config file.
bilingual_str warnings;
for (const auto &section : args.GetUnrecognizedSections()) {
warnings += strprintf(Untranslated("%s:%i ") +
_("Section [%s] is not recognized.") +
Untranslated("\n"),
section.m_file, section.m_line, section.m_name);
}
if (!warnings.empty()) {
InitWarning(warnings);
}
if (!fs::is_directory(args.GetBlocksDirPath())) {
return InitError(
strprintf(_("Specified blocks directory \"%s\" does not exist."),
args.GetArg("-blocksdir", "")));
}
// parse and validate enabled filter types
std::string blockfilterindex_value =
args.GetArg("-blockfilterindex", DEFAULT_BLOCKFILTERINDEX);
if (blockfilterindex_value == "" || blockfilterindex_value == "1") {
g_enabled_filter_types = AllBlockFilterTypes();
} else if (blockfilterindex_value != "0") {
const std::vector<std::string> names =
args.GetArgs("-blockfilterindex");
for (const auto &name : names) {
BlockFilterType filter_type;
if (!BlockFilterTypeByName(name, filter_type)) {
return InitError(
strprintf(_("Unknown -blockfilterindex value %s."), name));
}
g_enabled_filter_types.insert(filter_type);
}
}
// Signal NODE_COMPACT_FILTERS if peerblockfilters and basic filters index
// are both enabled.
if (args.GetBoolArg("-peerblockfilters", DEFAULT_PEERBLOCKFILTERS)) {
if (g_enabled_filter_types.count(BlockFilterType::BASIC) != 1) {
return InitError(
_("Cannot set -peerblockfilters without -blockfilterindex."));
}
nLocalServices = ServiceFlags(nLocalServices | NODE_COMPACT_FILTERS);
}
if (args.GetIntArg("-prune", 0)) {
if (args.GetBoolArg("-txindex", DEFAULT_TXINDEX)) {
return InitError(_("Prune mode is incompatible with -txindex."));
}
if (args.GetBoolArg("-reindex-chainstate", false)) {
return InitError(
_("Prune mode is incompatible with -reindex-chainstate. Use "
"full -reindex instead."));
}
if (args.GetBoolArg("-chronik", DEFAULT_CHRONIK)) {
return InitError(_("Prune mode is incompatible with -chronik."));
}
}
// -bind and -whitebind can't be set when not listening
size_t nUserBind =
args.GetArgs("-bind").size() + args.GetArgs("-whitebind").size();
if (nUserBind != 0 && !args.GetBoolArg("-listen", DEFAULT_LISTEN)) {
return InitError(Untranslated(
"Cannot set -bind or -whitebind together with -listen=0"));
}
// Make sure enough file descriptors are available
int nBind = std::max(nUserBind, size_t(1));
nUserMaxConnections =
args.GetIntArg("-maxconnections", DEFAULT_MAX_PEER_CONNECTIONS);
nMaxConnections = std::max(nUserMaxConnections, 0);
// -maxavalancheoutbound takes precedence over -maxconnections
const int maxAvalancheOutbound = args.GetIntArg(
"-maxavalancheoutbound", DEFAULT_MAX_AVALANCHE_OUTBOUND_CONNECTIONS);
const bool fAvalanche =
args.GetBoolArg("-avalanche", AVALANCHE_DEFAULT_ENABLED);
if (fAvalanche && maxAvalancheOutbound > nMaxConnections) {
nMaxConnections = std::max(maxAvalancheOutbound, nMaxConnections);
// Indicate the value set by the user
LogPrintf("Increasing -maxconnections from %d to %d to comply with "
"-maxavalancheoutbound\n",
nUserMaxConnections, nMaxConnections);
}
// Trim requested connection counts, to fit into system limitations
// <int> in std::min<int>(...) to work around FreeBSD compilation issue
// described in #2695
nFD = RaiseFileDescriptorLimit(
nMaxConnections + nBind + MIN_CORE_FILEDESCRIPTORS +
MAX_ADDNODE_CONNECTIONS + NUM_FDS_MESSAGE_CAPTURE);
#ifdef USE_POLL
int fd_max = nFD;
#else
int fd_max = FD_SETSIZE;
#endif
nMaxConnections = std::max(
std::min<int>(nMaxConnections,
fd_max - nBind - MIN_CORE_FILEDESCRIPTORS -
MAX_ADDNODE_CONNECTIONS - NUM_FDS_MESSAGE_CAPTURE),
0);
if (nFD < MIN_CORE_FILEDESCRIPTORS) {
return InitError(_("Not enough file descriptors available."));
}
nMaxConnections =
std::min(nFD - MIN_CORE_FILEDESCRIPTORS - MAX_ADDNODE_CONNECTIONS,
nMaxConnections);
if (nMaxConnections < nUserMaxConnections) {
// Not categorizing as "Warning" because this is the normal behavior for
// platforms using the select() interface for which FD_SETSIZE is
// usually 1024.
LogPrintf("Reducing -maxconnections from %d to %d, because of system "
"limitations.\n",
nUserMaxConnections, nMaxConnections);
}
// Step 3: parameter-to-internal-flags
init::SetLoggingCategories(args);
// Configure excessive block size.
const int64_t nProposedExcessiveBlockSize =
args.GetIntArg("-excessiveblocksize", DEFAULT_MAX_BLOCK_SIZE);
if (nProposedExcessiveBlockSize <= 0 ||
!config.SetMaxBlockSize(nProposedExcessiveBlockSize)) {
return InitError(
_("Excessive block size must be > 1,000,000 bytes (1MB)"));
}
// Check blockmaxsize does not exceed maximum accepted block size.
const int64_t nProposedMaxGeneratedBlockSize =
args.GetIntArg("-blockmaxsize", DEFAULT_MAX_GENERATED_BLOCK_SIZE);
if (nProposedMaxGeneratedBlockSize <= 0) {
return InitError(_("Max generated block size must be greater than 0"));
}
if (uint64_t(nProposedMaxGeneratedBlockSize) > config.GetMaxBlockSize()) {
return InitError(_("Max generated block size (blockmaxsize) cannot "
"exceed the excessive block size "
"(excessiveblocksize)"));
}
nConnectTimeout = args.GetIntArg("-timeout", DEFAULT_CONNECT_TIMEOUT);
if (nConnectTimeout <= 0) {
nConnectTimeout = DEFAULT_CONNECT_TIMEOUT;
}
peer_connect_timeout =
args.GetIntArg("-peertimeout", DEFAULT_PEER_CONNECT_TIMEOUT);
if (peer_connect_timeout <= 0) {
return InitError(Untranslated(
"peertimeout cannot be configured with a negative value."));
}
// Sanity check argument for min fee for including tx in block
// TODO: Harmonize which arguments need sanity checking and where that
// happens.
if (args.IsArgSet("-blockmintxfee")) {
Amount n = Amount::zero();
if (!ParseMoney(args.GetArg("-blockmintxfee", ""), n)) {
return InitError(AmountErrMsg("blockmintxfee",
args.GetArg("-blockmintxfee", "")));
}
}
nBytesPerSigCheck =
args.IsArgSet("-bytespersigcheck")
? args.GetIntArg("-bytespersigcheck", nBytesPerSigCheck)
: args.GetIntArg("-bytespersigop", nBytesPerSigCheck);
if (!g_wallet_init_interface.ParameterInteraction()) {
return false;
}
// Option to startup with mocktime set (used for regression testing):
SetMockTime(args.GetIntArg("-mocktime", 0)); // SetMockTime(0) is a no-op
if (args.GetBoolArg("-peerbloomfilters", DEFAULT_PEERBLOOMFILTERS)) {
nLocalServices = ServiceFlags(nLocalServices | NODE_BLOOM);
}
if (args.IsArgSet("-proxy") && args.GetArg("-proxy", "").empty()) {
return InitError(_(
"No proxy server specified. Use -proxy=<ip> or -proxy=<ip:port>."));
}
// Avalanche parameters
const int64_t stakeUtxoMinConfirmations =
args.GetIntArg("-avaproofstakeutxoconfirmations",
AVALANCHE_DEFAULT_STAKE_UTXO_CONFIRMATIONS);
if (!chainparams.IsTestChain() &&
stakeUtxoMinConfirmations !=
AVALANCHE_DEFAULT_STAKE_UTXO_CONFIRMATIONS) {
return InitError(_("Avalanche stake UTXO minimum confirmations can "
"only be set on test chains."));
}
if (stakeUtxoMinConfirmations <= 0) {
return InitError(_("Avalanche stake UTXO minimum confirmations must be "
"a positive integer."));
}
if (args.IsArgSet("-avaproofstakeutxodustthreshold")) {
Amount amount = Amount::zero();
auto parsed = ParseMoney(
args.GetArg("-avaproofstakeutxodustthreshold", ""), amount);
if (!parsed || Amount::zero() == amount) {
return InitError(AmountErrMsg(
"avaproofstakeutxodustthreshold",
args.GetArg("-avaproofstakeutxodustthreshold", "")));
}
if (!chainparams.IsTestChain() &&
amount != avalanche::PROOF_DUST_THRESHOLD) {
return InitError(_("Avalanche stake UTXO dust threshold can "
"only be set on test chains."));
}
}
// This is a staking node
if (fAvalanche && args.IsArgSet("-avaproof")) {
if (!args.GetBoolArg("-listen", true)) {
return InitError(_("Running a staking node requires accepting "
"inbound connections. Please enable -listen."));
}
if (args.IsArgSet("-proxy")) {
return InitError(_("Running a staking node behind a proxy is not "
"supported. Please disable -proxy."));
}
if (args.IsArgSet("-i2psam")) {
return InitError(_("Running a staking node behind I2P is not "
"supported. Please disable -i2psam."));
}
if (args.IsArgSet("-onlynet")) {
return InitError(
_("Restricting the outbound network is not supported when "
"running a staking node. Please disable -onlynet."));
}
}
// Also report errors from parsing before daemonization
{
KernelNotifications notifications{};
ChainstateManager::Options chainman_opts_dummy{
.config = config,
.datadir = args.GetDataDirNet(),
.notifications = notifications,
};
if (const auto error{ApplyArgsManOptions(args, chainman_opts_dummy)}) {
return InitError(*error);
}
BlockManager::Options blockman_opts_dummy{
.chainparams = chainman_opts_dummy.config.GetChainParams(),
.blocks_dir = args.GetBlocksDirPath(),
};
if (const auto error{ApplyArgsManOptions(args, blockman_opts_dummy)}) {
return InitError(*error);
}
}
return true;
}
static bool LockDataDirectory(bool probeOnly) {
// Make sure only a single Bitcoin process is using the data directory.
fs::path datadir = gArgs.GetDataDirNet();
if (!DirIsWritable(datadir)) {
return InitError(strprintf(
_("Cannot write to data directory '%s'; check permissions."),
fs::PathToString(datadir)));
}
if (!LockDirectory(datadir, ".lock", probeOnly)) {
return InitError(strprintf(_("Cannot obtain a lock on data directory "
"%s. %s is probably already running."),
fs::PathToString(datadir), PACKAGE_NAME));
}
return true;
}
bool AppInitSanityChecks() {
// Step 4: sanity checks
init::SetGlobals();
// Sanity check
if (!init::SanityChecks()) {
return InitError(strprintf(
_("Initialization sanity check failed. %s is shutting down."),
PACKAGE_NAME));
}
// Probe the data directory lock to give an early error message, if possible
// We cannot hold the data directory lock here, as the forking for daemon()
// hasn't yet happened, and a fork will cause weird behavior to it.
return LockDataDirectory(true);
}
bool AppInitLockDataDirectory() {
// After daemonization get the data directory lock again and hold on to it
// until exit. This creates a slight window for a race condition to happen,
// however this condition is harmless: it will at most make us exit without
// printing a message to console.
if (!LockDataDirectory(false)) {
// Detailed error printed inside LockDataDirectory
return false;
}
return true;
}
bool AppInitInterfaces(NodeContext &node) {
node.chain = interfaces::MakeChain(node, Params());
// Create client interfaces for wallets that are supposed to be loaded
// according to -wallet and -disablewallet options. This only constructs
// the interfaces, it doesn't load wallet data. Wallets actually get loaded
// when load() and start() interface methods are called below.
g_wallet_init_interface.Construct(node);
return true;
}
bool AppInitMain(Config &config, RPCServer &rpcServer,
HTTPRPCRequestProcessor &httpRPCRequestProcessor,
NodeContext &node,
interfaces::BlockAndHeaderTipInfo *tip_info) {
// Step 4a: application initialization
const ArgsManager &args = *Assert(node.args);
const CChainParams &chainparams = config.GetChainParams();
if (!CreatePidFile(args)) {
// Detailed error printed inside CreatePidFile().
return false;
}
if (!init::StartLogging(args)) {
// Detailed error printed inside StartLogging().
return false;
}
LogPrintf("Using at most %i automatic connections (%i file descriptors "
"available)\n",
nMaxConnections, nFD);
// Warn about relative -datadir path.
if (args.IsArgSet("-datadir") &&
!args.GetPathArg("-datadir").is_absolute()) {
LogPrintf("Warning: relative datadir option '%s' specified, which will "
"be interpreted relative to the current working directory "
"'%s'. This is fragile, because if bitcoin is started in the "
"future from a different location, it will be unable to "
"locate the current data files. There could also be data "
"loss if bitcoin is started while in a temporary "
"directory.\n",
args.GetArg("-datadir", ""),
fs::PathToString(fs::current_path()));
}
ValidationCacheSizes validation_cache_sizes{};
ApplyArgsManOptions(args, validation_cache_sizes);
if (!InitSignatureCache(validation_cache_sizes.signature_cache_bytes)) {
return InitError(strprintf(
_("Unable to allocate memory for -maxsigcachesize: '%s' MiB"),
args.GetIntArg("-maxsigcachesize",
DEFAULT_MAX_SIG_CACHE_BYTES >> 20)));
}
if (!InitScriptExecutionCache(
validation_cache_sizes.script_execution_cache_bytes)) {
return InitError(strprintf(
_("Unable to allocate memory for -maxscriptcachesize: '%s' MiB"),
args.GetIntArg("-maxscriptcachesize",
DEFAULT_MAX_SCRIPT_CACHE_BYTES >> 20)));
}
int script_threads = args.GetIntArg("-par", DEFAULT_SCRIPTCHECK_THREADS);
if (script_threads <= 0) {
// -par=0 means autodetect (number of cores - 1 script threads)
// -par=-n means "leave n cores free" (number of cores - n - 1 script
// threads)
script_threads += GetNumCores();
}
// Subtract 1 because the main thread counts towards the par threads
script_threads = std::max(script_threads - 1, 0);
// Number of script-checking threads <= MAX_SCRIPTCHECK_THREADS
script_threads = std::min(script_threads, MAX_SCRIPTCHECK_THREADS);
LogPrintf("Script verification uses %d additional threads\n",
script_threads);
if (script_threads >= 1) {
StartScriptCheckWorkerThreads(script_threads);
}
assert(!node.scheduler);
node.scheduler = std::make_unique<CScheduler>();
// Start the lightweight task scheduler thread
node.scheduler->m_service_thread =
std::thread(&util::TraceThread, "scheduler",
[&] { node.scheduler->serviceQueue(); });
// Gather some entropy once per minute.
node.scheduler->scheduleEvery(
[] {
RandAddPeriodic();
return true;
},
std::chrono::minutes{1});
GetMainSignals().RegisterBackgroundSignalScheduler(*node.scheduler);
/**
* Register RPC commands regardless of -server setting so they will be
* available in the GUI RPC console even if external calls are disabled.
*/
RegisterAllRPCCommands(config, rpcServer, tableRPC);
for (const auto &client : node.chain_clients) {
client->registerRpcs();
}
#if ENABLE_ZMQ
RegisterZMQRPCCommands(tableRPC);
#endif
/**
* Start the RPC server. It will be started in "warmup" mode and not
* process calls yet (but it will verify that the server is there and will
* be ready later). Warmup mode will be completed when initialisation is
* finished.
*/
if (args.GetBoolArg("-server", false)) {
uiInterface.InitMessage_connect(SetRPCWarmupStatus);
if (!AppInitServers(config, httpRPCRequestProcessor, node)) {
return InitError(
_("Unable to start HTTP server. See debug log for details."));
}
}
// Step 5: verify wallet database integrity
for (const auto &client : node.chain_clients) {
if (!client->verify()) {
return false;
}
}
// Step 6: network initialization
// Note that we absolutely cannot open any actual connections
// until the very end ("start node") as the UTXO/block state
// is not yet setup and may end up being set up twice if we
// need to reindex later.
fListen = args.GetBoolArg("-listen", DEFAULT_LISTEN);
fDiscover = args.GetBoolArg("-discover", true);
{
// Initialize addrman
assert(!node.addrman);
// Read asmap file if configured
std::vector<bool> asmap;
if (args.IsArgSet("-asmap")) {
fs::path asmap_path =
args.GetPathArg("-asmap", DEFAULT_ASMAP_FILENAME);
if (!asmap_path.is_absolute()) {
asmap_path = args.GetDataDirNet() / asmap_path;
}
if (!fs::exists(asmap_path)) {
InitError(strprintf(_("Could not find asmap file %s"),
fs::quoted(fs::PathToString(asmap_path))));
return false;
}
asmap = DecodeAsmap(asmap_path);
if (asmap.size() == 0) {
InitError(strprintf(_("Could not parse asmap file %s"),
fs::quoted(fs::PathToString(asmap_path))));
return false;
}
const uint256 asmap_version = (HashWriter{} << asmap).GetHash();
LogPrintf("Using asmap version %s for IP bucketing\n",
asmap_version.ToString());
} else {
LogPrintf("Using /16 prefix for IP bucketing\n");
}
uiInterface.InitMessage(_("Loading P2P addresses...").translated);
auto addrman{LoadAddrman(chainparams, asmap, args)};
if (!addrman) {
return InitError(util::ErrorString(addrman));
}
node.addrman = std::move(*addrman);
}
assert(!node.banman);
node.banman = std::make_unique<BanMan>(
args.GetDataDirNet() / "banlist.dat", config.GetChainParams(),
&uiInterface, args.GetIntArg("-bantime", DEFAULT_MISBEHAVING_BANTIME));
assert(!node.connman);
node.connman = std::make_unique<CConnman>(
config, GetRand<uint64_t>(), GetRand<uint64_t>(), *node.addrman,
args.GetBoolArg("-networkactive", true));
// sanitize comments per BIP-0014, format user agent and check total size
std::vector<std::string> uacomments;
for (const std::string &cmt : args.GetArgs("-uacomment")) {
if (cmt != SanitizeString(cmt, SAFE_CHARS_UA_COMMENT)) {
return InitError(strprintf(
_("User Agent comment (%s) contains unsafe characters."), cmt));
}
uacomments.push_back(cmt);
}
const std::string client_name = args.GetArg("-uaclientname", CLIENT_NAME);
const std::string client_version =
args.GetArg("-uaclientversion", FormatVersion(CLIENT_VERSION));
if (client_name != SanitizeString(client_name, SAFE_CHARS_UA_COMMENT)) {
return InitError(strprintf(
_("-uaclientname (%s) contains invalid characters."), client_name));
}
if (client_version !=
SanitizeString(client_version, SAFE_CHARS_UA_COMMENT)) {
return InitError(
strprintf(_("-uaclientversion (%s) contains invalid characters."),
client_version));
}
const std::string strSubVersion =
FormatUserAgent(client_name, client_version, uacomments);
if (strSubVersion.size() > MAX_SUBVERSION_LENGTH) {
return InitError(strprintf(
_("Total length of network version string (%i) exceeds maximum "
"length (%i). Reduce the number or size of uacomments."),
strSubVersion.size(), MAX_SUBVERSION_LENGTH));
}
if (args.IsArgSet("-onlynet")) {
std::set<enum Network> nets;
for (const std::string &snet : args.GetArgs("-onlynet")) {
enum Network net = ParseNetwork(snet);
if (net == NET_UNROUTABLE) {
return InitError(strprintf(
_("Unknown network specified in -onlynet: '%s'"), snet));
}
nets.insert(net);
}
for (int n = 0; n < NET_MAX; n++) {
enum Network net = (enum Network)n;
if (!nets.count(net)) {
SetReachable(net, false);
}
}
}
// Check for host lookup allowed before parsing any network related
// parameters
fNameLookup = args.GetBoolArg("-dns", DEFAULT_NAME_LOOKUP);
bool proxyRandomize =
args.GetBoolArg("-proxyrandomize", DEFAULT_PROXYRANDOMIZE);
// -proxy sets a proxy for all outgoing network traffic
// -noproxy (or -proxy=0) as well as the empty string can be used to not set
// a proxy, this is the default
std::string proxyArg = args.GetArg("-proxy", "");
SetReachable(NET_ONION, false);
if (proxyArg != "" && proxyArg != "0") {
CService proxyAddr;
if (!Lookup(proxyArg, proxyAddr, 9050, fNameLookup)) {
return InitError(strprintf(
_("Invalid -proxy address or hostname: '%s'"), proxyArg));
}
proxyType addrProxy = proxyType(proxyAddr, proxyRandomize);
if (!addrProxy.IsValid()) {
return InitError(strprintf(
_("Invalid -proxy address or hostname: '%s'"), proxyArg));
}
SetProxy(NET_IPV4, addrProxy);
SetProxy(NET_IPV6, addrProxy);
SetProxy(NET_ONION, addrProxy);
SetNameProxy(addrProxy);
// by default, -proxy sets onion as reachable, unless -noonion later
SetReachable(NET_ONION, true);
}
// -onion can be used to set only a proxy for .onion, or override normal
// proxy for .onion addresses.
// -noonion (or -onion=0) disables connecting to .onion entirely. An empty
// string is used to not override the onion proxy (in which case it defaults
// to -proxy set above, or none)
std::string onionArg = args.GetArg("-onion", "");
if (onionArg != "") {
if (onionArg == "0") {
// Handle -noonion/-onion=0
SetReachable(NET_ONION, false);
} else {
CService onionProxy;
if (!Lookup(onionArg, onionProxy, 9050, fNameLookup)) {
return InitError(strprintf(
_("Invalid -onion address or hostname: '%s'"), onionArg));
}
proxyType addrOnion = proxyType(onionProxy, proxyRandomize);
if (!addrOnion.IsValid()) {
return InitError(strprintf(
_("Invalid -onion address or hostname: '%s'"), onionArg));
}
SetProxy(NET_ONION, addrOnion);
SetReachable(NET_ONION, true);
}
}
for (const std::string &strAddr : args.GetArgs("-externalip")) {
CService addrLocal;
if (Lookup(strAddr, addrLocal, GetListenPort(), fNameLookup) &&
addrLocal.IsValid()) {
AddLocal(addrLocal, LOCAL_MANUAL);
} else {
return InitError(ResolveErrMsg("externalip", strAddr));
}
}
#if ENABLE_ZMQ
g_zmq_notification_interface = CZMQNotificationInterface::Create(
[&chainman = node.chainman](CBlock &block, const CBlockIndex &index) {
assert(chainman);
return chainman->m_blockman.ReadBlockFromDisk(block, index);
});
if (g_zmq_notification_interface) {
RegisterValidationInterface(g_zmq_notification_interface.get());
}
#endif
// Step 7: load block chain
node.notifications = std::make_unique<KernelNotifications>();
fReindex = args.GetBoolArg("-reindex", false);
bool fReindexChainState = args.GetBoolArg("-reindex-chainstate", false);
ChainstateManager::Options chainman_opts{
.config = config,
.datadir = args.GetDataDirNet(),
.adjusted_time_callback = GetAdjustedTime,
.notifications = *node.notifications,
};
// no error can happen, already checked in AppInitParameterInteraction
Assert(!ApplyArgsManOptions(args, chainman_opts));
if (chainman_opts.checkpoints_enabled) {
LogPrintf("Checkpoints will be verified.\n");
} else {
LogPrintf("Skipping checkpoint verification.\n");
}
BlockManager::Options blockman_opts{
.chainparams = chainman_opts.config.GetChainParams(),
.blocks_dir = args.GetBlocksDirPath(),
};
// no error can happen, already checked in AppInitParameterInteraction
Assert(!ApplyArgsManOptions(args, blockman_opts));
// cache size calculations
CacheSizes cache_sizes =
CalculateCacheSizes(args, g_enabled_filter_types.size());
LogPrintf("Cache configuration:\n");
LogPrintf("* Using %.1f MiB for block index database\n",
cache_sizes.block_tree_db * (1.0 / 1024 / 1024));
if (args.GetBoolArg("-txindex", DEFAULT_TXINDEX)) {
LogPrintf("* Using %.1f MiB for transaction index database\n",
cache_sizes.tx_index * (1.0 / 1024 / 1024));
}
for (BlockFilterType filter_type : g_enabled_filter_types) {
LogPrintf("* Using %.1f MiB for %s block filter index database\n",
cache_sizes.filter_index * (1.0 / 1024 / 1024),
BlockFilterTypeName(filter_type));
}
LogPrintf("* Using %.1f MiB for chain state database\n",
cache_sizes.coins_db * (1.0 / 1024 / 1024));
assert(!node.mempool);
assert(!node.chainman);
CTxMemPool::Options mempool_opts{
.check_ratio = chainparams.DefaultConsistencyChecks() ? 1 : 0,
};
if (const auto err{ApplyArgsManOptions(args, chainparams, mempool_opts)}) {
return InitError(*err);
}
mempool_opts.check_ratio =
std::clamp<int>(mempool_opts.check_ratio, 0, 1'000'000);
// FIXME: this legacy limit comes from the DEFAULT_DESCENDANT_SIZE_LIMIT
// (101) that was enforced before the wellington activation. While it's
// still a good idea to have some minimum mempool size, using this value as
// a threshold is no longer relevant.
int64_t nMempoolSizeMin = 101 * 1000 * 40;
if (mempool_opts.max_size_bytes < 0 ||
(!chainparams.IsTestChain() &&
mempool_opts.max_size_bytes < nMempoolSizeMin)) {
return InitError(strprintf(_("-maxmempool must be at least %d MB"),
std::ceil(nMempoolSizeMin / 1000000.0)));
}
LogPrintf("* Using %.1f MiB for in-memory UTXO set (plus up to %.1f MiB of "
"unused mempool space)\n",
cache_sizes.coins * (1.0 / 1024 / 1024),
mempool_opts.max_size_bytes * (1.0 / 1024 / 1024));
for (bool fLoaded = false; !fLoaded && !ShutdownRequested();) {
node.mempool = std::make_unique<CTxMemPool>(mempool_opts);
node.chainman =
std::make_unique<ChainstateManager>(chainman_opts, blockman_opts);
ChainstateManager &chainman = *node.chainman;
node::ChainstateLoadOptions options;
options.mempool = Assert(node.mempool.get());
options.reindex = node::fReindex;
options.reindex_chainstate = fReindexChainState;
options.prune = chainman.m_blockman.IsPruneMode();
options.check_blocks =
args.GetIntArg("-checkblocks", DEFAULT_CHECKBLOCKS);
options.check_level = args.GetIntArg("-checklevel", DEFAULT_CHECKLEVEL);
options.require_full_verification =
args.IsArgSet("-checkblocks") || args.IsArgSet("-checklevel");
options.check_interrupt = ShutdownRequested;
options.coins_error_cb = [] {
uiInterface.ThreadSafeMessageBox(
_("Error reading from database, shutting down."), "",
CClientUIInterface::MSG_ERROR);
};
uiInterface.InitMessage(_("Loading block index...").translated);
const int64_t load_block_index_start_time = GetTimeMillis();
auto catch_exceptions = [](auto &&f) {
try {
return f();
} catch (const std::exception &e) {
LogPrintf("%s\n", e.what());
return std::make_tuple(node::ChainstateLoadStatus::FAILURE,
_("Error opening block database"));
}
};
auto [status, error] = catch_exceptions(
[&] { return LoadChainstate(chainman, cache_sizes, options); });
if (status == node::ChainstateLoadStatus::SUCCESS) {
uiInterface.InitMessage(_("Verifying blocks...").translated);
if (chainman.m_blockman.m_have_pruned &&
options.check_blocks > MIN_BLOCKS_TO_KEEP) {
LogPrintfCategory(BCLog::PRUNE,
"pruned datadir may not have more than %d "
"blocks; only checking available blocks\n",
MIN_BLOCKS_TO_KEEP);
}
std::tie(status, error) = catch_exceptions(
[&] { return VerifyLoadedChainstate(chainman, options); });
if (status == node::ChainstateLoadStatus::SUCCESS) {
WITH_LOCK(cs_main, return node.chainman->LoadRecentHeadersTime(
node.chainman->m_options.datadir /
HEADERS_TIME_FILE_NAME));
fLoaded = true;
LogPrintf(" block index %15dms\n",
GetTimeMillis() - load_block_index_start_time);
}
}
if (status == node::ChainstateLoadStatus::FAILURE_FATAL ||
status == node::ChainstateLoadStatus::FAILURE_INCOMPATIBLE_DB ||
status ==
node::ChainstateLoadStatus::FAILURE_INSUFFICIENT_DBCACHE) {
return InitError(error);
}
if (!fLoaded && !ShutdownRequested()) {
// first suggest a reindex
if (!options.reindex) {
bool fRet = uiInterface.ThreadSafeQuestion(
error + Untranslated(".\n\n") +
_("Do you want to rebuild the block database now?"),
error.original + ".\nPlease restart with -reindex or "
"-reindex-chainstate to recover.",
"",
CClientUIInterface::MSG_ERROR |
CClientUIInterface::BTN_ABORT);
if (fRet) {
fReindex = true;
AbortShutdown();
} else {
LogPrintf("Aborted block database rebuild. Exiting.\n");
return false;
}
} else {
return InitError(error);
}
}
}
// As LoadBlockIndex can take several minutes, it's possible the user
// requested to kill the GUI during the last operation. If so, exit.
// As the program has not fully started yet, Shutdown() is possibly
// overkill.
if (ShutdownRequested()) {
LogPrintf("Shutdown requested. Exiting.\n");
return false;
}
ChainstateManager &chainman = *Assert(node.chainman);
if (args.GetBoolArg("-avalanche", AVALANCHE_DEFAULT_ENABLED)) {
// Initialize Avalanche.
bilingual_str avalancheError;
node.avalanche = avalanche::Processor::MakeProcessor(
args, *node.chain, node.connman.get(), chainman, node.mempool.get(),
*node.scheduler, avalancheError);
if (!node.avalanche) {
InitError(avalancheError);
return false;
}
if (node.avalanche->isAvalancheServiceAvailable()) {
nLocalServices = ServiceFlags(nLocalServices | NODE_AVALANCHE);
}
}
PeerManager::Options peerman_opts{};
ApplyArgsManOptions(args, peerman_opts);
assert(!node.peerman);
node.peerman = PeerManager::make(*node.connman, *node.addrman,
node.banman.get(), chainman, *node.mempool,
node.avalanche.get(), peerman_opts);
RegisterValidationInterface(node.peerman.get());
// Encoded addresses using cashaddr instead of base58.
// We do this by default to avoid confusion with BTC addresses.
config.SetCashAddrEncoding(args.GetBoolArg("-usecashaddr", true));
// Step 8: load indexers
if (args.GetBoolArg("-txindex", DEFAULT_TXINDEX)) {
auto result{
WITH_LOCK(cs_main, return CheckLegacyTxindex(*Assert(
chainman.m_blockman.m_block_tree_db)))};
if (!result) {
return InitError(util::ErrorString(result));
}
g_txindex =
std::make_unique<TxIndex>(cache_sizes.tx_index, false, fReindex);
if (!g_txindex->Start(chainman.ActiveChainstate())) {
return false;
}
}
for (const auto &filter_type : g_enabled_filter_types) {
InitBlockFilterIndex(filter_type, cache_sizes.filter_index, false,
fReindex);
if (!GetBlockFilterIndex(filter_type)
->Start(chainman.ActiveChainstate())) {
return false;
}
}
if (args.GetBoolArg("-coinstatsindex", DEFAULT_COINSTATSINDEX)) {
g_coin_stats_index = std::make_unique<CoinStatsIndex>(
/* cache size */ 0, false, fReindex);
if (!g_coin_stats_index->Start(chainman.ActiveChainstate())) {
return false;
}
}
#if ENABLE_CHRONIK
if (args.GetBoolArg("-chronik", DEFAULT_CHRONIK)) {
const bool fReindexChronik =
fReindex || args.GetBoolArg("-chronikreindex", false);
if (!chronik::Start(args, config, node, fReindexChronik)) {
return false;
}
}
#endif
// Step 9: load wallet
for (const auto &client : node.chain_clients) {
if (!client->load()) {
return false;
}
}
// Step 10: data directory maintenance
// if pruning, unset the service bit and perform the initial blockstore
// prune after any wallet rescanning has taken place.
if (chainman.m_blockman.IsPruneMode()) {
LogPrintf("Unsetting NODE_NETWORK on prune mode\n");
nLocalServices = ServiceFlags(nLocalServices & ~NODE_NETWORK);
if (!fReindex) {
LOCK(cs_main);
for (Chainstate *chainstate : chainman.GetAll()) {
uiInterface.InitMessage(_("Pruning blockstore...").translated);
chainstate->PruneAndFlush();
}
}
}
// Step 11: import blocks
if (!CheckDiskSpace(args.GetDataDirNet())) {
InitError(
strprintf(_("Error: Disk space is low for %s"),
fs::quoted(fs::PathToString(args.GetDataDirNet()))));
return false;
}
if (!CheckDiskSpace(args.GetBlocksDirPath())) {
InitError(
strprintf(_("Error: Disk space is low for %s"),
fs::quoted(fs::PathToString(args.GetBlocksDirPath()))));
return false;
}
// Either install a handler to notify us when genesis activates, or set
// fHaveGenesis directly.
// No locking, as this happens before any background thread is started.
boost::signals2::connection block_notify_genesis_wait_connection;
if (WITH_LOCK(chainman.GetMutex(),
return chainman.ActiveChain().Tip() == nullptr)) {
block_notify_genesis_wait_connection =
uiInterface.NotifyBlockTip_connect(
std::bind(BlockNotifyGenesisWait, std::placeholders::_2));
} else {
fHaveGenesis = true;
}
#if defined(HAVE_SYSTEM)
const std::string block_notify = args.GetArg("-blocknotify", "");
if (!block_notify.empty()) {
uiInterface.NotifyBlockTip_connect([block_notify](
SynchronizationState sync_state,
const CBlockIndex *pBlockIndex) {
if (sync_state != SynchronizationState::POST_INIT || !pBlockIndex) {
return;
}
std::string command = block_notify;
ReplaceAll(command, "%s", pBlockIndex->GetBlockHash().GetHex());
std::thread t(runCommand, command);
// thread runs free
t.detach();
});
}
#endif
std::vector<fs::path> vImportFiles;
for (const std::string &strFile : args.GetArgs("-loadblock")) {
vImportFiles.push_back(fs::PathFromString(strFile));
}
avalanche::Processor *const avalanche = node.avalanche.get();
chainman.m_load_block =
std::thread(&util::TraceThread, "loadblk", [=, &chainman, &args] {
ThreadImport(chainman, avalanche, vImportFiles,
ShouldPersistMempool(args) ? MempoolPath(args)
: fs::path{});
});
// Wait for genesis block to be processed
{
WAIT_LOCK(g_genesis_wait_mutex, lock);
// We previously could hang here if StartShutdown() is called prior to
// ThreadImport getting started, so instead we just wait on a timer to
// check ShutdownRequested() regularly.
while (!fHaveGenesis && !ShutdownRequested()) {
g_genesis_wait_cv.wait_for(lock, std::chrono::milliseconds(500));
}
block_notify_genesis_wait_connection.disconnect();
}
if (ShutdownRequested()) {
return false;
}
// Step 12: start node
int chain_active_height;
//// debug print
{
LOCK(cs_main);
LogPrintf("block tree size = %u\n", chainman.BlockIndex().size());
chain_active_height = chainman.ActiveChain().Height();
if (tip_info) {
tip_info->block_height = chain_active_height;
tip_info->block_time =
chainman.ActiveChain().Tip()
? chainman.ActiveChain().Tip()->GetBlockTime()
: chainman.GetParams().GenesisBlock().GetBlockTime();
tip_info->verification_progress = GuessVerificationProgress(
chainman.GetParams().TxData(), chainman.ActiveChain().Tip());
}
if (tip_info && chainman.m_best_header) {
tip_info->header_height = chainman.m_best_header->nHeight;
tip_info->header_time = chainman.m_best_header->GetBlockTime();
}
}
LogPrintf("nBestHeight = %d\n", chain_active_height);
if (node.peerman) {
node.peerman->SetBestHeight(chain_active_height);
}
// Map ports with UPnP or NAT-PMP.
StartMapPort(args.GetBoolArg("-upnp", DEFAULT_UPNP),
args.GetBoolArg("-natpmp", DEFAULT_NATPMP));
CConnman::Options connOptions;
connOptions.nLocalServices = nLocalServices;
connOptions.nMaxConnections = nMaxConnections;
connOptions.m_max_avalanche_outbound =
node.avalanche
? args.GetIntArg("-maxavalancheoutbound",
DEFAULT_MAX_AVALANCHE_OUTBOUND_CONNECTIONS)
: 0;
connOptions.m_max_outbound_full_relay = std::min(
MAX_OUTBOUND_FULL_RELAY_CONNECTIONS,
connOptions.nMaxConnections - connOptions.m_max_avalanche_outbound);
connOptions.m_max_outbound_block_relay = std::min(
MAX_BLOCK_RELAY_ONLY_CONNECTIONS,
connOptions.nMaxConnections - connOptions.m_max_avalanche_outbound -
connOptions.m_max_outbound_full_relay);
connOptions.nMaxAddnode = MAX_ADDNODE_CONNECTIONS;
connOptions.nMaxFeeler = MAX_FEELER_CONNECTIONS;
connOptions.uiInterface = &uiInterface;
connOptions.m_banman = node.banman.get();
connOptions.m_msgproc.push_back(node.peerman.get());
if (node.avalanche) {
connOptions.m_msgproc.push_back(node.avalanche.get());
}
connOptions.nSendBufferMaxSize =
1000 * args.GetIntArg("-maxsendbuffer", DEFAULT_MAXSENDBUFFER);
connOptions.nReceiveFloodSize =
1000 * args.GetIntArg("-maxreceivebuffer", DEFAULT_MAXRECEIVEBUFFER);
connOptions.m_added_nodes = args.GetArgs("-addnode");
connOptions.nMaxOutboundLimit =
1024 * 1024 *
args.GetIntArg("-maxuploadtarget", DEFAULT_MAX_UPLOAD_TARGET);
connOptions.m_peer_connect_timeout = peer_connect_timeout;
connOptions.whitelist_forcerelay =
args.GetBoolArg("-whitelistforcerelay", DEFAULT_WHITELISTFORCERELAY);
connOptions.whitelist_relay =
args.GetBoolArg("-whitelistrelay", DEFAULT_WHITELISTRELAY);
// Port to bind to if `-bind=addr` is provided without a `:port` suffix.
const uint16_t default_bind_port = static_cast<uint16_t>(
args.GetIntArg("-port", config.GetChainParams().GetDefaultPort()));
const auto BadPortWarning = [](const char *prefix, uint16_t port) {
return strprintf(_("%s request to listen on port %u. This port is "
"considered \"bad\" and "
"thus it is unlikely that any Bitcoin ABC peers "
"connect to it. See "
"doc/p2p-bad-ports.md for details and a full list."),
prefix, port);
};
for (const std::string &bind_arg : args.GetArgs("-bind")) {
CService bind_addr;
const size_t index = bind_arg.rfind('=');
if (index == std::string::npos) {
if (Lookup(bind_arg, bind_addr, default_bind_port,
/*fAllowLookup=*/false)) {
connOptions.vBinds.push_back(bind_addr);
if (IsBadPort(bind_addr.GetPort())) {
InitWarning(BadPortWarning("-bind", bind_addr.GetPort()));
}
continue;
}
} else {
const std::string network_type = bind_arg.substr(index + 1);
if (network_type == "onion") {
const std::string truncated_bind_arg =
bind_arg.substr(0, index);
if (Lookup(truncated_bind_arg, bind_addr,
BaseParams().OnionServiceTargetPort(), false)) {
connOptions.onion_binds.push_back(bind_addr);
continue;
}
}
}
return InitError(ResolveErrMsg("bind", bind_arg));
}
for (const std::string &strBind : args.GetArgs("-whitebind")) {
NetWhitebindPermissions whitebind;
bilingual_str error;
if (!NetWhitebindPermissions::TryParse(strBind, whitebind, error)) {
return InitError(error);
}
connOptions.vWhiteBinds.push_back(whitebind);
}
// If the user did not specify -bind= or -whitebind= then we bind
// on any address - 0.0.0.0 (IPv4) and :: (IPv6).
connOptions.bind_on_any =
args.GetArgs("-bind").empty() && args.GetArgs("-whitebind").empty();
// Emit a warning if a bad port is given to -port= but only if -bind and
// -whitebind are not given, because if they are, then -port= is ignored.
if (connOptions.bind_on_any && args.IsArgSet("-port")) {
const uint16_t port_arg = args.GetIntArg("-port", 0);
if (IsBadPort(port_arg)) {
InitWarning(BadPortWarning("-port", port_arg));
}
}
CService onion_service_target;
if (!connOptions.onion_binds.empty()) {
onion_service_target = connOptions.onion_binds.front();
} else if (!connOptions.vBinds.empty()) {
onion_service_target = connOptions.vBinds.front();
} else {
onion_service_target = DefaultOnionServiceTarget();
connOptions.onion_binds.push_back(onion_service_target);
}
if (args.GetBoolArg("-listenonion", DEFAULT_LISTEN_ONION)) {
if (connOptions.onion_binds.size() > 1) {
InitWarning(strprintf(
_("More than one onion bind address is provided. Using %s "
"for the automatically created Tor onion service."),
onion_service_target.ToStringIPPort()));
}
StartTorControl(onion_service_target);
}
if (connOptions.bind_on_any) {
// Only add all IP addresses of the machine if we would be listening on
// any address - 0.0.0.0 (IPv4) and :: (IPv6).
Discover();
}
for (const auto &net : args.GetArgs("-whitelist")) {
NetWhitelistPermissions subnet;
ConnectionDirection connection_direction;
bilingual_str error;
if (!NetWhitelistPermissions::TryParse(net, subnet,
connection_direction, error)) {
return InitError(error);
}
if (connection_direction & ConnectionDirection::In) {
connOptions.vWhitelistedRangeIncoming.push_back(subnet);
}
if (connection_direction & ConnectionDirection::Out) {
connOptions.vWhitelistedRangeOutgoing.push_back(subnet);
}
}
connOptions.vSeedNodes = args.GetArgs("-seednode");
// Initiate outbound connections unless connect=0
connOptions.m_use_addrman_outgoing = !args.IsArgSet("-connect");
if (!connOptions.m_use_addrman_outgoing) {
const auto connect = args.GetArgs("-connect");
if (connect.size() != 1 || connect[0] != "0") {
connOptions.m_specified_outgoing = connect;
}
}
const std::string &i2psam_arg = args.GetArg("-i2psam", "");
if (!i2psam_arg.empty()) {
CService addr;
if (!Lookup(i2psam_arg, addr, 7656, fNameLookup) || !addr.IsValid()) {
return InitError(strprintf(
_("Invalid -i2psam address or hostname: '%s'"), i2psam_arg));
}
SetReachable(NET_I2P, true);
SetProxy(NET_I2P, proxyType{addr});
} else {
SetReachable(NET_I2P, false);
}
connOptions.m_i2p_accept_incoming =
args.GetBoolArg("-i2pacceptincoming", true);
if (!node.connman->Start(*node.scheduler, connOptions)) {
return false;
}
// Step 13: finished
// At this point, the RPC is "started", but still in warmup, which means it
// cannot yet be called. Before we make it callable, we need to make sure
// that the RPC's view of the best block is valid and consistent with
// ChainstateManager's active tip.
//
// If we do not do this, RPC's view of the best block will be height=0 and
// hash=0x0. This will lead to erroroneous responses for things like
// waitforblockheight.
RPCNotifyBlockChange(
WITH_LOCK(chainman.GetMutex(), return chainman.ActiveTip()));
SetRPCWarmupFinished();
uiInterface.InitMessage(_("Done loading").translated);
for (const auto &client : node.chain_clients) {
client->start(*node.scheduler);
}
BanMan *banman = node.banman.get();
node.scheduler->scheduleEvery(
[banman] {
banman->DumpBanlist();
return true;
},
DUMP_BANS_INTERVAL);
// Start Avalanche's event loop.
if (node.avalanche) {
node.avalanche->startEventLoop(*node.scheduler);
}
if (node.peerman) {
node.peerman->StartScheduledTasks(*node.scheduler);
}
#if HAVE_SYSTEM
StartupNotify(args);
#endif
return true;
}
diff --git a/test/functional/chronik_electrum_blockchain.py b/test/functional/chronik_electrum_blockchain.py
index 8004bcf41..ac372f9e0 100644
--- a/test/functional/chronik_electrum_blockchain.py
+++ b/test/functional/chronik_electrum_blockchain.py
@@ -1,771 +1,814 @@
# 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)
- def sorting_key(utxo):
- return utxo["tx_hash"], utxo["tx_pos"]
-
- actual_utxos = sorted(actual_utxos, key=sorting_key)
- expected_utxos = sorted(expected_utxos, 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()
- for _ in range(4):
- # Add an unconfirmaed transaction
+ 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=1337, fee=1000
+ from_node=self.node, scriptPubKey=script, amount=amount, fee=fee
)
- unconfirmed += 1337
- history.append({"fee": 1000, "height": 0, "tx_hash": txid})
- utxos.append({"height": 0, "tx_hash": txid, "tx_pos": n, "value": 1337})
+ 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):
- txid, n = self.wallet.send_to(
- from_node=self.node, scriptPubKey=script, amount=888, fee=999
- )
- unconfirmed += 888
- history.append({"fee": 999, "height": 0, "tx_hash": txid})
- utxos.append({"height": 0, "tx_hash": txid, "tx_pos": n, "value": 888})
+ 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(
+ 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

Mime Type
text/x-diff
Expires
Sun, Mar 2, 08:35 (21 h, 15 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5164552
Default Alt Text
(222 KB)

Event Timeline