Decred sighash and signing.

This commit is contained in:
tecnovert
2024-04-27 16:27:14 +02:00
parent 150caeec40
commit ab472c04be
8 changed files with 256 additions and 31 deletions

View File

@@ -19,8 +19,91 @@ from basicswap.util.crypto import (
ripemd160,
)
from basicswap.util.extkey import ExtKeyPair
from basicswap.util.integer import encode_varint
from basicswap.interface.dcr.rpc import make_rpc_func
from .messages import CTransaction
from .messages import CTransaction, SigHashType, TxSerializeType
from .script import push_script_data
from coincurve.keys import (
PrivateKey
)
SigHashSerializePrefix: int = 1
SigHashSerializeWitness: int = 3
def DCRSignatureHash(sign_script: bytes, hash_type: SigHashType, tx: CTransaction, idx: int) -> bytes:
masked_hash_type = hash_type & SigHashType.SigHashMask
if masked_hash_type != SigHashType.SigHashAll:
raise ValueError('todo')
# Prefix hash
sign_tx_in_idx: int = idx
sign_vins = tx.vin
if hash_type & SigHashType.SigHashAnyOneCanPay != 0:
sign_vins = [tx.vin[idx],]
sign_tx_in_idx = 0
hash_buffer = bytearray()
version: int = tx.version | (SigHashSerializePrefix << 16)
hash_buffer += version.to_bytes(4, 'little')
hash_buffer += encode_varint(len(sign_vins))
for txi_n, txi in enumerate(sign_vins):
hash_buffer += txi.prevout.hash.to_bytes(32, 'little')
hash_buffer += txi.prevout.n.to_bytes(4, 'little')
hash_buffer += txi.prevout.tree.to_bytes(1)
# In the case of SigHashNone and SigHashSingle, commit to 0 for everything that is not the input being signed instead.
if (masked_hash_type == SigHashType.SigHashNone
or masked_hash_type == SigHashType.SigHashSingle) and \
sign_tx_in_idx != txi_n:
hash_buffer += (0).to_bytes(4, 'little')
else:
hash_buffer += txi.sequence.to_bytes(4, 'little')
hash_buffer += encode_varint(len(tx.vout))
for txo_n, txo in enumerate(tx.vout):
if masked_hash_type == SigHashType.SigHashSingle and \
idx != txo_n:
hash_buffer += (-1).to_bytes(8, 'little')
hash_buffer += txo.version.to_bytes(2, 'little')
hash_buffer += encode_varint(0)
continue
hash_buffer += txo.value.to_bytes(8, 'little')
hash_buffer += txo.version.to_bytes(2, 'little')
hash_buffer += encode_varint(len(txo.script_pubkey))
hash_buffer += txo.script_pubkey
hash_buffer += tx.locktime.to_bytes(4, 'little')
hash_buffer += tx.expiry.to_bytes(4, 'little')
prefix_hash = blake256(hash_buffer)
# Witness hash
hash_buffer.clear()
version: int = tx.version | (SigHashSerializeWitness << 16)
hash_buffer += version.to_bytes(4, 'little')
hash_buffer += encode_varint(len(sign_vins))
for txi_n, txi in enumerate(sign_vins):
if sign_tx_in_idx != txi_n:
hash_buffer += encode_varint(0)
continue
hash_buffer += encode_varint(len(sign_script))
hash_buffer += sign_script
witness_hash = blake256(hash_buffer)
hash_buffer.clear()
hash_buffer += hash_type.to_bytes(4, 'little')
hash_buffer += prefix_hash
hash_buffer += witness_hash
return blake256(hash_buffer)
class DCRInterface(Secp256k1Interface):
@@ -121,7 +204,38 @@ class DCRInterface(Secp256k1Interface):
return hash160(ek_account.encode_p())
def decodeKey(self, encoded_key: str) -> (int, bytes):
key = b58decode(encoded_key)
checksum = key[-4:]
key = key[:-4]
if blake256(key)[:4] != checksum:
raise ValueError('Checksum mismatch')
return key[2], key[3:]
def loadTx(self, tx_bytes: bytes) -> CTransaction:
tx = CTransaction()
tx.deserialize(tx_bytes)
return tx
def signTx(self, key_bytes: bytes, tx_bytes: bytes, input_n: int, prevout_script: bytes, prevout_value: int) -> bytes:
tx = self.loadTx(tx_bytes)
sig_hash = DCRSignatureHash(prevout_script, SigHashType.SigHashAll, tx, input_n)
eck = PrivateKey(key_bytes)
return eck.sign(sig_hash, hasher=None) + bytes((SigHashType.SigHashAll,))
def setTxSignature(self, tx_bytes: bytes, stack, txi: int = 0) -> bytes:
tx = self.loadTx(tx_bytes)
script_data = bytearray()
for data in stack:
push_script_data(script_data, data)
tx.vin[txi].signature_script = script_data
return tx.serialize()
def stripTxSignature(self, tx_bytes) -> bytes:
tx = self.loadTx(tx_bytes)
return tx.serialize(TxSerializeType.NoWitness)

