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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions ecosystem.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
module.exports = {
apps: [{
name: 'electrumx-doriancoin',
script: './electrumx_server',
cwd: '/home/electrumx/electrumx',
interpreter: '/home/electrumx/electrumx/venv/bin/python3',
env: {
COIN: 'Doriancoin',
NET: 'mainnet',
DB_DIRECTORY: '/var/lib/electrumx/doriancoin',
DAEMON_URL: 'http://user:password@127.0.0.1:1948/',
SERVICES: 'tcp://:51001,ssl://:51002',
SSL_CERTFILE: '/home/electrumx/.electrumx/certs/server.crt',
SSL_KEYFILE: '/home/electrumx/.electrumx/certs/server.key',
CACHE_MB: '2000',
BANDWIDTH_UNIT_COST: '100000',
REQUEST_TIMEOUT: '60',
MAX_RECV: '50000000',
MAX_SEND: '20000000',
DB_BATCH_SIZE: '10000',
COST_SOFT_LIMIT: '2000',
COST_HARD_LIMIT: '100000',
REQUEST_SLEEP: '5000',
},
autorestart: true,
max_restarts: 10,
restart_delay: 5000,
}]
};
83 changes: 79 additions & 4 deletions src/electrumx/lib/coins.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
from typing import Sequence, Tuple, Optional

import electrumx.lib.util as util
from electrumx.lib.hash import Base58, double_sha256, hash_to_hex_str
from electrumx.lib.hash import Base58, Bech32, Bech32Error, double_sha256, hash_to_hex_str
from electrumx.lib.hash import HASHX_LEN, hex_str_to_hash
from electrumx.lib.script import (_match_ops, Script, ScriptError,
ScriptPubKey, OpCodes)
Expand Down Expand Up @@ -88,6 +88,9 @@ class Coin:
WIF_BYTE = bytes.fromhex("80")
ENCODE_CHECK = Base58.encode_check
DECODE_CHECK = Base58.decode_check
# Bech32/Bech32m HRP (Human Readable Part) for SegWit/Taproot addresses
# Set to None for coins that don't support bech32
BECH32_HRP = None
GENESIS_HASH = ('000000000019d6689c085ae165831e93'
'4ff763ae46a2a6c172b3f1b60a8ce26f')
GENESIS_ACTIVATION = 100_000_000
Expand Down Expand Up @@ -206,11 +209,28 @@ def hash160_to_P2PKH_hashX(cls, hash160):

@classmethod
def pay_to_address_script(cls, address):
'''Return a pubkey script that pays to a pubkey hash.
'''Return a pubkey script that pays to an address.

