Merge pull request #9 from justjamesdev/master
Sapling - Ledger, Ledger X
This commit is contained in:
commit
cd1c9aa48b
|
@ -1,5 +1,5 @@
|
|||
btchip-python==0.1.26 \
|
||||
--hash=sha256:427d67c5b4f4709605c51dd91d5d44a2ad8f541693673817765271e4b3a1461e
|
||||
btchip-python==0.1.28 \
|
||||
--hash=sha256:da09d0d7a6180d428833795ea9a233c3b317ddfcccea8cc6f0eba59435e5dd83
|
||||
certifi==2018.1.18 \
|
||||
--hash=sha256:14131608ad2fd56836d33a71ee60fa1c82bc9d2c8d98b7bdbc631fe1b3cd1296 \
|
||||
--hash=sha256:edbc3f203427eef571f79a7692bb160a2b0f7ccaa31953e99bd17e307cf63f7d
|
||||
|
|
|
@ -2,3 +2,5 @@ Cython>=0.27
|
|||
trezor>=0.9.0
|
||||
keepkey
|
||||
btchip-python
|
||||
libusb1
|
||||
|
||||
|
|
|
@ -0,0 +1,268 @@
|
|||
from binascii import hexlify, unhexlify
|
||||
from struct import pack, unpack
|
||||
|
||||
from btchip.btchip import btchip
|
||||
from btchip.bitcoinTransaction import bitcoinInput, bitcoinOutput
|
||||
from btchip.bitcoinVarint import readVarint, writeVarint
|
||||
from btchip.btchipHelpers import parse_bip32_path, writeUint32BE
|
||||
|
||||
from electrum_zclassic.transaction import (OVERWINTERED_VERSION_GROUP_ID,
|
||||
SAPLING_VERSION_GROUP_ID)
|
||||
|
||||
|
||||
class zcashTransaction:
|
||||
|
||||
def __init__(self, data=None):
|
||||
self.version = ''
|
||||
self.version_group_id = ''
|
||||
self.inputs = []
|
||||
self.outputs = []
|
||||
self.lockTime = ''
|
||||
self.expiry_height = ''
|
||||
self.value_balance = ''
|
||||
self.overwintered = False
|
||||
self.n_version = 0
|
||||
if data is not None:
|
||||
offset = 0
|
||||
self.version = data[offset:offset + 4]
|
||||
offset += 4
|
||||
header = unpack('<I', self.version)[0]
|
||||
if header & 0x80000000:
|
||||
self.n_version = header & 0x7FFFFFFF
|
||||
self.version_group_id = data[offset:offset + 4]
|
||||
offset += 4
|
||||
version_group_id = unpack('<I', self.version_group_id)[0]
|
||||
if (self.n_version == 3
|
||||
and version_group_id == OVERWINTERED_VERSION_GROUP_ID):
|
||||
self.overwintered = True
|
||||
elif (self.n_version == 4
|
||||
and version_group_id == SAPLING_VERSION_GROUP_ID):
|
||||
self.overwintered = True
|
||||
else:
|
||||
offset -= 4
|
||||
self.version_group_id = ''
|
||||
self.n_version = header
|
||||
inputSize = readVarint(data, offset)
|
||||
offset += inputSize['size']
|
||||
numInputs = inputSize['value']
|
||||
for i in range(numInputs):
|
||||
tmp = { 'buffer': data, 'offset' : offset}
|
||||
self.inputs.append(bitcoinInput(tmp))
|
||||
offset = tmp['offset']
|
||||
outputSize = readVarint(data, offset)
|
||||
offset += outputSize['size']
|
||||
numOutputs = outputSize['value']
|
||||
for i in range(numOutputs):
|
||||
tmp = { 'buffer': data, 'offset' : offset}
|
||||
self.outputs.append(bitcoinOutput(tmp))
|
||||
offset = tmp['offset']
|
||||
self.lockTime = data[offset:offset + 4]
|
||||
if self.overwintered:
|
||||
offset += 4
|
||||
self.expiry_height = data[offset:offset + 4]
|
||||
if self.n_version >= 4:
|
||||
offset += 4
|
||||
self.value_balance = data[offset:offset + 8]
|
||||
|
||||
def serializeOutputs(self):
|
||||
result = []
|
||||
writeVarint(len(self.outputs), result)
|
||||
for troutput in self.outputs:
|
||||
result.extend(troutput.serialize())
|
||||
return result
|
||||
|
||||
|
||||
class btchip_zcash(btchip):
|
||||
|
||||
def startUntrustedTransaction(self, newTransaction, inputIndex, outputList,
|
||||
redeemScript, version=0x02,
|
||||
overwintered=False):
|
||||
# Start building a fake transaction with the passed inputs
|
||||
if newTransaction:
|
||||
if overwintered:
|
||||
p2 = 0x05 if version == 4 else 0x04
|
||||
else:
|
||||
p2 = 0x00
|
||||
else:
|
||||
p2 = 0x80
|
||||
apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_START, 0x00, p2 ]
|
||||
if overwintered and version == 3:
|
||||
params = bytearray([version, 0x00, 0x00, 0x80, 0x70, 0x82, 0xc4, 0x03])
|
||||
elif overwintered and version == 4:
|
||||
params = bytearray([version, 0x00, 0x00, 0x80, 0x85, 0x20, 0x2f, 0x89])
|
||||
else:
|
||||
params = bytearray([version, 0x00, 0x00, 0x00])
|
||||
writeVarint(len(outputList), params)
|
||||
apdu.append(len(params))
|
||||
apdu.extend(params)
|
||||
self.dongle.exchange(bytearray(apdu))
|
||||
# Loop for each input
|
||||
currentIndex = 0
|
||||
for passedOutput in outputList:
|
||||
if ('sequence' in passedOutput) and passedOutput['sequence']:
|
||||
sequence = bytearray(unhexlify(passedOutput['sequence']))
|
||||
else:
|
||||
sequence = bytearray([0xFF, 0xFF, 0xFF, 0xFF]) # default sequence
|
||||
apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_START, 0x80, 0x00 ]
|
||||
params = []
|
||||
script = bytearray(redeemScript)
|
||||
if overwintered:
|
||||
params.append(0x02)
|
||||
elif ('trustedInput' in passedOutput) and passedOutput['trustedInput']:
|
||||
params.append(0x01)
|
||||
else:
|
||||
params.append(0x00)
|
||||
if ('trustedInput' in passedOutput) and passedOutput['trustedInput']:
|
||||
params.append(len(passedOutput['value']))
|
||||
params.extend(passedOutput['value'])
|
||||
if currentIndex != inputIndex:
|
||||
script = bytearray()
|
||||
writeVarint(len(script), params)
|
||||
if len(script) == 0:
|
||||
params.extend(sequence)
|
||||
apdu.append(len(params))
|
||||
apdu.extend(params)
|
||||
self.dongle.exchange(bytearray(apdu))
|
||||
offset = 0
|
||||
while(offset < len(script)):
|
||||
blockLength = 255
|
||||
if ((offset + blockLength) < len(script)):
|
||||
dataLength = blockLength
|
||||
else:
|
||||
dataLength = len(script) - offset
|
||||
params = script[offset : offset + dataLength]
|
||||
if ((offset + dataLength) == len(script)):
|
||||
params.extend(sequence)
|
||||
apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_START, 0x80, 0x00, len(params) ]
|
||||
apdu.extend(params)
|
||||
self.dongle.exchange(bytearray(apdu))
|
||||
offset += blockLength
|
||||
currentIndex += 1
|
||||
|
||||
def finalizeInput(self, outputAddress, amount, fees, changePath, rawTx=None):
|
||||
alternateEncoding = False
|
||||
donglePath = parse_bip32_path(changePath)
|
||||
if self.needKeyCache:
|
||||
self.resolvePublicKeysInPath(changePath)
|
||||
result = {}
|
||||
outputs = None
|
||||
if rawTx is not None:
|
||||
try:
|
||||
fullTx = zcashTransaction(bytearray(rawTx))
|
||||
outputs = fullTx.serializeOutputs()
|
||||
if len(donglePath) != 0:
|
||||
apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_FINALIZE_FULL, 0xFF, 0x00 ]
|
||||
params = []
|
||||
params.extend(donglePath)
|
||||
apdu.append(len(params))
|
||||
apdu.extend(params)
|
||||
response = self.dongle.exchange(bytearray(apdu))
|
||||
offset = 0
|
||||
while (offset < len(outputs)):
|
||||
blockLength = self.scriptBlockLength
|
||||
if ((offset + blockLength) < len(outputs)):
|
||||
dataLength = blockLength
|
||||
p1 = 0x00
|
||||
else:
|
||||
dataLength = len(outputs) - offset
|
||||
p1 = 0x80
|
||||
apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_FINALIZE_FULL, \
|
||||
p1, 0x00, dataLength ]
|
||||
apdu.extend(outputs[offset : offset + dataLength])
|
||||
response = self.dongle.exchange(bytearray(apdu))
|
||||
offset += dataLength
|
||||
alternateEncoding = True
|
||||
except:
|
||||
pass
|
||||
if not alternateEncoding:
|
||||
apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_FINALIZE, 0x02, 0x00 ]
|
||||
params = []
|
||||
params.append(len(outputAddress))
|
||||
params.extend(bytearray(outputAddress))
|
||||
writeHexAmountBE(btc_to_satoshi(str(amount)), params)
|
||||
writeHexAmountBE(btc_to_satoshi(str(fees)), params)
|
||||
params.extend(donglePath)
|
||||
apdu.append(len(params))
|
||||
apdu.extend(params)
|
||||
response = self.dongle.exchange(bytearray(apdu))
|
||||
result['confirmationNeeded'] = response[1 + response[0]] != 0x00
|
||||
result['confirmationType'] = response[1 + response[0]]
|
||||
if result['confirmationType'] == 0x02:
|
||||
result['keycardData'] = response[1 + response[0] + 1:]
|
||||
if result['confirmationType'] == 0x03:
|
||||
offset = 1 + response[0] + 1
|
||||
keycardDataLength = response[offset]
|
||||
offset = offset + 1
|
||||
result['keycardData'] = response[offset : offset + keycardDataLength]
|
||||
offset = offset + keycardDataLength
|
||||
result['secureScreenData'] = response[offset:]
|
||||
if result['confirmationType'] == 0x04:
|
||||
offset = 1 + response[0] + 1
|
||||
keycardDataLength = response[offset]
|
||||
result['keycardData'] = response[offset + 1 : offset + 1 + keycardDataLength]
|
||||
if outputs == None:
|
||||
result['outputData'] = response[1 : 1 + response[0]]
|
||||
else:
|
||||
result['outputData'] = outputs
|
||||
return result
|
||||
|
||||
def finalizeInputFull(self, outputData):
|
||||
result = {}
|
||||
offset = 0
|
||||
encryptedOutputData = b""
|
||||
while (offset < len(outputData)):
|
||||
blockLength = self.scriptBlockLength
|
||||
if ((offset + blockLength) < len(outputData)):
|
||||
dataLength = blockLength
|
||||
p1 = 0x00
|
||||
else:
|
||||
dataLength = len(outputData) - offset
|
||||
p1 = 0x80
|
||||
apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_FINALIZE_FULL, \
|
||||
p1, 0x00, dataLength ]
|
||||
apdu.extend(outputData[offset : offset + dataLength])
|
||||
response = self.dongle.exchange(bytearray(apdu))
|
||||
encryptedOutputData = encryptedOutputData + response[1 : 1 + response[0]]
|
||||
offset += dataLength
|
||||
if len(response) > 1:
|
||||
result['confirmationNeeded'] = response[1 + response[0]] != 0x00
|
||||
result['confirmationType'] = response[1 + response[0]]
|
||||
if result['confirmationType'] == 0x02:
|
||||
result['keycardData'] = response[1 + response[0] + 1:] # legacy
|
||||
if result['confirmationType'] == 0x03:
|
||||
offset = 1 + response[0] + 1
|
||||
keycardDataLength = response[offset]
|
||||
offset = offset + 1
|
||||
result['keycardData'] = response[offset : offset + keycardDataLength]
|
||||
offset = offset + keycardDataLength
|
||||
result['secureScreenData'] = response[offset:]
|
||||
result['encryptedOutputData'] = encryptedOutputData
|
||||
if result['confirmationType'] == 0x04:
|
||||
offset = 1 + response[0] + 1
|
||||
keycardDataLength = response[offset]
|
||||
result['keycardData'] = response[offset + 1 : offset + 1 + keycardDataLength]
|
||||
return result
|
||||
|
||||
def untrustedHashSign(self, path, pin="", lockTime=0, sighashType=0x01,
|
||||
version=0x02, overwintered=False):
|
||||
if isinstance(pin, str):
|
||||
pin = pin.encode('utf-8')
|
||||
donglePath = parse_bip32_path(path)
|
||||
if self.needKeyCache:
|
||||
self.resolvePublicKeysInPath(path)
|
||||
apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_SIGN, 0x00, 0x00 ]
|
||||
params = []
|
||||
params.extend(donglePath)
|
||||
params.append(len(pin))
|
||||
params.extend(bytearray(pin))
|
||||
writeUint32BE(lockTime, params)
|
||||
params.append(sighashType)
|
||||
if overwintered:
|
||||
params.extend(bytearray([0]*4))
|
||||
apdu.append(len(params))
|
||||
apdu.extend(params)
|
||||
result = self.dongle.exchange(bytearray(apdu))
|
||||
if not result:
|
||||
return
|
||||
result[0] = 0x30
|
||||
return result
|
|
@ -28,24 +28,29 @@ try:
|
|||
from btchip.btchipComm import HIDDongleHIDAPI, DongleWait
|
||||
from btchip.btchip import btchip
|
||||
from btchip.btchipUtils import compress_public_key,format_transaction, get_regular_input_script, get_p2sh_input_script
|
||||
from btchip.bitcoinTransaction import bitcoinTransaction
|
||||
from btchip.btchipFirmwareWizard import checkFirmware, updateFirmware
|
||||
from btchip.btchipException import BTChipException
|
||||
from .btchip_zcash import btchip_zcash, zcashTransaction
|
||||
btchip.setAlternateCoinVersions = setAlternateCoinVersions
|
||||
BTCHIP = True
|
||||
BTCHIP_DEBUG = is_verbose
|
||||
except ImportError:
|
||||
except ImportError as e:
|
||||
BTCHIP = False
|
||||
|
||||
MSG_NEEDS_FW_UPDATE_GENERIC = _('Firmware version too old. Please update at') + \
|
||||
' https://www.ledgerwallet.com'
|
||||
MSG_NEEDS_FW_UPDATE_OVERWINTER = (_('Firmware version too old for '
|
||||
'Overwinter/Sapling support. '
|
||||
'Please update at') +
|
||||
' https://www.ledgerwallet.com')
|
||||
MULTI_OUTPUT_SUPPORT = '1.1.4'
|
||||
ALTERNATIVE_COIN_VERSION = '1.0.1'
|
||||
OVERWINTER_SUPPORT = '1.3.3'
|
||||
|
||||
|
||||
class Ledger_Client():
|
||||
def __init__(self, hidDevice):
|
||||
self.dongleObject = btchip(hidDevice)
|
||||
self.dongleObject = btchip_zcash(hidDevice)
|
||||
self.preflightDone = False
|
||||
|
||||
def is_pairable(self):
|
||||
|
@ -90,7 +95,7 @@ class Ledger_Client():
|
|||
@test_pin_unlocked
|
||||
def get_xpub(self, bip32_path, xtype):
|
||||
self.checkDevice()
|
||||
# bip32_path is of the form 44'/147'/0'
|
||||
# bip32_path is of the form 44'/133'/1'
|
||||
# S-L-O-W - we don't handle the fingerprint directly, so compute
|
||||
# it manually from the previous node
|
||||
# This only happens once so it's bearable
|
||||
|
@ -139,11 +144,15 @@ class Ledger_Client():
|
|||
def supports_multi_output(self):
|
||||
return self.multiOutputSupported
|
||||
|
||||
def supports_overwinter(self):
|
||||
return self.overwinterSupported
|
||||
|
||||
def perform_hw1_preflight(self):
|
||||
try:
|
||||
firmwareInfo = self.dongleObject.getFirmwareVersion()
|
||||
firmware = firmwareInfo['version']
|
||||
self.multiOutputSupported = versiontuple(firmware) >= versiontuple(MULTI_OUTPUT_SUPPORT)
|
||||
self.overwinterSupported = versiontuple(firmware) >= versiontuple(OVERWINTER_SUPPORT)
|
||||
self.canAlternateCoinVersions = (versiontuple(firmware) >= versiontuple(ALTERNATIVE_COIN_VERSION)
|
||||
and firmwareInfo['specialVersion'] >= 0x20)
|
||||
|
||||
|
@ -190,7 +199,7 @@ class Ledger_Client():
|
|||
self.perform_hw1_preflight()
|
||||
except BTChipException as e:
|
||||
if (e.sw == 0x6d00 or e.sw == 0x6700):
|
||||
raise Exception(_("Device not in Zclassic mode")) from e
|
||||
raise Exception(_("Device not in ZClassic mode")) from e
|
||||
raise e
|
||||
self.preflightDone = True
|
||||
|
||||
|
@ -389,8 +398,15 @@ class Ledger_KeyStore(Hardware_KeyStore):
|
|||
# Get trusted inputs from the original transactions
|
||||
for utxo in inputs:
|
||||
sequence = int_to_hex(utxo[5], 4)
|
||||
if not p2shTransaction:
|
||||
txtmp = bitcoinTransaction(bfh(utxo[0]))
|
||||
if tx.overwintered:
|
||||
txtmp = zcashTransaction(bfh(utxo[0]))
|
||||
tmp = bfh(utxo[3])[::-1]
|
||||
tmp += bfh(int_to_hex(utxo[1], 4))
|
||||
tmp += txtmp.outputs[utxo[1]].amount
|
||||
chipInputs.append({'value' : tmp, 'sequence' : sequence})
|
||||
redeemScripts.append(bfh(utxo[2]))
|
||||
elif not p2shTransaction:
|
||||
txtmp = zcashTransaction(bfh(utxo[0]))
|
||||
trustedInput = self.get_client().getTrustedInput(txtmp, utxo[1])
|
||||
trustedInput['sequence'] = sequence
|
||||
chipInputs.append(trustedInput)
|
||||
|
@ -400,24 +416,30 @@ class Ledger_KeyStore(Hardware_KeyStore):
|
|||
tmp += bfh(int_to_hex(utxo[1], 4))
|
||||
chipInputs.append({'value' : tmp, 'sequence' : sequence})
|
||||
redeemScripts.append(bfh(utxo[2]))
|
||||
|
||||
# Sign all inputs
|
||||
firstTransaction = True
|
||||
inputIndex = 0
|
||||
rawTx = tx.serialize()
|
||||
self.get_client().enableAlternate2fa(False)
|
||||
while inputIndex < len(inputs):
|
||||
self.get_client().startUntrustedTransaction(firstTransaction, inputIndex,
|
||||
chipInputs, redeemScripts[inputIndex])
|
||||
if tx.overwintered:
|
||||
self.get_client().startUntrustedTransaction(True, inputIndex, chipInputs,
|
||||
redeemScripts[inputIndex],
|
||||
version=tx.version,
|
||||
overwintered=tx.overwintered)
|
||||
if changePath:
|
||||
# we don't set meaningful outputAddress, amount and fees
|
||||
# as we only care about the alternateEncoding==True branch
|
||||
outputData = self.get_client().finalizeInput(b'', 0, 0, changePath, bfh(rawTx))
|
||||
else:
|
||||
outputData = self.get_client().finalizeInputFull(txOutput)
|
||||
|
||||
if tx.overwintered:
|
||||
inputSignature = self.get_client().untrustedHashSign('',
|
||||
'', lockTime=tx.locktime,
|
||||
version=tx.version,
|
||||
overwintered=tx.overwintered)
|
||||
outputData['outputData'] = txOutput
|
||||
if firstTransaction:
|
||||
transactionOutput = outputData['outputData']
|
||||
transactionOutput = outputData['outputData']
|
||||
if outputData['confirmationNeeded']:
|
||||
outputData['address'] = output
|
||||
self.handler.finished()
|
||||
|
@ -426,14 +448,51 @@ class Ledger_KeyStore(Hardware_KeyStore):
|
|||
raise UserWarning()
|
||||
if pin != 'paired':
|
||||
self.handler.show_message(_("Confirmed. Signing Transaction..."))
|
||||
else:
|
||||
# Sign input with the provided PIN
|
||||
inputSignature = self.get_client().untrustedHashSign(inputsPaths[inputIndex], pin, lockTime=tx.locktime)
|
||||
while inputIndex < len(inputs):
|
||||
singleInput = [ chipInputs[inputIndex] ]
|
||||
self.get_client().startUntrustedTransaction(False, 0, singleInput,
|
||||
redeemScripts[inputIndex],
|
||||
version=tx.version,
|
||||
overwintered=tx.overwintered)
|
||||
inputSignature = self.get_client().untrustedHashSign(inputsPaths[inputIndex],
|
||||
pin, lockTime=tx.locktime,
|
||||
version=tx.version,
|
||||
overwintered=tx.overwintered)
|
||||
inputSignature[0] = 0x30 # force for 1.4.9+
|
||||
signatures.append(inputSignature)
|
||||
inputIndex = inputIndex + 1
|
||||
if pin != 'paired':
|
||||
firstTransaction = False
|
||||
else:
|
||||
while inputIndex < len(inputs):
|
||||
self.get_client().startUntrustedTransaction(firstTransaction, inputIndex,
|
||||
chipInputs, redeemScripts[inputIndex])
|
||||
if changePath:
|
||||
# we don't set meaningful outputAddress, amount and fees
|
||||
# as we only care about the alternateEncoding==True branch
|
||||
outputData = self.get_client().finalizeInput(b'', 0, 0, changePath, bfh(rawTx))
|
||||
else:
|
||||
outputData = self.get_client().finalizeInputFull(txOutput)
|
||||
outputData['outputData'] = txOutput
|
||||
if firstTransaction:
|
||||
transactionOutput = outputData['outputData']
|
||||
if outputData['confirmationNeeded']:
|
||||
outputData['address'] = output
|
||||
self.handler.finished()
|
||||
pin = self.handler.get_auth( outputData ) # does the authenticate dialog and returns pin
|
||||
if not pin:
|
||||
raise UserWarning()
|
||||
if pin != 'paired':
|
||||
self.handler.show_message(_("Confirmed. Signing Transaction..."))
|
||||
else:
|
||||
# Sign input with the provided PIN
|
||||
inputSignature = self.get_client().untrustedHashSign(inputsPaths[inputIndex],
|
||||
pin, lockTime=tx.locktime,
|
||||
version=tx.version,
|
||||
overwintered=tx.overwintered)
|
||||
inputSignature[0] = 0x30 # force for 1.4.9+
|
||||
signatures.append(inputSignature)
|
||||
inputIndex = inputIndex + 1
|
||||
if pin != 'paired':
|
||||
firstTransaction = False
|
||||
except UserWarning:
|
||||
self.handler.show_error(_('Cancelled by user'))
|
||||
return
|
||||
|
@ -483,7 +542,14 @@ class LedgerPlugin(HW_PluginBase):
|
|||
(0x2581, 0x3b7c), # HW.1 ledger production
|
||||
(0x2581, 0x4b7c), # HW.1 ledger test
|
||||
(0x2c97, 0x0000), # Blue
|
||||
(0x2c97, 0x0001) # Nano-S
|
||||
(0x2c97, 0x0001), # Nano-S
|
||||
(0x2c97, 0x0004), # Nano-X
|
||||
(0x2c97, 0x0005), # RFU
|
||||
(0x2c97, 0x0006), # RFU
|
||||
(0x2c97, 0x0007), # RFU
|
||||
(0x2c97, 0x0008), # RFU
|
||||
(0x2c97, 0x0009), # RFU
|
||||
(0x2c97, 0x000a) # RFU
|
||||
]
|
||||
|
||||
def __init__(self, parent, config, name):
|
||||
|
|
Loading…
Reference in New Issue