From a120fe4f9df0cf445eb08dcf17705f5441f704ca Mon Sep 17 00:00:00 2001 From: JoeGruff Date: Fri, 13 Dec 2019 13:13:04 +0900 Subject: [PATCH 1/8] staking: Add revocations Add a button to enable revocation of all missed and expired tickets. --- pydecred/account.py | 33 ++++ pydecred/dcrdata.py | 32 ++- pydecred/txscript.py | 452 ++++++++++++++++++++++++++++++++++++++++++- ui/screens.py | 25 ++- 4 files changed, 531 insertions(+), 11 deletions(-) diff --git a/pydecred/account.py b/pydecred/account.py index 7bde533d..b3cbb035 100644 --- a/pydecred/account.py +++ b/pydecred/account.py @@ -6,6 +6,8 @@ support. """ +from tinydecred.crypto.bytearray import decodeBA +from tinydecred.crypto import opcode, crypto from tinydecred.wallet.accounts import Account from tinydecred.util import tinyjson, helpers from tinydecred.crypto.crypto import AddressSecpPubKey, CrazyKeyError @@ -422,6 +424,37 @@ def purchaseTickets(self, qty, price): self.tickets.extend([tx.txid() for tx in txs[1]]) return txs[1] + def revokeTickets(self): + print("revoking") + revokableTickets = [utxo for utxo in self.utxos.values() if utxo.isExpiredOrMissedTicket()] + for utxo in revokableTickets: + try: + tx = self.blockchain.tx(utxo.txid) + except Exception as e: + log.error("error getting tx: %s" % e) + continue + print(self.net) + redeemHash = crypto.AddressScriptHash(self.net.ScriptHashAddrID, txscript.extractStakeScriptHash(tx.txOut[0].pkScript, opcode.OP_SSTX)) + redeemScript = [] + #purchaseHeight = utxo.tinfo.purchaseBlock. + for pool in self.stakePools: + print(pool.purchaseInfo.ticketAddress, redeemHash.string()) + if pool.purchaseInfo.ticketAddress == redeemHash.string(): + redeemScript = decodeBA(pool.purchaseInfo.script) + print(redeemScript) + break + else: + log.error("did not find redeem script for hash %s" % redeemHash) + continue + + keysource = KeySource( + # This will need to change when we start using different + # addresses for voting. + priv=lambda: self._votingKey, + internal=lambda: "", + ) + self.blockchain.revokeTicket(tx, keysource, redeemScript) + def sync(self, blockchain, signals): """ Synchronize the UTXO set with the server. This should be the first diff --git a/pydecred/dcrdata.py b/pydecred/dcrdata.py index 80988cfa..05bda601 100644 --- a/pydecred/dcrdata.py +++ b/pydecred/dcrdata.py @@ -16,7 +16,7 @@ import atexit import websocket from tinydecred.util import tinyjson, helpers, database, tinyhttp -from tinydecred.crypto import crypto +from tinydecred.crypto import crypto, opcode from tinydecred.crypto.bytearray import ByteArray from tinydecred.wallet.api import InsufficientFundsError from tinydecred.pydecred import txscript, calc @@ -707,6 +707,15 @@ def isLiveTicket(self): bool. True if this is a live ticket. """ return self.tinfo and self.tinfo.status in ("immature", "live") + def isExpiredOrMissedTicket(self): + """ + isExpiredOrMissedTicket will return True if this is an expired or + missed ticket. + + Returns: + bool. True if this is expired or missed ticket. + """ + return self.tinfo and self.tinfo.status in ("expired", "missed") tinyjson.register(UTXO, "dcr.UTXO") @@ -1626,3 +1635,24 @@ def purchaseTickets(self, keysource, utxosource, req): ) ) return (splitTx, tickets), splitSpent, internalOutputs + + def revokeTicket(self, tx, keysource, redeemScript): + + revocation = txscript.makeRevocation(tx, self.relayFee()) + + print(redeemScript.hex()) + if not revocation: + log.info("failed to make revocation") + return + script = txscript.signTxOutput(self.params, revocation, 0, redeemScript, txscript.SigHashAll, keysource, redeemScript, crypto.STEcdsaSecp256k1) + + signed = ByteArray(b'') + signed += txscript.addData(script[1:]) + signed += txscript.addData(redeemScript) + + revocation.txIn[0].signatureScript = signed + print("script", revocation.txIn[0].signatureScript.hex()) + log.info("published revocation %s" % revocation.txHex()) + self.broadcast(revocation.txHex()) + + log.info("published revocation %s" % revocation.txid()) diff --git a/pydecred/txscript.py b/pydecred/txscript.py index 0eea39ec..4e084ebe 100644 --- a/pydecred/txscript.py +++ b/pydecred/txscript.py @@ -120,6 +120,18 @@ # - 33 bytes serialized compressed pubkey RedeemP2PKHSigScriptSize = 1 + 73 + 1 + 33 +# RedeemP2SHSigScriptSize is the worst case (largest) serialize size +# of a transaction input script that redeems a P2SH output. +# It is calculated as: +# +# - OP_DATA_73 +# - 73-byte signature +# - OP_DATA_35 +# - OP_DATA_33 +# - 33 bytes serialized compressed pubkey +# - OP_CHECKSIG +RedeemP2SHSigScriptSize = 1 + 73 + 1 + 1 + 33 + 1 + # TicketCommitmentScriptSize is the size of a ticket purchase commitment # script. It is calculated as: # @@ -180,6 +192,33 @@ # fee limits imposed on a ticket. defaultTicketFeeLimits = 0x5800 +# SStxVoteReturnFractionMask extracts the return fraction from a +# commitment output version. +# If after applying this mask &0x003f is given, the entire amount of +# the output is allowed to be spent as fees if the flag to allow fees +# is set. +SStxVoteReturnFractionMask = 0x003f + +# SStxRevReturnFractionMask extracts the return fraction from a +# commitment output version. +# If after applying this mask &0x3f00 is given, the entire amount of +# the output is allowed to be spent as fees if the flag to allow fees +# is set. +SStxRevReturnFractionMask = 0x3f00 + + +# SStxVoteFractionFlag is a bitflag mask specifying whether or not to +# apply a fractional limit to the amount used for fees in a vote. +# 00000000 00000000 = No fees allowed +# 00000000 01000000 = Apply fees rule +SStxVoteFractionFlag = 0x0040 + +# SStxRevFractionFlag is a bitflag mask specifying whether or not to +# apply a fractional limit to the amount used for fees in a vote. +# 00000000 00000000 = No fees allowed +# 01000000 00000000 = Apply fees rule +SStxRevFractionFlag = 0x4000 + # A couple of hashing functions from the crypto module. mac = crypto.mac hashH = crypto.hashH @@ -2080,9 +2119,9 @@ def sign(chainParams, tx, idx, subScript, hashType, keysource, sigType): # return script, scriptClass, addresses, nrequired elif scriptClass == MultiSigTy: - raise Exception("spending multi-sig script not implemented") - # script = signMultiSig(tx, idx, subScript, hashType, addresses, nrequired, kdb) - # return script, scriptClass, addresses, nrequired + privKey = keysource.priv() + script = signMultiSig(tx, idx, subScript, hashType, addresses, nrequired, privKey) + return script, scriptClass, addresses, nrequired elif scriptClass == StakeSubmissionTy: return handleStakeOutSign( @@ -2236,9 +2275,9 @@ def mergeScripts( finalScript += addData(script) return finalScript elif scriptClass == MultiSigTy: - raise Exception("multisig signing unimplemented") - # return mergeMultiSig(tx, idx, addresses, nRequired, pkScript, - # sigScript, prevScript) + #raise Exception("multisig signing unimplemented") + return mergeMultiSig(tx, idx, addresses, nRequired, pkScript, + sigScript, prevScript) else: # It doesn't actually make sense to merge anything other than multiig # and scripthash (because it could contain multisig). Everything else @@ -2816,3 +2855,404 @@ def makeTicket( checkSStx(mtx) return mtx + + +class MinimalOutput: + """ + MinimalOutput is a class encoding a minimally sized output for use in parsing + stake related information. + """ + def __init__(self, pkScript, value, version): + self.pkScript = pkScript + self.value = value + self.version = version + + +def convertToMinimalOutputs(tx): + """ + ConvertToMinimalOutputs converts a transaction to its minimal outputs + derivative. +[]*MinimalOutput { + """ + minOuts = [] + for i in range(len(tx.txOut)): + minOuts.append(MinimalOutput(tx.txOut[i].pkScript, tx.txOut[i].value, tx.txOut[i].version)) + return minOuts + + +def sstxStakeOutputInfo(outs): + """ + sstxStakeOutputInfo takes an SStx as input and scans through its outputs, + returning the pubkeyhashs and amounts for any NullDataTy's (future + commitments to stake generation rewards). +([]bool, [][]byte, []int64, +[]int64, [][]bool, [][]uint16): + """ + isP2SH = [] # bool, expectedInLen) + addresses = [] #[] byte, expectedInLen) + amounts = [] # int64, expectedInLen) + changeAmounts = [] # int64, expectedInLen) + allSpendRules = [] #[] bool, expectedInLen) + allSpendLimits = [] #[] uint16, expectedInLen) + + # Cycle through the inputs and pull the proportional amounts + # and commit to PKHs/SHs. + for idx in range(len(outs)): + # We only care about the outputs where we get proportional + # amounts and the PKHs/SHs to send rewards to, which is all + # the odd numbered output indexes. + if (idx > 0) and (idx % 2 != 0): + # The MSB (sign), not used ever normally, encodes whether + # or not it is a P2PKH or P2SH for the input. + # amtEncoded = [] # byte, 8) + amtEncoded = outs[idx].pkScript[22:30] + # MSB set? + isP2SH.append(not (amtEncoded[7] & (1 << 7) == 0)) + # Clear bit + amtEncoded[7] &= 127 + + addresses.append(outs[idx].pkScript[2:22]) + # amounts[idx/2] = int64(binary.LittleEndian.Uint64(amtEncoded)) + amounts.append(ByteArray(amtEncoded, length=8).littleEndian().int()) + + # Get flags and restrictions for the outputs to be + # make in either a vote or revocation. + spendRules = [] # bool, 2) + spendLimits = [] # uint16, 2) + + # This bitflag is true/false. + # feeLimitUint16 := binary.LittleEndian.Uint16(out.PkScript[30:32]) + feeLimitUint16 = ByteArray(outs[idx].pkScript[30:32], length=4).littleEndian().int() + spendRules.append((feeLimitUint16 & SStxVoteFractionFlag) == SStxVoteFractionFlag) + spendRules.append((feeLimitUint16 & SStxRevFractionFlag) == SStxRevFractionFlag) + allSpendRules.append(spendRules) + + # This is the fraction to use out of 64. + spendLimits.append(feeLimitUint16 & SStxVoteReturnFractionMask) + spendLimits.append(feeLimitUint16 & SStxRevReturnFractionMask) + spendLimits[1] >>= 8 + allSpendLimits.append(spendLimits) + + # Here we only care about the change amounts, so scan + # the change outputs (even indices) and save their + # amounts. + if (idx > 0) and (idx % 2 == 0): + changeAmounts.append(outs[idx].value) + + print("stake outputs", isP2SH, addresses, amounts, changeAmounts, allSpendRules, allSpendLimits) + return isP2SH, addresses, amounts, changeAmounts, allSpendRules, allSpendLimits + + +def TxSStxStakeOutputInfo(tx): + """ + TxSStxStakeOutputInfo takes an SStx as input and scans through its outputs, + returning the pubkeyhashs and amounts for any NullDataTy's (future + commitments to stake generation rewards). + *wire.MsgTx + ([]bool, [][]byte, []int64, []int64, + [][]bool, [][]uint16) { + + """ + return sstxStakeOutputInfo(convertToMinimalOutputs(tx)) + +def calculateRewards(amounts, amountTicket, subsidy): + """ + CalculateRewards takes a list of SStx adjusted output amounts, the amount used + to purchase that ticket, and the reward for an SSGen tx and subsequently + generates what the outputs should be in the SSGen tx. If used for calculating + the outputs for an SSRtx, pass 0 for subsidy. +amounts []int64, amountTicket int64, + subsidy int64) []int64 + """ + outputsAmounts = [] + + # SSGen handling + amountWithStakebase = amountTicket + subsidy + + # Get the sum of the amounts contributed between both fees + # and contributions to the ticket. + totalContrib = 0 + for amount in amounts: + totalContrib += amount + + # Now we want to get the adjusted amounts including the reward. + # The algorithm is like this: + # 1 foreach amount + # 2 amount *= 2^32 + # 3 amount /= amountTicket + # 4 amount *= amountWithStakebase + # 5 amount /= 2^32 + + for amount in amounts: + # mul amountWithStakebase + amount *= amountWithStakebase + + # mul 2^32 + amount <<= 32 + + # div totalContrib + amount //= totalContrib + + # div 2^32 + amount >>= 32 + + # make int64 + outputsAmounts.append(amount) + + return outputsAmounts + + +def PayToSSRtxPKHDirect(pkh): + """ + PayToSSRtxPKHDirect creates a new script to pay a transaction output to a + public key hash, but tags the output with OP_SSRTX. For use in constructing + valid SSRtx. Unlike PayToSSRtx, this function directly uses the HASH160 + pubkeyhash (instead of an address). +pkh []byte) ([]byte, error + """ + script = ByteArray(b'') + script += opcode.OP_SSRTX + script += opcode.OP_DUP + script += opcode.OP_HASH160 + script += addData(pkh) + script += opcode.OP_EQUALVERIFY + script += opcode.OP_CHECKSIG + return script + + +def PayToSSRtxSHDirect(sh): + """ + PayToSSRtxSHDirect creates a new script to pay a transaction output to a + script hash, but tags the output with OP_SSRTX. For use in constructing + valid SSRtx. Unlike PayToSSRtx, this function directly uses the HASH160 + script hash (instead of an address). + []byte) ([]byte, error) + """ + script = ByteArray(b'') + script += opcode.OP_SSRTX + script += opcode.OP_HASH160 + script += addData(sh) + script += opcode.OP_EQUAL + return script + +def signMultiSig(tx, idx, subScript, hashType, addresses, nRequired, privKey): + """ + signMultiSig signs as many of the outputs in the provided multisig script as + possible. It returns the generated script and a boolean if the script + fulfills the contract (i.e. nrequired signatures are provided). Since it is + arguably legal to not be able to sign any of the outputs, no error is + returned. +(tx *wire.MsgTx, idx int, subScript []byte, hashType SigHashType, + addresses []dcrutil.Address, nRequired int, kdb KeyDB) ([]byte, bool) { + + """ + + # No need to add dummy in Decred. + signed = 0 + script = ByteArray(b'') + for addr in addresses: + + sig = rawTxInSignature(tx, idx, subScript, hashType, privKey.key) + + script += addData(sig) + signed += 1 + if signed == nRequired: + break + + return script #, signed == nRequired + +def mergeMultiSig(tx, idx, addresses, nRequired, pkScript, sigScript, prevScript): + """ + mergeMultiSig combines the two signature scripts sigScript and prevScript + that both provide signatures for pkScript in output idx of tx. addresses + and nRequired should be the results from extracting the addresses from + pkScript. Since this function is internal only we assume that the arguments + have come from other functions internally and thus are all consistent with + each other, behaviour is undefined if this contract is broken. + + NOTE: This function is only valid for version 0 scripts. Since the function + does not accept a script version, the results are undefined for other script + versions. + tx *wire.MsgTx, idx int, addresses []dcrutil.Address, + nRequired int, pkScript, sigScript, prevScript []byte) []byte + """ + + # Nothing to merge if either the new or previous signature scripts are + # empty. + if len(sigScript) == 0: + return prevScript + print("sig zero") + + if len(prevScript) == 0: + return sigScript + print("prev zero") + + #Still working on the rest of this... + return sigScript + + # Convenience function to avoid duplication. + possibleSigs = [] + + def extractSigs(script): + scriptVersion = 0 + tokenizer = ScriptTokenizer(scriptVersion, script) + while tokenizer.next(): + data = tokenizer.data() + if len(data) != 0: + possibleSigs.append(data) + if tokenizer.err is not None: + return None + + # Attempt to extract signatures from the two scripts. Return the other + # script that is intended to be merged in the case signature extraction + # fails for some reason. + if not extractSigs(sigScript): + return prevScript + + if not extractSigs(prevScript): + return sigScript + + # Now we need to match the signatures to pubkeys, the only real way to + # do that is to try to verify them all and match it to the pubkey + # that verifies it. we then can go through the addresses in order + # to build our script. Anything that doesn't parse or doesn't verify we + # throw away. + addrToSig = {} + """ +sigLoop: + for _, sig := range possibleSigs { + + # can't have a valid signature that doesn't at least have a + # hashtype, in practise it is even longer than this. but + # that'll be checked next. + if len(sig) < 1 { + continue + } + tSig := sig[:len(sig)-1] + hashType := SigHashType(sig[len(sig)-1]) + + pSig, err := secp256k1.ParseDERSignature(tSig) + if err != nil { + continue + } + + # We have to do this each round since hash types may vary + # between signatures and so the hash will vary. We can, + # however, assume no sigs etc are in the script since that + # would make the transaction nonstandard and thus not + # MultiSigTy, so we just need to hash the full thing. + hash, err := calcSignatureHash(pkScript, hashType, tx, idx, nil) + if err != nil { + # Decred -- is this the right handling for SIGHASH_SINGLE error ? + # TODO make sure this doesn't break anything. + continue + } + + for _, addr := range addresses { + # All multisig addresses should be pubkey addresses + # it is an error to call this internal function with + # bad input. + pkaddr := addr.(*dcrutil.AddressSecpPubKey) + + pubKey := pkaddr.PubKey() + + # If it matches we put it in the map. We only + # can take one signature per public key so if we + # already have one, we can throw this away. + r := pSig.GetR() + s := pSig.GetS() + if secp256k1.NewSignature(r, s).Verify(hash, pubKey) { + aStr := addr.Address() + if _, ok := addrToSig[aStr]; !ok { + addrToSig[aStr] = sig + } + continue sigLoop + } + } + } + + builder := NewScriptBuilder() + doneSigs := 0 + # This assumes that addresses are in the same order as in the script. + for _, addr := range addresses { + sig, ok := addrToSig[addr.Address()] + if !ok { + continue + } + builder.AddData(sig) + doneSigs++ + if doneSigs == nRequired { + break + } + } + + # padding for missing ones. + for i := doneSigs; i < nRequired; i++ { + builder.AddOp(OP_0) + } + + script, _ := builder.Script() + return script +""" + +def makeRevocation(ticketPurchase, feePerKB): + ''' + makeRevocation creates an unsigned revocation transaction that + revokes a missed or expired ticket. Revocations must carry a relay fee and + this function can error if the revocation contains no suitable output to + decrease the estimated relay fee from. +ticketHash *chainhash.Hash, ticketPurchase *wire.MsgTx, feePerKB dcrutil.Amount +(*wire.MsgTx, error) + ''' + # Parse the ticket purchase transaction to determine the required output + # destinations for vote rewards or revocations. + ticketPayKinds, ticketHash160s, ticketValues, _, _, _ = TxSStxStakeOutputInfo(ticketPurchase) + + # Calculate the output values for the revocation. Revocations do not + # contain any subsidy. + revocationValues = calculateRewards(ticketValues, ticketPurchase.txOut[0].value, 0) + + # Begin constructing the revocation transaction. + revocation = msgtx.MsgTx.new() + + # Revocations reference the ticket purchase with the first (and only) + # input. + print("ticket hash", ticketPurchase.hash()) + ticketOutPoint = msgtx.OutPoint(ticketPurchase.hash(), 0, msgtx.TxTreeStake) + ticketInput = msgtx.TxIn( + previousOutPoint=ticketOutPoint, + valueIn=ticketPurchase.txOut[ticketOutPoint.index].value, + ) + # ticketInput = wire.NewTxIn(ticketOutPoint, ticketPurchase.TxOut[ticketOutPoint.Index].Value, nil) + revocation.addTxIn(ticketInput) + scriptSizes = [RedeemP2SHSigScriptSize] + + # All remaining outputs pay to the output destinations and amounts tagged + # by the ticket purchase. + for i in range(len(ticketHash160s)): + scriptFn = PayToSSRtxPKHDirect + # P2SH + if ticketPayKinds[i]: + scriptFn = PayToSSRtxSHDirect + script = scriptFn(ticketHash160s[i]) + revocation.addTxOut(msgtx.TxOut(revocationValues[i], script)) + + # Revocations must pay a fee but do so by decreasing one of the output + # values instead of increasing the input value and using a change output. + # Calculate the estimated signed serialize size. + sizeEstimate = estimateSerializeSize(scriptSizes, revocation.txOut, 0) + feeEstimate = calcMinRequiredTxRelayFee(feePerKB, sizeEstimate) + + # Reduce the output value of one of the outputs to accommodate for the relay + # fee. To avoid creating dust outputs, a suitable output value is reduced + # by the fee estimate only if it is large enough to not create dust. This + # code does not currently handle reducing the output values of multiple + # commitment outputs to accommodate for the fee. + for output in revocation.txOut: + if output.value > feeEstimate: + amount = output.value - feeEstimate + if not isDustAmount(amount, len(output.pkScript), feePerKB): + output.value = amount + return revocation + print("missing suitable revocation output to pay relay fee") + return False diff --git a/ui/screens.py b/ui/screens.py index da661efb..883bce6e 100644 --- a/ui/screens.py +++ b/ui/screens.py @@ -1150,10 +1150,14 @@ def __init__(self, app): self.layout.addWidget(wgt) # A button to view agendas and choose how to vote. - btn = app.getButton(TINY, "Voting") - btn.clicked.connect(self.stackAgendas) - agendasWgt, _ = Q.makeSeries(Q.HORIZONTAL, btn) - self.layout.addWidget(agendasWgt) + agendaBtn = app.getButton(TINY, "Voting") + agendaBtn.clicked.connect(self.stackAgendas) + + # A button to revoke expired and missed tickets. + revokeBtn = app.getButton(TINY, "Revoke") + revokeBtn.clicked.connect(self.revokeTickets) + votingWgt, _ = Q.makeSeries(Q.HORIZONTAL, agendaBtn, revokeBtn) + self.layout.addWidget(votingWgt) # Affordability. A row that reads `You can afford X tickets` lbl = Q.makeLabel("You can afford ", 14) @@ -1221,6 +1225,19 @@ def stackAgendas(self): return self.app.appWindow.stack(self.agendasScreen) + def revokeTickets(self): + def revoke(wallet): + try: + wallet.openAccount.revokeTickets() + return True + except Exception as e: + log.error("revoke tickets error: %s" % formatTraceback(e)) + return False + self.app.withUnlockedWallet(revoke, self.revoked) + + def revoked(self, success): + self.app.appWindow.showSuccess("revoke tickets") + def setStats(self): """ Get the current ticket stats and update the display. From ef6082903e533af1ca09afb68be5ed7bc7bdc810 Mon Sep 17 00:00:00 2001 From: JoeGruff Date: Sat, 21 Dec 2019 21:29:20 +0900 Subject: [PATCH 2/8] Clean up code - Add basic dcrdata revoke ticket test. - Add documentation. - Format with black. - Move txscript functions around. --- pydecred/account.py | 34 +- pydecred/dcrdata.py | 44 +- pydecred/txscript.py | 605 ++++++++++++--------- tests/integration/pydecred/test_dcrdata.py | 50 +- ui/screens.py | 40 +- 5 files changed, 482 insertions(+), 291 deletions(-) diff --git a/pydecred/account.py b/pydecred/account.py index b3cbb035..7d042f23 100644 --- a/pydecred/account.py +++ b/pydecred/account.py @@ -425,35 +425,47 @@ def purchaseTickets(self, qty, price): return txs[1] def revokeTickets(self): - print("revoking") - revokableTickets = [utxo for utxo in self.utxos.values() if utxo.isExpiredOrMissedTicket()] - for utxo in revokableTickets: + """ + Iterate through missed and expired ticket utxo and revoke them. + + Returns: + bool: whether or not an error occured. + """ + revokableTickets = [ + utxo.txid for utxo in self.utxos.values() if utxo.isRevocableTicket() + ] + errored = False + txs = [] + for txid in revokableTickets: try: - tx = self.blockchain.tx(utxo.txid) + tx = self.blockchain.tx(txid) + txs.append(tx) except Exception as e: log.error("error getting tx: %s" % e) + errored = True continue - print(self.net) - redeemHash = crypto.AddressScriptHash(self.net.ScriptHashAddrID, txscript.extractStakeScriptHash(tx.txOut[0].pkScript, opcode.OP_SSTX)) + for tx in txs: + redeemHash = crypto.AddressScriptHash( + self.net.ScriptHashAddrID, + txscript.extractStakeScriptHash(tx.txOut[0].pkScript, opcode.OP_SSTX), + ) redeemScript = [] - #purchaseHeight = utxo.tinfo.purchaseBlock. for pool in self.stakePools: - print(pool.purchaseInfo.ticketAddress, redeemHash.string()) if pool.purchaseInfo.ticketAddress == redeemHash.string(): redeemScript = decodeBA(pool.purchaseInfo.script) - print(redeemScript) break else: log.error("did not find redeem script for hash %s" % redeemHash) + errored = True continue - keysource = KeySource( # This will need to change when we start using different # addresses for voting. - priv=lambda: self._votingKey, + priv=lambda _: self._votingKey, internal=lambda: "", ) self.blockchain.revokeTicket(tx, keysource, redeemScript) + return errored def sync(self, blockchain, signals): """ diff --git a/pydecred/dcrdata.py b/pydecred/dcrdata.py index 05bda601..05692012 100644 --- a/pydecred/dcrdata.py +++ b/pydecred/dcrdata.py @@ -704,16 +704,16 @@ def isLiveTicket(self): isLiveTicket will return True if this is a live ticket. Returns: - bool. True if this is a live ticket. + bool: True if this is a live ticket. """ return self.tinfo and self.tinfo.status in ("immature", "live") - def isExpiredOrMissedTicket(self): + + def isRevocableTicket(self): """ - isExpiredOrMissedTicket will return True if this is an expired or - missed ticket. + Returns True if this is an expired or missed ticket. Returns: - bool. True if this is expired or missed ticket. + bool: True if this is expired or missed ticket. """ return self.tinfo and self.tinfo.status in ("expired", "missed") @@ -1637,22 +1637,40 @@ def purchaseTickets(self, keysource, utxosource, req): return (splitTx, tickets), splitSpent, internalOutputs def revokeTicket(self, tx, keysource, redeemScript): + """ + revoke a ticket by signing the supplied redeem script and broadcasting the raw transaction. + + Args: + tx (object): the msgTx of the ticket purchase. + keysource (object): a KeySource object that holds a function to get the private key used for signing. + redeemScript (byte-like): the 1-of-2 multisig script that delegates voting rights for the ticket. + + Returns: + MsgTx: the signed revocation. + """ revocation = txscript.makeRevocation(tx, self.relayFee()) - print(redeemScript.hex()) if not revocation: log.info("failed to make revocation") return - script = txscript.signTxOutput(self.params, revocation, 0, redeemScript, txscript.SigHashAll, keysource, redeemScript, crypto.STEcdsaSecp256k1) - signed = ByteArray(b'') - signed += txscript.addData(script[1:]) - signed += txscript.addData(redeemScript) + signedScript = txscript.signTxOutput( + self.params, + revocation, + 0, + redeemScript, + txscript.SigHashAll, + keysource, + revocation.txIn[0].signatureScript, + crypto.STEcdsaSecp256k1, + ) + + # Append the redeem script to the signature + signedScript += txscript.addData(redeemScript) + revocation.txIn[0].signatureScript = signedScript - revocation.txIn[0].signatureScript = signed - print("script", revocation.txIn[0].signatureScript.hex()) - log.info("published revocation %s" % revocation.txHex()) self.broadcast(revocation.txHex()) log.info("published revocation %s" % revocation.txid()) + return revocation diff --git a/pydecred/txscript.py b/pydecred/txscript.py index 4e084ebe..7b07d81a 100644 --- a/pydecred/txscript.py +++ b/pydecred/txscript.py @@ -12,8 +12,11 @@ msgtx, ) # A couple of usefule serialization functions. from tinydecred.crypto import opcode, crypto +from tinydecred.util import helpers from tinydecred.crypto.secp256k1.curve import curve as Curve +log = helpers.getLogger("TXSCRIPT") + HASH_SIZE = 32 SHA256_SIZE = 32 BLAKE256_SIZE = 32 @@ -197,14 +200,14 @@ # If after applying this mask &0x003f is given, the entire amount of # the output is allowed to be spent as fees if the flag to allow fees # is set. -SStxVoteReturnFractionMask = 0x003f +SStxVoteReturnFractionMask = 0x003F # SStxRevReturnFractionMask extracts the return fraction from a # commitment output version. # If after applying this mask &0x3f00 is given, the entire amount of # the output is allowed to be spent as fees if the flag to allow fees # is set. -SStxRevReturnFractionMask = 0x3f00 +SStxRevReturnFractionMask = 0x3F00 # SStxVoteFractionFlag is a bitflag mask specifying whether or not to @@ -273,6 +276,87 @@ def serialize(self): b[offset] = sb return b + @staticmethod + def parseSig(sigBytes): + """ + # Originally this code used encoding/asn1 in order to parse the + # signature, but a number of problems were found with this approach. + # Despite the fact that signatures are stored as DER, the difference + # between go's idea of a bignum (and that they have sign) doesn't agree + # with the openssl one (where they do not). The above is true as of + # Go 1.1. In the end it was simpler to rewrite the code to explicitly + # understand the format which is this: + # 0x30 <0x02> 0x2 + # . +sigStr []byte, der bool) (*Signature, error) { + """ + + # minimal message is when both numbers are 1 bytes. adding up to: + # 0x30 + len + 0x02 + 0x01 + + 0x2 + 0x01 + + if len(sigBytes) < 8: + log.error("malformed signature: too short") + return None + + # 0x30 + index = 0 + if sigBytes[index] != 0x30: + log.error("malformed signature: no header magic") + return None + index += 1 + # length of remaining message + siglen = sigBytes[index] + index += 1 + if siglen + 2 > len(sigBytes): + log.error("malformed signature: bad length") + return None + # trim the slice we're working on so we only look at what matters. + sigBytes = sigBytes[: siglen + 2] + + # 0x02 + if sigBytes[index] != 0x02: + log.error("malformed signature: no 1st int marker") + return None + index += 1 + + # Length of signature R. + rLen = sigBytes[index] + # must be positive, must be able to fit in another 0x2, + # hence the -3. We assume that the length must be at least one byte. + index += 1 + if rLen <= 0 or rLen > len(sigBytes) - index - 3: + log.error("malformed signature: bogus R length") + return None + + # Then R itself. + rBytes = sigBytes[index : index + rLen] + index += rLen + # 0x02. length already checked in previous if. + if sigBytes[index] != 0x02: + log.error("malformed signature: no 2nd int marker") + return None + index += 1 + + # Length of signature S. + sLen = sigBytes[index] + index += 1 + # S should be the rest of the bytes. + if sLen <= 0 or sLen > len(sigBytes) - index: + log.error("malformed signature: bogus S length") + return None + + # Then S itself. + sBytes = sigBytes[index : index + sLen] + index += sLen + + # sanity check length parsing + if index != len(sigBytes): + log.error( + "malformed signature: bad final length %s != %s" % index, len(sigBytes) + ) + return None + + return Signature(rBytes, sBytes) + class ScriptTokenizer: """ @@ -1276,6 +1360,50 @@ def payToSStx(addr): return payToStakeSHScript(addr, opcode.OP_SSTX) +def payToSSRtxPKHDirect(pkh): + """ + payToSSRtxPKHDirect creates a new script to pay a transaction output to a + public key hash, but tags the output with OP_SSRTX. For use in constructing + valid SSRtx. Unlike payToSSRtx, this function directly uses the HASH160 + pubkeyhash (instead of an address). + + Args: + sh (byte-like): raw script. + + Returns: + byte-like: script to pay a stake based public key hash. + """ + script = ByteArray(b"") + script += opcode.OP_SSRTX + script += opcode.OP_DUP + script += opcode.OP_HASH160 + script += addData(pkh) + script += opcode.OP_EQUALVERIFY + script += opcode.OP_CHECKSIG + return script + + +def payToSSRtxSHDirect(sh): + """ + payToSSRtxSHDirect creates a new script to pay a transaction output to a + script hash, but tags the output with OP_SSRTX. For use in constructing + valid SSRtx. Unlike payToSSRtx, this function directly uses the HASH160 + script hash (instead of an address). + + Args: + sh (byte-like): raw script. + + Returns: + byte-like: script to pay a stake based script hash. + """ + script = ByteArray(b"") + script += opcode.OP_SSRTX + script += opcode.OP_HASH160 + script += addData(sh) + script += opcode.OP_EQUAL + return script + + def generateSStxAddrPush(addr, amount, limits): """ generateSStxAddrPush generates an OP_RETURN push for SSGen payment addresses in @@ -2119,8 +2247,12 @@ def sign(chainParams, tx, idx, subScript, hashType, keysource, sigType): # return script, scriptClass, addresses, nrequired elif scriptClass == MultiSigTy: - privKey = keysource.priv() - script = signMultiSig(tx, idx, subScript, hashType, addresses, nrequired, privKey) + privKeys = [] + for addr in addresses: + privKeys.append(keysource.priv(addr)) + script = signMultiSig( + tx, idx, subScript, hashType, addresses, nrequired, privKeys + ) return script, scriptClass, addresses, nrequired elif scriptClass == StakeSubmissionTy: @@ -2181,6 +2313,42 @@ def sign(chainParams, tx, idx, subScript, hashType, keysource, sigType): raise Exception("can't sign unknown transactions") +def signMultiSig(tx, idx, subScript, hashType, addresses, nRequired, privKeys): + """ + signMultiSig signs as many of the outputs in the provided multisig script as + possible. It returns the generated script and a boolean if the script + fulfills the contract (i.e. nrequired signatures are provided). Since it is + arguably legal to not be able to sign any of the outputs, no error is + returned. + + Args: + tx (object): the ticket purchase MsgTx. + idx (int): the output index that contains the multisig. + subScript (byte-like): the multisig script. + hashType (int): the type of hash needed + addresses (list(object)): the addresses that make up the multisig. + nRequired (int): the number of signatures required to fulfill the pkScript. + privKeys (list(byte-like)): the private keys for addresses. + + Returns: + byte-like: the signed multisig script. + """ + + # No need to add dummy in Decred. + signed = 0 + script = ByteArray(b"") + for idx in range(len(addresses)): + + sig = rawTxInSignature(tx, idx, subScript, hashType, privKeys[idx].key) + + script += addData(sig) + signed += 1 + if signed == nRequired: + break + + return script + + def handleStakeOutSign( tx, idx, subScript, hashType, keysource, addresses, scriptClass, subClass, nrequired ): @@ -2275,9 +2443,9 @@ def mergeScripts( finalScript += addData(script) return finalScript elif scriptClass == MultiSigTy: - #raise Exception("multisig signing unimplemented") - return mergeMultiSig(tx, idx, addresses, nRequired, pkScript, - sigScript, prevScript) + return mergeMultiSig( + tx, idx, addresses, nRequired, pkScript, sigScript, prevScript + ) else: # It doesn't actually make sense to merge anything other than multiig # and scripthash (because it could contain multisig). Everything else @@ -2290,6 +2458,117 @@ def mergeScripts( return prevScript +def mergeMultiSig(tx, idx, addresses, nRequired, pkScript, sigScript, prevScript): + """ + mergeMultiSig combines the two signature scripts sigScript and prevScript + that both provide signatures for pkScript in output idx of tx. addresses + and nRequired should be the results from extracting the addresses from + pkScript. Since this function is internal only we assume that the arguments + have come from other functions internally and thus are all consistent with + each other, behaviour is undefined if this contract is broken. + + NOTE: This function is only valid for version 0 scripts. Since the function + does not accept a script version, the results are undefined for other script + versions. + + Args: + tx (object): the ticket purchase MsgTx. + idx (int): the output index that contains the multisig. + addresses (object): the addresses that make up the multisig. + nRequired (int): the number of signatures required to fulfill the pkScript. + pkScript (byte-like): the multisig script. + sigScript (byte-like): the mulitsig script's signature. + prevScript (byte-like): the output's previous signature script. + + Returns: + byte-like: the merged signature scripts. + """ + + # Nothing to merge if either the new or previous signature scripts are + # empty. + if len(sigScript) == 0: + return prevScript + + if len(prevScript) == 0: + return sigScript + + # Convenience function to avoid duplication. + possibleSigs = [] + + def extractSigs(script): + scriptVersion = 0 + tokenizer = ScriptTokenizer(scriptVersion, script) + while tokenizer.next(): + data = tokenizer.data() + if len(data) != 0: + possibleSigs.append(data) + if tokenizer.err is not None: + return None + + # Attempt to extract signatures from the two scripts. Return the other + # script that is intended to be merged in the case signature extraction + # fails for some reason. + if not extractSigs(sigScript): + return prevScript + + if not extractSigs(prevScript): + return sigScript + + # Now we need to match the signatures to pubkeys, the only real way to + # do that is to try to verify them all and match it to the pubkey + # that verifies it. we then can go through the addresses in order + # to build our script. Anything that doesn't parse or doesn't verify we + # throw away. + addrToSig = {} + for sig in possibleSigs: + + # can't have a valid signature that doesn't at least have a + # hashtype, in practise it is even longer than this. but + # that'll be checked next. + if len(sig) < 1: + continue + tSig = sig[:-1] + hashType = sig[-1] + + pSig = Signature.parseSig(tSig) + if not pSig: + continue + + # We have to do this each round since hash types may vary + # between signatures and so the hash will vary. We can, + # however, assume no sigs etc are in the script since that + # would make the transaction nonstandard and thus not + # MultiSigTy, so we just need to hash the full thing. + hash = calcSignatureHash(pkScript, hashType, tx, idx, None) + + for addr in addresses: + # All multisig addresses should be pubkey addresses + # it is an error to call this internal function with + # bad input. + + # If it matches we put it in the map. We only + # can take one signature per public key so if we + # already have one, we can throw this away. + if verifySig(addr.pubkey, hash, pSig.r.int(), pSig.s.int()): + addrToSig[addr.string()] = sig + + script = ByteArray(b"") + doneSigs = 0 + # This assumes that addresses are in the same order as in the script. + for addr in addresses: + if addr.string() in addrToSig: + script += addData(addrToSig[addr.string()]) + doneSigs += 1 + if doneSigs == nRequired: + break + + # padding for missing ones + for i in range(nRequired - doneSigs): + script += opcode.OP_0 + + return script + + def signTxOutput( chainParams, tx, idx, pkScript, hashType, keysource, previousScript, sigType ): @@ -2857,43 +3136,29 @@ def makeTicket( return mtx -class MinimalOutput: - """ - MinimalOutput is a class encoding a minimally sized output for use in parsing - stake related information. - """ - def __init__(self, pkScript, value, version): - self.pkScript = pkScript - self.value = value - self.version = version - - -def convertToMinimalOutputs(tx): - """ - ConvertToMinimalOutputs converts a transaction to its minimal outputs - derivative. -[]*MinimalOutput { - """ - minOuts = [] - for i in range(len(tx.txOut)): - minOuts.append(MinimalOutput(tx.txOut[i].pkScript, tx.txOut[i].value, tx.txOut[i].version)) - return minOuts - - def sstxStakeOutputInfo(outs): """ sstxStakeOutputInfo takes an SStx as input and scans through its outputs, returning the pubkeyhashs and amounts for any NullDataTy's (future commitments to stake generation rewards). -([]bool, [][]byte, []int64, -[]int64, [][]bool, [][]uint16): - """ - isP2SH = [] # bool, expectedInLen) - addresses = [] #[] byte, expectedInLen) - amounts = [] # int64, expectedInLen) - changeAmounts = [] # int64, expectedInLen) - allSpendRules = [] #[] bool, expectedInLen) - allSpendLimits = [] #[] uint16, expectedInLen) + + Args: + outs (list(object)): an SStx MsgTx outputs + + Returns: + list(bool): is pay to script. + list(byte-like): the output addresses. + list(int): the subsidy amounts. + list(int): the change amounts. + list(list(bool)): the spend rules. + list(list(int)): the spend limits. + """ + isP2SH = [] + addresses = [] + amounts = [] + changeAmounts = [] + allSpendRules = [] + allSpendLimits = [] # Cycle through the inputs and pull the proportional amounts # and commit to PKHs/SHs. @@ -2904,7 +3169,6 @@ def sstxStakeOutputInfo(outs): if (idx > 0) and (idx % 2 != 0): # The MSB (sign), not used ever normally, encodes whether # or not it is a P2PKH or P2SH for the input. - # amtEncoded = [] # byte, 8) amtEncoded = outs[idx].pkScript[22:30] # MSB set? isP2SH.append(not (amtEncoded[7] & (1 << 7) == 0)) @@ -2916,15 +3180,20 @@ def sstxStakeOutputInfo(outs): amounts.append(ByteArray(amtEncoded, length=8).littleEndian().int()) # Get flags and restrictions for the outputs to be - # make in either a vote or revocation. - spendRules = [] # bool, 2) - spendLimits = [] # uint16, 2) + # made in either a vote or revocation. + spendRules = [] + spendLimits = [] # This bitflag is true/false. - # feeLimitUint16 := binary.LittleEndian.Uint16(out.PkScript[30:32]) - feeLimitUint16 = ByteArray(outs[idx].pkScript[30:32], length=4).littleEndian().int() - spendRules.append((feeLimitUint16 & SStxVoteFractionFlag) == SStxVoteFractionFlag) - spendRules.append((feeLimitUint16 & SStxRevFractionFlag) == SStxRevFractionFlag) + feeLimitUint16 = ( + ByteArray(outs[idx].pkScript[30:32], length=4).littleEndian().int() + ) + spendRules.append( + (feeLimitUint16 & SStxVoteFractionFlag) == SStxVoteFractionFlag + ) + spendRules.append( + (feeLimitUint16 & SStxRevFractionFlag) == SStxRevFractionFlag + ) allSpendRules.append(spendRules) # This is the fraction to use out of 64. @@ -2939,30 +3208,23 @@ def sstxStakeOutputInfo(outs): if (idx > 0) and (idx % 2 == 0): changeAmounts.append(outs[idx].value) - print("stake outputs", isP2SH, addresses, amounts, changeAmounts, allSpendRules, allSpendLimits) return isP2SH, addresses, amounts, changeAmounts, allSpendRules, allSpendLimits -def TxSStxStakeOutputInfo(tx): - """ - TxSStxStakeOutputInfo takes an SStx as input and scans through its outputs, - returning the pubkeyhashs and amounts for any NullDataTy's (future - commitments to stake generation rewards). - *wire.MsgTx - ([]bool, [][]byte, []int64, []int64, - [][]bool, [][]uint16) { - - """ - return sstxStakeOutputInfo(convertToMinimalOutputs(tx)) - def calculateRewards(amounts, amountTicket, subsidy): """ CalculateRewards takes a list of SStx adjusted output amounts, the amount used to purchase that ticket, and the reward for an SSGen tx and subsequently generates what the outputs should be in the SSGen tx. If used for calculating the outputs for an SSRtx, pass 0 for subsidy. -amounts []int64, amountTicket int64, - subsidy int64) []int64 + + Args: + amounts list (int): output amounts. + amountTicket (int): amount used to purchase ticket. + subsidy (int): amount to pay. + + Returns: + list(int): list of SStx adjusted output amounts. """ outputsAmounts = [] @@ -3002,211 +3264,25 @@ def calculateRewards(amounts, amountTicket, subsidy): return outputsAmounts -def PayToSSRtxPKHDirect(pkh): - """ - PayToSSRtxPKHDirect creates a new script to pay a transaction output to a - public key hash, but tags the output with OP_SSRTX. For use in constructing - valid SSRtx. Unlike PayToSSRtx, this function directly uses the HASH160 - pubkeyhash (instead of an address). -pkh []byte) ([]byte, error - """ - script = ByteArray(b'') - script += opcode.OP_SSRTX - script += opcode.OP_DUP - script += opcode.OP_HASH160 - script += addData(pkh) - script += opcode.OP_EQUALVERIFY - script += opcode.OP_CHECKSIG - return script - - -def PayToSSRtxSHDirect(sh): - """ - PayToSSRtxSHDirect creates a new script to pay a transaction output to a - script hash, but tags the output with OP_SSRTX. For use in constructing - valid SSRtx. Unlike PayToSSRtx, this function directly uses the HASH160 - script hash (instead of an address). - []byte) ([]byte, error) - """ - script = ByteArray(b'') - script += opcode.OP_SSRTX - script += opcode.OP_HASH160 - script += addData(sh) - script += opcode.OP_EQUAL - return script - -def signMultiSig(tx, idx, subScript, hashType, addresses, nRequired, privKey): - """ - signMultiSig signs as many of the outputs in the provided multisig script as - possible. It returns the generated script and a boolean if the script - fulfills the contract (i.e. nrequired signatures are provided). Since it is - arguably legal to not be able to sign any of the outputs, no error is - returned. -(tx *wire.MsgTx, idx int, subScript []byte, hashType SigHashType, - addresses []dcrutil.Address, nRequired int, kdb KeyDB) ([]byte, bool) { - - """ - - # No need to add dummy in Decred. - signed = 0 - script = ByteArray(b'') - for addr in addresses: - - sig = rawTxInSignature(tx, idx, subScript, hashType, privKey.key) - - script += addData(sig) - signed += 1 - if signed == nRequired: - break - - return script #, signed == nRequired - -def mergeMultiSig(tx, idx, addresses, nRequired, pkScript, sigScript, prevScript): - """ - mergeMultiSig combines the two signature scripts sigScript and prevScript - that both provide signatures for pkScript in output idx of tx. addresses - and nRequired should be the results from extracting the addresses from - pkScript. Since this function is internal only we assume that the arguments - have come from other functions internally and thus are all consistent with - each other, behaviour is undefined if this contract is broken. - - NOTE: This function is only valid for version 0 scripts. Since the function - does not accept a script version, the results are undefined for other script - versions. - tx *wire.MsgTx, idx int, addresses []dcrutil.Address, - nRequired int, pkScript, sigScript, prevScript []byte) []byte - """ - - # Nothing to merge if either the new or previous signature scripts are - # empty. - if len(sigScript) == 0: - return prevScript - print("sig zero") - - if len(prevScript) == 0: - return sigScript - print("prev zero") - - #Still working on the rest of this... - return sigScript - - # Convenience function to avoid duplication. - possibleSigs = [] - - def extractSigs(script): - scriptVersion = 0 - tokenizer = ScriptTokenizer(scriptVersion, script) - while tokenizer.next(): - data = tokenizer.data() - if len(data) != 0: - possibleSigs.append(data) - if tokenizer.err is not None: - return None - - # Attempt to extract signatures from the two scripts. Return the other - # script that is intended to be merged in the case signature extraction - # fails for some reason. - if not extractSigs(sigScript): - return prevScript - - if not extractSigs(prevScript): - return sigScript - - # Now we need to match the signatures to pubkeys, the only real way to - # do that is to try to verify them all and match it to the pubkey - # that verifies it. we then can go through the addresses in order - # to build our script. Anything that doesn't parse or doesn't verify we - # throw away. - addrToSig = {} - """ -sigLoop: - for _, sig := range possibleSigs { - - # can't have a valid signature that doesn't at least have a - # hashtype, in practise it is even longer than this. but - # that'll be checked next. - if len(sig) < 1 { - continue - } - tSig := sig[:len(sig)-1] - hashType := SigHashType(sig[len(sig)-1]) - - pSig, err := secp256k1.ParseDERSignature(tSig) - if err != nil { - continue - } - - # We have to do this each round since hash types may vary - # between signatures and so the hash will vary. We can, - # however, assume no sigs etc are in the script since that - # would make the transaction nonstandard and thus not - # MultiSigTy, so we just need to hash the full thing. - hash, err := calcSignatureHash(pkScript, hashType, tx, idx, nil) - if err != nil { - # Decred -- is this the right handling for SIGHASH_SINGLE error ? - # TODO make sure this doesn't break anything. - continue - } - - for _, addr := range addresses { - # All multisig addresses should be pubkey addresses - # it is an error to call this internal function with - # bad input. - pkaddr := addr.(*dcrutil.AddressSecpPubKey) - - pubKey := pkaddr.PubKey() - - # If it matches we put it in the map. We only - # can take one signature per public key so if we - # already have one, we can throw this away. - r := pSig.GetR() - s := pSig.GetS() - if secp256k1.NewSignature(r, s).Verify(hash, pubKey) { - aStr := addr.Address() - if _, ok := addrToSig[aStr]; !ok { - addrToSig[aStr] = sig - } - continue sigLoop - } - } - } - - builder := NewScriptBuilder() - doneSigs := 0 - # This assumes that addresses are in the same order as in the script. - for _, addr := range addresses { - sig, ok := addrToSig[addr.Address()] - if !ok { - continue - } - builder.AddData(sig) - doneSigs++ - if doneSigs == nRequired { - break - } - } - - # padding for missing ones. - for i := doneSigs; i < nRequired; i++ { - builder.AddOp(OP_0) - } - - script, _ := builder.Script() - return script -""" - def makeRevocation(ticketPurchase, feePerKB): - ''' + """ makeRevocation creates an unsigned revocation transaction that revokes a missed or expired ticket. Revocations must carry a relay fee and this function can error if the revocation contains no suitable output to decrease the estimated relay fee from. -ticketHash *chainhash.Hash, ticketPurchase *wire.MsgTx, feePerKB dcrutil.Amount -(*wire.MsgTx, error) - ''' + + Args: + ticketPurchase (object): the ticket to revoke's MsgTx. + feePerKB (int): the miner's fee per kb. + + Returns: + object: the unsigned revocation MsgTx or None in case of error. + """ # Parse the ticket purchase transaction to determine the required output # destinations for vote rewards or revocations. - ticketPayKinds, ticketHash160s, ticketValues, _, _, _ = TxSStxStakeOutputInfo(ticketPurchase) + ticketPayKinds, ticketHash160s, ticketValues, _, _, _ = sstxStakeOutputInfo( + ticketPurchase.txOut + ) # Calculate the output values for the revocation. Revocations do not # contain any subsidy. @@ -3217,7 +3293,6 @@ def makeRevocation(ticketPurchase, feePerKB): # Revocations reference the ticket purchase with the first (and only) # input. - print("ticket hash", ticketPurchase.hash()) ticketOutPoint = msgtx.OutPoint(ticketPurchase.hash(), 0, msgtx.TxTreeStake) ticketInput = msgtx.TxIn( previousOutPoint=ticketOutPoint, @@ -3230,10 +3305,10 @@ def makeRevocation(ticketPurchase, feePerKB): # All remaining outputs pay to the output destinations and amounts tagged # by the ticket purchase. for i in range(len(ticketHash160s)): - scriptFn = PayToSSRtxPKHDirect + scriptFn = payToSSRtxPKHDirect # P2SH if ticketPayKinds[i]: - scriptFn = PayToSSRtxSHDirect + scriptFn = payToSSRtxSHDirect script = scriptFn(ticketHash160s[i]) revocation.addTxOut(msgtx.TxOut(revocationValues[i], script)) @@ -3254,5 +3329,5 @@ def makeRevocation(ticketPurchase, feePerKB): if not isDustAmount(amount, len(output.pkScript), feePerKB): output.value = amount return revocation - print("missing suitable revocation output to pay relay fee") - return False + log.error("missing suitable revocation output to pay relay fee") + return None diff --git a/tests/integration/pydecred/test_dcrdata.py b/tests/integration/pydecred/test_dcrdata.py index 831f9389..4e3a0c74 100644 --- a/tests/integration/pydecred/test_dcrdata.py +++ b/tests/integration/pydecred/test_dcrdata.py @@ -8,8 +8,9 @@ from tempfile import TemporaryDirectory import time -from tinydecred.pydecred import mainnet, testnet, txscript, dcrdata +from tinydecred.pydecred import mainnet, testnet, txscript, dcrdata, account from tinydecred.pydecred.wire import msgtx +from tinydecred.crypto.bytearray import ByteArray from tinydecred.crypto import crypto @@ -145,3 +146,50 @@ class request: ticket, spent, newUTXOs = blockchain.purchaseTickets( KeySource(), utxosource, request() ) + + def test_revoke_ticket(self): + with TemporaryDirectory() as tempDir: + blockchain = dcrdata.DcrdataBlockchain( + os.path.join(tempDir, "db.db"), testnet, "https://testnet.dcrdata.org" + ) + blockchain.connect() + + def broadcast(txHex): + print("test skipping broadcast of transaction: %s" % txHex) + return True + + blockchain.broadcast = broadcast + + class test: + def __init__( + self, ticket="", privKey="", redeemScript="", revocation="", + ): + self.ticket = ticket + self.privKey = privKey + self.redeemScript = redeemScript + self.revocation = revocation + + tests = [ + test( + "010000000210fd1f5623e2469d9bb390ad21b12f6710f5d6be0e130df074cfd8614d0c4e050400000000ffffffff10fd1f5623e2469d9bb390ad21b12f6710f5d6be0e130df074cfd8614d0c4e050500000000ffffffff05508f4a7401000000000018baa91438a8a93737e62e806f49d1465518a02f110d57fe8700000000000000000000206a1e1aee120db13f4e4f785aec3da97b48963de58ebd37570200000000000058000000000000000000001abd76a914000000000000000000000000000000000000000088ac00000000000000000000206a1eabc7372997a530b43e8e17a4850602b28e0768dd454d4874010000000058000000000000000000001abd76a914000000000000000000000000000000000000000088ac000000000000000002375702000000000000000000000000006a47304402202b5f4a97abf78d95875de75b244f9b4c7f60bb40b01a91881331022a03a3bf32022047f951e0414e4f28518b5254974abeb93f77727bee7c6fbd5010f8bf375dd10f012102ec908402cb3ab128a9a68978fbb3b33f1c97d715afb8f76dbbe300619878f095454d48740100000000000000000000006a47304402200435095049ac7b3f3c47a43d92afc22db032b929357f2f216eb24e91cd0d2d2802203ca93654cc1f193f11ce0c7e74f9959edd984c15d54f99b0d227b1cdd99a4a3e012102ec908402cb3ab128a9a68978fbb3b33f1c97d715afb8f76dbbe300619878f095", + "d407f81cb789e65579590d5e50027431f1fdea21c2ebef12944b7842e71eaf", + "51210289a43bf822daf338bb07555476a967cc46545a58c513a0badc99861c81f985782103b1f62148c92802a47ce98a49d1f14f397adc759131f6f6a5c88ad9dfedd53f9b52ae", + "0100000001113d36ae0156c1f5a3071de1f7f10e9e4521a77266b1927f832babb9fe3d5cd90000000001ffffffff022c4d02000000000000001abc76a9141aee120db13f4e4f785aec3da97b48963de58ebd88ac193848740100000000001abc76a914abc7372997a530b43e8e17a4850602b28e0768dd88ac000000000000000001508f4a740100000000000000000000009148304502210099b8e13022e13d19229fff3a2b08ebf54f5cd4ea79c85618d04a57a735283e9102203e0fd5a66a168edff1bbcaeaa9d5b2d5e224cbd5d90a6c0099c73810d7493924014751210289a43bf822daf338bb07555476a967cc46545a58c513a0badc99861c81f985782103b1f62148c92802a47ce98a49d1f14f397adc759131f6f6a5c88ad9dfedd53f9b52ae", + ), + test( + "010000000228a301cae233e252143e6fff3ccd348e156bbcc8ea158cfb4a1fe19f78cad1e80a00000000ffffffff28a301cae233e252143e6fff3ccd348e156bbcc8ea158cfb4a1fe19f78cad1e80b00000000ffffffff05d00fb2fb00000000000018baa91438a8a93737e62e806f49d1465518a02f110d57fe8700000000000000000000206a1e1aee120db13f4e4f785aec3da97b48963de58ebdcf550200000000000058000000000000000000001abd76a914000000000000000000000000000000000000000088ac00000000000000000000206a1e949293b5f8acd5a0a871ce417cd5354d9715a2912dcfaffb000000000058000000000000000000001abd76a914000000000000000000000000000000000000000088ac000000000000000002cf5502000000000000000000000000006b483045022100a4072a6a09058cf1b2713b75e636bcdf418c9897aa18cd584cfcf0229bf6b7d102200cb94cfef85a31943ab1e0daba90578c3b465be26e4a94a19e1da5ec70586193012103ac3936e6c8d0fd9cefde6cc9552289c943a1eb4abe1601de8f8e46b8f7ed508d2dcfaffb0000000000000000000000006a47304402202fc0578332b69746109066cc7d7cb9b86f0a693639bbe4dc84c83f26ef0e3bad022010ad57133bcc77bb36600488679f5da6ab6740b4d743470bcd60f2ecda70aa7f012103ac3936e6c8d0fd9cefde6cc9552289c943a1eb4abe1601de8f8e46b8f7ed508d", + "d407f81cb789e65579590d5e50027431f1fdea21c2ebef12944b7842e71eaf", + "51210289a43bf822daf338bb07555476a967cc46545a58c513a0badc99861c81f985782103b1f62148c92802a47ce98a49d1f14f397adc759131f6f6a5c88ad9dfedd53f9b52ae", + "0100000001d033e1ddf9c44a1402d8dc8d6cfcee634a459537a078359a3c51153b7ba67b220000000001ffffffff02c44b02000000000000001abc76a9141aee120db13f4e4f785aec3da97b48963de58ebd88ac01baaffb0000000000001abc76a914949293b5f8acd5a0a871ce417cd5354d9715a29188ac000000000000000001d00fb2fb00000000000000000000000091483045022100f83aa623b21d302cdc65b6b227fe53f3796379031dd1fee9bc398680f846221d022062282396b391ba38612afea9a648b7db7ef54b0d4b06bddae7783bb25cf6ba1f014751210289a43bf822daf338bb07555476a967cc46545a58c513a0badc99861c81f985782103b1f62148c92802a47ce98a49d1f14f397adc759131f6f6a5c88ad9dfedd53f9b52ae", + ), + ] + + for test in tests: + ticket = msgtx.MsgTx.deserialize(ByteArray(test.ticket)) + keysource = account.KeySource( + priv=lambda _: crypto.privKeyFromBytes(ByteArray(test.privKey)), + internal=lambda: "", + ) + redeemScript = ByteArray(test.redeemScript) + revocation = blockchain.revokeTicket(ticket, keysource, redeemScript) + self.assertEqual(test.revocation, revocation.txHex()) diff --git a/ui/screens.py b/ui/screens.py index 883bce6e..8d07fb64 100644 --- a/ui/screens.py +++ b/ui/screens.py @@ -1157,8 +1157,13 @@ def __init__(self, app): revokeBtn = app.getButton(TINY, "Revoke") revokeBtn.clicked.connect(self.revokeTickets) votingWgt, _ = Q.makeSeries(Q.HORIZONTAL, agendaBtn, revokeBtn) + self.revokeBtn = revokeBtn self.layout.addWidget(votingWgt) + # Hide revoke button unless we have revokable tickets. + revokeBtn.hide() + self.app.registerSignal(ui.SYNC_SIGNAL, self.checkRevocable) + # Affordability. A row that reads `You can afford X tickets` lbl = Q.makeLabel("You can afford ", 14) self.affordLbl = Q.makeLabel(" ", 17, fontFamily="Roboto-Bold") @@ -1225,18 +1230,51 @@ def stackAgendas(self): return self.app.appWindow.stack(self.agendasScreen) + def checkRevocable(self): + """ + On SYNC_SIGNAL signal hide or show revoke button based on wether or not + we have revocable tickets. + """ + acct = self.app.wallet.selectedAccount + n = 0 + plural = "" + for utxo in acct.utxos.values(): + if utxo.isRevocableTicket(): + n += 1 + if n > 0: + if n > 1: + plural = "s" + self.revokeBtn.setText("revoke {} ticket{}".format(n, plural)) + self.revokeBtn.show() + else: + self.revokeBtn.hide() + def revokeTickets(self): + """ + Revoke all revocable tickets. + """ + def revoke(wallet): try: + self.app.emitSignal(ui.WORKING_SIGNAL) wallet.openAccount.revokeTickets() return True except Exception as e: log.error("revoke tickets error: %s" % formatTraceback(e)) return False + self.app.emitSignal(ui.DONE_SIGNAL) + self.app.withUnlockedWallet(revoke, self.revoked) def revoked(self, success): - self.app.appWindow.showSuccess("revoke tickets") + """ + revokeTickets callback. Prints success or failure to the screen. + """ + if success: + self.app.appWindow.showSuccess("revoke tickets completed without error") + self.revokeBtn.hide() + else: + self.app.appWindow.showError("revoke tickets finished with error") def setStats(self): """ From 6b68f6a07130a3e70e5708dcca7875eb9c1aef33 Mon Sep 17 00:00:00 2001 From: JoeGruff Date: Tue, 31 Dec 2019 16:39:55 +0900 Subject: [PATCH 3/8] Fix grammar. Also add more parsing checks for signature. Raise more exceptions. Log how many tickets revoked upon successful revocation. --- pydecred/account.py | 18 ++----- pydecred/dcrdata.py | 6 +-- pydecred/txscript.py | 113 ++++++++++++++++++++++++++----------------- ui/screens.py | 11 ++++- 4 files changed, 83 insertions(+), 65 deletions(-) diff --git a/pydecred/account.py b/pydecred/account.py index 7d042f23..dbce2af0 100644 --- a/pydecred/account.py +++ b/pydecred/account.py @@ -426,7 +426,7 @@ def purchaseTickets(self, qty, price): def revokeTickets(self): """ - Iterate through missed and expired ticket utxo and revoke them. + Iterate through missed and expired tickets and revoke them. Returns: bool: whether or not an error occured. @@ -434,16 +434,10 @@ def revokeTickets(self): revokableTickets = [ utxo.txid for utxo in self.utxos.values() if utxo.isRevocableTicket() ] - errored = False txs = [] for txid in revokableTickets: - try: - tx = self.blockchain.tx(txid) - txs.append(tx) - except Exception as e: - log.error("error getting tx: %s" % e) - errored = True - continue + tx = self.blockchain.tx(txid) + txs.append(tx) for tx in txs: redeemHash = crypto.AddressScriptHash( self.net.ScriptHashAddrID, @@ -455,9 +449,8 @@ def revokeTickets(self): redeemScript = decodeBA(pool.purchaseInfo.script) break else: - log.error("did not find redeem script for hash %s" % redeemHash) - errored = True - continue + raise Exception("did not find redeem script for hash %s" % redeemHash) + keysource = KeySource( # This will need to change when we start using different # addresses for voting. @@ -465,7 +458,6 @@ def revokeTickets(self): internal=lambda: "", ) self.blockchain.revokeTicket(tx, keysource, redeemScript) - return errored def sync(self, blockchain, signals): """ diff --git a/pydecred/dcrdata.py b/pydecred/dcrdata.py index 05692012..a047bad4 100644 --- a/pydecred/dcrdata.py +++ b/pydecred/dcrdata.py @@ -1638,7 +1638,7 @@ def purchaseTickets(self, keysource, utxosource, req): def revokeTicket(self, tx, keysource, redeemScript): """ - revoke a ticket by signing the supplied redeem script and broadcasting the raw transaction. + Revoke a ticket by signing the supplied redeem script and broadcasting the raw transaction. Args: tx (object): the msgTx of the ticket purchase. @@ -1651,10 +1651,6 @@ def revokeTicket(self, tx, keysource, redeemScript): revocation = txscript.makeRevocation(tx, self.relayFee()) - if not revocation: - log.info("failed to make revocation") - return - signedScript = txscript.signTxOutput( self.params, revocation, diff --git a/pydecred/txscript.py b/pydecred/txscript.py index 7b07d81a..32df144e 100644 --- a/pydecred/txscript.py +++ b/pydecred/txscript.py @@ -227,6 +227,19 @@ hashH = crypto.hashH +def canonicalPadding(b): + """ + canonicalPadding checks whether a big-endian encoded integer could + possibly be misinterpreted as a negative number (even though OpenSSL + treats all numbers as unsigned), or if there is any unnecessary + leading zero padding. + """ + if b[0] & 0x80 == 0x80: + raise Exception("negative number") + if len(b) > 1 and b[0] == 0x00 and b[1] & 0x80 != 0x80: + raise Exception("excessive padding") + + class Signature: """ The Signature class represents an ECDSA-algorithm signature. @@ -277,85 +290,97 @@ def serialize(self): return b @staticmethod - def parseSig(sigBytes): - """ - # Originally this code used encoding/asn1 in order to parse the - # signature, but a number of problems were found with this approach. - # Despite the fact that signatures are stored as DER, the difference - # between go's idea of a bignum (and that they have sign) doesn't agree - # with the openssl one (where they do not). The above is true as of - # Go 1.1. In the end it was simpler to rewrite the code to explicitly - # understand the format which is this: - # 0x30 <0x02> 0x2 - # . -sigStr []byte, der bool) (*Signature, error) { + def parse(sigBytes): """ + Parse sigBytes to make sure they make up a valid Signature. + Args: + sigBytes (byte-like): The bytes of the signature. + Returns: + object: the ECDSA Signature. + """ # minimal message is when both numbers are 1 bytes. adding up to: # 0x30 + len + 0x02 + 0x01 + + 0x2 + 0x01 + if len(sigBytes) < 8: - log.error("malformed signature: too short") - return None + raise Exception("malformed signature: too short") # 0x30 index = 0 if sigBytes[index] != 0x30: - log.error("malformed signature: no header magic") - return None + raise Exception("malformed signature: no header magic") index += 1 # length of remaining message siglen = sigBytes[index] index += 1 if siglen + 2 > len(sigBytes): - log.error("malformed signature: bad length") - return None + raise Exception("malformed signature: bad length") # trim the slice we're working on so we only look at what matters. sigBytes = sigBytes[: siglen + 2] # 0x02 if sigBytes[index] != 0x02: - log.error("malformed signature: no 1st int marker") - return None + raise Exception("malformed signature: no 1st int marker") index += 1 - # Length of signature R. + # Length of signature r. rLen = sigBytes[index] # must be positive, must be able to fit in another 0x2, # hence the -3. We assume that the length must be at least one byte. index += 1 if rLen <= 0 or rLen > len(sigBytes) - index - 3: - log.error("malformed signature: bogus R length") - return None + raise Exception("malformed signature: bogus r length") - # Then R itself. + # Then r itself. rBytes = sigBytes[index : index + rLen] + try: + canonicalPadding(rBytes) + except Exception as e: + raise Exception( + "malformed signature: bogus r padding or sign: {}".format(e) + ) + index += rLen # 0x02. length already checked in previous if. if sigBytes[index] != 0x02: - log.error("malformed signature: no 2nd int marker") - return None + raise Exception("malformed signature: no 2nd int marker") index += 1 - # Length of signature S. + # Length of signature s. sLen = sigBytes[index] index += 1 - # S should be the rest of the bytes. + # s should be the rest of the bytes. if sLen <= 0 or sLen > len(sigBytes) - index: - log.error("malformed signature: bogus S length") - return None + raise Exception("malformed signature: bogus S length") - # Then S itself. + # Then s itself. sBytes = sigBytes[index : index + sLen] - index += sLen + try: + canonicalPadding(rBytes) + except Exception as e: + raise Exception( + "malformed signature: bogus s padding or sign: {}".format(e) + ) + index += sLen # sanity check length parsing if index != len(sigBytes): - log.error( + raise Exception( "malformed signature: bad final length %s != %s" % index, len(sigBytes) ) - return None - return Signature(rBytes, sBytes) + signature = Signature(rBytes, sBytes) + + # FWIW the ecdsa spec states that r and s must be | 1, N - 1 | + if signature.r.int() < 1: + raise Exception("signature r is less than one") + if signature.s.int() < 1: + raise Exception("signature s is less than one") + if signature.r.int() >= Curve.N: + raise Exception("signature r is >= curve.N") + if signature.s.int() >= Curve.N: + raise Exception("signature s is >= curve.N") + + return signature class ScriptTokenizer: @@ -2503,7 +2528,7 @@ def extractSigs(script): if len(data) != 0: possibleSigs.append(data) if tokenizer.err is not None: - return None + raise Exception("mergeMultisig: extractSigs: {}".format(tokenizer.err)) # Attempt to extract signatures from the two scripts. Return the other # script that is intended to be merged in the case signature extraction @@ -2530,7 +2555,7 @@ def extractSigs(script): tSig = sig[:-1] hashType = sig[-1] - pSig = Signature.parseSig(tSig) + pSig = Signature.parse(tSig) if not pSig: continue @@ -3138,15 +3163,15 @@ def makeTicket( def sstxStakeOutputInfo(outs): """ - sstxStakeOutputInfo takes an SStx as input and scans through its outputs, - returning the pubkeyhashs and amounts for any NullDataTy's (future - commitments to stake generation rewards). + sstxStakeOutputInfo takes a list of msgtx.txOut as input and scans through + its outputs, returning the pubkeyhashs and amounts for any NullDataTy's + (future commitments to stake generation rewards). Args: outs (list(object)): an SStx MsgTx outputs Returns: - list(bool): is pay to script. + list(bool): is pay-to-script-hash. list(byte-like): the output addresses. list(int): the subsidy amounts. list(int): the change amounts. @@ -3213,7 +3238,7 @@ def sstxStakeOutputInfo(outs): def calculateRewards(amounts, amountTicket, subsidy): """ - CalculateRewards takes a list of SStx adjusted output amounts, the amount used + calculateRewards takes a list of SStx adjusted output amounts, the amount used to purchase that ticket, and the reward for an SSGen tx and subsequently generates what the outputs should be in the SSGen tx. If used for calculating the outputs for an SSRtx, pass 0 for subsidy. @@ -3298,7 +3323,6 @@ def makeRevocation(ticketPurchase, feePerKB): previousOutPoint=ticketOutPoint, valueIn=ticketPurchase.txOut[ticketOutPoint.index].value, ) - # ticketInput = wire.NewTxIn(ticketOutPoint, ticketPurchase.TxOut[ticketOutPoint.Index].Value, nil) revocation.addTxIn(ticketInput) scriptSizes = [RedeemP2SHSigScriptSize] @@ -3329,5 +3353,4 @@ def makeRevocation(ticketPurchase, feePerKB): if not isDustAmount(amount, len(output.pkScript), feePerKB): output.value = amount return revocation - log.error("missing suitable revocation output to pay relay fee") - return None + raise Exception("missing suitable revocation output to pay relay fee") diff --git a/ui/screens.py b/ui/screens.py index 8d07fb64..b3b78422 100644 --- a/ui/screens.py +++ b/ui/screens.py @@ -1123,6 +1123,7 @@ def __init__(self, app): self.wgt.setContentsMargins(5, 5, 5, 5) self.wgt.setMinimumWidth(400) self.blockchain = app.dcrdata + self.revocableTicketsCount = 0 # Register for a few key signals. self.app.registerSignal(ui.BLOCKCHAIN_CONNECTED, self.blockchainConnected) @@ -1236,7 +1237,7 @@ def checkRevocable(self): we have revocable tickets. """ acct = self.app.wallet.selectedAccount - n = 0 + n = self.revocableTicketsCount plural = "" for utxo in acct.utxos.values(): if utxo.isRevocableTicket(): @@ -1271,7 +1272,13 @@ def revoked(self, success): revokeTickets callback. Prints success or failure to the screen. """ if success: - self.app.appWindow.showSuccess("revoke tickets completed without error") + n = self.revocableTicketsCount + plural = "" + if n > 0: + if n > 1: + plural = "s" + self.app.appWindow.showSuccess("revoked {} ticket{}".format(n, plural)) + self.revocableTicketsCount = 0 self.revokeBtn.hide() else: self.app.appWindow.showError("revoke tickets finished with error") From 7fdb6deea7b81a5b4a4a4c2a8ff32ff9ba764cbd Mon Sep 17 00:00:00 2001 From: JoeGruff Date: Wed, 1 Jan 2020 11:55:30 +0900 Subject: [PATCH 4/8] Use generators revokeTickets --- pydecred/account.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/pydecred/account.py b/pydecred/account.py index dbce2af0..f48a01b7 100644 --- a/pydecred/account.py +++ b/pydecred/account.py @@ -431,24 +431,24 @@ def revokeTickets(self): Returns: bool: whether or not an error occured. """ - revokableTickets = [ + revocableTickets = ( utxo.txid for utxo in self.utxos.values() if utxo.isRevocableTicket() - ] - txs = [] - for txid in revokableTickets: - tx = self.blockchain.tx(txid) - txs.append(tx) + ) + txs = (self.blockchain.tx(txid) for txid in revocableTickets) for tx in txs: redeemHash = crypto.AddressScriptHash( self.net.ScriptHashAddrID, txscript.extractStakeScriptHash(tx.txOut[0].pkScript, opcode.OP_SSTX), ) - redeemScript = [] - for pool in self.stakePools: - if pool.purchaseInfo.ticketAddress == redeemHash.string(): - redeemScript = decodeBA(pool.purchaseInfo.script) - break - else: + redeemScript = next( + ( + decodeBA(p.purchaseInfo.script) + for p in self.stakePools + if p.purchaseInfo.ticketAddress == redeemHash.string() + ), + None, + ) + if not redeemScript: raise Exception("did not find redeem script for hash %s" % redeemHash) keysource = KeySource( From 8ba10e7a41ab74627126c5bd38e5917c77eaf792 Mon Sep 17 00:00:00 2001 From: JoeGruff Date: Wed, 1 Jan 2020 13:23:51 +0900 Subject: [PATCH 5/8] Add signature test --- pydecred/txscript.py | 31 +-- tests/unit/pydecred/test_signature.py | 290 ++++++++++++++++++++++++++ 2 files changed, 307 insertions(+), 14 deletions(-) create mode 100644 tests/unit/pydecred/test_signature.py diff --git a/pydecred/txscript.py b/pydecred/txscript.py index 32df144e..a6b5bd94 100644 --- a/pydecred/txscript.py +++ b/pydecred/txscript.py @@ -290,12 +290,13 @@ def serialize(self): return b @staticmethod - def parse(sigBytes): + def parse(sigBytes, der): """ Parse sigBytes to make sure they make up a valid Signature. Args: sigBytes (byte-like): The bytes of the signature. + der (bool): Whether to check for padding and sign. Returns: object: the ECDSA Signature. """ @@ -332,12 +333,13 @@ def parse(sigBytes): # Then r itself. rBytes = sigBytes[index : index + rLen] - try: - canonicalPadding(rBytes) - except Exception as e: - raise Exception( - "malformed signature: bogus r padding or sign: {}".format(e) - ) + if der: + try: + canonicalPadding(rBytes) + except Exception as e: + raise Exception( + "malformed signature: bogus r padding or sign: {}".format(e) + ) index += rLen # 0x02. length already checked in previous if. @@ -354,12 +356,13 @@ def parse(sigBytes): # Then s itself. sBytes = sigBytes[index : index + sLen] - try: - canonicalPadding(rBytes) - except Exception as e: - raise Exception( - "malformed signature: bogus s padding or sign: {}".format(e) - ) + if der: + try: + canonicalPadding(rBytes) + except Exception as e: + raise Exception( + "malformed signature: bogus s padding or sign: {}".format(e) + ) index += sLen # sanity check length parsing @@ -2555,7 +2558,7 @@ def extractSigs(script): tSig = sig[:-1] hashType = sig[-1] - pSig = Signature.parse(tSig) + pSig = Signature.parse(tSig, True) if not pSig: continue diff --git a/tests/unit/pydecred/test_signature.py b/tests/unit/pydecred/test_signature.py new file mode 100644 index 00000000..44ffe750 --- /dev/null +++ b/tests/unit/pydecred/test_signature.py @@ -0,0 +1,290 @@ +from tinydecred.crypto.bytearray import ByteArray +from tinydecred.pydecred.txscript import Signature + + +def test_signature(): + class test: + def __init__(self, name, sig, der, isValid): + self.name = name + self.sig = sig + self.der = der + self.isValid = isValid + + # fmt: off + tests = [ + # signatures from bitcoin blockchain tx + # 0437cd7f8525ceed2324359c2d0ba26006d92d85 + test( + "valid signature.", + [0x30, 0x44, 0x02, 0x20, 0x4e, 0x45, 0xe1, 0x69, + 0x32, 0xb8, 0xaf, 0x51, 0x49, 0x61, 0xa1, 0xd3, 0xa1, + 0xa2, 0x5f, 0xdf, 0x3f, 0x4f, 0x77, 0x32, 0xe9, 0xd6, + 0x24, 0xc6, 0xc6, 0x15, 0x48, 0xab, 0x5f, 0xb8, 0xcd, + 0x41, 0x02, 0x20, 0x18, 0x15, 0x22, 0xec, 0x8e, 0xca, + 0x07, 0xde, 0x48, 0x60, 0xa4, 0xac, 0xdd, 0x12, 0x90, + 0x9d, 0x83, 0x1c, 0xc5, 0x6c, 0xbb, 0xac, 0x46, 0x22, + 0x08, 0x22, 0x21, 0xa8, 0x76, 0x8d, 0x1d, 0x09], + True, + True, + ), + test( + "empty.", + [], + "", + False, + ), + test( + "bad magic.", + [0x31, 0x44, 0x02, 0x20, 0x4e, 0x45, 0xe1, 0x69, + 0x32, 0xb8, 0xaf, 0x51, 0x49, 0x61, 0xa1, 0xd3, 0xa1, + 0xa2, 0x5f, 0xdf, 0x3f, 0x4f, 0x77, 0x32, 0xe9, 0xd6, + 0x24, 0xc6, 0xc6, 0x15, 0x48, 0xab, 0x5f, 0xb8, 0xcd, + 0x41, 0x02, 0x20, 0x18, 0x15, 0x22, 0xec, 0x8e, 0xca, + 0x07, 0xde, 0x48, 0x60, 0xa4, 0xac, 0xdd, 0x12, 0x90, + 0x9d, 0x83, 0x1c, 0xc5, 0x6c, 0xbb, 0xac, 0x46, 0x22, + 0x08, 0x22, 0x21, 0xa8, 0x76, 0x8d, 0x1d, 0x09], + True, + False, + ), + test( + "bad 1st int marker magic.", + [0x30, 0x44, 0x03, 0x20, 0x4e, 0x45, 0xe1, 0x69, + 0x32, 0xb8, 0xaf, 0x51, 0x49, 0x61, 0xa1, 0xd3, 0xa1, + 0xa2, 0x5f, 0xdf, 0x3f, 0x4f, 0x77, 0x32, 0xe9, 0xd6, + 0x24, 0xc6, 0xc6, 0x15, 0x48, 0xab, 0x5f, 0xb8, 0xcd, + 0x41, 0x02, 0x20, 0x18, 0x15, 0x22, 0xec, 0x8e, 0xca, + 0x07, 0xde, 0x48, 0x60, 0xa4, 0xac, 0xdd, 0x12, 0x90, + 0x9d, 0x83, 0x1c, 0xc5, 0x6c, 0xbb, 0xac, 0x46, 0x22, + 0x08, 0x22, 0x21, 0xa8, 0x76, 0x8d, 0x1d, 0x09], + True, + False, + ), + test( + "bad 2nd int marker.", + [0x30, 0x44, 0x02, 0x20, 0x4e, 0x45, 0xe1, 0x69, + 0x32, 0xb8, 0xaf, 0x51, 0x49, 0x61, 0xa1, 0xd3, 0xa1, + 0xa2, 0x5f, 0xdf, 0x3f, 0x4f, 0x77, 0x32, 0xe9, 0xd6, + 0x24, 0xc6, 0xc6, 0x15, 0x48, 0xab, 0x5f, 0xb8, 0xcd, + 0x41, 0x03, 0x20, 0x18, 0x15, 0x22, 0xec, 0x8e, 0xca, + 0x07, 0xde, 0x48, 0x60, 0xa4, 0xac, 0xdd, 0x12, 0x90, + 0x9d, 0x83, 0x1c, 0xc5, 0x6c, 0xbb, 0xac, 0x46, 0x22, + 0x08, 0x22, 0x21, 0xa8, 0x76, 0x8d, 0x1d, 0x09], + True, + False, + ), + test( + "short len", + [0x30, 0x43, 0x02, 0x20, 0x4e, 0x45, 0xe1, 0x69, + 0x32, 0xb8, 0xaf, 0x51, 0x49, 0x61, 0xa1, 0xd3, 0xa1, + 0xa2, 0x5f, 0xdf, 0x3f, 0x4f, 0x77, 0x32, 0xe9, 0xd6, + 0x24, 0xc6, 0xc6, 0x15, 0x48, 0xab, 0x5f, 0xb8, 0xcd, + 0x41, 0x02, 0x20, 0x18, 0x15, 0x22, 0xec, 0x8e, 0xca, + 0x07, 0xde, 0x48, 0x60, 0xa4, 0xac, 0xdd, 0x12, 0x90, + 0x9d, 0x83, 0x1c, 0xc5, 0x6c, 0xbb, 0xac, 0x46, 0x22, + 0x08, 0x22, 0x21, 0xa8, 0x76, 0x8d, 0x1d, 0x09], + True, + False, + ), + test( + "long len", + [0x30, 0x45, 0x02, 0x20, 0x4e, 0x45, 0xe1, 0x69, + 0x32, 0xb8, 0xaf, 0x51, 0x49, 0x61, 0xa1, 0xd3, 0xa1, + 0xa2, 0x5f, 0xdf, 0x3f, 0x4f, 0x77, 0x32, 0xe9, 0xd6, + 0x24, 0xc6, 0xc6, 0x15, 0x48, 0xab, 0x5f, 0xb8, 0xcd, + 0x41, 0x02, 0x20, 0x18, 0x15, 0x22, 0xec, 0x8e, 0xca, + 0x07, 0xde, 0x48, 0x60, 0xa4, 0xac, 0xdd, 0x12, 0x90, + 0x9d, 0x83, 0x1c, 0xc5, 0x6c, 0xbb, 0xac, 0x46, 0x22, + 0x08, 0x22, 0x21, 0xa8, 0x76, 0x8d, 0x1d, 0x09], + True, + False, + ), + test( + "long X", + [0x30, 0x44, 0x02, 0x42, 0x4e, 0x45, 0xe1, 0x69, + 0x32, 0xb8, 0xaf, 0x51, 0x49, 0x61, 0xa1, 0xd3, 0xa1, + 0xa2, 0x5f, 0xdf, 0x3f, 0x4f, 0x77, 0x32, 0xe9, 0xd6, + 0x24, 0xc6, 0xc6, 0x15, 0x48, 0xab, 0x5f, 0xb8, 0xcd, + 0x41, 0x02, 0x20, 0x18, 0x15, 0x22, 0xec, 0x8e, 0xca, + 0x07, 0xde, 0x48, 0x60, 0xa4, 0xac, 0xdd, 0x12, 0x90, + 0x9d, 0x83, 0x1c, 0xc5, 0x6c, 0xbb, 0xac, 0x46, 0x22, + 0x08, 0x22, 0x21, 0xa8, 0x76, 0x8d, 0x1d, 0x09], + True, + False, + ), + test( + "long Y", + [0x30, 0x44, 0x02, 0x20, 0x4e, 0x45, 0xe1, 0x69, + 0x32, 0xb8, 0xaf, 0x51, 0x49, 0x61, 0xa1, 0xd3, 0xa1, + 0xa2, 0x5f, 0xdf, 0x3f, 0x4f, 0x77, 0x32, 0xe9, 0xd6, + 0x24, 0xc6, 0xc6, 0x15, 0x48, 0xab, 0x5f, 0xb8, 0xcd, + 0x41, 0x02, 0x21, 0x18, 0x15, 0x22, 0xec, 0x8e, 0xca, + 0x07, 0xde, 0x48, 0x60, 0xa4, 0xac, 0xdd, 0x12, 0x90, + 0x9d, 0x83, 0x1c, 0xc5, 0x6c, 0xbb, 0xac, 0x46, 0x22, + 0x08, 0x22, 0x21, 0xa8, 0x76, 0x8d, 0x1d, 0x09], + True, + False, + ), + test( + "short Y", + [0x30, 0x44, 0x02, 0x20, 0x4e, 0x45, 0xe1, 0x69, + 0x32, 0xb8, 0xaf, 0x51, 0x49, 0x61, 0xa1, 0xd3, 0xa1, + 0xa2, 0x5f, 0xdf, 0x3f, 0x4f, 0x77, 0x32, 0xe9, 0xd6, + 0x24, 0xc6, 0xc6, 0x15, 0x48, 0xab, 0x5f, 0xb8, 0xcd, + 0x41, 0x02, 0x19, 0x18, 0x15, 0x22, 0xec, 0x8e, 0xca, + 0x07, 0xde, 0x48, 0x60, 0xa4, 0xac, 0xdd, 0x12, 0x90, + 0x9d, 0x83, 0x1c, 0xc5, 0x6c, 0xbb, 0xac, 0x46, 0x22, + 0x08, 0x22, 0x21, 0xa8, 0x76, 0x8d, 0x1d, 0x09], + True, + False, + ), + test( + "trailing crap.", + [0x30, 0x44, 0x02, 0x20, 0x4e, 0x45, 0xe1, 0x69, + 0x32, 0xb8, 0xaf, 0x51, 0x49, 0x61, 0xa1, 0xd3, 0xa1, + 0xa2, 0x5f, 0xdf, 0x3f, 0x4f, 0x77, 0x32, 0xe9, 0xd6, + 0x24, 0xc6, 0xc6, 0x15, 0x48, 0xab, 0x5f, 0xb8, 0xcd, + 0x41, 0x02, 0x20, 0x18, 0x15, 0x22, 0xec, 0x8e, 0xca, + 0x07, 0xde, 0x48, 0x60, 0xa4, 0xac, 0xdd, 0x12, 0x90, + 0x9d, 0x83, 0x1c, 0xc5, 0x6c, 0xbb, 0xac, 0x46, 0x22, + 0x08, 0x22, 0x21, 0xa8, 0x76, 0x8d, 0x1d, 0x09, 0x01], + True, + # This test is now passing (used to be failing) because there + # are signatures in the blockchain that have trailing zero + # bytes before the hashtype. So ParseSignature was fixed to + # permit buffers with trailing nonsense after the actual + # signature. + True, + ), + test( + "X == N ", + [0x30, 0x44, 0x02, 0x20, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFE, 0xBA, 0xAE, 0xDC, 0xE6, 0xAF, 0x48, + 0xA0, 0x3B, 0xBF, 0xD2, 0x5E, 0x8C, 0xD0, 0x36, 0x41, + 0x41, 0x02, 0x20, 0x18, 0x15, 0x22, 0xec, 0x8e, 0xca, + 0x07, 0xde, 0x48, 0x60, 0xa4, 0xac, 0xdd, 0x12, 0x90, + 0x9d, 0x83, 0x1c, 0xc5, 0x6c, 0xbb, 0xac, 0x46, 0x22, + 0x08, 0x22, 0x21, 0xa8, 0x76, 0x8d, 0x1d, 0x09], + True, + False, + ), + test( + "X == N ", + [0x30, 0x44, 0x02, 0x20, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFE, 0xBA, 0xAE, 0xDC, 0xE6, 0xAF, 0x48, + 0xA0, 0x3B, 0xBF, 0xD2, 0x5E, 0x8C, 0xD0, 0x36, 0x41, + 0x42, 0x02, 0x20, 0x18, 0x15, 0x22, 0xec, 0x8e, 0xca, + 0x07, 0xde, 0x48, 0x60, 0xa4, 0xac, 0xdd, 0x12, 0x90, + 0x9d, 0x83, 0x1c, 0xc5, 0x6c, 0xbb, 0xac, 0x46, 0x22, + 0x08, 0x22, 0x21, 0xa8, 0x76, 0x8d, 0x1d, 0x09], + False, + False, + ), + test( + "Y == N", + [0x30, 0x44, 0x02, 0x20, 0x4e, 0x45, 0xe1, 0x69, + 0x32, 0xb8, 0xaf, 0x51, 0x49, 0x61, 0xa1, 0xd3, 0xa1, + 0xa2, 0x5f, 0xdf, 0x3f, 0x4f, 0x77, 0x32, 0xe9, 0xd6, + 0x24, 0xc6, 0xc6, 0x15, 0x48, 0xab, 0x5f, 0xb8, 0xcd, + 0x41, 0x02, 0x20, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFE, 0xBA, 0xAE, 0xDC, 0xE6, 0xAF, 0x48, 0xA0, 0x3B, + 0xBF, 0xD2, 0x5E, 0x8C, 0xD0, 0x36, 0x41, 0x41], + True, + False, + ), + test( + "Y > N", + [0x30, 0x44, 0x02, 0x20, 0x4e, 0x45, 0xe1, 0x69, + 0x32, 0xb8, 0xaf, 0x51, 0x49, 0x61, 0xa1, 0xd3, 0xa1, + 0xa2, 0x5f, 0xdf, 0x3f, 0x4f, 0x77, 0x32, 0xe9, 0xd6, + 0x24, 0xc6, 0xc6, 0x15, 0x48, 0xab, 0x5f, 0xb8, 0xcd, + 0x41, 0x02, 0x20, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFE, 0xBA, 0xAE, 0xDC, 0xE6, 0xAF, 0x48, 0xA0, 0x3B, + 0xBF, 0xD2, 0x5E, 0x8C, 0xD0, 0x36, 0x41, 0x42], + False, + False, + ), + test( + "0 len X.", + [0x30, 0x24, 0x02, 0x00, 0x02, 0x20, 0x18, 0x15, + 0x22, 0xec, 0x8e, 0xca, 0x07, 0xde, 0x48, 0x60, 0xa4, + 0xac, 0xdd, 0x12, 0x90, 0x9d, 0x83, 0x1c, 0xc5, 0x6c, + 0xbb, 0xac, 0x46, 0x22, 0x08, 0x22, 0x21, 0xa8, 0x76, + 0x8d, 0x1d, 0x09], + True, + False, + ), + test( + "0 len Y.", + [0x30, 0x24, 0x02, 0x20, 0x4e, 0x45, 0xe1, 0x69, + 0x32, 0xb8, 0xaf, 0x51, 0x49, 0x61, 0xa1, 0xd3, 0xa1, + 0xa2, 0x5f, 0xdf, 0x3f, 0x4f, 0x77, 0x32, 0xe9, 0xd6, + 0x24, 0xc6, 0xc6, 0x15, 0x48, 0xab, 0x5f, 0xb8, 0xcd, + 0x41, 0x02, 0x00], + True, + False, + ), + test( + "extra R padding.", + [0x30, 0x45, 0x02, 0x21, 0x00, 0x4e, 0x45, 0xe1, 0x69, + 0x32, 0xb8, 0xaf, 0x51, 0x49, 0x61, 0xa1, 0xd3, 0xa1, + 0xa2, 0x5f, 0xdf, 0x3f, 0x4f, 0x77, 0x32, 0xe9, 0xd6, + 0x24, 0xc6, 0xc6, 0x15, 0x48, 0xab, 0x5f, 0xb8, 0xcd, + 0x41, 0x02, 0x20, 0x18, 0x15, 0x22, 0xec, 0x8e, 0xca, + 0x07, 0xde, 0x48, 0x60, 0xa4, 0xac, 0xdd, 0x12, 0x90, + 0x9d, 0x83, 0x1c, 0xc5, 0x6c, 0xbb, 0xac, 0x46, 0x22, + 0x08, 0x22, 0x21, 0xa8, 0x76, 0x8d, 0x1d, 0x09], + True, + False, + ), + test( + "extra S padding.", + [0x30, 0x45, 0x02, 0x20, 0x4e, 0x45, 0xe1, 0x69, + 0x32, 0xb8, 0xaf, 0x51, 0x49, 0x61, 0xa1, 0xd3, 0xa1, + 0xa2, 0x5f, 0xdf, 0x3f, 0x4f, 0x77, 0x32, 0xe9, 0xd6, + 0x24, 0xc6, 0xc6, 0x15, 0x48, 0xab, 0x5f, 0xb8, 0xcd, + 0x41, 0x02, 0x21, 0x00, 0x18, 0x15, 0x22, 0xec, 0x8e, 0xca, + 0x07, 0xde, 0x48, 0x60, 0xa4, 0xac, 0xdd, 0x12, 0x90, + 0x9d, 0x83, 0x1c, 0xc5, 0x6c, 0xbb, 0xac, 0x46, 0x22, + 0x08, 0x22, 0x21, 0xa8, 0x76, 0x8d, 0x1d, 0x09], + True, + False, + ), + # Standard checks (in BER format, without checking for 'canonical' DER + # signatures) don't test for negative numbers here because there isn't + # a way that is the same between openssl and go that will mark a number + # as negative. The Go ASN.1 parser marks numbers as negative when + # openssl does not (it doesn't handle negative numbers that I can tell + # at all. When not parsing DER signatures, which is done by by bitcoind + # when accepting transactions into its mempool, we otherwise only check + # for the coordinates being zero. + test( + "X == 0", + [0x30, 0x25, 0x02, 0x01, 0x00, 0x02, 0x20, 0x18, + 0x15, 0x22, 0xec, 0x8e, 0xca, 0x07, 0xde, 0x48, 0x60, + 0xa4, 0xac, 0xdd, 0x12, 0x90, 0x9d, 0x83, 0x1c, 0xc5, + 0x6c, 0xbb, 0xac, 0x46, 0x22, 0x08, 0x22, 0x21, 0xa8, + 0x76, 0x8d, 0x1d, 0x09], + False, + False, + ), + test( + "Y == 0.", + [0x30, 0x25, 0x02, 0x20, 0x4e, 0x45, 0xe1, 0x69, + 0x32, 0xb8, 0xaf, 0x51, 0x49, 0x61, 0xa1, 0xd3, 0xa1, + 0xa2, 0x5f, 0xdf, 0x3f, 0x4f, 0x77, 0x32, 0xe9, 0xd6, + 0x24, 0xc6, 0xc6, 0x15, 0x48, 0xab, 0x5f, 0xb8, 0xcd, + 0x41, 0x02, 0x01, 0x00], + False, + False, + ), + ] + # fmt: on + for test in tests: + try: + Signature.parse(ByteArray(test.sig), test.der) + except Exception: + assert test.isValid is False From 5578dba47d9840aca64978dfb630736e9cd6be35 Mon Sep 17 00:00:00 2001 From: JoeGruff Date: Sat, 4 Jan 2020 14:10:25 +0900 Subject: [PATCH 6/8] Add to pydecred test Add tests for signing txoutput. Not complete. Add ability to create multisig scripts. --- pydecred/txscript.py | 35 +- tests/unit/pydecred/test_pydecred.py | 499 +++++++++++++++++++++++++- tests/unit/pydecred/test_signature.py | 290 --------------- ui/screens.py | 4 +- 4 files changed, 532 insertions(+), 296 deletions(-) delete mode 100644 tests/unit/pydecred/test_signature.py diff --git a/pydecred/txscript.py b/pydecred/txscript.py index a6b5bd94..1dacc191 100644 --- a/pydecred/txscript.py +++ b/pydecred/txscript.py @@ -1360,6 +1360,21 @@ def payToStakeSHScript(addr, stakeCode): return script +def multiSigScript(addrs, nRequired): + if len(addrs) < nRequired: + raise Exception( + "unable to generate multisig script with {} required signatures when there are only {} public keys available".format( + nRequired, len(addrs) + ) + ) + script = ByteArray(addInt(nRequired)) + for addr in addrs: + script += addData(addr.scriptAddress()) + script += addInt(len(addrs)) + script += opcode.OP_CHECKMULTISIG + return script + + def payToSStx(addr): """ payToSStx creates a new script to pay a transaction output to a script hash or @@ -1703,6 +1718,22 @@ def putVarInt(val): return reversed(ByteArray(0xFF, length=9)) | ByteArray(val, length=8).littleEndian() +def addInt(val): + """ + addInt pushes the passed integer to the end of the script. + """ + b = ByteArray(b"") + + # Fast path for small integers and OP_1NEGATE. + if val == 0: + b += opcode.OP_0 + return b + if val == -1 or (val >= 1 and val <= 16): + b += opcode.OP_1 - 1 + val + return b + raise Exception("adding integers over 16 not yet implemented") + + def addData(data): dataLen = len(data) b = ByteArray(b"") @@ -2514,10 +2545,10 @@ def mergeMultiSig(tx, idx, addresses, nRequired, pkScript, sigScript, prevScript # Nothing to merge if either the new or previous signature scripts are # empty. - if len(sigScript) == 0: + if not sigScript or len(sigScript) == 0: return prevScript - if len(prevScript) == 0: + if not prevScript or len(prevScript) == 0: return sigScript # Convenience function to avoid duplication. diff --git a/tests/unit/pydecred/test_pydecred.py b/tests/unit/pydecred/test_pydecred.py index f9b4c840..6b2b5524 100644 --- a/tests/unit/pydecred/test_pydecred.py +++ b/tests/unit/pydecred/test_pydecred.py @@ -1128,6 +1128,293 @@ def test_script_tokenizer(self): % (test_name, tokenizerIdx, test_finalIdx), ) + def test_signature(self): + class test: + def __init__(self, name, sig, der, isValid): + self.name = name + self.sig = sig + self.der = der + self.isValid = isValid + + # fmt: off + tests = [ + # signatures from bitcoin blockchain tx + # 0437cd7f8525ceed2324359c2d0ba26006d92d85 + test( + "valid signature.", + [0x30, 0x44, 0x02, 0x20, 0x4e, 0x45, 0xe1, 0x69, + 0x32, 0xb8, 0xaf, 0x51, 0x49, 0x61, 0xa1, 0xd3, 0xa1, + 0xa2, 0x5f, 0xdf, 0x3f, 0x4f, 0x77, 0x32, 0xe9, 0xd6, + 0x24, 0xc6, 0xc6, 0x15, 0x48, 0xab, 0x5f, 0xb8, 0xcd, + 0x41, 0x02, 0x20, 0x18, 0x15, 0x22, 0xec, 0x8e, 0xca, + 0x07, 0xde, 0x48, 0x60, 0xa4, 0xac, 0xdd, 0x12, 0x90, + 0x9d, 0x83, 0x1c, 0xc5, 0x6c, 0xbb, 0xac, 0x46, 0x22, + 0x08, 0x22, 0x21, 0xa8, 0x76, 0x8d, 0x1d, 0x09], + True, + True, + ), + test( + "empty.", + [], + "", + False, + ), + test( + "bad magic.", + [0x31, 0x44, 0x02, 0x20, 0x4e, 0x45, 0xe1, 0x69, + 0x32, 0xb8, 0xaf, 0x51, 0x49, 0x61, 0xa1, 0xd3, 0xa1, + 0xa2, 0x5f, 0xdf, 0x3f, 0x4f, 0x77, 0x32, 0xe9, 0xd6, + 0x24, 0xc6, 0xc6, 0x15, 0x48, 0xab, 0x5f, 0xb8, 0xcd, + 0x41, 0x02, 0x20, 0x18, 0x15, 0x22, 0xec, 0x8e, 0xca, + 0x07, 0xde, 0x48, 0x60, 0xa4, 0xac, 0xdd, 0x12, 0x90, + 0x9d, 0x83, 0x1c, 0xc5, 0x6c, 0xbb, 0xac, 0x46, 0x22, + 0x08, 0x22, 0x21, 0xa8, 0x76, 0x8d, 0x1d, 0x09], + True, + False, + ), + test( + "bad 1st int marker magic.", + [0x30, 0x44, 0x03, 0x20, 0x4e, 0x45, 0xe1, 0x69, + 0x32, 0xb8, 0xaf, 0x51, 0x49, 0x61, 0xa1, 0xd3, 0xa1, + 0xa2, 0x5f, 0xdf, 0x3f, 0x4f, 0x77, 0x32, 0xe9, 0xd6, + 0x24, 0xc6, 0xc6, 0x15, 0x48, 0xab, 0x5f, 0xb8, 0xcd, + 0x41, 0x02, 0x20, 0x18, 0x15, 0x22, 0xec, 0x8e, 0xca, + 0x07, 0xde, 0x48, 0x60, 0xa4, 0xac, 0xdd, 0x12, 0x90, + 0x9d, 0x83, 0x1c, 0xc5, 0x6c, 0xbb, 0xac, 0x46, 0x22, + 0x08, 0x22, 0x21, 0xa8, 0x76, 0x8d, 0x1d, 0x09], + True, + False, + ), + test( + "bad 2nd int marker.", + [0x30, 0x44, 0x02, 0x20, 0x4e, 0x45, 0xe1, 0x69, + 0x32, 0xb8, 0xaf, 0x51, 0x49, 0x61, 0xa1, 0xd3, 0xa1, + 0xa2, 0x5f, 0xdf, 0x3f, 0x4f, 0x77, 0x32, 0xe9, 0xd6, + 0x24, 0xc6, 0xc6, 0x15, 0x48, 0xab, 0x5f, 0xb8, 0xcd, + 0x41, 0x03, 0x20, 0x18, 0x15, 0x22, 0xec, 0x8e, 0xca, + 0x07, 0xde, 0x48, 0x60, 0xa4, 0xac, 0xdd, 0x12, 0x90, + 0x9d, 0x83, 0x1c, 0xc5, 0x6c, 0xbb, 0xac, 0x46, 0x22, + 0x08, 0x22, 0x21, 0xa8, 0x76, 0x8d, 0x1d, 0x09], + True, + False, + ), + test( + "short len", + [0x30, 0x43, 0x02, 0x20, 0x4e, 0x45, 0xe1, 0x69, + 0x32, 0xb8, 0xaf, 0x51, 0x49, 0x61, 0xa1, 0xd3, 0xa1, + 0xa2, 0x5f, 0xdf, 0x3f, 0x4f, 0x77, 0x32, 0xe9, 0xd6, + 0x24, 0xc6, 0xc6, 0x15, 0x48, 0xab, 0x5f, 0xb8, 0xcd, + 0x41, 0x02, 0x20, 0x18, 0x15, 0x22, 0xec, 0x8e, 0xca, + 0x07, 0xde, 0x48, 0x60, 0xa4, 0xac, 0xdd, 0x12, 0x90, + 0x9d, 0x83, 0x1c, 0xc5, 0x6c, 0xbb, 0xac, 0x46, 0x22, + 0x08, 0x22, 0x21, 0xa8, 0x76, 0x8d, 0x1d, 0x09], + True, + False, + ), + test( + "long len", + [0x30, 0x45, 0x02, 0x20, 0x4e, 0x45, 0xe1, 0x69, + 0x32, 0xb8, 0xaf, 0x51, 0x49, 0x61, 0xa1, 0xd3, 0xa1, + 0xa2, 0x5f, 0xdf, 0x3f, 0x4f, 0x77, 0x32, 0xe9, 0xd6, + 0x24, 0xc6, 0xc6, 0x15, 0x48, 0xab, 0x5f, 0xb8, 0xcd, + 0x41, 0x02, 0x20, 0x18, 0x15, 0x22, 0xec, 0x8e, 0xca, + 0x07, 0xde, 0x48, 0x60, 0xa4, 0xac, 0xdd, 0x12, 0x90, + 0x9d, 0x83, 0x1c, 0xc5, 0x6c, 0xbb, 0xac, 0x46, 0x22, + 0x08, 0x22, 0x21, 0xa8, 0x76, 0x8d, 0x1d, 0x09], + True, + False, + ), + test( + "long X", + [0x30, 0x44, 0x02, 0x42, 0x4e, 0x45, 0xe1, 0x69, + 0x32, 0xb8, 0xaf, 0x51, 0x49, 0x61, 0xa1, 0xd3, 0xa1, + 0xa2, 0x5f, 0xdf, 0x3f, 0x4f, 0x77, 0x32, 0xe9, 0xd6, + 0x24, 0xc6, 0xc6, 0x15, 0x48, 0xab, 0x5f, 0xb8, 0xcd, + 0x41, 0x02, 0x20, 0x18, 0x15, 0x22, 0xec, 0x8e, 0xca, + 0x07, 0xde, 0x48, 0x60, 0xa4, 0xac, 0xdd, 0x12, 0x90, + 0x9d, 0x83, 0x1c, 0xc5, 0x6c, 0xbb, 0xac, 0x46, 0x22, + 0x08, 0x22, 0x21, 0xa8, 0x76, 0x8d, 0x1d, 0x09], + True, + False, + ), + test( + "long Y", + [0x30, 0x44, 0x02, 0x20, 0x4e, 0x45, 0xe1, 0x69, + 0x32, 0xb8, 0xaf, 0x51, 0x49, 0x61, 0xa1, 0xd3, 0xa1, + 0xa2, 0x5f, 0xdf, 0x3f, 0x4f, 0x77, 0x32, 0xe9, 0xd6, + 0x24, 0xc6, 0xc6, 0x15, 0x48, 0xab, 0x5f, 0xb8, 0xcd, + 0x41, 0x02, 0x21, 0x18, 0x15, 0x22, 0xec, 0x8e, 0xca, + 0x07, 0xde, 0x48, 0x60, 0xa4, 0xac, 0xdd, 0x12, 0x90, + 0x9d, 0x83, 0x1c, 0xc5, 0x6c, 0xbb, 0xac, 0x46, 0x22, + 0x08, 0x22, 0x21, 0xa8, 0x76, 0x8d, 0x1d, 0x09], + True, + False, + ), + test( + "short Y", + [0x30, 0x44, 0x02, 0x20, 0x4e, 0x45, 0xe1, 0x69, + 0x32, 0xb8, 0xaf, 0x51, 0x49, 0x61, 0xa1, 0xd3, 0xa1, + 0xa2, 0x5f, 0xdf, 0x3f, 0x4f, 0x77, 0x32, 0xe9, 0xd6, + 0x24, 0xc6, 0xc6, 0x15, 0x48, 0xab, 0x5f, 0xb8, 0xcd, + 0x41, 0x02, 0x19, 0x18, 0x15, 0x22, 0xec, 0x8e, 0xca, + 0x07, 0xde, 0x48, 0x60, 0xa4, 0xac, 0xdd, 0x12, 0x90, + 0x9d, 0x83, 0x1c, 0xc5, 0x6c, 0xbb, 0xac, 0x46, 0x22, + 0x08, 0x22, 0x21, 0xa8, 0x76, 0x8d, 0x1d, 0x09], + True, + False, + ), + test( + "trailing crap.", + [0x30, 0x44, 0x02, 0x20, 0x4e, 0x45, 0xe1, 0x69, + 0x32, 0xb8, 0xaf, 0x51, 0x49, 0x61, 0xa1, 0xd3, 0xa1, + 0xa2, 0x5f, 0xdf, 0x3f, 0x4f, 0x77, 0x32, 0xe9, 0xd6, + 0x24, 0xc6, 0xc6, 0x15, 0x48, 0xab, 0x5f, 0xb8, 0xcd, + 0x41, 0x02, 0x20, 0x18, 0x15, 0x22, 0xec, 0x8e, 0xca, + 0x07, 0xde, 0x48, 0x60, 0xa4, 0xac, 0xdd, 0x12, 0x90, + 0x9d, 0x83, 0x1c, 0xc5, 0x6c, 0xbb, 0xac, 0x46, 0x22, + 0x08, 0x22, 0x21, 0xa8, 0x76, 0x8d, 0x1d, 0x09, 0x01], + True, + # This test is now passing (used to be failing) because there + # are signatures in the blockchain that have trailing zero + # bytes before the hashtype. So ParseSignature was fixed to + # permit buffers with trailing nonsense after the actual + # signature. + True, + ), + test( + "X == N ", + [0x30, 0x44, 0x02, 0x20, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFE, 0xBA, 0xAE, 0xDC, 0xE6, 0xAF, 0x48, + 0xA0, 0x3B, 0xBF, 0xD2, 0x5E, 0x8C, 0xD0, 0x36, 0x41, + 0x41, 0x02, 0x20, 0x18, 0x15, 0x22, 0xec, 0x8e, 0xca, + 0x07, 0xde, 0x48, 0x60, 0xa4, 0xac, 0xdd, 0x12, 0x90, + 0x9d, 0x83, 0x1c, 0xc5, 0x6c, 0xbb, 0xac, 0x46, 0x22, + 0x08, 0x22, 0x21, 0xa8, 0x76, 0x8d, 0x1d, 0x09], + True, + False, + ), + test( + "X == N ", + [0x30, 0x44, 0x02, 0x20, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFE, 0xBA, 0xAE, 0xDC, 0xE6, 0xAF, 0x48, + 0xA0, 0x3B, 0xBF, 0xD2, 0x5E, 0x8C, 0xD0, 0x36, 0x41, + 0x42, 0x02, 0x20, 0x18, 0x15, 0x22, 0xec, 0x8e, 0xca, + 0x07, 0xde, 0x48, 0x60, 0xa4, 0xac, 0xdd, 0x12, 0x90, + 0x9d, 0x83, 0x1c, 0xc5, 0x6c, 0xbb, 0xac, 0x46, 0x22, + 0x08, 0x22, 0x21, 0xa8, 0x76, 0x8d, 0x1d, 0x09], + False, + False, + ), + test( + "Y == N", + [0x30, 0x44, 0x02, 0x20, 0x4e, 0x45, 0xe1, 0x69, + 0x32, 0xb8, 0xaf, 0x51, 0x49, 0x61, 0xa1, 0xd3, 0xa1, + 0xa2, 0x5f, 0xdf, 0x3f, 0x4f, 0x77, 0x32, 0xe9, 0xd6, + 0x24, 0xc6, 0xc6, 0x15, 0x48, 0xab, 0x5f, 0xb8, 0xcd, + 0x41, 0x02, 0x20, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFE, 0xBA, 0xAE, 0xDC, 0xE6, 0xAF, 0x48, 0xA0, 0x3B, + 0xBF, 0xD2, 0x5E, 0x8C, 0xD0, 0x36, 0x41, 0x41], + True, + False, + ), + test( + "Y > N", + [0x30, 0x44, 0x02, 0x20, 0x4e, 0x45, 0xe1, 0x69, + 0x32, 0xb8, 0xaf, 0x51, 0x49, 0x61, 0xa1, 0xd3, 0xa1, + 0xa2, 0x5f, 0xdf, 0x3f, 0x4f, 0x77, 0x32, 0xe9, 0xd6, + 0x24, 0xc6, 0xc6, 0x15, 0x48, 0xab, 0x5f, 0xb8, 0xcd, + 0x41, 0x02, 0x20, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFE, 0xBA, 0xAE, 0xDC, 0xE6, 0xAF, 0x48, 0xA0, 0x3B, + 0xBF, 0xD2, 0x5E, 0x8C, 0xD0, 0x36, 0x41, 0x42], + False, + False, + ), + test( + "0 len X.", + [0x30, 0x24, 0x02, 0x00, 0x02, 0x20, 0x18, 0x15, + 0x22, 0xec, 0x8e, 0xca, 0x07, 0xde, 0x48, 0x60, 0xa4, + 0xac, 0xdd, 0x12, 0x90, 0x9d, 0x83, 0x1c, 0xc5, 0x6c, + 0xbb, 0xac, 0x46, 0x22, 0x08, 0x22, 0x21, 0xa8, 0x76, + 0x8d, 0x1d, 0x09], + True, + False, + ), + test( + "0 len Y.", + [0x30, 0x24, 0x02, 0x20, 0x4e, 0x45, 0xe1, 0x69, + 0x32, 0xb8, 0xaf, 0x51, 0x49, 0x61, 0xa1, 0xd3, 0xa1, + 0xa2, 0x5f, 0xdf, 0x3f, 0x4f, 0x77, 0x32, 0xe9, 0xd6, + 0x24, 0xc6, 0xc6, 0x15, 0x48, 0xab, 0x5f, 0xb8, 0xcd, + 0x41, 0x02, 0x00], + True, + False, + ), + test( + "extra R padding.", + [0x30, 0x45, 0x02, 0x21, 0x00, 0x4e, 0x45, 0xe1, 0x69, + 0x32, 0xb8, 0xaf, 0x51, 0x49, 0x61, 0xa1, 0xd3, 0xa1, + 0xa2, 0x5f, 0xdf, 0x3f, 0x4f, 0x77, 0x32, 0xe9, 0xd6, + 0x24, 0xc6, 0xc6, 0x15, 0x48, 0xab, 0x5f, 0xb8, 0xcd, + 0x41, 0x02, 0x20, 0x18, 0x15, 0x22, 0xec, 0x8e, 0xca, + 0x07, 0xde, 0x48, 0x60, 0xa4, 0xac, 0xdd, 0x12, 0x90, + 0x9d, 0x83, 0x1c, 0xc5, 0x6c, 0xbb, 0xac, 0x46, 0x22, + 0x08, 0x22, 0x21, 0xa8, 0x76, 0x8d, 0x1d, 0x09], + True, + False, + ), + test( + "extra S padding.", + [0x30, 0x45, 0x02, 0x20, 0x4e, 0x45, 0xe1, 0x69, + 0x32, 0xb8, 0xaf, 0x51, 0x49, 0x61, 0xa1, 0xd3, 0xa1, + 0xa2, 0x5f, 0xdf, 0x3f, 0x4f, 0x77, 0x32, 0xe9, 0xd6, + 0x24, 0xc6, 0xc6, 0x15, 0x48, 0xab, 0x5f, 0xb8, 0xcd, + 0x41, 0x02, 0x21, 0x00, 0x18, 0x15, 0x22, 0xec, 0x8e, 0xca, + 0x07, 0xde, 0x48, 0x60, 0xa4, 0xac, 0xdd, 0x12, 0x90, + 0x9d, 0x83, 0x1c, 0xc5, 0x6c, 0xbb, 0xac, 0x46, 0x22, + 0x08, 0x22, 0x21, 0xa8, 0x76, 0x8d, 0x1d, 0x09], + True, + False, + ), + # Standard checks (in BER format, without checking for 'canonical' DER + # signatures) don't test for negative numbers here because there isn't + # a way that is the same between openssl and go that will mark a number + # as negative. The Go ASN.1 parser marks numbers as negative when + # openssl does not (it doesn't handle negative numbers that I can tell + # at all. When not parsing DER signatures, which is done by by bitcoind + # when accepting transactions into its mempool, we otherwise only check + # for the coordinates being zero. + test( + "X == 0", + [0x30, 0x25, 0x02, 0x01, 0x00, 0x02, 0x20, 0x18, + 0x15, 0x22, 0xec, 0x8e, 0xca, 0x07, 0xde, 0x48, 0x60, + 0xa4, 0xac, 0xdd, 0x12, 0x90, 0x9d, 0x83, 0x1c, 0xc5, + 0x6c, 0xbb, 0xac, 0x46, 0x22, 0x08, 0x22, 0x21, 0xa8, + 0x76, 0x8d, 0x1d, 0x09], + False, + False, + ), + test( + "Y == 0.", + [0x30, 0x25, 0x02, 0x20, 0x4e, 0x45, 0xe1, 0x69, + 0x32, 0xb8, 0xaf, 0x51, 0x49, 0x61, 0xa1, 0xd3, 0xa1, + 0xa2, 0x5f, 0xdf, 0x3f, 0x4f, 0x77, 0x32, 0xe9, 0xd6, + 0x24, 0xc6, 0xc6, 0x15, 0x48, 0xab, 0x5f, 0xb8, 0xcd, + 0x41, 0x02, 0x01, 0x00], + False, + False, + ), + ] + # fmt: on + for test in tests: + try: + txscript.Signature.parse(ByteArray(test.sig), test.der) + except Exception: + assert test.isValid is False + def test_sign_tx(self): """ Based on dcrd TestSignTxOutput. @@ -1248,7 +1535,7 @@ def test_sign_tx(self): # ), # ) - # Pay to Pubkey Hash (uncompressed) + # Pay to Pubkey Hash (compressed) testingParams = mainnet for hashType in hashTypes: for suite in signatureSuites: @@ -1297,7 +1584,215 @@ def priv(addr): ByteArray(sigStr), msg="%d:%d:%d" % (hashType, idx, suite), ) - return + + # Pay to Pubkey Hash for a ticket (SStx) (compressed) + # For compressed keys + tests = ( + ( + "b78a743c0c6557f24a51192b82925942ebade0be86efd7dad58b9fa358d3857c", + "4730440220411b0a068d5b1c5fd6ec98a0e3f17ce632a863a9d57876c0bde2647a8dcd26c602204f05f109f0f185cc79a43168411075eb58fd350cc135f4872b0b8c81015e21c3012102e11d2c0e415343435294079ac0774a21c8e6b1e6fd9b671cb08af43a397f3df1", + ), + ( + "a00616c21b117ba621d4c72faf30d30cd665416bdc3c24e549de2348ac68cfb8", + "473044022050a359daf7db3db11e95ceb8494173f8ca168b32ccc6cc57dcad5f78564678af02200c09e2c7c72736ef9835f05eb0c6eb72fdd2e1e98cdaf7af7f2d9523ed5f410501210224397bd81b0e80ec1bbfe104fb251b57eb0adcf044c3eec05d913e2e8e04396b", + ), + ( + "8902ea1f64c6fb7aa40dfbe798f5dc53b466a3fc01534e867581936a8ecbff5b", + "4730440220257fe3c52ce408561aec4446c30bca6d6fad98ba554917c4e7714a89badbfdbf02201aa569c5e28d728dd20ce32656915729ebc6679527bfe2401ea3723791e04538012103255f71eab9eb2a7e3f822569484448acbe2880d61b4db61020f73fd54cbe370d", + ), + ) + + testingParams = mainnet + for hashType in hashTypes: + for suite in signatureSuites: + for idx in range(len(tx.txIn)): + # var keyDB, pkBytes []byte + # var key chainec.PrivateKey + # var pk chainec.PublicKey + kStr, sigStr = tests[idx] + + if suite == crypto.STEcdsaSecp256k1: + # k = Curve.generateKey(rand.Reader) + k = ByteArray(kStr) + privKey = crypto.privKeyFromBytes(k) + pkBytes = privKey.pub.serializeCompressed() + else: + raise Exception( + "test for signature suite %d not implemented" % suite + ) + + address = crypto.newAddressPubKeyHash( + crypto.hash160(pkBytes.bytes()), testingParams, suite + ) + + pkScript = txscript.payToSStx(address) + + class keysource: + @staticmethod + def priv(addr): + return privKey + + sigScript = txscript.signTxOutput( + testingParams, + tx, + idx, + pkScript, + hashType, + keysource, + None, + suite, + ) + + self.assertEqual( + sigScript, + ByteArray(sigStr), + msg="%d:%d:%d" % (hashType, idx, suite), + ) + + # Pay to Pubkey Hash for a ticket revocation (SSRtx) (compressed) + # For compressed keys + tests = ( + ( + "b78a743c0c6557f24a51192b82925942ebade0be86efd7dad58b9fa358d3857c", + "483045022100ad46b5bd365af6964562bfac90abad9d9cf30fdc53ae4011103c646df04a7d5f022076209ea5626cb9a3f16add11c361f6f66c7436eec8efe1688e43ac9f71a86b88012102e11d2c0e415343435294079ac0774a21c8e6b1e6fd9b671cb08af43a397f3df1", + ), + ( + "a00616c21b117ba621d4c72faf30d30cd665416bdc3c24e549de2348ac68cfb8", + "483045022100eeacc7f3fcba009f6ab319b2221e64d52d94d5009cfd037ef03c86dc1bcb2c990220212000f05d1a904d3d995b18b8b94bd0e84dc35aa308df5149094678f6cd40e501210224397bd81b0e80ec1bbfe104fb251b57eb0adcf044c3eec05d913e2e8e04396b", + ), + ( + "8902ea1f64c6fb7aa40dfbe798f5dc53b466a3fc01534e867581936a8ecbff5b", + "47304402200fa66dd2be65cd8c0e89bc299b99cadac36805af627432cbdc968c53b4c4f41b02200b117b145dfdb6ba7846b9b02c63d85d11bfc2188f58f083da6bb88220a9e517012103255f71eab9eb2a7e3f822569484448acbe2880d61b4db61020f73fd54cbe370d", + ), + ) + + testingParams = mainnet + for hashType in hashTypes: + for suite in signatureSuites: + for idx in range(len(tx.txIn)): + # var keyDB, pkBytes []byte + # var key chainec.PrivateKey + # var pk chainec.PublicKey + kStr, sigStr = tests[idx] + + if suite == crypto.STEcdsaSecp256k1: + # k = Curve.generateKey(rand.Reader) + k = ByteArray(kStr) + privKey = crypto.privKeyFromBytes(k) + pkBytes = privKey.pub.serializeCompressed() + else: + raise Exception( + "test for signature suite %d not implemented" % suite + ) + + address = crypto.newAddressPubKeyHash( + crypto.hash160(pkBytes.bytes()), testingParams, suite + ) + + pkScript = txscript.payToSSRtxPKHDirect( + txscript.decodeAddress( + address.string(), testingParams + ).scriptAddress() + ) + + class keysource: + @staticmethod + def priv(addr): + return privKey + + sigScript = txscript.signTxOutput( + testingParams, + tx, + idx, + pkScript, + hashType, + keysource, + None, + suite, + ) + + self.assertEqual( + sigScript, + ByteArray(sigStr), + msg="%d:%d:%d" % (hashType, idx, suite), + ) + + # Basic Multisig (compressed) + # For compressed keys + tests = ( + ( + "b78a743c0c6557f24a51192b82925942ebade0be86efd7dad58b9fa358d3857c", + "483045022100f12b12474e64b807eaeda6ac05b26d4b6bee2519385a84815f4ec2ccdf0aa45b022055c590d36a172c4735c8886572723037dc65329e70b8e5e012a9ec24993c284201483045022100ae2fec7236910b0bbc5eab37b7d987d61f22139f6381f2cc9781373e4f470c37022037d8b1658c2a83c40cc1b97036239eb0f4b313f3d2bf4558de33412e834c45d50147522102e11d2c0e415343435294079ac0774a21c8e6b1e6fd9b671cb08af43a397f3df1210224397bd81b0e80ec1bbfe104fb251b57eb0adcf044c3eec05d913e2e8e04396b52ae", + ), + ( + "a00616c21b117ba621d4c72faf30d30cd665416bdc3c24e549de2348ac68cfb8", + "473044022047b34afd287cacbc4ba0d95d985b23a55069c0bd81d61eb32435348bef2dc6c602201e4c7c0c437d4d53172cac355eadd70c8b87d3936c7a0a0179201b9b9327852d01483045022100df1975379ac38dcc5caddb1f55974b5b08a22b4fdb6e88be9ba12da0c0ecfbed022042bc3420adde7410f463caa998a460d58b214bf082e004b5067a4c0f061e0769014752210224397bd81b0e80ec1bbfe104fb251b57eb0adcf044c3eec05d913e2e8e04396b2103255f71eab9eb2a7e3f822569484448acbe2880d61b4db61020f73fd54cbe370d52ae", + ), + ( + "8902ea1f64c6fb7aa40dfbe798f5dc53b466a3fc01534e867581936a8ecbff5b", + "473044022002d1251cb8a2f1a20225948f99e6c71a188915c3ca0dc433ca9c35c050ee1dd602206880d041a9a9f9888ab751a371768bffd89251edf354eccdac73fe1376095ba20147304402204ddebf367aea5750123c2b4807815487d07239c776b6cc70a99c46a8b3261f4c022044549b4aeda7eb08692fa500b5518655be61fd5299c07adf0caddf41ab391dd00147522103255f71eab9eb2a7e3f822569484448acbe2880d61b4db61020f73fd54cbe370d2102e11d2c0e415343435294079ac0774a21c8e6b1e6fd9b671cb08af43a397f3df152ae", + ), + ) + + testingParams = mainnet + for hashType in hashTypes: + # TODO enable this test after script-hash script signing is implemented + break + for suite in signatureSuites: + for idx in range(len(tx.txIn)): + # var keyDB, pkBytes []byte + # var key chainec.PrivateKey + # var pk chainec.PublicKey + kStr, sigStr = tests[idx] + kStr2, _ = tests[(idx + 1) % 3] + + if suite == crypto.STEcdsaSecp256k1: + # k = Curve.generateKey(rand.Reader) + k = ByteArray(kStr) + k2 = ByteArray(kStr2) + privKey = crypto.privKeyFromBytes(k) + privKey2 = crypto.privKeyFromBytes(k2) + pkBytes = privKey.pub.serializeCompressed() + pkBytes2 = privKey2.pub.serializeCompressed() + else: + raise Exception( + "test for signature suite %d not implemented" % suite + ) + + address = crypto.AddressSecpPubKey(pkBytes.bytes(), testingParams) + + address2 = crypto.AddressSecpPubKey(pkBytes2.bytes(), testingParams) + + pkScript = txscript.multiSigScript([address, address2], 2) + + scriptAddr = crypto.newAddressScriptHash(pkScript, testingParams) + + scriptPkScript = txscript.payToAddrScript(scriptAddr) + + keys = iter([privKey, privKey2]) + + class keysource: + @staticmethod + def priv(addr): + return next(keys) + + sigScript = txscript.signTxOutput( + testingParams, + tx, + idx, + scriptPkScript, + hashType, + keysource, + None, + suite, + ) + print(sigScript.hex()) + + self.assertEqual( + sigScript, + ByteArray(sigStr), + msg="%d:%d:%d" % (hashType, idx, suite), + ) def test_sign_stake_p2pkh_outputs(self): from tinydecred.crypto.secp256k1 import curve as Curve diff --git a/tests/unit/pydecred/test_signature.py b/tests/unit/pydecred/test_signature.py deleted file mode 100644 index 44ffe750..00000000 --- a/tests/unit/pydecred/test_signature.py +++ /dev/null @@ -1,290 +0,0 @@ -from tinydecred.crypto.bytearray import ByteArray -from tinydecred.pydecred.txscript import Signature - - -def test_signature(): - class test: - def __init__(self, name, sig, der, isValid): - self.name = name - self.sig = sig - self.der = der - self.isValid = isValid - - # fmt: off - tests = [ - # signatures from bitcoin blockchain tx - # 0437cd7f8525ceed2324359c2d0ba26006d92d85 - test( - "valid signature.", - [0x30, 0x44, 0x02, 0x20, 0x4e, 0x45, 0xe1, 0x69, - 0x32, 0xb8, 0xaf, 0x51, 0x49, 0x61, 0xa1, 0xd3, 0xa1, - 0xa2, 0x5f, 0xdf, 0x3f, 0x4f, 0x77, 0x32, 0xe9, 0xd6, - 0x24, 0xc6, 0xc6, 0x15, 0x48, 0xab, 0x5f, 0xb8, 0xcd, - 0x41, 0x02, 0x20, 0x18, 0x15, 0x22, 0xec, 0x8e, 0xca, - 0x07, 0xde, 0x48, 0x60, 0xa4, 0xac, 0xdd, 0x12, 0x90, - 0x9d, 0x83, 0x1c, 0xc5, 0x6c, 0xbb, 0xac, 0x46, 0x22, - 0x08, 0x22, 0x21, 0xa8, 0x76, 0x8d, 0x1d, 0x09], - True, - True, - ), - test( - "empty.", - [], - "", - False, - ), - test( - "bad magic.", - [0x31, 0x44, 0x02, 0x20, 0x4e, 0x45, 0xe1, 0x69, - 0x32, 0xb8, 0xaf, 0x51, 0x49, 0x61, 0xa1, 0xd3, 0xa1, - 0xa2, 0x5f, 0xdf, 0x3f, 0x4f, 0x77, 0x32, 0xe9, 0xd6, - 0x24, 0xc6, 0xc6, 0x15, 0x48, 0xab, 0x5f, 0xb8, 0xcd, - 0x41, 0x02, 0x20, 0x18, 0x15, 0x22, 0xec, 0x8e, 0xca, - 0x07, 0xde, 0x48, 0x60, 0xa4, 0xac, 0xdd, 0x12, 0x90, - 0x9d, 0x83, 0x1c, 0xc5, 0x6c, 0xbb, 0xac, 0x46, 0x22, - 0x08, 0x22, 0x21, 0xa8, 0x76, 0x8d, 0x1d, 0x09], - True, - False, - ), - test( - "bad 1st int marker magic.", - [0x30, 0x44, 0x03, 0x20, 0x4e, 0x45, 0xe1, 0x69, - 0x32, 0xb8, 0xaf, 0x51, 0x49, 0x61, 0xa1, 0xd3, 0xa1, - 0xa2, 0x5f, 0xdf, 0x3f, 0x4f, 0x77, 0x32, 0xe9, 0xd6, - 0x24, 0xc6, 0xc6, 0x15, 0x48, 0xab, 0x5f, 0xb8, 0xcd, - 0x41, 0x02, 0x20, 0x18, 0x15, 0x22, 0xec, 0x8e, 0xca, - 0x07, 0xde, 0x48, 0x60, 0xa4, 0xac, 0xdd, 0x12, 0x90, - 0x9d, 0x83, 0x1c, 0xc5, 0x6c, 0xbb, 0xac, 0x46, 0x22, - 0x08, 0x22, 0x21, 0xa8, 0x76, 0x8d, 0x1d, 0x09], - True, - False, - ), - test( - "bad 2nd int marker.", - [0x30, 0x44, 0x02, 0x20, 0x4e, 0x45, 0xe1, 0x69, - 0x32, 0xb8, 0xaf, 0x51, 0x49, 0x61, 0xa1, 0xd3, 0xa1, - 0xa2, 0x5f, 0xdf, 0x3f, 0x4f, 0x77, 0x32, 0xe9, 0xd6, - 0x24, 0xc6, 0xc6, 0x15, 0x48, 0xab, 0x5f, 0xb8, 0xcd, - 0x41, 0x03, 0x20, 0x18, 0x15, 0x22, 0xec, 0x8e, 0xca, - 0x07, 0xde, 0x48, 0x60, 0xa4, 0xac, 0xdd, 0x12, 0x90, - 0x9d, 0x83, 0x1c, 0xc5, 0x6c, 0xbb, 0xac, 0x46, 0x22, - 0x08, 0x22, 0x21, 0xa8, 0x76, 0x8d, 0x1d, 0x09], - True, - False, - ), - test( - "short len", - [0x30, 0x43, 0x02, 0x20, 0x4e, 0x45, 0xe1, 0x69, - 0x32, 0xb8, 0xaf, 0x51, 0x49, 0x61, 0xa1, 0xd3, 0xa1, - 0xa2, 0x5f, 0xdf, 0x3f, 0x4f, 0x77, 0x32, 0xe9, 0xd6, - 0x24, 0xc6, 0xc6, 0x15, 0x48, 0xab, 0x5f, 0xb8, 0xcd, - 0x41, 0x02, 0x20, 0x18, 0x15, 0x22, 0xec, 0x8e, 0xca, - 0x07, 0xde, 0x48, 0x60, 0xa4, 0xac, 0xdd, 0x12, 0x90, - 0x9d, 0x83, 0x1c, 0xc5, 0x6c, 0xbb, 0xac, 0x46, 0x22, - 0x08, 0x22, 0x21, 0xa8, 0x76, 0x8d, 0x1d, 0x09], - True, - False, - ), - test( - "long len", - [0x30, 0x45, 0x02, 0x20, 0x4e, 0x45, 0xe1, 0x69, - 0x32, 0xb8, 0xaf, 0x51, 0x49, 0x61, 0xa1, 0xd3, 0xa1, - 0xa2, 0x5f, 0xdf, 0x3f, 0x4f, 0x77, 0x32, 0xe9, 0xd6, - 0x24, 0xc6, 0xc6, 0x15, 0x48, 0xab, 0x5f, 0xb8, 0xcd, - 0x41, 0x02, 0x20, 0x18, 0x15, 0x22, 0xec, 0x8e, 0xca, - 0x07, 0xde, 0x48, 0x60, 0xa4, 0xac, 0xdd, 0x12, 0x90, - 0x9d, 0x83, 0x1c, 0xc5, 0x6c, 0xbb, 0xac, 0x46, 0x22, - 0x08, 0x22, 0x21, 0xa8, 0x76, 0x8d, 0x1d, 0x09], - True, - False, - ), - test( - "long X", - [0x30, 0x44, 0x02, 0x42, 0x4e, 0x45, 0xe1, 0x69, - 0x32, 0xb8, 0xaf, 0x51, 0x49, 0x61, 0xa1, 0xd3, 0xa1, - 0xa2, 0x5f, 0xdf, 0x3f, 0x4f, 0x77, 0x32, 0xe9, 0xd6, - 0x24, 0xc6, 0xc6, 0x15, 0x48, 0xab, 0x5f, 0xb8, 0xcd, - 0x41, 0x02, 0x20, 0x18, 0x15, 0x22, 0xec, 0x8e, 0xca, - 0x07, 0xde, 0x48, 0x60, 0xa4, 0xac, 0xdd, 0x12, 0x90, - 0x9d, 0x83, 0x1c, 0xc5, 0x6c, 0xbb, 0xac, 0x46, 0x22, - 0x08, 0x22, 0x21, 0xa8, 0x76, 0x8d, 0x1d, 0x09], - True, - False, - ), - test( - "long Y", - [0x30, 0x44, 0x02, 0x20, 0x4e, 0x45, 0xe1, 0x69, - 0x32, 0xb8, 0xaf, 0x51, 0x49, 0x61, 0xa1, 0xd3, 0xa1, - 0xa2, 0x5f, 0xdf, 0x3f, 0x4f, 0x77, 0x32, 0xe9, 0xd6, - 0x24, 0xc6, 0xc6, 0x15, 0x48, 0xab, 0x5f, 0xb8, 0xcd, - 0x41, 0x02, 0x21, 0x18, 0x15, 0x22, 0xec, 0x8e, 0xca, - 0x07, 0xde, 0x48, 0x60, 0xa4, 0xac, 0xdd, 0x12, 0x90, - 0x9d, 0x83, 0x1c, 0xc5, 0x6c, 0xbb, 0xac, 0x46, 0x22, - 0x08, 0x22, 0x21, 0xa8, 0x76, 0x8d, 0x1d, 0x09], - True, - False, - ), - test( - "short Y", - [0x30, 0x44, 0x02, 0x20, 0x4e, 0x45, 0xe1, 0x69, - 0x32, 0xb8, 0xaf, 0x51, 0x49, 0x61, 0xa1, 0xd3, 0xa1, - 0xa2, 0x5f, 0xdf, 0x3f, 0x4f, 0x77, 0x32, 0xe9, 0xd6, - 0x24, 0xc6, 0xc6, 0x15, 0x48, 0xab, 0x5f, 0xb8, 0xcd, - 0x41, 0x02, 0x19, 0x18, 0x15, 0x22, 0xec, 0x8e, 0xca, - 0x07, 0xde, 0x48, 0x60, 0xa4, 0xac, 0xdd, 0x12, 0x90, - 0x9d, 0x83, 0x1c, 0xc5, 0x6c, 0xbb, 0xac, 0x46, 0x22, - 0x08, 0x22, 0x21, 0xa8, 0x76, 0x8d, 0x1d, 0x09], - True, - False, - ), - test( - "trailing crap.", - [0x30, 0x44, 0x02, 0x20, 0x4e, 0x45, 0xe1, 0x69, - 0x32, 0xb8, 0xaf, 0x51, 0x49, 0x61, 0xa1, 0xd3, 0xa1, - 0xa2, 0x5f, 0xdf, 0x3f, 0x4f, 0x77, 0x32, 0xe9, 0xd6, - 0x24, 0xc6, 0xc6, 0x15, 0x48, 0xab, 0x5f, 0xb8, 0xcd, - 0x41, 0x02, 0x20, 0x18, 0x15, 0x22, 0xec, 0x8e, 0xca, - 0x07, 0xde, 0x48, 0x60, 0xa4, 0xac, 0xdd, 0x12, 0x90, - 0x9d, 0x83, 0x1c, 0xc5, 0x6c, 0xbb, 0xac, 0x46, 0x22, - 0x08, 0x22, 0x21, 0xa8, 0x76, 0x8d, 0x1d, 0x09, 0x01], - True, - # This test is now passing (used to be failing) because there - # are signatures in the blockchain that have trailing zero - # bytes before the hashtype. So ParseSignature was fixed to - # permit buffers with trailing nonsense after the actual - # signature. - True, - ), - test( - "X == N ", - [0x30, 0x44, 0x02, 0x20, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFE, 0xBA, 0xAE, 0xDC, 0xE6, 0xAF, 0x48, - 0xA0, 0x3B, 0xBF, 0xD2, 0x5E, 0x8C, 0xD0, 0x36, 0x41, - 0x41, 0x02, 0x20, 0x18, 0x15, 0x22, 0xec, 0x8e, 0xca, - 0x07, 0xde, 0x48, 0x60, 0xa4, 0xac, 0xdd, 0x12, 0x90, - 0x9d, 0x83, 0x1c, 0xc5, 0x6c, 0xbb, 0xac, 0x46, 0x22, - 0x08, 0x22, 0x21, 0xa8, 0x76, 0x8d, 0x1d, 0x09], - True, - False, - ), - test( - "X == N ", - [0x30, 0x44, 0x02, 0x20, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFE, 0xBA, 0xAE, 0xDC, 0xE6, 0xAF, 0x48, - 0xA0, 0x3B, 0xBF, 0xD2, 0x5E, 0x8C, 0xD0, 0x36, 0x41, - 0x42, 0x02, 0x20, 0x18, 0x15, 0x22, 0xec, 0x8e, 0xca, - 0x07, 0xde, 0x48, 0x60, 0xa4, 0xac, 0xdd, 0x12, 0x90, - 0x9d, 0x83, 0x1c, 0xc5, 0x6c, 0xbb, 0xac, 0x46, 0x22, - 0x08, 0x22, 0x21, 0xa8, 0x76, 0x8d, 0x1d, 0x09], - False, - False, - ), - test( - "Y == N", - [0x30, 0x44, 0x02, 0x20, 0x4e, 0x45, 0xe1, 0x69, - 0x32, 0xb8, 0xaf, 0x51, 0x49, 0x61, 0xa1, 0xd3, 0xa1, - 0xa2, 0x5f, 0xdf, 0x3f, 0x4f, 0x77, 0x32, 0xe9, 0xd6, - 0x24, 0xc6, 0xc6, 0x15, 0x48, 0xab, 0x5f, 0xb8, 0xcd, - 0x41, 0x02, 0x20, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFE, 0xBA, 0xAE, 0xDC, 0xE6, 0xAF, 0x48, 0xA0, 0x3B, - 0xBF, 0xD2, 0x5E, 0x8C, 0xD0, 0x36, 0x41, 0x41], - True, - False, - ), - test( - "Y > N", - [0x30, 0x44, 0x02, 0x20, 0x4e, 0x45, 0xe1, 0x69, - 0x32, 0xb8, 0xaf, 0x51, 0x49, 0x61, 0xa1, 0xd3, 0xa1, - 0xa2, 0x5f, 0xdf, 0x3f, 0x4f, 0x77, 0x32, 0xe9, 0xd6, - 0x24, 0xc6, 0xc6, 0x15, 0x48, 0xab, 0x5f, 0xb8, 0xcd, - 0x41, 0x02, 0x20, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFE, 0xBA, 0xAE, 0xDC, 0xE6, 0xAF, 0x48, 0xA0, 0x3B, - 0xBF, 0xD2, 0x5E, 0x8C, 0xD0, 0x36, 0x41, 0x42], - False, - False, - ), - test( - "0 len X.", - [0x30, 0x24, 0x02, 0x00, 0x02, 0x20, 0x18, 0x15, - 0x22, 0xec, 0x8e, 0xca, 0x07, 0xde, 0x48, 0x60, 0xa4, - 0xac, 0xdd, 0x12, 0x90, 0x9d, 0x83, 0x1c, 0xc5, 0x6c, - 0xbb, 0xac, 0x46, 0x22, 0x08, 0x22, 0x21, 0xa8, 0x76, - 0x8d, 0x1d, 0x09], - True, - False, - ), - test( - "0 len Y.", - [0x30, 0x24, 0x02, 0x20, 0x4e, 0x45, 0xe1, 0x69, - 0x32, 0xb8, 0xaf, 0x51, 0x49, 0x61, 0xa1, 0xd3, 0xa1, - 0xa2, 0x5f, 0xdf, 0x3f, 0x4f, 0x77, 0x32, 0xe9, 0xd6, - 0x24, 0xc6, 0xc6, 0x15, 0x48, 0xab, 0x5f, 0xb8, 0xcd, - 0x41, 0x02, 0x00], - True, - False, - ), - test( - "extra R padding.", - [0x30, 0x45, 0x02, 0x21, 0x00, 0x4e, 0x45, 0xe1, 0x69, - 0x32, 0xb8, 0xaf, 0x51, 0x49, 0x61, 0xa1, 0xd3, 0xa1, - 0xa2, 0x5f, 0xdf, 0x3f, 0x4f, 0x77, 0x32, 0xe9, 0xd6, - 0x24, 0xc6, 0xc6, 0x15, 0x48, 0xab, 0x5f, 0xb8, 0xcd, - 0x41, 0x02, 0x20, 0x18, 0x15, 0x22, 0xec, 0x8e, 0xca, - 0x07, 0xde, 0x48, 0x60, 0xa4, 0xac, 0xdd, 0x12, 0x90, - 0x9d, 0x83, 0x1c, 0xc5, 0x6c, 0xbb, 0xac, 0x46, 0x22, - 0x08, 0x22, 0x21, 0xa8, 0x76, 0x8d, 0x1d, 0x09], - True, - False, - ), - test( - "extra S padding.", - [0x30, 0x45, 0x02, 0x20, 0x4e, 0x45, 0xe1, 0x69, - 0x32, 0xb8, 0xaf, 0x51, 0x49, 0x61, 0xa1, 0xd3, 0xa1, - 0xa2, 0x5f, 0xdf, 0x3f, 0x4f, 0x77, 0x32, 0xe9, 0xd6, - 0x24, 0xc6, 0xc6, 0x15, 0x48, 0xab, 0x5f, 0xb8, 0xcd, - 0x41, 0x02, 0x21, 0x00, 0x18, 0x15, 0x22, 0xec, 0x8e, 0xca, - 0x07, 0xde, 0x48, 0x60, 0xa4, 0xac, 0xdd, 0x12, 0x90, - 0x9d, 0x83, 0x1c, 0xc5, 0x6c, 0xbb, 0xac, 0x46, 0x22, - 0x08, 0x22, 0x21, 0xa8, 0x76, 0x8d, 0x1d, 0x09], - True, - False, - ), - # Standard checks (in BER format, without checking for 'canonical' DER - # signatures) don't test for negative numbers here because there isn't - # a way that is the same between openssl and go that will mark a number - # as negative. The Go ASN.1 parser marks numbers as negative when - # openssl does not (it doesn't handle negative numbers that I can tell - # at all. When not parsing DER signatures, which is done by by bitcoind - # when accepting transactions into its mempool, we otherwise only check - # for the coordinates being zero. - test( - "X == 0", - [0x30, 0x25, 0x02, 0x01, 0x00, 0x02, 0x20, 0x18, - 0x15, 0x22, 0xec, 0x8e, 0xca, 0x07, 0xde, 0x48, 0x60, - 0xa4, 0xac, 0xdd, 0x12, 0x90, 0x9d, 0x83, 0x1c, 0xc5, - 0x6c, 0xbb, 0xac, 0x46, 0x22, 0x08, 0x22, 0x21, 0xa8, - 0x76, 0x8d, 0x1d, 0x09], - False, - False, - ), - test( - "Y == 0.", - [0x30, 0x25, 0x02, 0x20, 0x4e, 0x45, 0xe1, 0x69, - 0x32, 0xb8, 0xaf, 0x51, 0x49, 0x61, 0xa1, 0xd3, 0xa1, - 0xa2, 0x5f, 0xdf, 0x3f, 0x4f, 0x77, 0x32, 0xe9, 0xd6, - 0x24, 0xc6, 0xc6, 0x15, 0x48, 0xab, 0x5f, 0xb8, 0xcd, - 0x41, 0x02, 0x01, 0x00], - False, - False, - ), - ] - # fmt: on - for test in tests: - try: - Signature.parse(ByteArray(test.sig), test.der) - except Exception: - assert test.isValid is False diff --git a/ui/screens.py b/ui/screens.py index b3b78422..b11c2654 100644 --- a/ui/screens.py +++ b/ui/screens.py @@ -1151,11 +1151,11 @@ def __init__(self, app): self.layout.addWidget(wgt) # A button to view agendas and choose how to vote. - agendaBtn = app.getButton(TINY, "Voting") + agendaBtn = app.getButton(TINY, "voting") agendaBtn.clicked.connect(self.stackAgendas) # A button to revoke expired and missed tickets. - revokeBtn = app.getButton(TINY, "Revoke") + revokeBtn = app.getButton(TINY, "") revokeBtn.clicked.connect(self.revokeTickets) votingWgt, _ = Q.makeSeries(Q.HORIZONTAL, agendaBtn, revokeBtn) self.revokeBtn = revokeBtn From 21bf7af20724af3c5d1f754b59c05b09ed77d6fe Mon Sep 17 00:00:00 2001 From: Brian Date: Sat, 4 Jan 2020 04:48:05 -0600 Subject: [PATCH 7/8] scriptNumBytes --- pydecred/txscript.py | 39 +++++++++++++++++++++- tests/unit/pydecred/test_pydecred.py | 49 ++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/pydecred/txscript.py b/pydecred/txscript.py index 1dacc191..6f195bce 100644 --- a/pydecred/txscript.py +++ b/pydecred/txscript.py @@ -635,6 +635,43 @@ def canonicalizeInt(val): return b +def scriptNumBytes(n): + """ + scriptNumBytes returns a minimal encoding for a signed integer as bytes. + Based on dcrd/txscript (scriptNum).Bytes. + + Args: + n (int): The integer to encode. + + Returns: + ByteArray: The encoded bytes. + """ + if n == 0: + return ByteArray() + + isNegative = n < 0 + if isNegative: + n = -n + + result = ByteArray(length=9) + i = 0 + while n > 0: + result[i] = n & 0xFF + n = n >> 8 + i += 1 + + if result[i - 1] & 0x80 != 0: + extraByte = 0x00 + if isNegative: + extraByte = 0x80 + result[i] = extraByte + i += 1 + elif isNegative: + result[i - 1] |= 0x80 + + return result[:i] + + def hashToInt(h): """ hashToInt converts a hash value to an integer. There is some disagreement @@ -1731,7 +1768,7 @@ def addInt(val): if val == -1 or (val >= 1 and val <= 16): b += opcode.OP_1 - 1 + val return b - raise Exception("adding integers over 16 not yet implemented") + raise addData(scriptNumBytes(val)) def addData(data): diff --git a/tests/unit/pydecred/test_pydecred.py b/tests/unit/pydecred/test_pydecred.py index 6b2b5524..baccde04 100644 --- a/tests/unit/pydecred/test_pydecred.py +++ b/tests/unit/pydecred/test_pydecred.py @@ -2734,6 +2734,55 @@ def test_calc_signature_hash_reference(self): self.assertEqual(sigHash, expectedHash) + def test_scriptNumBytes(self): + tests = [ + (0, ByteArray()), + (1, ByteArray("01")), + (-1, ByteArray("81")), + (127, ByteArray("7f")), + (-127, ByteArray("ff")), + (128, ByteArray("8000")), + (-128, ByteArray("8080")), + (129, ByteArray("8100")), + (-129, ByteArray("8180")), + (256, ByteArray("0001")), + (-256, ByteArray("0081")), + (32767, ByteArray("ff7f")), + (-32767, ByteArray("ffff")), + (32768, ByteArray("008000")), + (-32768, ByteArray("008080")), + (65535, ByteArray("ffff00")), + (-65535, ByteArray("ffff80")), + (524288, ByteArray("000008")), + (-524288, ByteArray("000088")), + (7340032, ByteArray("000070")), + (-7340032, ByteArray("0000f0")), + (8388608, ByteArray("00008000")), + (-8388608, ByteArray("00008080")), + (2147483647, ByteArray("ffffff7f")), + (-2147483647, ByteArray("ffffffff")), + (2147483648, ByteArray("0000008000")), + (-2147483648, ByteArray("0000008080")), + (2415919104, ByteArray("0000009000")), + (-2415919104, ByteArray("0000009080")), + (4294967295, ByteArray("ffffffff00")), + (-4294967295, ByteArray("ffffffff80")), + (4294967296, ByteArray("0000000001")), + (-4294967296, ByteArray("0000000081")), + (281474976710655, ByteArray("ffffffffffff00")), + (-281474976710655, ByteArray("ffffffffffff80")), + (72057594037927935, ByteArray("ffffffffffffff00")), + (-72057594037927935, ByteArray("ffffffffffffff80")), + (9223372036854775807, ByteArray("ffffffffffffff7f")), + (-9223372036854775807, ByteArray("ffffffffffffffff")), + ] + + for num, serialized in tests: + gotBytes = txscript.scriptNumBytes(num) + assert gotBytes == serialized, ( + str(num) + ": wanted " + serialized.hex() + ", got " + gotBytes.hex() + ) + class TestVSP(unittest.TestCase): def setUp(self): From 9088162c1078b33bd7dca8160e5656dc69df9ad2 Mon Sep 17 00:00:00 2001 From: JoeGruff Date: Mon, 6 Jan 2020 09:13:19 +0900 Subject: [PATCH 8/8] Return on addInt --- pydecred/txscript.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydecred/txscript.py b/pydecred/txscript.py index 6f195bce..3d14a7f2 100644 --- a/pydecred/txscript.py +++ b/pydecred/txscript.py @@ -1768,7 +1768,7 @@ def addInt(val): if val == -1 or (val >= 1 and val <= 16): b += opcode.OP_1 - 1 + val return b - raise addData(scriptNumBytes(val)) + return addData(scriptNumBytes(val)) def addData(data):