Pass the address (either P2PKH or P2SH) in base58 form.
Supports:
- P2PKH and P2SH addresses in base58 form
- P2WPKH and P2WSH addresses in bech32 form (SegWit v0)
- P2TR addresses in bech32m form (Taproot, SegWit v1)
'''
raw = cls.DECODE_CHECK(address)
# Try bech32/bech32m first if the coin supports it
if cls.BECH32_HRP is not None:
try:
hrp, witness_version, witness_program = Bech32.decode(address)
if hrp != cls.BECH32_HRP:
raise CoinError(f'invalid address HRP: expected {cls.BECH32_HRP}, got {hrp}')
return ScriptPubKey.witness_program_script(witness_version, witness_program)
except Bech32Error:
pass # Not a valid bech32 address, try base58

# Try base58
try:
raw = cls.DECODE_CHECK(address)
except Exception:
raise CoinError(f'invalid address: {address}')

# Require version byte(s) plus hash160.
verbyte = -1
Expand Down Expand Up @@ -297,6 +317,7 @@ class BitcoinMixin:
XPUB_VERBYTES = bytes.fromhex("0488b21e")
XPRV_VERBYTES = bytes.fromhex("0488ade4")
RPC_PORT = 8332
BECH32_HRP = "bc"


class Bitcoin(BitcoinMixin, Coin):
Expand Down Expand Up @@ -373,6 +394,7 @@ class BitcoinTestnetMixin:
GENESIS_HASH = ('000000000933ea01ad0ee984209779ba'
'aec3ced90fa3f408719526f8d77f4943')
REORG_LIMIT = 8000
BECH32_HRP = "tb"
TX_COUNT = 12242438
TX_COUNT_HEIGHT = 1035428
TX_PER_BLOCK = 21
Expand Down Expand Up @@ -1038,6 +1060,7 @@ class Litecoin(Coin):
TX_COUNT_HEIGHT = 1105256
TX_PER_BLOCK = 10
RPC_PORT = 9332
BECH32_HRP = "ltc"
REORG_LIMIT = 800
PEERS = [
'ex.lug.gs s444',
Expand Down Expand Up @@ -1065,6 +1088,7 @@ class LitecoinTestnet(Litecoin):
RPC_PORT = 19332
REORG_LIMIT = 4000
PEER_DEFAULT_PORTS = {'t': '51001', 's': '51002'}
BECH32_HRP = "tltc"
PEERS = [
'electrum-ltc.bysh.me s t',
'electrum.ltc.xurious.com s t',
Expand All @@ -1081,6 +1105,57 @@ class LitecoinRegtest(LitecoinTestnet):
TX_COUNT_HEIGHT = 1


class Doriancoin(Coin):
NAME = "Doriancoin"
SHORTNAME = "DSV"
NET = "mainnet"
XPUB_VERBYTES = bytes.fromhex("0488b21e")
XPRV_VERBYTES = bytes.fromhex("0488ade4")
P2PKH_VERBYTE = bytes.fromhex("1e")
P2SH_VERBYTES = (bytes.fromhex("05"), bytes.fromhex("1c"))
WIF_BYTE = bytes.fromhex("b0")
GENESIS_HASH = ('d21da25e277bd20b7456087d69c5fee2'
'ebc6091b410271b5cb0623c7d1e7d1b9')
DESERIALIZER = lib_tx.DeserializerLitecoin
TX_COUNT = 1608098
TX_COUNT_HEIGHT = 1243844
TX_PER_BLOCK = 2
RPC_PORT = 1948
REORG_LIMIT = 800
PEERS = []


class DoriancoinTestnet(Doriancoin):
SHORTNAME = "tDSV"
NET = "testnet"
XPUB_VERBYTES = bytes.fromhex("043587cf")
XPRV_VERBYTES = bytes.fromhex("04358394")
P2PKH_VERBYTE = bytes.fromhex("1e")
P2SH_VERBYTES = (bytes.fromhex("16"), bytes.fromhex("3a"))
WIF_BYTE = bytes.fromhex("ef")
GENESIS_HASH = ('707769464eb59fdd7b75cdbc5f0e7222'
'6345281852325c965b8ee1fd592fbf51')
TX_COUNT = 1
TX_COUNT_HEIGHT = 1
TX_PER_BLOCK = 1
RPC_PORT = 11948
REORG_LIMIT = 4000
PEER_DEFAULT_PORTS = {'t': '51001', 's': '51002'}
PEERS = []


class DoriancoinRegtest(DoriancoinTestnet):
NET = "regtest"
P2PKH_VERBYTE = bytes.fromhex("6f")
P2SH_VERBYTES = (bytes.fromhex("c4"), bytes.fromhex("3a"))
GENESIS_HASH = ('707769464eb59fdd7b75cdbc5f0e7222'
'6345281852325c965b8ee1fd592fbf51')
RPC_PORT = 19443
PEERS = []
TX_COUNT = 1
TX_COUNT_HEIGHT = 1


class BitcoinCashRegtest(BitcoinTestnetMixin, Coin):
NAME = "BitcoinCash"
NET = "regtest"
Expand Down
147 changes: 147 additions & 0 deletions src/electrumx/lib/hash.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,150 @@ def encode_check(payload, *, hash_fn=double_sha256):
into a Base58Check string."""
be_bytes = payload + hash_fn(payload)[:4]
return Base58.encode(be_bytes)


class Bech32Error(Exception):
'''Exception used for Bech32 errors.'''


class Bech32:
'''Class providing bech32 and bech32m functionality (BIP173/BIP350).'''

CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'
BECH32_CONST = 1
BECH32M_CONST = 0x2bc830a3

@staticmethod
def _polymod(values):
'''Internal function that computes the bech32 checksum.'''
GEN = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]
chk = 1
for v in values:
b = chk >> 25
chk = ((chk & 0x1ffffff) << 5) ^ v
for i in range(5):
chk ^= GEN[i] if ((b >> i) & 1) else 0
return chk

@staticmethod
def _hrp_expand(hrp):
'''Expand the HRP into values for checksum computation.'''
return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp]

@classmethod
def _verify_checksum(cls, hrp, data):
'''Verify a checksum given HRP and converted data characters.
Returns the encoding constant (BECH32_CONST or BECH32M_CONST) if valid.'''
const = cls._polymod(cls._hrp_expand(hrp) + data)
if const == cls.BECH32_CONST:
return cls.BECH32_CONST
if const == cls.BECH32M_CONST:
return cls.BECH32M_CONST
return None

@classmethod
def _create_checksum(cls, hrp, data, spec):
'''Compute the checksum values given HRP, data and spec (BECH32 or BECH32M).'''
const = cls.BECH32M_CONST if spec == 'bech32m' else cls.BECH32_CONST
polymod = cls._polymod(cls._hrp_expand(hrp) + data + [0, 0, 0, 0, 0, 0]) ^ const
return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)]

@classmethod
def _convertbits(cls, data, frombits, tobits, pad=True):
'''General power-of-2 base conversion.'''
acc = 0
bits = 0
ret = []
maxv = (1 << tobits) - 1
for value in data:
if value < 0 or (value >> frombits):
raise Bech32Error('invalid data value')
acc = (acc << frombits) | value
bits += frombits
while bits >= tobits:
bits -= tobits
ret.append((acc >> bits) & maxv)
if pad:
if bits:
ret.append((acc << (tobits - bits)) & maxv)
elif bits >= frombits or ((acc << (tobits - bits)) & maxv):
raise Bech32Error('invalid padding')
return ret

@classmethod
def decode(cls, addr):
'''Decode a bech32 or bech32m string.