View File

@@ -17,14 +17,37 @@ class TxSerializeType(IntEnum):
OnlyWitness = 2
class SigHashType(IntEnum):
SigHashAll = 0x1
SigHashNone = 0x2
SigHashSingle = 0x3
SigHashAnyOneCanPay = 0x80
SigHashMask = 0x1f
class SignatureType(IntEnum):
STEcdsaSecp256k1 = 0
STEd25519 = 1
STSchnorrSecp256k1 = 2
class COutpoint:
__slots__ = ('hash', 'n', 'tree')
def get_hash(self) -> bytes:
return self.hash.to_bytes(32, 'big')
class CTxIn:
__slots__ = ('prevout', 'sequence',
'value_in', 'block_height', 'block_index', 'signature_script') # Witness
def __init__(self, tx=None):
self.value_in = -1
self.block_height = 0
self.block_index = 0xffffffff
class CTxOut:
__slots__ = ('value', 'version', 'script_pubkey')
@@ -47,7 +70,6 @@ class CTransaction:
self.locktime = tx.locktime
self.expiry = tx.expiry
def deserialize(self, data: bytes) -> None:
version = int.from_bytes(data[:4], 'little')
@@ -92,6 +114,9 @@ class CTransaction:
self.expiry = int.from_bytes(data[o:o + 4], 'little')
o += 4
if ser_type == TxSerializeType.NoWitness:
return
num_wit_scripts, nb = decode_varint(data, o)
o += nb
@@ -140,7 +165,8 @@ class CTransaction:
if ser_type == TxSerializeType.Full or ser_type == TxSerializeType.OnlyWitness:
data += encode_varint(len(self.vin))
for txi in self.vin:
data += txi.value_in.to_bytes(8, 'little')
tc_value_in = txi.value_in & 0xffffffffffffffff # Convert negative values
data += tc_value_in.to_bytes(8, 'little')
data += txi.block_height.to_bytes(4, 'little')
data += txi.block_index.to_bytes(4, 'little')
data += encode_varint(len(txi.signature_script))

View File

@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2024 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
OP_0 = 0x00
OP_DATA_1 = 0x01
OP_1NEGATE = 0x4f
OP_1 = 0x51
OP_PUSHDATA1 = 0x4c
OP_PUSHDATA2 = 0x4d
OP_PUSHDATA4 = 0x4e
def push_script_data(data_array: bytearray, data: bytes) -> None:
len_data: int = len(data)
if len_data == 0 or (len_data == 1 and data[0] == 0):
data_array += bytes((OP_0,))
return
if len_data == 1 and data[0] <= 16:
data_array += bytes((OP_1 - 1 + data[0],))
return
if len_data == 1 and data[0] == 0x81:
data_array += bytes((OP_1NEGATE,))
return
if len_data < OP_PUSHDATA1:
data_array += bytes(((OP_DATA_1 - 1) + len_data,))
elif len_data <= 0xff:
data_array += bytes((OP_PUSHDATA1, len_data))
elif len_data <= 0xffff:
data_array += bytes((OP_PUSHDATA2,)) + len_data.to_bytes(2, 'little')
else:
data_array += bytes((OP_PUSHDATA4,)) + len_data.to_bytes(4, 'little')
data_array += data