Skip to content
Closed
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
38 changes: 25 additions & 13 deletions bitcointx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
# Note that setup.py can break if __init__.py imports any external
# dependencies, as these might not be installed when setup.py runs. In this
# case __version__ could be moved to a separate version.py and imported here.
__version__ = '1.1.1.post2'
__version__ = '1.1.2.dev0'


# initialized at the end of the module, because it
Expand Down Expand Up @@ -68,9 +68,6 @@ def __new__(cls: Type[T_ChainParamsMeta], cls_name: str,
if 'RPC_PORT' in dct and not isinstance(dct['RPC_PORT'], int):
raise TypeError(f'{cls_name}: RPC_PORT is not an int')

# Cannot check the type of WALLET_DISPATCHER without
# importing bitcointx.wallet, this check has to be done at runtime

if name is not None:
if isinstance(name, str):
names = [name]
Expand Down Expand Up @@ -122,6 +119,11 @@ class ChainParamsBase(metaclass=ChainParamsMeta):
Type['bitcointx.wallet.WalletCoinClassDispatcher'],
Callable[[object], Type['bitcointx.wallet.WalletCoinClassDispatcher']]
]
PSBT_DISPATCHER: Union[
Type['bitcointx.core.psbt.PSBT_CoinClassDispatcher'],
Callable[[object],
Type['bitcointx.core.psbt.PSBT_CoinClassDispatcher']]
]

def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__()
Expand Down Expand Up @@ -190,6 +192,12 @@ class BitcoinMainnetParams(ChainParamsBase,
Callable[[object], Type['bitcointx.wallet.WalletCoinClassDispatcher']]
] = lambda _: bitcointx.wallet.WalletBitcoinClassDispatcher

PSBT_DISPATCHER: Union[
Type['bitcointx.core.psbt.PSBT_CoinClassDispatcher'],
Callable[[object],
Type['bitcointx.core.psbt.PSBT_CoinClassDispatcher']]
] = lambda _: bitcointx.core.psbt.PSBT_BitcoinClassDispatcher


