Remove altcoin-only server and tx code
Deletes the transaction deserializers of other coins (tx.py keeps only the Bitcoin/segwit ones), tx_dash.py, tx_axe.py, and the session, daemon and block processor subclasses that only other coins used. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
73c96101bd
commit
05f5fc9058
6 files changed
+3
-2745
No files matched your search
+2
-1139
File diff suppressed because it is too large.
Load diff
@@ -1,489 +0,0 @@
|
||||
# Copyright (c) 2016-2018, Neil Booth
|
||||
# Copyright (c) 2018, 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.
|
||||
|
||||
'''Deserializer for AXE DIP2 special transaction types'''
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from electrumx.lib.tx import Deserializer, Tx
|
||||
from electrumx.lib.util import (pack_le_uint16, pack_le_int32, pack_le_uint32,
|
||||
pack_le_int64, pack_varint, pack_varbytes,
|
||||
pack_be_uint16)
|
||||
|
||||
|
||||
# https://github.com/dashpay/dips/blob/master/dip-0002.md
|
||||
@dataclass(kw_only=True, slots=True)
|
||||
class AxeTx(Tx):
|
||||
'''Class representing a Axe transaction'''
|
||||
tx_type: int
|
||||
extra_payload: Any
|
||||
|
||||
def serialize(self):
|
||||
nLocktime = pack_le_uint32(self.locktime)
|
||||
txins = (pack_varint(len(self.inputs)) +
|
||||
b''.join(tx_in.serialize() for tx_in in self.inputs))
|
||||
txouts = (pack_varint(len(self.outputs)) +
|
||||
b''.join(tx_out.serialize() for tx_out in self.outputs))
|
||||
|
||||
if self.tx_type:
|
||||
uVersion = pack_le_uint16(self.version)
|
||||
uTxType = pack_le_uint16(self.tx_type)
|
||||
vExtra = self._serialize_extra_payload()
|
||||
return uVersion + uTxType + txins + txouts + nLocktime + vExtra
|
||||
else:
|
||||
nVersion = pack_le_int32(self.version)
|
||||
return nVersion + txins + txouts + nLocktime
|
||||
|
||||
def _serialize_extra_payload(self):
|
||||
extra = self.extra_payload
|
||||
spec_tx_class = DeserializerAxe.SPEC_TX_HANDLERS.get(self.tx_type)
|
||||
if not spec_tx_class:
|
||||
assert isinstance(extra, (bytes, bytearray))
|
||||
return pack_varbytes(extra)
|
||||
|
||||
if not isinstance(extra, spec_tx_class):
|
||||
raise ValueError('Axe tx_type does not conform with extra'
|
||||
' payload class: %s, %s' % (self.tx_type, extra))
|
||||
return pack_varbytes(extra.serialize())
|
||||
|
||||
|
||||
# https://github.com/dashpay/dips/blob/master/dip-0002-special-transactions.md
|
||||
@dataclass(kw_only=True, slots=True)
|
||||
class AxeProRegTx:
|
||||
'''Class representing DIP3 ProRegTx'''
|
||||
version: int
|
||||
type: int
|
||||
mode: int
|
||||
collateralOutpoint: 'TxOutPoint'
|
||||
ipAddress: bytes
|
||||
port: int
|
||||
KeyIdOwner: bytes
|
||||
PubKeyOperator: bytes
|
||||
KeyIdVoting: bytes
|
||||
operatorReward: int
|
||||
scriptPayout: bytes
|
||||
inputsHash: bytes
|
||||
payloadSig: bytes
|
||||
|
||||
def serialize(self):
|
||||
assert (len(self.ipAddress) == 16
|
||||
and len(self.KeyIdOwner) == 20
|
||||
and len(self.PubKeyOperator) == 48
|
||||
and len(self.KeyIdVoting) == 20
|
||||
and len(self.inputsHash) == 32)
|
||||
return (
|
||||
pack_le_uint16(self.version) + # version
|
||||
pack_le_uint16(self.type) + # type
|
||||
pack_le_uint16(self.mode) + # mode
|
||||
self.collateralOutpoint.serialize() + # collateralOutpoint
|
||||
self.ipAddress + # ipAddress
|
||||
pack_be_uint16(self.port) + # port
|
||||
self.KeyIdOwner + # KeyIdOwner
|
||||
self.PubKeyOperator + # PubKeyOperator
|
||||
self.KeyIdVoting + # KeyIdVoting
|
||||
pack_le_uint16(self.operatorReward) + # operatorReward
|
||||
pack_varbytes(self.scriptPayout) + # scriptPayout
|
||||
self.inputsHash + # inputsHash
|
||||
pack_varbytes(self.payloadSig) # payloadSig
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def read_tx_extra(cls, deser):
|
||||
return AxeProRegTx(
|
||||
version=deser._read_le_uint16(),
|
||||
type=deser._read_le_uint16(),
|
||||
mode=deser._read_le_uint16(),
|
||||
collateralOutpoint=deser._read_outpoint(),
|
||||
ipAddress=deser._read_nbytes(16),
|
||||
port=deser._read_be_uint16(),
|
||||
KeyIdOwner=deser._read_nbytes(20),
|
||||
PubKeyOperator=deser._read_nbytes(48),
|
||||
KeyIdVoting=deser._read_nbytes(20),
|
||||
operatorReward=deser._read_le_uint16(),
|
||||
scriptPayout=deser._read_varbytes(),
|
||||
inputsHash=deser._read_nbytes(32),
|
||||
payloadSig=deser._read_varbytes(),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(kw_only=True, slots=True)
|
||||
class AxeProUpServTx:
|
||||
'''Class representing DIP3 ProUpServTx'''
|
||||
version: int
|
||||
proTxHash: bytes
|
||||
ipAddress: bytes
|
||||
port: int
|
||||
scriptOperatorPayout: bytes
|
||||
inputsHash: bytes
|
||||
payloadSig: bytes
|
||||
|
||||
def serialize(self):
|
||||
assert (len(self.proTxHash) == 32
|
||||
and len(self.ipAddress) == 16
|
||||
and len(self.inputsHash) == 32
|
||||
and len(self.payloadSig) == 96)
|
||||
return (
|
||||
pack_le_uint16(self.version) + # version
|
||||
self.proTxHash + # proTxHash
|
||||
self.ipAddress + # ipAddress
|
||||
pack_be_uint16(self.port) + # port
|
||||
pack_varbytes(self.scriptOperatorPayout) + # scriptOperatorPayout
|
||||
self.inputsHash + # inputsHash
|
||||
self.payloadSig # payloadSig
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def read_tx_extra(cls, deser):
|
||||
return AxeProUpServTx(
|
||||
version=deser._read_le_uint16(),
|
||||
proTxHash=deser._read_nbytes(32),
|
||||
ipAddress=deser._read_nbytes(16),
|
||||
port=deser._read_be_uint16(),
|
||||
scriptOperatorPayout=deser._read_varbytes(),
|
||||
inputsHash=deser._read_nbytes(32),
|
||||
payloadSig=deser._read_nbytes(96),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(kw_only=True, slots=True)
|
||||
class AxeProUpRegTx:
|
||||
'''Class representing DIP3 ProUpRegTx'''
|
||||
version: int
|
||||
proTxHash: bytes
|
||||
mode: int
|
||||
PubKeyOperator: bytes
|
||||
KeyIdVoting: bytes
|
||||
scriptPayout: bytes
|
||||
inputsHash: bytes
|
||||
payloadSig: bytes
|
||||
|
||||
def serialize(self):
|
||||
assert (len(self.proTxHash) == 32
|
||||
and len(self.PubKeyOperator) == 48
|
||||
and len(self.KeyIdVoting) == 20
|
||||
and len(self.inputsHash) == 32)
|
||||
return (
|
||||
pack_le_uint16(self.version) + # version
|
||||
self.proTxHash + # proTxHash
|
||||
pack_le_uint16(self.mode) + # mode
|
||||
self.PubKeyOperator + # PubKeyOperator
|
||||
self.KeyIdVoting + # KeyIdVoting
|
||||
pack_varbytes(self.scriptPayout) + # scriptPayout
|
||||
self.inputsHash + # inputsHash
|
||||
pack_varbytes(self.payloadSig) # payloadSig
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def read_tx_extra(cls, deser):
|
||||
return AxeProUpRegTx(
|
||||
version=deser._read_le_uint16(),
|
||||
proTxHash=deser._read_nbytes(32),
|
||||
mode=deser._read_le_uint16(),
|
||||
PubKeyOperator=deser._read_nbytes(48),
|
||||
KeyIdVoting=deser._read_nbytes(20),
|
||||
scriptPayout=deser._read_varbytes(),
|
||||
inputsHash=deser._read_nbytes(32),
|
||||
payloadSig=deser._read_varbytes(),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(kw_only=True, slots=True)
|
||||
class AxeProUpRevTx:
|
||||
'''Class representing DIP3 ProUpRevTx'''
|
||||
version: int
|
||||
proTxHash: bytes
|
||||
reason: int
|
||||
inputsHash: bytes
|
||||
payloadSig: bytes
|
||||
|
||||
def serialize(self):
|
||||
assert (len(self.proTxHash) == 32
|
||||
and len(self.inputsHash) == 32
|
||||
and len(self.payloadSig) == 96)
|
||||
return (
|
||||
pack_le_uint16(self.version) + # version
|
||||
self.proTxHash + # proTxHash
|
||||
pack_le_uint16(self.reason) + # reason
|
||||
self.inputsHash + # inputsHash
|
||||
self.payloadSig # payloadSig
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def read_tx_extra(cls, deser):
|
||||
return AxeProUpRevTx(
|
||||
version=deser._read_le_uint16(),
|
||||
proTxHash=deser._read_nbytes(32),
|
||||
reason=deser._read_le_uint16(),
|
||||
inputsHash=deser._read_nbytes(32),
|
||||
payloadSig=deser._read_nbytes(96),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(kw_only=True, slots=True)
|
||||
class AxeCbTx:
|
||||
'''Class representing DIP4 coinbase special tx'''
|
||||
version: int
|
||||
height: int
|
||||
merkleRootMNList: bytes
|
||||
merkleRootQuorums: bytes
|
||||
|
||||
def serialize(self):
|
||||
assert len(self.merkleRootMNList) == 32
|
||||
res = (
|
||||
pack_le_uint16(self.version) + # version
|
||||
pack_le_uint32(self.height) + # height
|
||||
self.merkleRootMNList # merkleRootMNList
|
||||
)
|
||||
if self.version > 1:
|
||||
assert len(self.merkleRootQuorums) == 32
|
||||
res += self.merkleRootQuorums # merkleRootQuorums
|
||||
return res
|
||||
|
||||
@classmethod
|
||||
def read_tx_extra(cls, deser):
|
||||
version = deser._read_le_uint16()
|
||||
height = deser._read_le_uint32()
|
||||
merkleRootMNList = deser._read_nbytes(32)
|
||||
merkleRootQuorums = b''
|
||||
if version > 1:
|
||||
merkleRootQuorums = deser._read_nbytes(32)
|
||||
return AxeCbTx(
|
||||
version=version,
|
||||
height=height,
|
||||
merkleRootMNList=merkleRootMNList,
|
||||
merkleRootQuorums=merkleRootQuorums,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(kw_only=True, slots=True)
|
||||
class AxeSubTxRegister:
|
||||
'''Class representing DIP5 SubTxRegister'''
|
||||
version: int
|
||||
userName: bytes
|
||||
pubKey: bytes
|
||||
payloadSig: bytes
|
||||
|
||||
def serialize(self):
|
||||
assert (len(self.pubKey) == 48
|
||||
and len(self.payloadSig) == 96)
|
||||
return (
|
||||
pack_le_uint16(self.version) + # version
|
||||
pack_varbytes(self.userName) + # userName
|
||||
self.pubKey + # pubKey
|
||||
self.payloadSig # payloadSig
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def read_tx_extra(cls, deser):
|
||||
return AxeSubTxRegister(
|
||||
version=deser._read_le_uint16(),
|
||||
userName=deser._read_varbytes(),
|
||||
pubKey=deser._read_nbytes(48),
|
||||
payloadSig=deser._read_nbytes(96),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(kw_only=True, slots=True)
|
||||
class AxeSubTxTopup:
|
||||
'''Class representing DIP5 SubTxTopup'''
|
||||
version: int
|
||||
regTxHash: bytes
|
||||
|
||||
def serialize(self):
|
||||
assert len(self.regTxHash) == 32
|
||||
return (
|
||||
pack_le_uint16(self.version) + # version
|
||||
self.regTxHash # regTxHash
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def read_tx_extra(cls, deser):
|
||||
return AxeSubTxTopup(
|
||||
version=deser._read_le_uint16(),
|
||||
regTxHash=deser._read_nbytes(32),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(kw_only=True, slots=True)
|
||||
class AxeSubTxResetKey:
|
||||
'''Class representing DIP5 SubTxResetKey'''
|
||||
version: int
|
||||
regTxHash: bytes
|
||||
hashPrevSubTx: bytes
|
||||
creditFee: int
|
||||
newPubKey: bytes
|
||||
payloadSig: bytes
|
||||
|
||||
def serialize(self):
|
||||
assert (len(self.regTxHash) == 32
|
||||
and len(self.hashPrevSubTx) == 32
|
||||
and len(self.newPubKey) == 48
|
||||
and len(self.payloadSig) == 96)
|
||||
return (
|
||||
pack_le_uint16(self.version) + # version
|
||||
self.regTxHash + # regTxHash
|
||||
self.hashPrevSubTx + # hashPrevSubTx
|
||||
pack_le_int64(self.creditFee) + # creditFee
|
||||
self.newPubKey + # newPubKey
|
||||
self.payloadSig # payloadSig
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def read_tx_extra(cls, deser):
|
||||
return AxeSubTxResetKey(
|
||||
version=deser._read_le_uint16(),
|
||||
regTxHash=deser._read_nbytes(32),
|
||||
hashPrevSubTx=deser._read_nbytes(32),
|
||||
creditFee=deser._read_le_int64(),
|
||||
newPubKey=deser._read_nbytes(48),
|
||||
payloadSig=deser._read_nbytes(96),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(kw_only=True, slots=True)
|
||||
class AxeSubTxCloseAccount:
|
||||
'''Class representing DIP5 SubTxCloseAccount'''
|
||||
version: int
|
||||
regTxHash: bytes
|
||||
hashPrevSubTx: bytes
|
||||
creditFee: int
|
||||
payloadSig: bytes
|
||||
|
||||
def serialize(self):
|
||||
assert (len(self.regTxHash) == 32
|
||||
and len(self.hashPrevSubTx) == 32
|
||||
and len(self.payloadSig) == 96)
|
||||
return (
|
||||
pack_le_uint16(self.version) + # version
|
||||
self.regTxHash + # regTxHash
|
||||
self.hashPrevSubTx + # hashPrevSubTx
|
||||
pack_le_int64(self.creditFee) + # creditFee
|
||||
self.payloadSig # payloadSig
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def read_tx_extra(cls, deser):
|
||||
return AxeSubTxCloseAccount(
|
||||
version=deser._read_le_uint16(),
|
||||
regTxHash=deser._read_nbytes(32),
|
||||
hashPrevSubTx=deser._read_nbytes(32),
|
||||
creditFee=deser._read_le_int64(),
|
||||
payloadSig=deser._read_nbytes(96),
|
||||
)
|
||||
|
||||
|
||||
# https://dash-docs.github.io/en/developer-reference#outpoint
|
||||
@dataclass(kw_only=True, slots=True)
|
||||
class TxOutPoint:
|
||||
'''Class representing tx output outpoint'''
|
||||
hash: bytes
|
||||
index: int
|
||||
|
||||
def serialize(self):
|
||||
assert len(self.hash) == 32
|
||||
return (
|
||||
self.hash + # hash
|
||||
pack_le_uint32(self.index) # index
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def read_outpoint(cls, deser):
|
||||
return TxOutPoint(
|
||||
hash=deser._read_nbytes(32),
|
||||
index=deser._read_le_uint32(),
|
||||
)
|
||||
|
||||
|
||||
class DeserializerAxe(Deserializer):
|
||||
'''Deserializer for AXE DIP2 special tx types'''
|
||||
# Supported Spec Tx types and corresponding classes mapping
|
||||
PRO_REG_TX = 1
|
||||
PRO_UP_SERV_TX = 2
|
||||
PRO_UP_REG_TX = 3
|
||||
PRO_UP_REV_TX = 4
|
||||
CB_TX = 5
|
||||
SUB_TX_REGISTER = 8
|
||||
SUB_TX_TOPUP = 9
|
||||
SUB_TX_RESET_KEY = 10
|
||||
SUB_TX_CLOSE_ACCOUNT = 11
|
||||
|
||||
SPEC_TX_HANDLERS = {
|
||||
PRO_REG_TX: AxeProRegTx,
|
||||
PRO_UP_SERV_TX: AxeProUpServTx,
|
||||
PRO_UP_REG_TX: AxeProUpRegTx,
|
||||
PRO_UP_REV_TX: AxeProUpRevTx,
|
||||
CB_TX: AxeCbTx,
|
||||
SUB_TX_REGISTER: AxeSubTxRegister,
|
||||
SUB_TX_TOPUP: AxeSubTxTopup,
|
||||
SUB_TX_RESET_KEY: AxeSubTxResetKey,
|
||||
SUB_TX_CLOSE_ACCOUNT: AxeSubTxCloseAccount,
|
||||
}
|
||||
|
||||
def _read_outpoint(self):
|
||||
return TxOutPoint.read_outpoint(self)
|
||||
|
||||
def read_tx(self):
|
||||
start = self.cursor
|
||||
header = self._read_le_uint32()
|
||||
tx_type = header >> 16 # DIP2 tx type
|
||||
if tx_type:
|
||||
version = header & 0x0000ffff
|
||||
else:
|
||||
version = header
|
||||
|
||||
if tx_type and version < 3:
|
||||
version = header
|
||||
tx_type = 0
|
||||
|
||||
inputs = self._read_inputs()
|
||||
outputs = self._read_outputs()
|
||||
locktime = self._read_le_uint32()
|
||||
if tx_type:
|
||||
extra_payload_size = self._read_varint()
|
||||
end = self.cursor + extra_payload_size
|
||||
spec_tx_class = DeserializerAxe.SPEC_TX_HANDLERS.get(tx_type)
|
||||
if spec_tx_class:
|
||||
read_method = getattr(spec_tx_class, 'read_tx_extra', None)
|
||||
extra_payload = read_method(self)
|
||||
assert isinstance(extra_payload, spec_tx_class)
|
||||
else:
|
||||
extra_payload = self._read_nbytes(extra_payload_size)
|
||||
assert self.cursor == end
|
||||
else:
|
||||
extra_payload = b''
|
||||
txid = self.TX_HASH_FN(self.binary[start:self.cursor])
|
||||
tx = AxeTx(
|
||||
version=version,
|
||||
inputs=inputs,
|
||||
outputs=outputs,
|
||||
locktime=locktime,
|
||||
tx_type=tx_type,
|
||||
extra_payload=extra_payload,
|
||||
txid_rev=txid,
|
||||
wtxid_rev=txid,
|
||||
)
|
||||
return tx
|
||||
@@ -1,442 +0,0 @@
|
||||
# Copyright (c) 2016-2018, Neil Booth
|
||||
# Copyright (c) 2018, 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.
|
||||
|
||||
'''Deserializer for Dash DIP2 special transaction types'''
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from electrumx.lib.tx import Deserializer, Tx
|
||||
from electrumx.lib.util import (pack_le_uint16, pack_le_int32, pack_le_uint32,
|
||||
pack_le_int64, pack_varint, pack_varbytes,
|
||||
pack_be_uint16)
|
||||
|
||||
|
||||
# https://github.com/dashpay/dips/blob/master/dip-0002.md
|
||||
@dataclass(kw_only=True, slots=True)
|
||||
class DashTx(Tx):
|
||||
'''Class representing a Dash transaction'''
|
||||
tx_type: int
|
||||
extra_payload: Any
|
||||
|
||||
def serialize(self):
|
||||
nLocktime = pack_le_uint32(self.locktime)
|
||||
txins = (pack_varint(len(self.inputs)) +
|
||||
b''.join(tx_in.serialize() for tx_in in self.inputs))
|
||||
txouts = (pack_varint(len(self.outputs)) +
|
||||
b''.join(tx_out.serialize() for tx_out in self.outputs))
|
||||
|
||||
if self.tx_type:
|
||||
uVersion = pack_le_uint16(self.version)
|
||||
uTxType = pack_le_uint16(self.tx_type)
|
||||
vExtra = self._serialize_extra_payload()
|
||||
return uVersion + uTxType + txins + txouts + nLocktime + vExtra
|
||||
else:
|
||||
nVersion = pack_le_int32(self.version)
|
||||
return nVersion + txins + txouts + nLocktime
|
||||
|
||||
def _serialize_extra_payload(self):
|
||||
extra = self.extra_payload
|
||||
spec_tx_class = DeserializerDash.SPEC_TX_HANDLERS.get(self.tx_type)
|
||||
if not spec_tx_class:
|
||||
assert isinstance(extra, (bytes, bytearray))
|
||||
return pack_varbytes(extra)
|
||||
|
||||
if not isinstance(extra, spec_tx_class):
|
||||
raise ValueError('Dash tx_type does not conform with extra'
|
||||
' payload class: %s, %s' % (self.tx_type, extra))
|
||||
return pack_varbytes(extra.serialize())
|
||||
|
||||
|
||||
# https://github.com/dashpay/dips/blob/master/dip-0002-special-transactions.md
|
||||
@dataclass(kw_only=True, slots=True)
|
||||
class DashProRegTx:
|
||||
'''Class representing DIP3 ProRegTx'''
|
||||
version: int
|
||||
type: int
|
||||
mode: int
|
||||
collateralOutpoint: 'TxOutPoint'
|
||||
ipAddress: bytes
|
||||
port: int
|
||||
KeyIdOwner: bytes
|
||||
PubKeyOperator: bytes
|
||||
KeyIdVoting: bytes
|
||||
operatorReward: int
|
||||
scriptPayout: bytes
|
||||
inputsHash: bytes
|
||||
platformNodeID: bytes
|
||||
platformP2PPort: int
|
||||
platformHTTPPort: int
|
||||
payloadSig: bytes
|
||||
|
||||
def serialize(self):
|
||||
assert (len(self.ipAddress) == 16
|
||||
and len(self.KeyIdOwner) == 20
|
||||
and len(self.PubKeyOperator) == 48
|
||||
and len(self.KeyIdVoting) == 20
|
||||
and len(self.inputsHash) == 32)
|
||||
res = (
|
||||
pack_le_uint16(self.version) + # version
|
||||
pack_le_uint16(self.type) + # type
|
||||
pack_le_uint16(self.mode) + # mode
|
||||
self.collateralOutpoint.serialize() + # collateralOutpoint
|
||||
self.ipAddress + # ipAddress
|
||||
pack_be_uint16(self.port) + # port
|
||||
self.KeyIdOwner + # KeyIdOwner
|
||||
self.PubKeyOperator + # PubKeyOperator
|
||||
self.KeyIdVoting + # KeyIdVoting
|
||||
pack_le_uint16(self.operatorReward) + # operatorReward
|
||||
pack_varbytes(self.scriptPayout) + # scriptPayout
|
||||
self.inputsHash) # inputsHash
|
||||
if self.version >= 2:
|
||||
res += (self.platformNodeID + # platformNodeID
|
||||
pack_le_uint16(self.platformP2PPort) + # platformP2PPort
|
||||
pack_le_uint16(self.platformHTTPPort)) # platformHTTPPort
|
||||
res += pack_varbytes(self.payloadSig) # payloadSig
|
||||
return res
|
||||
|
||||
@classmethod
|
||||
def read_tx_extra(cls, deser):
|
||||
version = deser._read_le_uint16() # version
|
||||
ntype = deser._read_le_uint16() # type
|
||||
mode = deser._read_le_uint16() # mode
|
||||
collateralOutpoint = deser._read_outpoint() # collateralOutpoint
|
||||
ipAddress = deser._read_nbytes(16) # ipAddress
|
||||
port = deser._read_be_uint16() # port
|
||||
KeyIdOwner = deser._read_nbytes(20) # KeyIdOwner
|
||||
PubKeyOperator = deser._read_nbytes(48) # PubKeyOperator
|
||||
KeyIdVoting = deser._read_nbytes(20) # KeyIdVoting
|
||||
operatorReward = deser._read_le_uint16() # operatorReward
|
||||
scriptPayout = deser._read_varbytes() # scriptPayout
|
||||
inputsHash = deser._read_nbytes(32) # inputsHash
|
||||
platformNodeID = b''
|
||||
platformP2PPort = 0
|
||||
platformHTTPPort = 0
|
||||
if version >= 2 and ntype == 1:
|
||||
platformNodeID = deser._read_nbytes(20) # platformNodeID
|
||||
platformP2PPort = deser._read_le_uint16() # platformP2PPort
|
||||
platformHTTPPort = deser._read_le_uint16() # platformHTTPPort
|
||||
payloadSig = deser._read_varbytes() # payloadSig
|
||||
return DashProRegTx(
|
||||
version=version,
|
||||
type=ntype,
|
||||
mode=mode,
|
||||
collateralOutpoint=collateralOutpoint,
|
||||
ipAddress=ipAddress,
|
||||
port=port,
|
||||
KeyIdOwner=KeyIdOwner,
|
||||
PubKeyOperator=PubKeyOperator,
|
||||
KeyIdVoting=KeyIdVoting,
|
||||
operatorReward=operatorReward,
|
||||
scriptPayout=scriptPayout,
|
||||
inputsHash=inputsHash,
|
||||
platformNodeID=platformNodeID,
|
||||
platformP2PPort=platformP2PPort,
|
||||
platformHTTPPort=platformHTTPPort,
|
||||
payloadSig=payloadSig,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(kw_only=True, slots=True)
|
||||
class DashProUpServTx:
|
||||
'''Class representing DIP3 ProUpServTx'''
|
||||
version: int
|
||||
type: int
|
||||
proTxHash: bytes
|
||||
ipAddress: bytes
|
||||
port: int
|
||||
scriptOperatorPayout: bytes
|
||||
inputsHash: bytes
|
||||
platformNodeID: bytes
|
||||
platformP2PPort: int
|
||||
platformHTTPPort: int
|
||||
payloadSig: bytes
|
||||
|
||||
def serialize(self):
|
||||
assert (len(self.proTxHash) == 32
|
||||
and len(self.ipAddress) == 16
|
||||
and len(self.inputsHash) == 32
|
||||
and len(self.payloadSig) == 96)
|
||||
res = pack_le_uint16(self.version) # version
|
||||
if self.version >= 2:
|
||||
res += pack_le_uint16(self.type) # type
|
||||
res += (
|
||||
self.proTxHash + # proTxHash
|
||||
self.ipAddress + # ipAddress
|
||||
pack_be_uint16(self.port) + # port
|
||||
pack_varbytes(self.scriptOperatorPayout) + # scriptOperatorPayout
|
||||
self.inputsHash) # inputsHash
|
||||
if self.version >= 2:
|
||||
res += (self.platformNodeID + # platformNodeID
|
||||
pack_le_uint16(self.platformP2PPort) + # platformP2PPort
|
||||
pack_le_uint16(self.platformHTTPPort)) # platformHTTPPort
|
||||
res += self.payloadSig # payloadSig
|
||||
return res
|
||||
|
||||
@classmethod
|
||||
def read_tx_extra(cls, deser):
|
||||
version = deser._read_le_uint16() # version
|
||||
ntype = 0
|
||||
if version >= 2:
|
||||
ntype = deser._read_le_uint16() # type
|
||||
proTxHash = deser._read_nbytes(32) # proTxHash
|
||||
ipAddress = deser._read_nbytes(16) # ipAddress
|
||||
port = deser._read_be_uint16() # port
|
||||
scriptOperatorPayout = deser._read_varbytes() # scriptOperatorPayout
|
||||
inputsHash = deser._read_nbytes(32) # inputsHash
|
||||
platformNodeID = b''
|
||||
platformP2PPort = 0
|
||||
platformHTTPPort = 0
|
||||
if version >= 2 and ntype == 1:
|
||||
platformNodeID = deser._read_nbytes(20) # platformNodeID
|
||||
platformP2PPort = deser._read_le_uint16() # platformP2PPort
|
||||
platformHTTPPort = deser._read_le_uint16() # platformHTTPPort
|
||||
payloadSig = deser._read_nbytes(96) # payloadSig
|
||||
return DashProUpServTx(
|
||||
version=version,
|
||||
type=ntype,
|
||||
proTxHash=proTxHash,
|
||||
ipAddress=ipAddress,
|
||||
port=port,
|
||||
scriptOperatorPayout=scriptOperatorPayout,
|
||||
inputsHash=inputsHash,
|
||||
platformNodeID=platformNodeID,
|
||||
platformP2PPort=platformP2PPort,
|
||||
platformHTTPPort=platformHTTPPort,
|
||||
payloadSig=payloadSig,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(kw_only=True, slots=True)
|
||||
class DashProUpRegTx:
|
||||
'''Class representing DIP3 ProUpRegTx'''
|
||||
version: int
|
||||
proTxHash: bytes
|
||||
mode: int
|
||||
PubKeyOperator: bytes
|
||||
KeyIdVoting: bytes
|
||||
scriptPayout: bytes
|
||||
inputsHash: bytes
|
||||
payloadSig: bytes
|
||||
|
||||
def serialize(self):
|
||||
assert (len(self.proTxHash) == 32
|
||||
and len(self.PubKeyOperator) == 48
|
||||
and len(self.KeyIdVoting) == 20
|
||||
and len(self.inputsHash) == 32)
|
||||
return (
|
||||
pack_le_uint16(self.version) + # version
|
||||
self.proTxHash + # proTxHash
|
||||
pack_le_uint16(self.mode) + # mode
|
||||
self.PubKeyOperator + # PubKeyOperator
|
||||
self.KeyIdVoting + # KeyIdVoting
|
||||
pack_varbytes(self.scriptPayout) + # scriptPayout
|
||||
self.inputsHash + # inputsHash
|
||||
pack_varbytes(self.payloadSig) # payloadSig
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def read_tx_extra(cls, deser):
|
||||
return DashProUpRegTx(
|
||||
version=deser._read_le_uint16(),
|
||||
proTxHash=deser._read_nbytes(32),
|
||||
mode=deser._read_le_uint16(),
|
||||
PubKeyOperator=deser._read_nbytes(48),
|
||||
KeyIdVoting=deser._read_nbytes(20),
|
||||
scriptPayout=deser._read_varbytes(),
|
||||
inputsHash=deser._read_nbytes(32),
|
||||
payloadSig=deser._read_varbytes(),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(kw_only=True, slots=True)
|
||||
class DashProUpRevTx:
|
||||
'''Class representing DIP3 ProUpRevTx'''
|
||||
version: int
|
||||
proTxHash: bytes
|
||||
reason: int
|
||||
inputsHash: bytes
|
||||
payloadSig: bytes
|
||||
|
||||
def serialize(self):
|
||||
assert (len(self.proTxHash) == 32
|
||||
and len(self.inputsHash) == 32
|
||||
and len(self.payloadSig) == 96)
|
||||
return (
|
||||
pack_le_uint16(self.version) + # version
|
||||
self.proTxHash + # proTxHash
|
||||
pack_le_uint16(self.reason) + # reason
|
||||
self.inputsHash + # inputsHash
|
||||
self.payloadSig # payloadSig
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def read_tx_extra(cls, deser):
|
||||
return DashProUpRevTx(
|
||||
version=deser._read_le_uint16(),
|
||||
proTxHash=deser._read_nbytes(32),
|
||||
reason=deser._read_le_uint16(),
|
||||
inputsHash=deser._read_nbytes(32),
|
||||
payloadSig=deser._read_nbytes(96),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(kw_only=True, slots=True)
|
||||
class DashCbTx:
|
||||
'''Class representing DIP4 coinbase special tx'''
|
||||
version: int
|
||||
height: int
|
||||
merkleRootMNList: bytes
|
||||
merkleRootQuorums: bytes
|
||||
bestCLHeightDiff: int
|
||||
bestCLSignature: bytes
|
||||
assetLockedAmount: int
|
||||
|
||||
def serialize(self):
|
||||
assert len(self.merkleRootMNList) == 32
|
||||
res = (
|
||||
pack_le_uint16(self.version) + # version
|
||||
pack_le_uint32(self.height) + # height
|
||||
self.merkleRootMNList # merkleRootMNList
|
||||
)
|
||||
if self.version > 1:
|
||||
assert len(self.merkleRootQuorums) == 32
|
||||
res += self.merkleRootQuorums # merkleRootQuorums
|
||||
if self.version > 2:
|
||||
res += pack_varint(self.bestCLHeightDiff)
|
||||
res += self.bestCLSignature
|
||||
res += pack_le_int64(self.assetLockedAmount)
|
||||
return res
|
||||
|
||||
@classmethod
|
||||
def read_tx_extra(cls, deser):
|
||||
version = deser._read_le_uint16()
|
||||
height = deser._read_le_uint32()
|
||||
merkleRootMNList = deser._read_nbytes(32)
|
||||
merkleRootQuorums = b''
|
||||
bestCLHeightDiff = 0
|
||||
bestCLSignature = b''
|
||||
assetLockedAmount = 0
|
||||
if version > 1:
|
||||
merkleRootQuorums = deser._read_nbytes(32)
|
||||
if version > 2:
|
||||
bestCLHeightDiff = deser._read_varint()
|
||||
bestCLSignature = deser._read_nbytes(96)
|
||||
assetLockedAmount = deser._read_le_uint64()
|
||||
return DashCbTx(
|
||||
version=version,
|
||||
height=height,
|
||||
merkleRootMNList=merkleRootMNList,
|
||||
merkleRootQuorums=merkleRootQuorums,
|
||||
bestCLHeightDiff=bestCLHeightDiff,
|
||||
bestCLSignature=bestCLSignature,
|
||||
assetLockedAmount=assetLockedAmount,
|
||||
)
|
||||
|
||||
|
||||
# https://dash-docs.github.io/en/developer-reference#outpoint
|
||||
@dataclass(kw_only=True, slots=True)
|
||||
class TxOutPoint:
|
||||
'''Class representing tx output outpoint'''
|
||||
hash: bytes
|
||||
index: int
|
||||
|
||||
def serialize(self):
|
||||
assert len(self.hash) == 32
|
||||
return (
|
||||
self.hash + # hash
|
||||
pack_le_uint32(self.index) # index
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def read_outpoint(cls, deser):
|
||||
return TxOutPoint(
|
||||
hash=deser._read_nbytes(32),
|
||||
index=deser._read_le_uint32(),
|
||||
)
|
||||
|
||||
|
||||
class DeserializerDash(Deserializer):
|
||||
'''Deserializer for Dash DIP2 special tx types'''
|
||||
# Supported Spec Tx types and corresponding classes mapping
|
||||
PRO_REG_TX = 1
|
||||
PRO_UP_SERV_TX = 2
|
||||
PRO_UP_REG_TX = 3
|
||||
PRO_UP_REV_TX = 4
|
||||
CB_TX = 5
|
||||
|
||||
SPEC_TX_HANDLERS = {
|
||||
PRO_REG_TX: DashProRegTx,
|
||||
PRO_UP_SERV_TX: DashProUpServTx,
|
||||
PRO_UP_REG_TX: DashProUpRegTx,
|
||||
PRO_UP_REV_TX: DashProUpRevTx,
|
||||
CB_TX: DashCbTx,
|
||||
}
|
||||
|
||||
def _read_outpoint(self):
|
||||
return TxOutPoint.read_outpoint(self)
|
||||
|
||||
def read_tx(self):
|
||||
start = self.cursor
|
||||
header = self._read_le_uint32()
|
||||
tx_type = header >> 16 # DIP2 tx type
|
||||
if tx_type:
|
||||
version = header & 0x0000ffff
|
||||
else:
|
||||
version = header
|
||||
|
||||
if tx_type and version < 3:
|
||||
version = header
|
||||
tx_type = 0
|
||||
|
||||
inputs = self._read_inputs()
|
||||
outputs = self._read_outputs()
|
||||
locktime = self._read_le_uint32()
|
||||
if tx_type:
|
||||
extra_payload_size = self._read_varint()
|
||||
end = self.cursor + extra_payload_size
|
||||
spec_tx_class = DeserializerDash.SPEC_TX_HANDLERS.get(tx_type)
|
||||
if spec_tx_class:
|
||||
read_method = getattr(spec_tx_class, 'read_tx_extra', None)
|
||||
extra_payload = read_method(self)
|
||||
assert isinstance(extra_payload, spec_tx_class)
|
||||
else:
|
||||
extra_payload = self._read_nbytes(extra_payload_size)
|
||||
assert self.cursor == end
|
||||
else:
|
||||
extra_payload = b''
|
||||
txid = self.TX_HASH_FN(self.binary[start:self.cursor])
|
||||
tx = DashTx(
|
||||
version=version,
|
||||
inputs=inputs,
|
||||
outputs=outputs,
|
||||
locktime=locktime,
|
||||
tx_type=tx_type,
|
||||
extra_payload=extra_payload,
|
||||
txid_rev=txid,
|
||||
wtxid_rev=txid,
|
||||
)
|
||||
return tx
|
||||
@@ -904,107 +904,3 @@ class BlockProcessor:
|
||||
self.blocks_event.set()
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class DecredBlockProcessor(BlockProcessor):
|
||||
async def calc_reorg_range(self, count):
|
||||
start, count = await super().calc_reorg_range(count)
|
||||
if start > 0:
|
||||
# A reorg in Decred can invalidate the previous block
|
||||
start -= 1
|
||||
count += 1
|
||||
return start, count
|
||||
|
||||
|
||||
class NameIndexBlockProcessor(BlockProcessor):
|
||||
|
||||
def advance_txs_process_outputs(
|
||||
self,
|
||||
txs: Sequence[Tx],
|
||||
*,
|
||||
height: int,
|
||||
) -> None:
|
||||
result = super().advance_txs_process_outputs(txs, height=height)
|
||||
|
||||
tx_num_start = self.db.tx_counts[height - 1] if height > 0 else 0
|
||||
tx_num = tx_num_start
|
||||
script_name_hashX = self.coin.name_hashX_from_script
|
||||
update_touched_hashxs = self.touched_hashxs.update
|
||||
hashXs_by_tx = []
|
||||
append_hashXs = hashXs_by_tx.append
|
||||
|
||||
for tx in txs:
|
||||
hashXs = []
|
||||
append_hashX = hashXs.append
|
||||
|
||||
# Add the new UTXOs and associate them with the name script
|
||||
for txout in tx.outputs:
|
||||
# Get the hashX of the name script. Ignore non-name scripts.
|
||||
hashX = script_name_hashX(txout.pk_script)
|
||||
if hashX:
|
||||
append_hashX(hashX)
|
||||
|
||||
append_hashXs(hashXs)
|
||||
update_touched_hashxs(hashXs)
|
||||
tx_num += 1
|
||||
|
||||
self.db.history.add_unflushed(hashXs_by_tx, tx_num_start)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class LTORBlockProcessor(BlockProcessor):
|
||||
# for Lexicographical Transaction ordering in blocks
|
||||
# (as opposed to Topological order as used in Bitcoin)
|
||||
# note: the base class already does not care about tx ordering inside a block
|
||||
# for the forward (catch-up) direction, as that better parallelizes.
|
||||
# This subclass is only kept for the reverse (backup) direction, for now.
|
||||
# We could also parallelize backup_txs in the base class for better perf,
|
||||
# likely removing the tx ordering assumptions, then we could rm this subclass.
|
||||
|
||||
def backup_txs(self, txs, is_unspendable):
|
||||
undo_info = self.db.read_undo_info(self.height)
|
||||
if undo_info is None:
|
||||
raise ChainError(
|
||||
f'no undo information found for height {self.height:,d}'
|
||||
)
|
||||
|
||||
# Use local vars for speed in the loops
|
||||
put_utxo = self.utxo_cache.__setitem__
|
||||
spend_utxo = self.spend_utxo
|
||||
add_touched_hashx = self.touched_hashxs.add
|
||||
add_touched_outpoint = self.touched_outpoints.add
|
||||
undo_entry_len = HASHX_LEN + TXNUM_LEN + 8
|
||||
|
||||
# Restore coins that had been spent
|
||||
# (may include coins made then spent in this block)
|
||||
n = 0
|
||||
for tx in txs:
|
||||
for txin in tx.inputs:
|
||||
if txin.is_generation():
|
||||
continue
|
||||
undo_item = undo_info[n:n + undo_entry_len]
|
||||
prevout = txin.prev_txid_rev + pack_txoutidx(txin.prev_idx)
|
||||
put_utxo(prevout, undo_item)
|
||||
add_touched_hashx(undo_item[:HASHX_LEN])
|
||||
add_touched_outpoint((txin.prev_txid_rev, txin.prev_idx))
|
||||
n += undo_entry_len
|
||||
|
||||
assert n == len(undo_info)
|
||||
|
||||
# Remove tx outputs made in this block, by spending them.
|
||||
for tx in txs:
|
||||
txid_rev = tx.txid_rev
|
||||
for idx, txout in enumerate(tx.outputs):
|
||||
# Spend the TX outputs. Be careful with unspendable
|
||||
# outputs - we didn't save those in the first place.
|
||||
if is_unspendable(txout.pk_script):
|
||||
continue
|
||||
|
||||
# Get the hashX
|
||||
cache_value = spend_utxo(txid_rev, idx)
|
||||
hashX = cache_value[:HASHX_LEN]
|
||||
add_touched_hashx(hashX)
|
||||
add_touched_outpoint((txid_rev, idx))
|
||||
|
||||
self.tx_count -= len(txs)
|
||||
@@ -11,18 +11,13 @@ daemon.'''
|
||||
import asyncio
|
||||
import itertools
|
||||
import time
|
||||
from calendar import timegm
|
||||
from struct import pack
|
||||
from typing import TYPE_CHECKING, Type, Sequence, Any, Iterable, Optional
|
||||
|
||||
import aiohttp
|
||||
from aiorpcx import JSONRPC
|
||||
|
||||
from electrumx.lib.hash import hash_to_hex_str, hex_str_to_hash
|
||||
from electrumx.lib.tx import DeserializerDecred
|
||||
from electrumx.lib.util import (class_logger, hex_to_bytes, json_deserialize,
|
||||
json_serialize, pack_varint,
|
||||
unpack_le_uint16_from)
|
||||
json_serialize)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from electrumx.lib.coins import Coin
|
||||
@@ -404,235 +399,3 @@ class Daemon:
|
||||
|
||||
If the daemon has not been queried yet this returns None.'''
|
||||
return self._height
|
||||
|
||||
|
||||
class DashDaemon(Daemon):
|
||||
|
||||
async def masternode_broadcast(self, params):
|
||||
'''Broadcast a transaction to the network.'''
|
||||
return await self._send_single('masternodebroadcast', params)
|
||||
|
||||
async def masternode_list(self, params):
|
||||
'''Return the masternode status.'''
|
||||
return await self._send_single('masternodelist', params)
|
||||
|
||||
async def protx(self, params):
|
||||
'''Set of commands to execute ProTx related actions.'''
|
||||
return await self._send_single('protx', params)
|
||||
|
||||
|
||||
class FakeEstimateFeeDaemon(Daemon):
|
||||
'''Daemon that simulates estimatefee and relayfee RPC calls. Coin that
|
||||
wants to use this daemon must define ESTIMATE_FEE & RELAY_FEE'''
|
||||
|
||||
async def estimatefee(self, block_count):
|
||||
'''Return the fee estimate for the given parameters.'''
|
||||
return self.coin.ESTIMATE_FEE
|
||||
|
||||
async def relayfee(self):
|
||||
"""The minimum fee required for a transaction to be relayed on by the server to the
|
||||
bitcoin network. Doesn't guarantee mempool acceptance."""
|
||||
return self.coin.RELAY_FEE
|
||||
|
||||
|
||||
class LegacyRPCDaemon(Daemon):
|
||||
'''Handles connections to a daemon at the given URL.
|
||||
|
||||
This class is useful for daemons that don't have the new 'getblock'
|
||||
RPC call that returns the block in hex, the workaround is to manually
|
||||
recreate the block bytes. The recreated block bytes may not be the exact
|
||||
as in the underlying blockchain but it is good enough for our indexing
|
||||
purposes.'''
|
||||
|
||||
async def raw_blocks(self, bhashes_hum):
|
||||
'''Return the raw binary blocks with the given hex hashes.'''
|
||||
params_iterable = ((h, ) for h in bhashes_hum)
|
||||
block_info = await self._send_vector('getblock', params_iterable)
|
||||
|
||||
blocks = []
|
||||
for i in block_info:
|
||||
raw_block = await self.make_raw_block(i)
|
||||
blocks.append(raw_block)
|
||||
|
||||
# Convert hex string to bytes
|
||||
return blocks
|
||||
|
||||
async def make_raw_header(self, b):
|
||||
pbh = b.get('previousblockhash')
|
||||
if pbh is None:
|
||||
pbh = '0' * 64
|
||||
return b''.join([
|
||||
pack('<L', b.get('version')),
|
||||
hex_str_to_hash(pbh),
|
||||
hex_str_to_hash(b.get('merkleroot')),
|
||||
pack('<L', self.timestamp_safe(b['time'])),
|
||||
pack('<L', int(b.get('bits'), 16)),
|
||||
pack('<L', int(b.get('nonce')))
|
||||
])
|
||||
|
||||
async def make_raw_block(self, b):
|
||||
'''Construct a raw block'''
|
||||
|
||||
header = await self.make_raw_header(b)
|
||||
|
||||
transactions = []
|
||||
if b.get('height') > 0:
|
||||
transactions = await self.getrawtransactions(b.get('tx'), False)
|
||||
|
||||
raw_block = header
|
||||
num_txs = len(transactions)
|
||||
if num_txs > 0:
|
||||
raw_block += pack_varint(num_txs)
|
||||
raw_block += b''.join(transactions)
|
||||
else:
|
||||
raw_block += b'\x00'
|
||||
|
||||
return raw_block
|
||||
|
||||
def timestamp_safe(self, t):
|
||||
if isinstance(t, int):
|
||||
return t
|
||||
return timegm(time.strptime(t, "%Y-%m-%d %H:%M:%S %Z"))
|
||||
|
||||
|
||||
class FakeEstimateLegacyRPCDaemon(LegacyRPCDaemon, FakeEstimateFeeDaemon):
|
||||
pass
|
||||
|
||||
|
||||
class DecredDaemon(Daemon):
|
||||
async def raw_blocks(self, bhashes_hum):
|
||||
'''Return the raw binary blocks with the given hex hashes.'''
|
||||
|
||||
params_iterable = ((h, False) for h in bhashes_hum)
|
||||
blocks = await self._send_vector('getblock', params_iterable)
|
||||
|
||||
raw_blocks = []
|
||||
valid_tx_tree = {}
|
||||
for block in blocks:
|
||||
# Convert to bytes from hex
|
||||
raw_block = hex_to_bytes(block)
|
||||
raw_blocks.append(raw_block)
|
||||
# Check if previous block is valid
|
||||
prev = self.prev_hex_hash(raw_block)
|
||||
votebits = unpack_le_uint16_from(raw_block[100:102])[0]
|
||||
valid_tx_tree[prev] = self.is_valid_tx_tree(votebits)
|
||||
|
||||
processed_raw_blocks = []
|
||||
for hash, raw_block in zip(bhashes_hum, raw_blocks):
|
||||
if hash in valid_tx_tree:
|
||||
is_valid = valid_tx_tree[hash]
|
||||
else:
|
||||
# Do something complicated to figure out if this block is valid
|
||||
header = await self._send_single('getblockheader', (hash, ))
|
||||
if 'nextblockhash' not in header:
|
||||
raise DaemonError(f'Could not find next block for {hash}')
|
||||
next_hash = header['nextblockhash']
|
||||
next_header = await self._send_single('getblockheader',
|
||||
(next_hash, ))
|
||||
is_valid = self.is_valid_tx_tree(next_header['votebits'])
|
||||
|
||||
if is_valid:
|
||||
processed_raw_blocks.append(raw_block)
|
||||
else:
|
||||
# If this block is invalid remove the normal transactions
|
||||
self.logger.info(f'block {hash} is invalidated')
|
||||
processed_raw_blocks.append(self.strip_tx_tree(raw_block))
|
||||
|
||||
return processed_raw_blocks
|
||||
|
||||
@staticmethod
|
||||
def prev_hex_hash(raw_block):
|
||||
return hash_to_hex_str(raw_block[4:36])
|
||||
|
||||
@staticmethod
|
||||
def is_valid_tx_tree(votebits):
|
||||
# Check if previous block was invalidated.
|
||||
return bool(votebits & (1 << 0) != 0)
|
||||
|
||||
def strip_tx_tree(self, raw_block):
|
||||
c = self.coin
|
||||
assert issubclass(c.DESERIALIZER, DeserializerDecred)
|
||||
d = c.DESERIALIZER(raw_block, start=c.BASIC_HEADER_SIZE)
|
||||
d.read_tx_tree() # Skip normal transactions
|
||||
# Create a fake block without any normal transactions
|
||||
return raw_block[:c.BASIC_HEADER_SIZE] + b'\x00' + raw_block[d.cursor:]
|
||||
|
||||
async def height(self):
|
||||
height = await super().height()
|
||||
if height > 0:
|
||||
# Lie about the daemon height as the current tip can be invalidated
|
||||
height -= 1
|
||||
self._height = height
|
||||
return height
|
||||
|
||||
async def mempool_txids_hum(self):
|
||||
mempool = await super().mempool_txids_hum()
|
||||
# Add current tip transactions to the 'fake' mempool.
|
||||
real_height = await self._send_single('getblockcount')
|
||||
tip_hash = await self._send_single('getblockhash', (real_height,))
|
||||
tip = await self.deserialised_block(tip_hash)
|
||||
# Add normal transactions except coinbase
|
||||
mempool += tip['tx'][1:]
|
||||
# Add stake transactions if applicable
|
||||
mempool += tip.get('stx', [])
|
||||
return mempool
|
||||
|
||||
def connector(self):
|
||||
# FIXME allow self signed certificates
|
||||
return aiohttp.TCPConnector(verify_ssl=False)
|
||||
|
||||
|
||||
class PreLegacyRPCDaemon(LegacyRPCDaemon):
|
||||
'''Handles connections to a daemon at the given URL.
|
||||
|
||||
This class is useful for daemons that don't have the new 'getblock'
|
||||
RPC call that returns the block in hex, and need the False parameter
|
||||
for the getblock'''
|
||||
|
||||
async def deserialised_block(self, hex_hash):
|
||||
'''Return the deserialised block with the given hex hash.'''
|
||||
return await self._send_single('getblock', (hex_hash, False))
|
||||
|
||||
|
||||
class SmartCashDaemon(Daemon):
|
||||
|
||||
async def masternode_broadcast(self, params):
|
||||
'''Broadcast a smartnode to the network.'''
|
||||
return await self._send_single('smartnodebroadcast', params)
|
||||
|
||||
async def masternode_list(self, params):
|
||||
'''Return the smartnode status.'''
|
||||
return await self._send_single('smartnodelist', params)
|
||||
|
||||
async def smartrewards(self, params):
|
||||
'''Return smartrewards data.'''
|
||||
return await self._send_single('smartrewards', params)
|
||||
|
||||
|
||||
class ZcoinMtpDaemon(Daemon):
|
||||
|
||||
def strip_mtp_data(self, raw_block):
|
||||
if self.coin.is_mtp(raw_block):
|
||||
return \
|
||||
raw_block[:self.coin.MTP_HEADER_DATA_START*2] + \
|
||||
raw_block[self.coin.MTP_HEADER_DATA_END*2:]
|
||||
return raw_block
|
||||
|
||||
async def raw_blocks(self, bhashes_hum):
|
||||
'''Return the raw binary blocks with the given hex hashes.'''
|
||||
params_iterable = ((h, False) for h in bhashes_hum)
|
||||
blocks = await self._send_vector('getblock', params_iterable)
|
||||
# Convert hex string to bytes
|
||||
return [hex_to_bytes(self.strip_mtp_data(block)) for block in blocks]
|
||||
|
||||
async def masternode_broadcast(self, params):
|
||||
'''Broadcast a transaction to the network.'''
|
||||
return await self._send_single('znodebroadcast', params)
|
||||
|
||||
async def masternode_list(self, params):
|
||||
'''Return the masternode status.'''
|
||||
return await self._send_single('znodelist', params)
|
||||
|
||||
async def protx(self, params):
|
||||
'''Set of commands to execute ProTx related actions.'''
|
||||
return await self._send_single('protx', params)
|
||||
@@ -10,7 +10,6 @@
|
||||
import asyncio
|
||||
import codecs
|
||||
from dataclasses import dataclass
|
||||
import datetime
|
||||
import itertools
|
||||
import math
|
||||
import os
|
||||
@@ -2439,335 +2438,3 @@ class LocalRPC(SessionBase):
|
||||
|
||||
def protocol_version_string(self):
|
||||
return 'RPC'
|
||||
|
||||
|
||||
######################################################################
|
||||
# Non-Bitcoin stuff goes strictly below this line.
|
||||
######################################################################
|
||||
|
||||
|
||||
class DashElectrumX(ElectrumX):
|
||||
'''A TCP server that handles incoming Electrum Dash connections.'''
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.mns = set()
|
||||
self.mn_cache_height = 0
|
||||
self.mn_cache = []
|
||||
|
||||
def set_request_handlers(self, ptuple):
|
||||
super().set_request_handlers(ptuple)
|
||||
self.request_handlers.update({
|
||||
'masternode.announce.broadcast': self.phandle_masternode_announce_broadcast,
|
||||
'masternode.subscribe': self.phandle_masternode_subscribe,
|
||||
'masternode.list': self.phandle_masternode_list,
|
||||
'protx.diff': self.phandle_protx_diff,
|
||||
'protx.info': self.phandle_protx_info,
|
||||
})
|
||||
|
||||
async def _notify_inner(
|
||||
self,
|
||||
*,
|
||||
touched_hashxs,
|
||||
touched_outpoints,
|
||||
height_changed,
|
||||
):
|
||||
'''Notify the client about changes in masternode list.'''
|
||||
await super()._notify_inner(
|
||||
touched_hashxs=touched_hashxs,
|
||||
touched_outpoints=touched_outpoints,
|
||||
height_changed=height_changed,
|
||||
)
|
||||
for mn in self.mns.copy():
|
||||
status = await self.daemon_request('masternode_list',
|
||||
('status', mn))
|
||||
await self.send_notification('masternode.subscribe',
|
||||
(mn, status.get(mn)))
|
||||
|
||||
# Masternode command handlers
|
||||
async def phandle_masternode_announce_broadcast(self, signmnb):
|
||||
'''Pass through the masternode announce message to be broadcast
|
||||
by the daemon.
|
||||
|
||||
signmnb: signed masternode broadcast message.'''
|
||||
try:
|
||||
return await self.daemon_request('masternode_broadcast',
|
||||
('relay', signmnb))
|
||||
except DaemonError as e:
|
||||
error, = e.args
|
||||
message = error['message']
|
||||
self.logger.info(f'masternode_broadcast: {message}')
|
||||
raise RPCError(BAD_REQUEST, 'the masternode broadcast was '
|
||||
f'rejected.\n\n{message}\n[{signmnb}]')
|
||||
|
||||
async def phandle_masternode_subscribe(self, collateral):
|
||||
'''Returns the status of masternode.
|
||||
|
||||
collateral: masternode collateral.
|
||||
'''
|
||||
result = await self.daemon_request('masternode_list',
|
||||
('status', collateral))
|
||||
if result is not None:
|
||||
self.mns.add(collateral)
|
||||
return result.get(collateral)
|
||||
return None
|
||||
|
||||
async def phandle_masternode_list(self, payees):
|
||||
'''
|
||||
Returns the list of masternodes.
|
||||
|
||||
payees: a list of masternode payee addresses.
|
||||
'''
|
||||
if not isinstance(payees, list):
|
||||
raise RPCError(BAD_REQUEST, 'expected a list of payees')
|
||||
|
||||
def get_masternode_payment_queue(mns):
|
||||
'''Returns the calculated position in the payment queue for all the
|
||||
valid masterernodes in the given mns list.
|
||||
|
||||
mns: a list of masternodes information.
|
||||
'''
|
||||
now = int(datetime.datetime.utcnow().strftime("%s"))
|
||||
mn_queue = []
|
||||
|
||||
# Only ENABLED masternodes are considered for the list.
|
||||
for line in mns:
|
||||
mnstat = mns[line].split()
|
||||
if mnstat[0] == 'ENABLED':
|
||||
# if last paid time == 0
|
||||
if int(mnstat[5]) == 0:
|
||||
# use active seconds
|
||||
mnstat.append(int(mnstat[4]))
|
||||
else:
|
||||
# now minus last paid
|
||||
delta = now - int(mnstat[5])
|
||||
# if > active seconds, use active seconds
|
||||
if delta >= int(mnstat[4]):
|
||||
mnstat.append(int(mnstat[4]))
|
||||
# use active seconds
|
||||
else:
|
||||
mnstat.append(delta)
|
||||
mn_queue.append(mnstat)
|
||||
mn_queue = sorted(mn_queue, key=lambda x: x[8], reverse=True)
|
||||
return mn_queue
|
||||
|
||||
def get_payment_position(payment_queue, address):
|
||||
'''
|
||||
Returns the position of the payment list for the given address.
|
||||
|
||||
payment_queue: position in the payment queue for the masternode.
|
||||
address: masternode payee address.
|
||||
'''
|
||||
position = -1
|
||||
for pos, mn in enumerate(payment_queue, start=1):
|
||||
if mn[2] == address:
|
||||
position = pos
|
||||
break
|
||||
return position
|
||||
|
||||
# Accordingly with the masternode payment queue, a custom list
|
||||
# with the masternode information including the payment
|
||||
# position is returned.
|
||||
cache = self.session_mgr.mn_cache
|
||||
if not cache or self.session_mgr.mn_cache_height != self.db.db_height:
|
||||
full_mn_list = await self.daemon_request('masternode_list',
|
||||
('full',))
|
||||
mn_payment_queue = get_masternode_payment_queue(full_mn_list)
|
||||
mn_payment_count = len(mn_payment_queue)
|
||||
mn_list = []
|
||||
for key, value in full_mn_list.items():
|
||||
mn_data = value.split()
|
||||
mn_info = {
|
||||
'vin': key,
|
||||
'status': mn_data[0],
|
||||
'protocol': mn_data[1],
|
||||
'payee': mn_data[2],
|
||||
'lastseen': mn_data[3],
|
||||
'activeseconds': mn_data[4],
|
||||
'lastpaidtime': mn_data[5],
|
||||
'lastpaidblock': mn_data[6],
|
||||
'ip': mn_data[7]
|
||||
}
|
||||
mn_info['paymentposition'] = get_payment_position(
|
||||
mn_payment_queue, mn_info['payee']
|
||||
)
|
||||
mn_info['inselection'] = (
|
||||
mn_info['paymentposition'] < mn_payment_count // 10
|
||||
)
|
||||
hashX = self.coin.address_to_hashX(mn_info['payee'])
|
||||
balance = await self.get_balance(hashX)
|
||||
mn_info['balance'] = (sum(balance.values())
|
||||
/ self.coin.VALUE_PER_COIN)
|
||||
mn_list.append(mn_info)
|
||||
cache.clear()
|
||||
cache.extend(mn_list)
|
||||
self.session_mgr.mn_cache_height = self.db.db_height
|
||||
|
||||
# If payees is an empty list the whole masternode list is returned
|
||||
if payees:
|
||||
return [mn for mn in cache if mn['payee'] in payees]
|
||||
else:
|
||||
return cache
|
||||
|
||||
async def phandle_protx_diff(self, base_height, height):
|
||||
'''
|
||||
Calculates a diff between two deterministic masternode lists.
|
||||
The result also contains proof data.
|
||||
|
||||
base_height: The starting block height (starting from 1).
|
||||
height: The ending block height.
|
||||
'''
|
||||
if not isinstance(base_height, int) or not isinstance(height, int):
|
||||
raise RPCError(BAD_REQUEST, 'expected a int block heights')
|
||||
|
||||
max_height = self.db.db_height
|
||||
if (not 1 <= base_height <= max_height or
|
||||
not base_height <= height <= max_height):
|
||||
raise RPCError(BAD_REQUEST,
|
||||
f'require 1 <= base_height {base_height:,d} <= '
|
||||
f'height {height:,d} <= '
|
||||
f'chain height {max_height:,d}')
|
||||
|
||||
return await self.daemon_request('protx',
|
||||
('diff', base_height, height))
|
||||
|
||||
async def phandle_protx_info(self, protx_hash):
|
||||
'''
|
||||
Returns detailed information about a deterministic masternode.
|
||||
|
||||
protx_hash: The hash of the initial ProRegTx
|
||||
'''
|
||||
if not isinstance(protx_hash, str):
|
||||
raise RPCError(BAD_REQUEST, 'expected protx hash string')
|
||||
|
||||
res = await self.daemon_request('protx', ('info', protx_hash))
|
||||
if 'wallet' in res:
|
||||
del res['wallet']
|
||||
return res
|
||||
|
||||
|
||||
class SmartCashElectrumX(DashElectrumX):
|
||||
'''A TCP server that handles incoming Electrum-SMART connections.'''
|
||||
|
||||
def set_request_handlers(self, ptuple):
|
||||
super().set_request_handlers(ptuple)
|
||||
self.request_handlers.update({
|
||||
'smartrewards.current': self.phandle_smartrewards_current,
|
||||
'smartrewards.check': self.phandle_smartrewards_check
|
||||
})
|
||||
|
||||
async def phandle_smartrewards_current(self):
|
||||
'''Returns the current smartrewards info.'''
|
||||
result = await self.daemon_request('smartrewards', ('current',))
|
||||
if result is not None:
|
||||
return result
|
||||
return None
|
||||
|
||||
async def phandle_smartrewards_check(self, addr):
|
||||
'''
|
||||
Returns the status of an address
|
||||
|
||||
addr: a single smartcash address
|
||||
'''
|
||||
result = await self.daemon_request('smartrewards', ('check', addr))
|
||||
if result is not None:
|
||||
return result
|
||||
return None
|
||||
|
||||
|
||||
class AuxPoWElectrumX(ElectrumX):
|
||||
async def phandle_block_header(self, height, cp_height=0):
|
||||
result = await super().phandle_block_header(height, cp_height)
|
||||
|
||||
# Older protocol versions don't truncate AuxPoW
|
||||
if self.protocol_tuple < (1, 4, 1):
|
||||
return result
|
||||
|
||||
# Not covered by a checkpoint; return full AuxPoW data
|
||||
if cp_height == 0:
|
||||
return result
|
||||
|
||||
# Covered by a checkpoint; truncate AuxPoW data
|
||||
result['header'] = self.truncate_auxpow_single(result['header'])
|
||||
return result
|
||||
|
||||
async def phandle_block_headers(self, start_height, count, cp_height=0):
|
||||
# Older protocol versions don't truncate AuxPoW
|
||||
if self.protocol_tuple < (1, 4, 1):
|
||||
return await super().phandle_block_headers(start_height, count, cp_height)
|
||||
|
||||
# Not covered by a checkpoint; return full AuxPoW data
|
||||
if cp_height == 0:
|
||||
return await super().phandle_block_headers(start_height, count, cp_height)
|
||||
|
||||
result = await super().block_headers_array(start_height, count, cp_height)
|
||||
|
||||
# Covered by a checkpoint; truncate AuxPoW data
|
||||
result['headers'] = self.truncate_auxpow_headers(result['headers'])
|
||||
|
||||
# Return headers in array form
|
||||
if self.protocol_tuple >= (1, 6):
|
||||
return result
|
||||
|
||||
# Return headers in concatenated form
|
||||
result['hex'] = ''.join(result['headers'])
|
||||
del result['headers']
|
||||
return result
|
||||
|
||||
def truncate_auxpow_headers(self, headers):
|
||||
result = []
|
||||
for header in headers:
|
||||
result.append(self.truncate_auxpow_single(header))
|
||||
return result
|
||||
|
||||
def truncate_auxpow_single(self, header: str):
|
||||
# 2 hex chars per byte
|
||||
return header[:2*self.coin.TRUNCATED_HEADER_SIZE]
|
||||
|
||||
|
||||
class NameIndexElectrumX(ElectrumX):
|
||||
def set_request_handlers(self, ptuple):
|
||||
super().set_request_handlers(ptuple)
|
||||
|
||||
if ptuple >= (1, 4, 3):
|
||||
self.request_handlers['blockchain.name.get_value_proof'] = self.phandle_name_get_value_proof
|
||||
|
||||
async def phandle_name_get_value_proof(self, scripthash, cp_height=0):
|
||||
history = await self.phandle_scripthash_get_history(scripthash)
|
||||
|
||||
trimmed_history = []
|
||||
prev_height = None
|
||||
|
||||
for update in history[::-1]:
|
||||
txid = update['tx_hash']
|
||||
height = update['height']
|
||||
|
||||
if (self.coin.NAME_EXPIRATION is not None
|
||||
and prev_height is not None
|
||||
and height < prev_height - self.coin.NAME_EXPIRATION):
|
||||
break
|
||||
|
||||
tx = await self.phandle_transaction_get(txid)
|
||||
update['tx'] = tx
|
||||
del update['tx_hash']
|
||||
|
||||
tx_merkle = await self.phandle_transaction_merkle(txid, height)
|
||||
del tx_merkle['block_height']
|
||||
update['tx_merkle'] = tx_merkle
|
||||
|
||||
if height <= cp_height:
|
||||
header = await self.phandle_block_header(height, cp_height)
|
||||
update['header'] = header
|
||||
|
||||
trimmed_history.append(update)
|
||||
|
||||
if height <= cp_height:
|
||||
break
|
||||
|
||||
prev_height = height
|
||||
|
||||
return {scripthash: trimmed_history}
|
||||
|
||||
|
||||
class NameIndexAuxPoWElectrumX(NameIndexElectrumX, AuxPoWElectrumX):
|
||||
pass
|
||||
Reference in new issue
Block a user