Returns (hrp, witness_version, witness_program) or raises Bech32Error.
'''
if any(ord(x) < 33 or ord(x) > 126 for x in addr):
raise Bech32Error('invalid character')
if addr.lower() != addr and addr.upper() != addr:
raise Bech32Error('mixed case')
addr = addr.lower()
pos = addr.rfind('1')
if pos < 1 or pos + 7 > len(addr) or len(addr) > 90:
raise Bech32Error('invalid separator position')
hrp = addr[:pos]
data_part = addr[pos + 1:]

# Decode data part
try:
data = [cls.CHARSET.index(c) for c in data_part]
except ValueError:
raise Bech32Error('invalid character in data part')

# Verify checksum and get encoding type
encoding = cls._verify_checksum(hrp, data)
if encoding is None:
raise Bech32Error('invalid checksum')

# Extract witness version and program
if len(data) < 7:
raise Bech32Error('data too short')

witness_version = data[0]
if witness_version > 16:
raise Bech32Error(f'invalid witness version: {witness_version}')

# Convert from 5-bit to 8-bit
try:
witness_program = bytes(cls._convertbits(data[1:-6], 5, 8, pad=False))
except Bech32Error:
raise Bech32Error('invalid witness program padding')

# Validate witness program length
if len(witness_program) < 2 or len(witness_program) > 40:
raise Bech32Error(f'invalid witness program length: {len(witness_program)}')

# Witness version 0 must use bech32, version 1+ must use bech32m
if witness_version == 0 and encoding != cls.BECH32_CONST:
raise Bech32Error('witness v0 must use bech32 encoding')
if witness_version != 0 and encoding != cls.BECH32M_CONST:
raise Bech32Error('witness v1+ must use bech32m encoding')

# Version-specific length requirements
if witness_version == 0:
if len(witness_program) != 20 and len(witness_program) != 32:
raise Bech32Error('witness v0 program must be 20 or 32 bytes')
elif witness_version == 1:
if len(witness_program) != 32:
raise Bech32Error('witness v1 (taproot) program must be 32 bytes')

return hrp, witness_version, witness_program

@classmethod
def encode(cls, hrp, witness_version, witness_program):
'''Encode a witness program to bech32 or bech32m.

