Files
plm-electrumx/src/electrumx/lib/coins.py
T

337 lines
12 KiB
Python

# 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
from typing import Sequence, Optional, Type
import electrumx.lib.util as util
from electrumx.lib.hash import Base58, Bech32, double_sha256, hash_to_hex_str, HASHX_LEN
from electrumx.lib.script import ScriptPubKey
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
from electrumx.server.session import ElectrumX
@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
BECH32_HRP: Optional[str] = None # human-readable part of the segwit addresses
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.
# For Palladium (segwit, as Bitcoin), that is 4 M weight units, i.e. 4 MB on the p2p wire.
# 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()
# whether the daemon can tell which tx spends an outpoint (gettxspendingprevout); needed to subscribe to outpoints
DAEMON_HAS_TXO_SPENDER_INDEX = True
# 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 in base58 form (P2PKH or P2SH) or in bech32 form (segwit).
Only used for the local RPC 'query' command.
'''
if cls.BECH32_HRP and address.lower().startswith(cls.BECH32_HRP + '1'):
return ScriptPubKey.segwit_script(*Bech32.decode_segwit(cls.BECH32_HRP, address))
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.
For example 1 PLM is returned for 100 million satoshis.
'''
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
class Palladium(Coin):
"""Palladium (PLM). Standard 80-byte headers, sha256d PoW, segwit and taproot (scripts are indexed by hash,
so no special handling is needed for either).
Its daemon has no 'txospenderindex', 'getindexinfo' or 'submitpackage', so
'blockchain.outpoint.subscribe' is refused (DAEMON_HAS_TXO_SPENDER_INDEX) and
'blockchain.transaction.broadcast_package' returns an explicit error to clients.
The daemon needs 'txindex=1'.
"""
NAME = "Palladium"
SHORTNAME = "PLM"
NET = "mainnet"
BECH32_HRP = "plm"
P2PKH_VERBYTE = bytes.fromhex("37")
P2SH_VERBYTES = (bytes.fromhex("05"),)
# same genesis block as Bitcoin mainnet
GENESIS_HASH = ('000000000019d6689c085ae165831e93'
'4ff763ae46a2a6c172b3f1b60a8ce26f')
MEMPOOL_HISTOGRAM_REFRESH_SECS = 120
DESERIALIZER = lib_tx.DeserializerSegWit
DAEMON_HAS_TXO_SPENDER_INDEX = False
RPC_PORT = 2332
# snapshot (UpdateTip tx= count), only used for sync ETAs
TX_COUNT = 662_327
TX_COUNT_HEIGHT = 542_786
TX_PER_BLOCK = 2
PEERS = []
@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
class PalladiumTestnet(Palladium):
SHORTNAME = "TPLM"
NET = "testnet"
BECH32_HRP = "tplm"
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):
SHORTNAME = "RPLM"
NET = "regtest"
BECH32_HRP = "rplm"
# same genesis block as Bitcoin regtest
GENESIS_HASH = ('0f9188f13cb7b2c71f2a335e3a4fc328'
'bf5beb436012afca590b1a11466e2206')
RPC_PORT = 28443