2026-10-22 15:03:56 +02:00
|
|
|
# Copyright (c) 2016-2017, Neil Booth
|
|
|
|
|
# Copyright (c) 2017, the ElectrumX authors
|
|
|
|
|
#
|
|
|
|
|
# All rights reserved.
|
|
|
|
|
#
|
|
|
|
|
# The MIT License (MIT)
|
|
|
|
|
#
|
|
|
|
|
# Permission is hereby granted, free of charge, to any person obtaining
|
|
|
|
|
# a copy of this software and associated documentation files (the
|
|
|
|
|
# "Software"), to deal in the Software without restriction, including
|
|
|
|
|
# without limitation the rights to use, copy, modify, merge, publish,
|
|
|
|
|
# distribute, sublicense, and/or sell copies of the Software, and to
|
|
|
|
|
# permit persons to whom the Software is furnished to do so, subject to
|
|
|
|
|
# the following conditions:
|
|
|
|
|
#
|
|
|
|
|
# The above copyright notice and this permission notice shall be
|
|
|
|
|
# included in all copies or substantial portions of the Software.
|
|
|
|
|
#
|
|
|
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
|
|
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
|
|
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
|
|
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
|
|
|
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
|
|
|
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
|
|
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
|
|
|
|
|
|
|
|
'''Module providing coin abstraction.
|
|
|
|
|
|
|
|
|
|
Anything coin-specific should go in this file and be subclassed where
|
|
|
|
|
necessary for appropriate handling.
|
|
|
|
|
'''
|
|
|
|
|
import re
|
|
|
|
|
from dataclasses import dataclass
|
|
|
|
|
from decimal import Decimal
|
|
|
|
|
from hashlib import sha256
|
2026-10-22 14:59:11 +02:00
|
|
|
from typing import Sequence, Optional, Type
|
2026-10-22 15:03:56 +02:00
|
|
|
|
|
|
|
|
import electrumx.lib.util as util
|
2026-10-22 14:59:11 +02:00
|
|
|
from electrumx.lib.hash import Base58, double_sha256, hash_to_hex_str, HASHX_LEN
|
|
|
|
|
from electrumx.lib.script import ScriptPubKey
|
2026-10-22 15:03:56 +02:00
|
|
|
import electrumx.lib.tx as lib_tx
|
|
|
|
|
from electrumx.lib.tx import Tx
|
|
|
|
|
import electrumx.server.block_processor as block_proc
|
|
|
|
|
import electrumx.server.daemon as daemon
|
2026-10-22 14:59:11 +02:00
|
|
|
from electrumx.server.session import ElectrumX
|
2026-10-22 15:03:56 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(slots=True)
|
|
|
|
|
class Block:
|
|
|
|
|
raw: bytes
|
|
|
|
|
header: bytes
|
|
|
|
|
transactions: Sequence[Tx]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CoinError(Exception):
|
|
|
|
|
'''Exception raised for coin-related errors.'''
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Coin:
|
|
|
|
|
'''Base class of coin hierarchy.'''
|
|
|
|
|
|
|
|
|
|
REORG_LIMIT = 200
|
|
|
|
|
# Not sure if these are coin-specific
|
|
|
|
|
RPC_URL_REGEX = re.compile('.+@(\\[[0-9a-fA-F:]+\\]|[^:]+)(:[0-9]+)?')
|
|
|
|
|
VALUE_PER_COIN = 100000000
|
|
|
|
|
BASIC_HEADER_SIZE = 80
|
|
|
|
|
STATIC_BLOCK_HEADERS = True
|
|
|
|
|
SESSIONCLS = ElectrumX
|
|
|
|
|
DESERIALIZER = lib_tx.Deserializer
|
|
|
|
|
DAEMON = daemon.Daemon
|
|
|
|
|
BLOCK_PROCESSOR = block_proc.BlockProcessor
|
|
|
|
|
P2PKH_VERBYTE = bytes.fromhex("00")
|
|
|
|
|
P2SH_VERBYTES = (bytes.fromhex("05"),)
|
|
|
|
|
DECODE_CHECK = Base58.decode_check
|
|
|
|
|
GENESIS_HASH = ('000000000019d6689c085ae165831e93'
|
|
|
|
|
'4ff763ae46a2a6c172b3f1b60a8ce26f') # 'hum' byte-order
|
|
|
|
|
|
|
|
|
|
# max byte-size of single jsonrpc message
|
|
|
|
|
# note: we should probably be able to serve a client asking for the largest consensus-valid tx.
|
2026-10-23 15:21:24 +02:00
|
|
|
# For Palladium (segwit, as Bitcoin), that is 4 M weight units, i.e. 4 MB on the p2p wire.
|
2026-10-22 15:03:56 +02:00
|
|
|
# Double that due to our JSON-RPC hex-encoding, plus overhead, we need to send 8+ MB.
|
|
|
|
|
# When receiving from a client, e.g. to broadcast, such a tx would be non-standard anyway.
|
|
|
|
|
DEFAULT_MAX_SEND = 8_100_000
|
|
|
|
|
DEFAULT_MAX_RECV = 1_000_000
|
|
|
|
|
|
|
|
|
|
MEMPOOL_HISTOGRAM_REFRESH_SECS = 500
|
|
|
|
|
# first bin size in vbytes. smaller bins mean more precision but also bandwidth:
|
|
|
|
|
MEMPOOL_COMPACT_HISTOGRAM_BINSIZE = 30_000
|
|
|
|
|
|
|
|
|
|
# Peer discovery
|
|
|
|
|
PEER_DEFAULT_PORTS = {'t': '50001', 's': '50002'}
|
|
|
|
|
PEERS = []
|
|
|
|
|
CRASH_CLIENT_VER = None
|
|
|
|
|
BLACKLIST_URL = None
|
|
|
|
|
ESTIMATEFEE_MODES = (None, 'CONSERVATIVE', 'ECONOMICAL')
|
|
|
|
|
|
|
|
|
|
RPC_PORT: int
|
|
|
|
|
NAME: str
|
|
|
|
|
NET: str | None
|
|
|
|
|
MIN_REQUIRED_DAEMON_VERSION: Optional[str] = None
|
|
|
|
|
REQUIRED_DAEMON_INDEXES: Sequence[str] = tuple()
|
|
|
|
|
|
|
|
|
|
# only used for initial db sync ETAs:
|
|
|
|
|
TX_COUNT_HEIGHT: int # at a given snapshot of the chain,
|
|
|
|
|
TX_COUNT: int # there have been this many txs so far,
|
|
|
|
|
TX_PER_BLOCK: int # and from that height onwards, we guess this many txs per block
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def lookup_coin_class(cls, name: str, net: str) -> Type['Coin']:
|
|
|
|
|
'''Return a coin class given name and network.
|
|
|
|
|
|
|
|
|
|
Raise an exception if unrecognised.'''
|
|
|
|
|
req_attrs = ('TX_COUNT', 'TX_COUNT_HEIGHT', 'TX_PER_BLOCK')
|
|
|
|
|
for coin in util.subclasses(Coin):
|
|
|
|
|
if (
|
|
|
|
|
coin.NAME.lower() == name.lower()
|
|
|
|
|
and coin.NET is not None
|
|
|
|
|
and coin.NET.lower() == net.lower()
|
|
|
|
|
):
|
|
|
|
|
missing = [
|
|
|
|
|
attr
|
|
|
|
|
for attr in req_attrs
|
|
|
|
|
if not hasattr(coin, attr)
|
|
|
|
|
]
|
|
|
|
|
if missing:
|
|
|
|
|
raise CoinError(
|
|
|
|
|
f'coin {name} missing {missing} attributes'
|
|
|
|
|
)
|
|
|
|
|
return coin
|
|
|
|
|
raise CoinError(f'unknown coin {name} and network {net} combination')
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def sanitize_url(cls, url: str):
|
|
|
|
|
# Remove surrounding ws and trailing /s
|
|
|
|
|
url = url.strip().rstrip('/')
|
|
|
|
|
match = cls.RPC_URL_REGEX.match(url)
|
|
|
|
|
if not match:
|
|
|
|
|
raise CoinError(f'invalid daemon URL: "{url}"')
|
|
|
|
|
if match.groups()[1] is None:
|
|
|
|
|
url = f'{url}:{cls.RPC_PORT:d}'
|
|
|
|
|
if not url.startswith(('http://', 'https://')):
|
|
|
|
|
url = f'http://{url}'
|
|
|
|
|
return url + '/'
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def max_fetch_blocks(cls, height: int) -> int:
|
|
|
|
|
if height < 130000:
|
|
|
|
|
return 1000
|
|
|
|
|
return 100
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def genesis_block(cls, block: bytes) -> bytes:
|
|
|
|
|
'''Check the Genesis block is the right one for this coin.
|
|
|
|
|
|
|
|
|
|
Return the block less its unspendable coinbase.
|
|
|
|
|
'''
|
|
|
|
|
header = cls.block_header(block, 0)
|
|
|
|
|
header_hex_hash = hash_to_hex_str(cls.header_hash_rev(header))
|
|
|
|
|
if header_hex_hash != cls.GENESIS_HASH:
|
|
|
|
|
raise CoinError(f'genesis block has hash {header_hex_hash} '
|
|
|
|
|
f'expected {cls.GENESIS_HASH}')
|
|
|
|
|
|
|
|
|
|
return header + b'\0'
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def hashX_from_script(cls, script):
|
|
|
|
|
'''Returns a hashX from a script.'''
|
|
|
|
|
return sha256(script).digest()[:HASHX_LEN]
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def address_to_hashX(cls, address):
|
|
|
|
|
'''Return a hashX given a coin address.'''
|
|
|
|
|
return cls.hashX_from_script(cls.pay_to_address_script(address))
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def hash160_to_P2PKH_script(cls, hash160):
|
|
|
|
|
return ScriptPubKey.P2PKH_script(hash160)
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def hash160_to_P2PKH_hashX(cls, hash160):
|
|
|
|
|
return cls.hashX_from_script(cls.hash160_to_P2PKH_script(hash160))
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def pay_to_address_script(cls, address):
|
|
|
|
|
'''Return a pubkey script that pays to a pubkey hash.
|
|
|
|
|
|
|
|
|
|
Pass the address (either P2PKH or P2SH) in base58 form.
|
2026-10-22 16:25:58 +02:00
|
|
|
Only used for the local RPC 'query' command (bech32 addresses are not supported).
|
2026-10-22 15:03:56 +02:00
|
|
|
'''
|
|
|
|
|
raw = cls.DECODE_CHECK(address)
|
|
|
|
|
|
|
|
|
|
# Require version byte(s) plus hash160.
|
|
|
|
|
verbyte = -1
|
|
|
|
|
verlen = len(raw) - 20
|
|
|
|
|
if verlen > 0:
|
|
|
|
|
verbyte, hash160 = raw[:verlen], raw[verlen:]
|
|
|
|
|
|
|
|
|
|
if verbyte == cls.P2PKH_VERBYTE:
|
|
|
|
|
return cls.hash160_to_P2PKH_script(hash160)
|
|
|
|
|
if verbyte in cls.P2SH_VERBYTES:
|
|
|
|
|
return ScriptPubKey.P2SH_script(hash160)
|
|
|
|
|
|
|
|
|
|
raise CoinError(f'invalid address: {address}')
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def header_hash_rev(cls, header: bytes) -> bytes:
|
|
|
|
|
'''Given a header return hash. (reverse of human-readable blockhash)'''
|
|
|
|
|
return double_sha256(header)
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def header_prevhash_rev(cls, header: bytes) -> bytes:
|
|
|
|
|
'''Given a header return previous hash. (reverse of human-readable blockhash)'''
|
|
|
|
|
return header[4:36]
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def static_header_offset(cls, height: int) -> int:
|
|
|
|
|
'''Given a header height return its offset in the headers file.
|
|
|
|
|
|
|
|
|
|
If header sizes change at some point, this is the only code
|
|
|
|
|
that needs updating.'''
|
|
|
|
|
assert cls.STATIC_BLOCK_HEADERS
|
|
|
|
|
return height * cls.BASIC_HEADER_SIZE
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def static_header_len(cls, height: int) -> int:
|
|
|
|
|
'''Given a header height return its length.'''
|
|
|
|
|
return (cls.static_header_offset(height + 1)
|
|
|
|
|
- cls.static_header_offset(height))
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def block_header(cls, block: bytes, height: int) -> bytes:
|
|
|
|
|
'''Returns the block header given a block and its height.'''
|
|
|
|
|
return block[:cls.static_header_len(height)]
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def block(cls, raw_block: bytes, height: int) -> 'Block':
|
|
|
|
|
'''Return a Block namedtuple given a raw block and its height.'''
|
|
|
|
|
header = cls.block_header(raw_block, height)
|
|
|
|
|
txs = cls.DESERIALIZER(raw_block, start=len(header)).read_tx_block()
|
|
|
|
|
return Block(raw_block, header, txs)
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def decimal_value(cls, value: int) -> Decimal:
|
|
|
|
|
'''Return the number of standard coin units as a Decimal given a
|
|
|
|
|
quantity of smallest units.
|
|
|
|
|
|
2026-10-23 15:21:24 +02:00
|
|
|
For example 1 PLM is returned for 100 million satoshis.
|
2026-10-22 15:03:56 +02:00
|
|
|
'''
|
|
|
|
|
return Decimal(value) / cls.VALUE_PER_COIN
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def warn_old_client_on_tx_broadcast(cls, _client_ver):
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def bucket_estimatefee_block_target(cls, n: int) -> int:
|
|
|
|
|
'''For caching purposes, it might be desirable to restrict the
|
|
|
|
|
set of values that can be queried as an estimatefee block target.
|
|
|
|
|
'''
|
|
|
|
|
return n
|
|
|
|
|
|
|
|
|
|
|
2026-10-22 14:59:11 +02:00
|
|
|
class Palladium(Coin):
|
|
|
|
|
"""Palladium (PLM), a Bitcoin Core 0.19-based chain. Standard 80-byte headers, sha256d PoW, segwit.
|
|
|
|
|
|
|
|
|
|
Its daemon predates 'txospenderindex', 'getindexinfo' and 'submitpackage', so
|
|
|
|
|
'blockchain.outpoint.subscribe' and 'blockchain.transaction.broadcast_package' will
|
|
|
|
|
return daemon errors to clients. The daemon needs 'txindex=1'.
|
|
|
|
|
"""
|
|
|
|
|
NAME = "Palladium"
|
|
|
|
|
SHORTNAME = "PLM"
|
|
|
|
|
NET = "mainnet"
|
|
|
|
|
P2PKH_VERBYTE = bytes.fromhex("37")
|
|
|
|
|
P2SH_VERBYTES = (bytes.fromhex("05"),)
|
|
|
|
|
# same genesis block as Bitcoin mainnet
|
|
|
|
|
GENESIS_HASH = ('000000000019d6689c085ae165831e93'
|
|
|
|
|
'4ff763ae46a2a6c172b3f1b60a8ce26f')
|
2026-10-22 15:03:56 +02:00
|
|
|
MEMPOOL_HISTOGRAM_REFRESH_SECS = 120
|
|
|
|
|
DESERIALIZER = lib_tx.DeserializerSegWit
|
2026-10-22 14:59:11 +02:00
|
|
|
RPC_PORT = 2332
|
|
|
|
|
# snapshot (UpdateTip tx= count), only used for sync ETAs
|
|
|
|
|
TX_COUNT = 661_421
|
|
|
|
|
TX_COUNT_HEIGHT = 542_032
|
|
|
|
|
TX_PER_BLOCK = 2
|
|
|
|
|
PEERS = []
|
2026-10-22 15:03:56 +02:00
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def bucket_estimatefee_block_target(cls, n: int) -> int:
|
|
|
|
|
# values based on https://github.com/bitcoin/bitcoin/blob/af05bd9e1e362c3148e3b434b7fac96a9a5155a1/src/policy/fees.h#L131 # noqa
|
|
|
|
|
if n <= 1:
|
|
|
|
|
return 1
|
|
|
|
|
if n <= 12:
|
|
|
|
|
return n
|
|
|
|
|
if n == 25: # so common that we make an exception for it
|
|
|
|
|
return n
|
|
|
|
|
if n <= 48:
|
|
|
|
|
return n // 2 * 2
|
|
|
|
|
if n <= 1008:
|
|
|
|
|
return n // 24 * 24
|
|
|
|
|
return 1008
|
|
|
|
|
|
|
|
|
|
|
2026-10-22 14:54:54 +02:00
|
|
|
class PalladiumTestnet(Palladium):
|
|
|
|
|
SHORTNAME = "TPLM"
|
|
|
|
|
NET = "testnet"
|
|
|
|
|
P2PKH_VERBYTE = bytes.fromhex("7f")
|
|
|
|
|
P2SH_VERBYTES = (bytes.fromhex("73"),)
|
|
|
|
|
# same genesis block as Bitcoin testnet3
|
|
|
|
|
GENESIS_HASH = ('000000000933ea01ad0ee984209779ba'
|
|
|
|
|
'aec3ced90fa3f408719526f8d77f4943')
|
|
|
|
|
RPC_PORT = 12332
|
|
|
|
|
REORG_LIMIT = 8000
|
|
|
|
|
TX_COUNT = 1
|
|
|
|
|
TX_COUNT_HEIGHT = 1
|
|
|
|
|
TX_PER_BLOCK = 2
|
|
|
|
|
PEER_DEFAULT_PORTS = {'t': '51001', 's': '51002'}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PalladiumRegtest(PalladiumTestnet):
|
|
|
|
|
NET = "regtest"
|
|
|
|
|
# same genesis block as Bitcoin regtest
|
|
|
|
|
GENESIS_HASH = ('0f9188f13cb7b2c71f2a335e3a4fc328'
|
|
|
|
|
'bf5beb436012afca590b1a11466e2206')
|
|
|
|
|
RPC_PORT = 28443
|