class BitcoinTestnetParams(BitcoinMainnetParams, name='bitcoin/testnet'):
RPC_PORT = 18332
Expand Down Expand Up @@ -281,19 +289,23 @@ def select_chain_params(params: Union[str, ChainParamsBase,
prev_params = _chain_params_context.params
_chain_params_context.params = params

# It might not be imported yet
import bitcointx.wallet
import bitcointx.core.psbt

wd = params.WALLET_DISPATCHER
if isinstance(wd, types.MethodType):
wd_cls = wd()
else:
assert isinstance(wd, type)
wd_cls = wd
for disp_name in ('WALLET', 'PSBT'):
disp = getattr(params, f'{disp_name}_DISPATCHER', None)
if disp is None:
continue

if isinstance(disp, types.MethodType):
disp_cls = disp()
else:
assert isinstance(disp, type)
disp_cls = disp

assert issubclass(wd_cls, bitcointx.wallet.WalletCoinClassDispatcher)
assert issubclass(disp_cls, bitcointx.util.ClassMappingDispatcher)

bitcointx.util.activate_class_dispatcher(wd_cls)
bitcointx.util.activate_class_dispatcher(disp_cls)

return prev_params, params

Expand Down
2 changes: 1 addition & 1 deletion bitcointx/base58.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ def base58_from_bytes_match_prefix(cls: Type[T_CBase58Data], data: bytes
return candidate.base58_from_bytes_match_prefix(data)
except UnexpectedBase58PrefixError:
pass
elif data[:len(pfx)] == pfx:
elif data.startswith(pfx):
return candidate.from_bytes(data[len(pfx):])

if len(candidates) == 1:
Expand Down
83 changes: 52 additions & 31 deletions bitcointx/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,8 @@ def MAX_MONEY(cls) -> int:


class CoreBitcoinParams(CoreCoinParams, CoreBitcoinClass):
...
PSBT_MAGIC_HEADER_BYTES = b'psbt\xff'
PSBT_MAGIC_HEADER_BASE64 = 'cHNidP'


def MoneyRange(nValue: int) -> bool:
Expand Down Expand Up @@ -801,8 +802,12 @@ class CTxInWitness(CoreCoinClass, next_dispatch_final=True):
to_mutable: Callable[['CTxInWitness'], 'CMutableTxInWitness']
to_immutable: Callable[['CTxInWitness'], 'CTxInWitness']

def __init__(self, scriptWitness: script.CScriptWitness = script.CScriptWitness()):
ensure_isinstance(scriptWitness, script.CScriptWitness, 'scriptWitness')
def __init__(self, scriptWitness: Optional[script.CScriptWitness] = None):
if scriptWitness is None:
scriptWitness = script.CScriptWitness()
else:
ensure_isinstance(scriptWitness, script.CScriptWitness, 'scriptWitness')

object.__setattr__(self, 'scriptWitness', scriptWitness)

@no_bool_use_as_property
Expand Down Expand Up @@ -1015,13 +1020,18 @@ def __init__(self, vin: Iterable[CTxIn] = (), vout: Iterable[CTxOut] = (),
else:
ensure_isinstance(nVersion, int, 'nVersion')

if witness is None or witness.is_null():
if witness is None and self.is_immutable():
witness = CTxWitness()
if witness is None or (witness.is_null() and self.is_mutable()):
if self.is_mutable():
new_witness = CTxWitness([CTxInWitness() for dummy in vin],
[CTxOutWitness() for dummy in vout])
if witness is not None:
ensure_isinstance(witness, new_witness._immutable_cls,
'witness')
witness = new_witness
else:
witness = CTxWitness(
[CTxInWitness() for dummy in vin],
[CTxOutWitness() for dummy in vout])
assert witness is None
witness = CTxWitness()

else:
witness = CTxWitness.from_witness(witness)

Expand Down Expand Up @@ -1095,38 +1105,49 @@ def stream_deserialize(cls: Type[T_CTransaction], f: ByteStream_Type,
inputs. If the behavior of DecodeHexTx() is needed it could be added,
but not here.
"""
# FIXME can't assume f is seekable
nVersion = struct.unpack(b"<i", ser_read(f, 4))[0]
pos = f.tell()
markerbyte = struct.unpack(b'B', ser_read(f, 1))[0]
flagbyte = struct.unpack(b'B', ser_read(f, 1))[0]

if markerbyte == 0:
if flagbyte != 1:
raise ValueError(
f'unexpected flag value {flagbyte} after segwit marker, '
f'expected 1')
vin = VectorSerializer.stream_deserialize(f, element_class=CTxIn,
**kwargs)
vout = VectorSerializer.stream_deserialize(f, element_class=CTxOut,
**kwargs)

# Try to read the vin.
# In case the dummy is there, this will be read as an empty vector.
vin = VectorSerializer.stream_deserialize(
f, element_class=CTxIn, **kwargs)

flags = 0

if not vin:
# We read a dummy or an empty vin
flags = struct.unpack(b'B', ser_read(f, 1))[0]
if flags != 0:
vin = VectorSerializer.stream_deserialize(
f, element_class=CTxIn, **kwargs)
vout = VectorSerializer.stream_deserialize(
f, element_class=CTxOut, **kwargs)
else:
# We read a non-empty vin. Assume a normal vout follows.
vout = VectorSerializer.stream_deserialize(
f, element_class=CTxOut, **kwargs)

wit = None

if flags & 1:
# The witness flag is present,
# and we unconditionally support witnesses (see docstring)
flags ^= 1

wit = CTxWitness.stream_deserialize(f, num_inputs=len(vin),
**kwargs)
if wit.is_null():
# It's illegal to encode witnesses
# when all witness stacks are empty.
raise ValueError('Superfluous witness record')

nLockTime = struct.unpack(b"<I", ser_read(f, 4))[0]
return cls(vin, vout, nLockTime, nVersion, wit)
if flags:
# Unknown flag in the serialization
raise ValueError('Unknown transaction optional data')

f.seek(pos) # put marker byte back, since we don't have peek
vin = VectorSerializer.stream_deserialize(f, element_class=CTxIn,
**kwargs)
vout = VectorSerializer.stream_deserialize(f, element_class=CTxOut,
**kwargs)
nLockTime = struct.unpack(b"<I", ser_read(f, 4))[0]
return cls(vin, vout, nLockTime, nVersion)

return cls(vin, vout, nLockTime, nVersion, wit)

# NOTE: for_sighash is ignored, but may be used in other implementations
def stream_serialize(self, f: ByteStream_Type,
Expand Down
59 changes: 52 additions & 7 deletions bitcointx/core/key.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,23 @@ def load_openssl_library(path: Optional[str] = None) -> Optional[ctypes.CDLL]:
return handle


def _raw_sig_has_low_r(raw_sig: bytes) -> bool:
compact_sig = ctypes.create_string_buffer(64)
result = _secp256k1.secp256k1_ecdsa_signature_serialize_compact(
secp256k1_context_sign, compact_sig, raw_sig)
assert result == 1

# In DER serialization, all values are interpreted as big-endian,
# signed integers. The highest bit in the integer indicates
# its signed-ness; 0 is positive, 1 is negative.
# When the value is interpreted as a negative integer,
# it must be converted to a positive value by prepending a 0x00 byte
# so that the highest bit is 0. We can avoid this prepending by ensuring
# that our highest bit is always 0, and thus we must check that the
# first byte is less than 0x80.
return compact_sig.raw[0] < 0x80


class CKeyBase:
"""An encapsulated private key

Expand Down Expand Up @@ -185,25 +202,53 @@ def secret_bytes(self) -> bytes:
def pub(self) -> 'CPubKey':
return self.__pub

def sign(self, hash: Union[bytes, bytearray]) -> bytes:
def sign(self, hash: Union[bytes, bytearray], *,
_ecdsa_sig_grind_low_r: bool = True,
_ecdsa_sig_extra_entropy: int = 0
) -> bytes:

ensure_isinstance(hash, (bytes, bytearray), 'hash')
if len(hash) != 32:
raise ValueError('Hash must be exactly 32 bytes long')

raw_sig = ctypes.create_string_buffer(64)
result = _secp256k1.secp256k1_ecdsa_sign(
secp256k1_context_sign, raw_sig, hash, self.secret_bytes, None, None)
if 1 != result:
assert(result == 0)
raise RuntimeError('secp256k1_ecdsa_sign returned failure')

if _ecdsa_sig_grind_low_r:
counter = 0
else:
counter = _ecdsa_sig_extra_entropy

def maybe_extra_entropy() -> Optional[bytes]:
if counter == 0:
return None

# mimic Bitcoin Core that uses 32-bit counter for the entropy
assert counter < 2**32
return counter.to_bytes(4, byteorder="little") + b'\x00'*28

while True:
result = _secp256k1.secp256k1_ecdsa_sign(
secp256k1_context_sign, raw_sig, hash, self.secret_bytes, None,
maybe_extra_entropy())
if 1 != result:
assert(result == 0)
raise RuntimeError('secp256k1_ecdsa_sign returned failure')

if not _ecdsa_sig_grind_low_r or _raw_sig_has_low_r(raw_sig.raw):
break

counter += 1

sig_size0 = ctypes.c_size_t()
sig_size0.value = SIGNATURE_SIZE
mb_sig = ctypes.create_string_buffer(sig_size0.value)
mb_sig = ctypes.create_string_buffer(SIGNATURE_SIZE)

result = _secp256k1.secp256k1_ecdsa_signature_serialize_der(
secp256k1_context_sign, mb_sig, ctypes.byref(sig_size0), raw_sig)
if 1 != result:
assert(result == 0)
raise RuntimeError('secp256k1_ecdsa_signature_parse_der returned failure')

# secp256k1 creates signatures already in lower-S form, no further
# conversion needed.
return mb_sig.raw[:sig_size0.value]
Expand Down
Loading