Uses bech32 for witness version 0, bech32m for version 1+.
'''
if witness_version < 0 or witness_version > 16:
raise Bech32Error(f'invalid witness version: {witness_version}')
if len(witness_program) < 2 or len(witness_program) > 40:
raise Bech32Error(f'invalid witness program length: {len(witness_program)}')

spec = 'bech32' if witness_version == 0 else 'bech32m'
data = [witness_version] + cls._convertbits(witness_program, 8, 5)
checksum = cls._create_checksum(hrp, data, spec)
return hrp + '1' + ''.join(cls.CHARSET[d] for d in data + checksum)
34 changes: 34 additions & 0 deletions src/electrumx/lib/script.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,40 @@ def P2PKH_script(cls, hash160):
+ Script.push_data(hash160)
+ bytes((OpCodes.OP_EQUALVERIFY, OpCodes.OP_CHECKSIG)))

@classmethod
def P2WPKH_script(cls, hash160):
'''Create a P2WPKH (SegWit v0 pubkey hash) script.'''
# OP_0 <20-byte-hash>
return bytes((OpCodes.OP_0, 20)) + hash160

@classmethod
def P2WSH_script(cls, hash256):
'''Create a P2WSH (SegWit v0 script hash) script.'''
# OP_0 <32-byte-hash>
return bytes((OpCodes.OP_0, 32)) + hash256

@classmethod
def P2TR_script(cls, output_key):
'''Create a P2TR (Taproot, SegWit v1) script.'''
# OP_1 <32-byte-key>
return bytes((OpCodes.OP_1, 32)) + output_key

@classmethod
def witness_program_script(cls, version, program):
'''Create a witness program script for any version.

version: 0-16
program: witness program bytes (length varies by version)
'''
if version < 0 or version > 16:
raise ScriptError(f'invalid witness version: {version}')
# OP_0 is 0x00, OP_1 through OP_16 are 0x51 through 0x60
if version == 0:
version_op = OpCodes.OP_0
else:
version_op = OpCodes.OP_1 + (version - 1)
return bytes((version_op, len(program))) + program


class Script:

Expand Down
14 changes: 14 additions & 0 deletions tests/blocks/doriancoin_mainnet_1000.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"hash": "ac0d6def1ef1aa96464fbeccf0b767f48a138d2fe7d80d8a949a76e54bf39f70",
"size": 188,
"height": 1000,
"merkleroot": "ee72c5be92df4bc817209224aac62dcc835dd51c2e80dfbf58be91949834cc80",
"tx": [
"ee72c5be92df4bc817209224aac62dcc835dd51c2e80dfbf58be91949834cc80"
],
"time": 1394418275,
"nonce": 3633,
"bits": "1e0ffff0",
"previousblockhash": "bfe9d4b4231401ba515493300fbfd09b21fedeb9465362aa68b4c6aaebfa9afd",
"block": "02000000fd9afaebaac6b468aa625346b9defe219bd0bf0f30935451ba011423b4d4e9bf80cc34989491be58bfdf802e1cd55d83cc2dc6aa24922017c84bdf92bec572ee63221d53f0ff0f1e310e00000101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0c02e8030106062f503253482fffffffff0100f2052a0100000023210364e9edba6808aeb21b9475423240d9c8b5be9abd0ee129aede6bfbbcf9c37884ac00000000"
}
Loading