Example #1
0
    def __init__(self, wordfile='wordlist.txt'):

        # Read create the mnemonic wordlist object
        self.mnemonic = Mnemonic("english")

        # Set up a default reference wallet
        self.wallet = Wallet.from_master_secret(bytes(0))

        # Set up a Polly communication pipe
        self.polly = PollyCom()

        # Default print padding
        self.PAD = "{:35}"
Example #2
0
 def __init__(self, wordfile = 'wordlist.txt'):
     
     # Read create the mnemonic wordlist object
     self.mnemonic = Mnemonic("english")
 
     # Set up a default reference wallet
     self.wallet = Wallet.from_master_secret(bytes(0))
     
     # Set up a Polly communication pipe
     self.polly = PollyCom()
     
     # Default print padding
     self.PAD = "{:35}"
Example #3
0
class PollyAudit():
    """
    Auditing tests and utilities for Polly.
    """
    
    
    def __init__(self, wordfile = 'wordlist.txt'):
        
        # Read create the mnemonic wordlist object
        self.mnemonic = Mnemonic("english")
    
        # Set up a default reference wallet
        self.wallet = Wallet.from_master_secret(bytes(0))
        
        # Set up a Polly communication pipe
        self.polly = PollyCom()
        
        # Default print padding
        self.PAD = "{:35}"
        
    #
    # Tests
    #
        
    def test_set_seed(self, wordlist):
        """
        Sets the wallet seed for Polly and the reference wallet.
        
        Note: Subsequent tests will use the seed set by this routine.
        
        wordlist -  a space separated string of 18 mnemonic words from the Polly wordlist.
                    Note: the checksum must be correct (part of the 18th word) - see BIP0039.
                    gen_wordlist can be used to generate a wordlist including the proper checksum.
        """
        
        assert len(wordlist.split(" ")) == 18, "expecting 18 words"
        assert self.mnemonic.check(wordlist) == True, "invalid word list"
        
        print (self.PAD.format("Set seed"), end='')
        
        # Set polly
        self.polly.send_set_master_seed(wordlist)
        print (self.__outok())
        
        # Set the reference wallet
        seed = self.mnemonic.to_seed(wordlist)
        self.wallet = Wallet.from_master_secret(seed)
        
        
    def test_key(self, keytype, account = 0, chain = 0, address = 0):
        """
        Performs a public key retrieval test, comparing Polly's key against the reference wallet.
       
        keytype - Type of key to retrieve, valid values are KEY_MASTER, KEY_ACCOUNT, KEY_CHAIN, or KEY_ADDRESS.
        account - Account to use for type KEY_ACCOUNT, KEY_CHAIN, KEY_ADDRESS.
        chain   - Chain to use for type KEY_CHAIN, KEY_ADDRESS.
        address - Index (0 - 0x7FFFFFFF) to use for type KEY_ADDRESS.
        """
        
        assert address < 0x80000000, "hardened address keys are not supported"
        
        if keytype == PollyCom.KEY_MASTER:
            print(self.PAD.format("Get master key"), end='')
            refkey = self.wallet
            check_chaincode = False
            
        elif keytype == PollyCom.KEY_ACCOUNT:
            print(self.PAD.format("Get account key m/" + str(account) + "h"), end='')
            refkey = self.wallet.subkey(account, is_hardened = True)
            check_chaincode = True
            
        elif keytype == PollyCom.KEY_CHAIN:
            print(self.PAD.format("Get chain key   m/" + str(account) + "h/" + str(chain)), end='')
            refkey = self.wallet.subkey(account, is_hardened = True).subkey(chain)
            check_chaincode = True
            
        else: # keytype == PollyCom.KEY_ADDRESS
            print(self.PAD.format("Get address key m/" + str(account) + "h/" + str(chain) + "/" + str(address)), end='')
            refkey = self.wallet.subkey(account, is_hardened = True).subkey(chain).subkey(address)
            check_chaincode = False
    
        # Get keypair from Polly 
        (pubx, puby, chaincode) = self.polly.send_get_public_key(keytype, account, chain, address)

        print (self.__outok())

        # Check against reference wallet
        addr       = encoding.public_pair_to_hash160_sec((pubx, puby))
        addr_check = encoding.public_pair_to_hash160_sec(refkey.public_pair)
        
        assert addr == addr_check, "public key mismatch\nexpected: " + self.hexstr(addr_check) + "\nactual:   " + self.hexstr(addr) 
        
        if check_chaincode == True :
            assert refkey.chain_code == chaincode, "chain code mismatch\nexpected: " + self.hexstr(refkey.chain_code) + "\nactual:   " + self.hexstr(chaincode) 
            
        


    def test_sign(self, keynums_satoshi, out_addr, out_satoshi, change_keynum, change_satoshi, prevtx_keynums, prevtx_outputs, prevtx_inputs):
        """
        Performs a tx signing test, comparing Polly's signed tx against the reference wallet.
    
        Basic tx signing parameters:
        
        keynums_satoshi - list of tuples (keynum, satoshis) with key indices and their unspent value to 
                          use as tx inputs. Funding above out_satoshi + change_satoshi will be fees.
        out_addr        - output address in bitcoin address format. 
        out_satoshi     - output amount in satoshis.
        change_keynum   - change key index in the wallet, use None for no change.
        change_satoshi  - change amount in satoshis, use 0 for no change. 
        
        
        Supporting (previous) txs will be created to fund keynums and are controlled by these parameters:
        
        prevtx_keynums  - keynums will show up as outputs of previous txs. A number randomly picked 
                          from this list controls how many keynums are chosen to include per prev tx.
        prevtx_outputs  - in addition to previous tx outputs funding keynums, other outputs may 
                          be present. A number randomly picked from this list controls how many 
                          ignored outputs are injected per keynum. 
        prevtx_inputs   - previous txs need inputs too. A number randomly picked from this list 
                          controls how many inputs are chosen per previous tx.
        """
        
        total_in_satoshi = sum(satoshi for _, satoshi in keynums_satoshi) 
        fee_satoshi      = total_in_satoshi - out_satoshi - change_satoshi
        chain0           = self.wallet.subkey(0, is_hardened = True).subkey(0)
        chain1           = self.wallet.subkey(0, is_hardened = True).subkey(1)
        
        assert total_in_satoshi >= out_satoshi + change_satoshi
        assert len(keynums_satoshi) <= 32
    
        #
        # Step 1: send the inputs and outputs to use in the signed tx
        #
        
        # Create the (key num, compressed public key) tuple, input keys assume an m/0h/0/keynum path for now. 
        keys = [(keynum, encoding.public_pair_to_sec(chain0.subkey(keynum).public_pair)) 
                for (keynum, _) in keynums_satoshi] 
        
        # Convert base58 address to raw hex address
        out_addr_160 = encoding.bitcoin_address_to_hash160_sec(out_addr)
        
        print()
        print("Sign tx parameters:", "")
        for i, (keynum, satoshi) in enumerate(keynums_satoshi):
            print("{:<10}{:16.8f} btc < key {}".format (" inputs" if 0 == i else "", satoshi          / 100000000, keynum))
        print("{:<10}{:16.8f} btc > {}".format         (" output",                   out_satoshi      / 100000000, self.hexstr(out_addr_160)))
        print("{:<10}{:16.8f} btc > key {}".format     (" change",                   change_satoshi   / 100000000, change_keynum))
        print("{:<10}{:16.8f} btc".format              (" fee",                      fee_satoshi      / 100000000))
        print("{:<10}{:16.8f} btc".format              (" total",                    total_in_satoshi / 100000000))
       
        print()
        print(self.PAD.format("Send tx parameters"), end='')
        
        # ---> send to Polly 
        self.polly.send_sign_tx(keys, out_addr_160, out_satoshi, change_keynum, change_satoshi) 

        print(self.__outok())
    
        #
        # Step 2: send previous txs to fund the inputs
        #
    
        print()

        cur = 0
        prevtx_info = []
    
        while cur < len(keynums_satoshi) :
    
            prevtx_outputs_satoshi = []
            
            # Calculate how many keynums will be associated with this prev tx
            end = min(cur + random.choice(prevtx_keynums), len(keynums_satoshi))
            
            # Create the prev tx output list
            for keynum, satoshi in keynums_satoshi[cur:end] :
        
                # Inject a random number of outputs not associated with tx input keynums
                for _ in range(0, random.choice(prevtx_outputs)) :
                    prevtx_outputs_satoshi.append((random.randint(0, 0x7FFFFFFF),  
                                                    random.randint(0, 2099999997690000)))
    
                # Add the outputs funding the tx input keynums 
                prevtx_outputs_satoshi.append((keynum, satoshi))
    
                # Create output script
                addr   = chain0.subkey(keynum, as_private = True).bitcoin_address()
                script = standard_tx_out_script(addr)
    
                # Capture some info we'll use later to verify the signed tx
                prevtx_info.append((keynum, 
                                    satoshi,
                                    script,
                                    0,                                # This is the hash and will be replaced later
                                    len(prevtx_outputs_satoshi) - 1)) # Index of the valid output
                
            print("{:30}{}".format("Make prev tx for keys", " ".join(str(keynum) for (keynum, _, _, _, _) in prevtx_info[cur:])))
            
            # Create the prev tx
            prevtx = self.create_prev_tx(win                 = Wallet.from_master_secret(bytes(0)), # create a dummy wallet 
                                         in_keynum           = list(range(0, random.choice(prevtx_inputs))), 
                                         sources_per_input   = 1, 
                                         wout                = chain0, 
                                         out_keynum_satoshi  = prevtx_outputs_satoshi, 
                                         fees_satoshi        = random.randint(100, 1000))
            
            # We have built the prev tx, calculate its hash (and reverse the bytes) 
            prevtx_hash = encoding.double_sha256(prevtx)[::-1] 
    
            # Update the hashes now that we have a full prev tx
            for i, (keynum, satoshi, script, _, outidx) in enumerate(prevtx_info[cur:]) :
                prevtx_info[i + cur] = (keynum, satoshi, script, prevtx_hash, outidx)
                
            # Create the index table that matches a keynum index with an ouput index in this prev tx
            idx_table = [(keynum_idx + cur, outidx) for keynum_idx, (_, _, _, _, outidx) in enumerate(prevtx_info[cur:])] 
            
            print(self.PAD.format("Send prev tx "), end='')
            
            # ---> send to Polly
            self.polly.send_prev_tx(idx_table, prevtx)
    
            print(self.__outok())
    
            cur = end
        
        #
        # Step 3: generate a signed tx with the reference wallet and compare against Polly's
        #
    
        spendables = []
        wifs       = []
        
        # Make sure that the inputs add up correctly, and prep the input_sources for reference wallet signing
        for (keynum, satoshi, script, prevtx_hash, outidx) in prevtx_info:
            spendables.append(Spendable(satoshi, script, prevtx_hash, outidx))
            wifs.append(chain0.subkey(keynum, as_private = True).wif())
        
        change_addr = chain1.subkey(change_keynum).bitcoin_address()
        
        payables = [(out_addr, out_satoshi), (change_addr, change_satoshi)]
        
        print()
        print(self.PAD.format("Make reference signature"))
    
        signed_tx     = create_signed_tx(spendables, payables, wifs, fee_satoshi)
        signed_tx     = self.get_tx_bytes(signed_tx)
        
        print(self.PAD.format("Get signed tx"), end='', flush = True)
        
        # <--- get the signed tx from Polly
        polly_signed_tx = self.polly.send_get_signed_tx()

        #print(self.txstr(polly_signed_tx))
        #print(self.txstr(signed_tx))
        
        print(self.__outok())
        
        # Compare reference wallet signed tx with polly's 
        assert signed_tx == polly_signed_tx, "test_sign: signature mismatch\nExpected:\n" + self.hexstr(signed_tx) + "\n\nActual:\n" + self.hexstr(polly_signed_tx)


    def test_ref_bip32(self):
        """
        Performs a test of the reference wallet's BIP32 key generation capability.
        """
        
        # BIP32 test vectors, see https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#Test_Vectors
        
        # Vector 1
        
        m = Wallet.from_master_secret(bytes.fromhex("000102030405060708090a0b0c0d0e0f"))
        
        assert m.wallet_key()                                     == "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8"
        assert m.wallet_key(as_private=True)                      == "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi"
        assert m.bitcoin_address()                                == "15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma"
        assert m.wif()                                            == "L52XzL2cMkHxqxBXRyEpnPQZGUs3uKiL3R11XbAdHigRzDozKZeW"

        m0h = m.subkey(is_hardened=True)
        assert m0h.wallet_key()                                   == "xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw"
        assert m0h.wallet_key(as_private=True)                    == "xprv9uHRZZhk6KAJC1avXpDAp4MDc3sQKNxDiPvvkX8Br5ngLNv1TxvUxt4cV1rGL5hj6KCesnDYUhd7oWgT11eZG7XnxHrnYeSvkzY7d2bhkJ7"

        m0h1 = m0h.subkey(i=1)
        assert m0h1.wallet_key()                                  == "xpub6ASuArnXKPbfEwhqN6e3mwBcDTgzisQN1wXN9BJcM47sSikHjJf3UFHKkNAWbWMiGj7Wf5uMash7SyYq527Hqck2AxYysAA7xmALppuCkwQ"
        assert m0h1.wallet_key(as_private=True)                   == "xprv9wTYmMFdV23N2TdNG573QoEsfRrWKQgWeibmLntzniatZvR9BmLnvSxqu53Kw1UmYPxLgboyZQaXwTCg8MSY3H2EU4pWcQDnRnrVA1xe8fs"

        m0h1_1_2h = m0h1.subkey(i=2, is_hardened=True)
        assert m0h1_1_2h.wallet_key()                             == "xpub6D4BDPcP2GT577Vvch3R8wDkScZWzQzMMUm3PWbmWvVJrZwQY4VUNgqFJPMM3No2dFDFGTsxxpG5uJh7n7epu4trkrX7x7DogT5Uv6fcLW5"
        assert m0h1_1_2h.wallet_key(as_private=True)              == "xprv9z4pot5VBttmtdRTWfWQmoH1taj2axGVzFqSb8C9xaxKymcFzXBDptWmT7FwuEzG3ryjH4ktypQSAewRiNMjANTtpgP4mLTj34bhnZX7UiM"

        m0h1_1_2h_2 = m0h1_1_2h.subkey(i=2)
        assert m0h1_1_2h_2.wallet_key()                           == "xpub6FHa3pjLCk84BayeJxFW2SP4XRrFd1JYnxeLeU8EqN3vDfZmbqBqaGJAyiLjTAwm6ZLRQUMv1ZACTj37sR62cfN7fe5JnJ7dh8zL4fiyLHV"
        assert m0h1_1_2h_2.wallet_key(as_private=True)            == "xprvA2JDeKCSNNZky6uBCviVfJSKyQ1mDYahRjijr5idH2WwLsEd4Hsb2Tyh8RfQMuPh7f7RtyzTtdrbdqqsunu5Mm3wDvUAKRHSC34sJ7in334"

        m0h1_1_2h_2_1000000000 = m0h1_1_2h_2.subkey(i=1000000000)
        assert m0h1_1_2h_2_1000000000.wallet_key()                == "xpub6H1LXWLaKsWFhvm6RVpEL9P4KfRZSW7abD2ttkWP3SSQvnyA8FSVqNTEcYFgJS2UaFcxupHiYkro49S8yGasTvXEYBVPamhGW6cFJodrTHy"
        assert m0h1_1_2h_2_1000000000.wallet_key(as_private=True) == "xprvA41z7zogVVwxVSgdKUHDy1SKmdb533PjDz7J6N6mV6uS3ze1ai8FHa8kmHScGpWmj4WggLyQjgPie1rFSruoUihUZREPSL39UNdE3BBDu76"

        
        # Vector 2
        
        m = Wallet.from_master_secret(bytes.fromhex("fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542"))
        
        assert m.wallet_key()                                             == "xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB"
        assert m.wallet_key(as_private=True)                              == "xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U"

        m0 = m.subkey()
        assert m0.wallet_key()                                            == "xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH"
        assert m0.wallet_key(as_private=True)                             == "xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt"

        m0_2147483647p = m0.subkey(i=2147483647, is_hardened=True)
        assert m0_2147483647p.wallet_key()                                == "xpub6ASAVgeehLbnwdqV6UKMHVzgqAG8Gr6riv3Fxxpj8ksbH9ebxaEyBLZ85ySDhKiLDBrQSARLq1uNRts8RuJiHjaDMBU4Zn9h8LZNnBC5y4a"
        assert m0_2147483647p.wallet_key(as_private=True)                 == "xprv9wSp6B7kry3Vj9m1zSnLvN3xH8RdsPP1Mh7fAaR7aRLcQMKTR2vidYEeEg2mUCTAwCd6vnxVrcjfy2kRgVsFawNzmjuHc2YmYRmagcEPdU9"

        m0_2147483647p_1 = m0_2147483647p.subkey(i=1)
        assert m0_2147483647p_1.wallet_key()                              == "xpub6DF8uhdarytz3FWdA8TvFSvvAh8dP3283MY7p2V4SeE2wyWmG5mg5EwVvmdMVCQcoNJxGoWaU9DCWh89LojfZ537wTfunKau47EL2dhHKon"
        assert m0_2147483647p_1.wallet_key(as_private=True)               == "xprv9zFnWC6h2cLgpmSA46vutJzBcfJ8yaJGg8cX1e5StJh45BBciYTRXSd25UEPVuesF9yog62tGAQtHjXajPPdbRCHuWS6T8XA2ECKADdw4Ef"

        m0_2147483647p_1_2147483646p = m0_2147483647p_1.subkey(i=2147483646, is_hardened=True)
        assert m0_2147483647p_1_2147483646p.wallet_key()                  == "xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL"
        assert m0_2147483647p_1_2147483646p.wallet_key(as_private=True)   == "xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc"

        m0_2147483647p_1_2147483646p_2 = m0_2147483647p_1_2147483646p.subkey(i=2)
        assert m0_2147483647p_1_2147483646p_2.wallet_key()                == "xpub6FnCn6nSzZAw5Tw7cgR9bi15UV96gLZhjDstkXXxvCLsUXBGXPdSnLFbdpq8p9HmGsApME5hQTZ3emM2rnY5agb9rXpVGyy3bdW6EEgAtqt"
        assert m0_2147483647p_1_2147483646p_2.wallet_key(as_private=True) == "xprvA2nrNbFZABcdryreWet9Ea4LvTJcGsqrMzxHx98MMrotbir7yrKCEXw7nadnHM8Dq38EGfSh6dqA9QWTyefMLEcBYJUuekgW4BYPJcr9E7j"

    def test_rfc6979(self):
        """
        Performs a test of the reference wallet's RFC6979 signatures against test vectors.
        """
        
        # Test vectors for RFC 6979 ECDSA (secp256k1, SHA-256).
        # Thanks to the Haskoin developer for these fully formed vectors.
        
        # (private key hex, private key WIF, message, r || r as hex, sig as DER)
        test_vectors = [
        ( 0x0000000000000000000000000000000000000000000000000000000000000001,
          "KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn",
          "Everything should be made as simple as possible, but not simpler.",
          "33a69cd2065432a30f3d1ce4eb0d59b8ab58c74f27c41a7fdb5696ad4e6108c96f807982866f785d3f6418d24163ddae117b7db4d5fdf0071de069fa54342262",
          "3044022033a69cd2065432a30f3d1ce4eb0d59b8ab58c74f27c41a7fdb5696ad4e6108c902206f807982866f785d3f6418d24163ddae117b7db4d5fdf0071de069fa54342262"
          ),
        ( 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140,
          "L5oLkpV3aqBjhki6LmvChTCV6odsp4SXM6FfU2Gppt5kFLaHLuZ9",
          "Equations are more important to me, because politics is for the present, but an equation is something for eternity.",
          "54c4a33c6423d689378f160a7ff8b61330444abb58fb470f96ea16d99d4a2fed07082304410efa6b2943111b6a4e0aaa7b7db55a07e9861d1fb3cb1f421044a5",
          "3044022054c4a33c6423d689378f160a7ff8b61330444abb58fb470f96ea16d99d4a2fed022007082304410efa6b2943111b6a4e0aaa7b7db55a07e9861d1fb3cb1f421044a5"
          ),
        ( 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140,
          "L5oLkpV3aqBjhki6LmvChTCV6odsp4SXM6FfU2Gppt5kFLaHLuZ9",
          "Not only is the Universe stranger than we think, it is stranger than we can think.",
          "ff466a9f1b7b273e2f4c3ffe032eb2e814121ed18ef84665d0f515360dab3dd06fc95f5132e5ecfdc8e5e6e616cc77151455d46ed48f5589b7db7771a332b283",
          "3045022100ff466a9f1b7b273e2f4c3ffe032eb2e814121ed18ef84665d0f515360dab3dd002206fc95f5132e5ecfdc8e5e6e616cc77151455d46ed48f5589b7db7771a332b283"
          ),
        ( 0x0000000000000000000000000000000000000000000000000000000000000001,
          "KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn",
          "How wonderful that we have met with a paradox. Now we have some hope of making progress.",
          "c0dafec8251f1d5010289d210232220b03202cba34ec11fec58b3e93a85b91d375afdc06b7d6322a590955bf264e7aaa155847f614d80078a90292fe205064d3",
          "3045022100c0dafec8251f1d5010289d210232220b03202cba34ec11fec58b3e93a85b91d3022075afdc06b7d6322a590955bf264e7aaa155847f614d80078a90292fe205064d3"
          ),
        ( 0x69ec59eaa1f4f2e36b639716b7c30ca86d9a5375c7b38d8918bd9c0ebc80ba64,
          "KzmcSTRmg8Gtoq8jbBCwsrvgiTKRrewQXniAHHTf7hsten8MZmBB",
          "Computer science is no more about computers than astronomy is about telescopes.",
          "7186363571d65e084e7f02b0b77c3ec44fb1b257dee26274c38c928986fea45d0de0b38e06807e46bda1f1e293f4f6323e854c86d58abdd00c46c16441085df6",
          "304402207186363571d65e084e7f02b0b77c3ec44fb1b257dee26274c38c928986fea45d02200de0b38e06807e46bda1f1e293f4f6323e854c86d58abdd00c46c16441085df6"
          ),
        ( 0x00000000000000000000000000007246174ab1e92e9149c6e446fe194d072637,
          "KwDiBf89QgGbjEhKnhXJwe1E2mCa8asowBrSKuCaBV6EsPYEAFZ8",
          "...if you aren't, at any given time, scandalized by code you wrote five or even three years ago, you're not learning anywhere near enough",
          "fbfe5076a15860ba8ed00e75e9bd22e05d230f02a936b653eb55b61c99dda4870e68880ebb0050fe4312b1b1eb0899e1b82da89baa5b895f612619edf34cbd37",
          "3045022100fbfe5076a15860ba8ed00e75e9bd22e05d230f02a936b653eb55b61c99dda48702200e68880ebb0050fe4312b1b1eb0899e1b82da89baa5b895f612619edf34cbd37"
          ),
        ( 0x000000000000000000000000000000000000000000056916d0f9b31dc9b637f3,
          "KwDiBf89QgGbjEhKnhXJuH7LrciVrZiib5S9h4knkymNojPUVsWN",
          "The question of whether computers can think is like the question of whether submarines can swim.",
          "cde1302d83f8dd835d89aef803c74a119f561fbaef3eb9129e45f30de86abbf906ce643f5049ee1f27890467b77a6a8e11ec4661cc38cd8badf90115fbd03cef",
          "3045022100cde1302d83f8dd835d89aef803c74a119f561fbaef3eb9129e45f30de86abbf9022006ce643f5049ee1f27890467b77a6a8e11ec4661cc38cd8badf90115fbd03cef"
          )
        ]
        
        for (secret_exponent, _, message, _, expected_sig) in test_vectors:
    
            h = hashlib.sha256(message.encode('utf-8')).digest()
            val = intbytes.from_bytes(h)        
            
            # This will use deterministic values of k based on 'val'
            r, s = ecdsa.sign(secp256k1.generator_secp256k1, secret_exponent, val)
                
            # Ensure that 's' is even to prevent attacks - see https://bitcointalk.org/index.php?topic=285142.msg3295518#msg3295518
            if s > (secp256k1.generator_secp256k1.order() / 2):
                s = secp256k1.generator_secp256k1.order() - s
            
            sig = der.sigencode_der(r, s)
            
            assert sig == bytes.fromhex(expected_sig), "ECDSA signature using RFC 6979 failed\nExpected: " + expected_sig + "\nActual:   " + self.hexstr(sig)
            
            
    def test_txhash(self):
        """
        Performs a test of the reference wallet's tx hashing against a known blockchain tx.
        """
        
        # 's' and 'expected' are from: 
        # https://blockchain.info/rawtx/a8196acaf3938b988f9816ae3e9da1df5a04afff0b5b460e4c1dc4a08dd52109?format=hex
    
        s = ("0100000002bca066b9cfe1eb81e667f219a442acdc5c5e2e470610659a314"
             "74dfb5e29c552000000008c493046022100b2857170045d5e59112e0d5200"
             "4a8f65d18945e52d42c2eb12f7d3c2314600b802210098bdab40dfe38b5d4"
             "fe02e1fa3057ada3e0a982a5c7979eabff86395e2a911e8014104a8075344"
             "0c651f7191f46085411679545486f1dc6bf34cdaba453966c5fe7cc34f3dd"
             "c15ae321974f426807faa34b3fc10034e129222067ec053c409a6ac1f30ff"
             "ffffffe047c65f9e560d799580f6a965c12a059ca1e82cebc3e220659ba29"
             "ee31c8d0a010000008b48304502206b7eaa2dec17b53022b57a55b48ac245"
             "6f1c22d87b0170aa969de04146b80bbc022100d6700f6eb9bde89c35b0545"
             "588c06dcfed95e0502941d79786b5ea24eafc2cfe01410435d1d08c6f5296"
             "0d056e60c3b5c858e5299c1a688395b589dbde6b58861b20fdd7ee58832b3"
             "528845973765038cafc1c81280dc635ee202ce06aa4a373db012fffffffff"
             "0200f2052a010000001976a914315bfd9ee07d6779e44b8e07229650f039f"
             "0942788aced931600000000001976a91484004861f9a742fc83ad4ab83c42"
             "e709b512df1888ac00000000")
    
        expected = "a8196acaf3938b988f9816ae3e9da1df5a04afff0b5b460e4c1dc4a08dd52109"
        expected = bytes.fromhex(expected)
    
        actual = encoding.double_sha256(bytes.fromhex(s))
        
        # Reverse the bytes to flow lsb -> msb
        actual = actual[::-1]
        
        assert actual == expected, "tx hash calculation mismatch\n" + "Expected: " + self.hexstr(expected) + "\nActual:   " + self.hexstr(actual)
        
    #
    # Utilities
    #
    def fw_update(self, fwfile):
        """
        Updates device firmware.
       
        fwfile  - Path to and name of the firmware file to use.
        """
        
        self.polly.send_fw_download(fwfile)
        
        
    def create_prev_tx(self, win, in_keynum, sources_per_input, wout, out_keynum_satoshi, fees_satoshi):
        """
        Creates and returns a supporting 'previous' tx of 100KB or less
    
        win                 - wallet to use for input addresses
        in_keynum           - key nums from win
        sources_per_input   - how many sources are used to fund each input
        wout                - wallet to use for output addresses
        out_keynum_satoshi  - list of key nums from wout and satoshis to spend in tuples of (num, satoshis)
        
        Returns a bytes object containing the previous tx.
        """
    
        # Calculate the total output
        payables = []
        total_spent = 0
    
        for (out_key_id, out_satoshi) in out_keynum_satoshi:
    
            address = wout.subkey(out_key_id).bitcoin_address()
            payables.append((address, out_satoshi))
            
            total_spent += out_satoshi
    
        # Split the total to spend across all of the inputs
        spendables  = []
        total_value = 0
    
        satoshi_per_input = int(total_spent + fees_satoshi) / len(in_keynum)
    
        for keynum in in_keynum:
    
            # Grab the address for the current key num
            addr = win.subkey(keynum, as_private = True).bitcoin_address();
            
            # Generate fake sources for funding the input coin
            spendables.extend(self.fake_sources_for_address(addr, sources_per_input, satoshi_per_input))

            total_value += satoshi_per_input
       
        # Calculate the fee
        tx_fee = total_value - total_spent
        
        assert tx_fee >= 0, "fee < 0: " + str(tx_fee)
    
        # Create and 'sign' the transaction
        unsigned_tx = create_tx(spendables, payables, tx_fee)
        signed_tx   = self.__sign_fake(unsigned_tx)
    
        return self.get_tx_bytes(signed_tx)


    def fake_sources_for_address(self, addr, num_sources, total_satoshi):
        """
        Returns a fake list of funding sources for a bitcoin address.
        
        Note: total_satoshi will be split evenly by num_sources
        
        addr          - bitcoin address to fund
        num_sources   - number of sources to fund it with
        total_satoshi - total satoshis to fund 'addr' with 
    
        Returns a list of Spendable objects
        """
    
        spendables     = []
        satoshi_left   = total_satoshi
        satoshi_per_tx = satoshi_left / num_sources   
        satoshi_per_tx = int(satoshi_per_tx)
    
        # Create the output script for the input to fund 
        script = standard_tx_out_script(addr)
    
        while satoshi_left > 0:
            if satoshi_left < satoshi_per_tx:
                satoshi_per_tx = satoshi_left
            
            # Create a random hash value 
            rand_hash = bytes([random.randint(0, 0xFF) for _ in range(0, 32)])
    
            # Create a random output index
            # This field is 32 bits, but typically transactions dont have that many, limit to 0xFF
            rand_output_index = random.randint(0, 0xFF)
    
            # Append the new fake source 
            spend = Spendable(satoshi_per_tx, script, rand_hash, rand_output_index)
            
            spendables.append(spend)
            
            satoshi_left -= satoshi_per_tx
            
        assert satoshi_left == 0, "incorrect funding"
    
        return spendables


    def get_tx_bytes(self, tx):
        """
        Takes a Tx object and returns a bytes object containing the tx bytes.
        """
        
        s = io.BytesIO()
        tx.stream(s)
        return s.getvalue()


    def gen_wordlist(self, seed):
        """
        Generates a polly mnemonic wordlist from a seed, including the checksum.
        
        seed -  a string of 24 hex bytes (for a strength of 192 bits)
        
        Returns a space separated string of 18 words from the wordlist.
        """
        
        assert len(seed) == 24, "incorrect seed length, expecting 24 bytes"
        
        return self.mnemonic.to_mnemonic(seed)


    def hexstr(self, data):
        """
        Takes a bytes object and returns a packed hex string.
        """
        
        # Hexlify the bytes object and strip off the leading b' and trailing '
        return str(binascii.hexlify(data))[2:-1]
    

    def txstr(self, tx):
        """
        Takes a tx bytes object and prints out its details field by field.
        """
       
        def hexy(tag, data):
            print ("{0:<20s} : {1}".format(tag, self.hexstr(data)))
            
        print("\n[tx details]\n")

        s = 0
        
        hexy("version",     tx[s:s + 4])
        s += 4
        
        in_count = ord(tx[s:s + 1])
        hexy("in count", tx[s:s + 1])
        s += 1
        
        for _ in range(0, in_count) :
            
            print(" -------------------")
        
            hexy(" prev out hash", tx[s:s + 32])
            s += 32
            
            hexy(" prev out index", tx[s:s + 4])
            s += 4
            
            scriptlen = ord(tx[s:s + 1])
            hexy(" scriptlen", tx[s:s + 1])
            s += 1
        
            hexy(" script", tx[s:s + scriptlen])
            s += scriptlen
            
            hexy(" sequence", tx[s:s + 4])
            s += 4
            
        print()
        out_count = ord(tx[s:s + 1])
        hexy("out count", tx[s:s + 1])
        s += 1
        
        for _ in range(0, out_count) :
            
            print(" -------------------")
        
            hexy(" value", tx[s:s + 8])
            s += 8
            
            scriptlen = ord(tx[s:s + 1])
            hexy(" pk scriptlen", tx[s:s + 1])
            s += 1
        
            hexy(" pk script", tx[s:s + scriptlen])
            s += scriptlen
            
        print()
        hexy("lock time", tx[s:s + 4])
        s += 4
        
    #
    # Private
    #

    def __outok(self):
        """
        Creates a standard successful completion string for Polly operations
        """
        return "ok (" + self.polly.get_cmd_time() + ")"

    def __sign_fake(self, tx):
        """
        Sign a transaction using a fake randomly generated signature.
        """
        
        # Create a fake ecdsa signature from 0x68 - 0x6b bytes
        rand_script = bytes([random.randint(0,0xFF) for _ in range(0, random.randint(0x68, 0x6b))])

        tx.check_unspents()
        for idx, tx_in in enumerate(tx.txs_in):
            if tx.unspents[idx]:
                tx_in.script = rand_script
                
        return tx
Example #4
0
class PollyAudit():
    """
    Auditing tests and utilities for Polly.
    """
    def __init__(self, wordfile='wordlist.txt'):

        # Read create the mnemonic wordlist object
        self.mnemonic = Mnemonic("english")

        # Set up a default reference wallet
        self.wallet = Wallet.from_master_secret(bytes(0))

        # Set up a Polly communication pipe
        self.polly = PollyCom()

        # Default print padding
        self.PAD = "{:35}"

    #
    # Tests
    #

    def test_set_seed(self, wordlist):
        """
        Sets the wallet seed for Polly and the reference wallet.
        
        Note: Subsequent tests will use the seed set by this routine.
        
        wordlist -  a space separated string of 18 mnemonic words from the Polly wordlist.
                    Note: the checksum must be correct (part of the 18th word) - see BIP0039.
                    gen_wordlist can be used to generate a wordlist including the proper checksum.
        """

        assert len(wordlist.split(" ")) == 18, "expecting 18 words"
        assert self.mnemonic.check(wordlist) == True, "invalid word list"

        print(self.PAD.format("Set seed"), end='')

        # Set polly
        self.polly.send_set_master_seed(wordlist)
        print(self.__outok())

        # Set the reference wallet
        seed = self.mnemonic.to_seed(wordlist)
        self.wallet = Wallet.from_master_secret(seed)

    def test_key(self, keytype, account=0, chain=0, address=0):
        """
        Performs a public key retrieval test, comparing Polly's key against the reference wallet.
       
        keytype - Type of key to retrieve, valid values are KEY_MASTER, KEY_ACCOUNT, KEY_CHAIN, or KEY_ADDRESS.
        account - Account to use for type KEY_ACCOUNT, KEY_CHAIN, KEY_ADDRESS.
        chain   - Chain to use for type KEY_CHAIN, KEY_ADDRESS.
        address - Index (0 - 0x7FFFFFFF) to use for type KEY_ADDRESS.
        """

        assert address < 0x80000000, "hardened address keys are not supported"

        if keytype == PollyCom.KEY_MASTER:
            print(self.PAD.format("Get master key"), end='')
            refkey = self.wallet
            check_chaincode = False

        elif keytype == PollyCom.KEY_ACCOUNT:
            print(self.PAD.format("Get account key m/" + str(account) + "h"),
                  end='')
            refkey = self.wallet.subkey(account, is_hardened=True)
            check_chaincode = True

        elif keytype == PollyCom.KEY_CHAIN:
            print(self.PAD.format("Get chain key   m/" + str(account) + "h/" +
                                  str(chain)),
                  end='')
            refkey = self.wallet.subkey(account,
                                        is_hardened=True).subkey(chain)
            check_chaincode = True

        else:  # keytype == PollyCom.KEY_ADDRESS
            print(self.PAD.format("Get address key m/" + str(account) + "h/" +
                                  str(chain) + "/" + str(address)),
                  end='')
            refkey = self.wallet.subkey(
                account, is_hardened=True).subkey(chain).subkey(address)
            check_chaincode = False

        # Get keypair from Polly
        (pubx, puby,
         chaincode) = self.polly.send_get_public_key(keytype, account, chain,
                                                     address)

        print(self.__outok())

        # Check against reference wallet
        addr = encoding.public_pair_to_hash160_sec((pubx, puby))
        addr_check = encoding.public_pair_to_hash160_sec(refkey.public_pair)

        assert addr == addr_check, "public key mismatch\nexpected: " + self.hexstr(
            addr_check) + "\nactual:   " + self.hexstr(addr)

        if check_chaincode == True:
            assert refkey.chain_code == chaincode, "chain code mismatch\nexpected: " + self.hexstr(
                refkey.chain_code) + "\nactual:   " + self.hexstr(chaincode)

    def test_sign(self, keynums_satoshi, out_addr, out_satoshi, change_keynum,
                  change_satoshi, prevtx_keynums, prevtx_outputs,
                  prevtx_inputs):
        """
        Performs a tx signing test, comparing Polly's signed tx against the reference wallet.
    
        Basic tx signing parameters:
        
        keynums_satoshi - list of tuples (keynum, satoshis) with key indices and their unspent value to 
                          use as tx inputs. Funding above out_satoshi + change_satoshi will be fees.
        out_addr        - output address in bitcoin address format. 
        out_satoshi     - output amount in satoshis.
        change_keynum   - change key index in the wallet, use None for no change.
        change_satoshi  - change amount in satoshis, use 0 for no change. 
        
        
        Supporting (previous) txs will be created to fund keynums and are controlled by these parameters:
        
        prevtx_keynums  - keynums will show up as outputs of previous txs. A number randomly picked 
                          from this list controls how many keynums are chosen to include per prev tx.
        prevtx_outputs  - in addition to previous tx outputs funding keynums, other outputs may 
                          be present. A number randomly picked from this list controls how many 
                          ignored outputs are injected per keynum. 
        prevtx_inputs   - previous txs need inputs too. A number randomly picked from this list 
                          controls how many inputs are chosen per previous tx.
        """

        total_in_satoshi = sum(satoshi for _, satoshi in keynums_satoshi)
        fee_satoshi = total_in_satoshi - out_satoshi - change_satoshi
        chain0 = self.wallet.subkey(0, is_hardened=True).subkey(0)
        chain1 = self.wallet.subkey(0, is_hardened=True).subkey(1)

        assert total_in_satoshi >= out_satoshi + change_satoshi
        assert len(keynums_satoshi) <= 32

        #
        # Step 1: send the inputs and outputs to use in the signed tx
        #

        # Create the (key num, compressed public key) tuple, input keys assume an m/0h/0/keynum path for now.
        keys = [
            (keynum,
             encoding.public_pair_to_sec(chain0.subkey(keynum).public_pair))
            for (keynum, _) in keynums_satoshi
        ]

        # Convert base58 address to raw hex address
        out_addr_160 = encoding.bitcoin_address_to_hash160_sec(out_addr)

        print()
        print("Sign tx parameters:", "")
        for i, (keynum, satoshi) in enumerate(keynums_satoshi):
            print("{:<10}{:16.8f} btc < key {}".format(
                " inputs" if 0 == i else "", satoshi / 100000000, keynum))
        print("{:<10}{:16.8f} btc > {}".format(" output",
                                               out_satoshi / 100000000,
                                               self.hexstr(out_addr_160)))
        print("{:<10}{:16.8f} btc > key {}".format(" change",
                                                   change_satoshi / 100000000,
                                                   change_keynum))
        print("{:<10}{:16.8f} btc".format(" fee", fee_satoshi / 100000000))
        print("{:<10}{:16.8f} btc".format(" total",
                                          total_in_satoshi / 100000000))

        print()
        print(self.PAD.format("Send tx parameters"), end='')

        # ---> send to Polly
        self.polly.send_sign_tx(keys, out_addr_160, out_satoshi, change_keynum,
                                change_satoshi)

        print(self.__outok())

        #
        # Step 2: send previous txs to fund the inputs
        #

        print()

        cur = 0
        prevtx_info = []

        while cur < len(keynums_satoshi):

            prevtx_outputs_satoshi = []

            # Calculate how many keynums will be associated with this prev tx
            end = min(cur + random.choice(prevtx_keynums),
                      len(keynums_satoshi))

            # Create the prev tx output list
            for keynum, satoshi in keynums_satoshi[cur:end]:

                # Inject a random number of outputs not associated with tx input keynums
                for _ in range(0, random.choice(prevtx_outputs)):
                    prevtx_outputs_satoshi.append(
                        (random.randint(0, 0x7FFFFFFF),
                         random.randint(0, 2099999997690000)))

                # Add the outputs funding the tx input keynums
                prevtx_outputs_satoshi.append((keynum, satoshi))

                # Create output script
                addr = chain0.subkey(keynum, as_private=True).bitcoin_address()
                script = standard_tx_out_script(addr)

                # Capture some info we'll use later to verify the signed tx
                prevtx_info.append((
                    keynum,
                    satoshi,
                    script,
                    0,  # This is the hash and will be replaced later
                    len(prevtx_outputs_satoshi) -
                    1))  # Index of the valid output

            print("{:30}{}".format(
                "Make prev tx for keys", " ".join(
                    str(keynum)
                    for (keynum, _, _, _, _) in prevtx_info[cur:])))

            # Create the prev tx
            prevtx = self.create_prev_tx(
                win=Wallet.from_master_secret(
                    bytes(0)),  # create a dummy wallet 
                in_keynum=list(range(0, random.choice(prevtx_inputs))),
                sources_per_input=1,
                wout=chain0,
                out_keynum_satoshi=prevtx_outputs_satoshi,
                fees_satoshi=random.randint(100, 1000))

            # We have built the prev tx, calculate its hash (and reverse the bytes)
            prevtx_hash = encoding.double_sha256(prevtx)[::-1]

            # Update the hashes now that we have a full prev tx
            for i, (keynum, satoshi, script, _,
                    outidx) in enumerate(prevtx_info[cur:]):
                prevtx_info[i + cur] = (keynum, satoshi, script, prevtx_hash,
                                        outidx)

            # Create the index table that matches a keynum index with an ouput index in this prev tx
            idx_table = [
                (keynum_idx + cur, outidx)
                for keynum_idx, (_, _, _, _,
                                 outidx) in enumerate(prevtx_info[cur:])
            ]

            print(self.PAD.format("Send prev tx "), end='')

            # ---> send to Polly
            self.polly.send_prev_tx(idx_table, prevtx)

            print(self.__outok())

            cur = end

        #
        # Step 3: generate a signed tx with the reference wallet and compare against Polly's
        #

        spendables = []
        wifs = []

        # Make sure that the inputs add up correctly, and prep the input_sources for reference wallet signing
        for (keynum, satoshi, script, prevtx_hash, outidx) in prevtx_info:
            spendables.append(Spendable(satoshi, script, prevtx_hash, outidx))
            wifs.append(chain0.subkey(keynum, as_private=True).wif())

        change_addr = chain1.subkey(change_keynum).bitcoin_address()

        payables = [(out_addr, out_satoshi), (change_addr, change_satoshi)]

        print()
        print(self.PAD.format("Make reference signature"))

        signed_tx = create_signed_tx(spendables, payables, wifs, fee_satoshi)
        signed_tx = self.get_tx_bytes(signed_tx)

        print(self.PAD.format("Get signed tx"), end='', flush=True)

        # <--- get the signed tx from Polly
        polly_signed_tx = self.polly.send_get_signed_tx()

        #print(self.txstr(polly_signed_tx))
        #print(self.txstr(signed_tx))

        print(self.__outok())

        # Compare reference wallet signed tx with polly's
        assert signed_tx == polly_signed_tx, "test_sign: signature mismatch\nExpected:\n" + self.hexstr(
            signed_tx) + "\n\nActual:\n" + self.hexstr(polly_signed_tx)

    def test_ref_bip32(self):
        """
        Performs a test of the reference wallet's BIP32 key generation capability.
        """

        # BIP32 test vectors, see https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#Test_Vectors

        # Vector 1

        m = Wallet.from_master_secret(
            bytes.fromhex("000102030405060708090a0b0c0d0e0f"))

        assert m.wallet_key(
        ) == "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8"
        assert m.wallet_key(
            as_private=True
        ) == "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi"
        assert m.bitcoin_address() == "15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma"
        assert m.wif(
        ) == "L52XzL2cMkHxqxBXRyEpnPQZGUs3uKiL3R11XbAdHigRzDozKZeW"

        m0h = m.subkey(is_hardened=True)
        assert m0h.wallet_key(
        ) == "xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw"
        assert m0h.wallet_key(
            as_private=True
        ) == "xprv9uHRZZhk6KAJC1avXpDAp4MDc3sQKNxDiPvvkX8Br5ngLNv1TxvUxt4cV1rGL5hj6KCesnDYUhd7oWgT11eZG7XnxHrnYeSvkzY7d2bhkJ7"

        m0h1 = m0h.subkey(i=1)
        assert m0h1.wallet_key(
        ) == "xpub6ASuArnXKPbfEwhqN6e3mwBcDTgzisQN1wXN9BJcM47sSikHjJf3UFHKkNAWbWMiGj7Wf5uMash7SyYq527Hqck2AxYysAA7xmALppuCkwQ"
        assert m0h1.wallet_key(
            as_private=True
        ) == "xprv9wTYmMFdV23N2TdNG573QoEsfRrWKQgWeibmLntzniatZvR9BmLnvSxqu53Kw1UmYPxLgboyZQaXwTCg8MSY3H2EU4pWcQDnRnrVA1xe8fs"

        m0h1_1_2h = m0h1.subkey(i=2, is_hardened=True)
        assert m0h1_1_2h.wallet_key(
        ) == "xpub6D4BDPcP2GT577Vvch3R8wDkScZWzQzMMUm3PWbmWvVJrZwQY4VUNgqFJPMM3No2dFDFGTsxxpG5uJh7n7epu4trkrX7x7DogT5Uv6fcLW5"
        assert m0h1_1_2h.wallet_key(
            as_private=True
        ) == "xprv9z4pot5VBttmtdRTWfWQmoH1taj2axGVzFqSb8C9xaxKymcFzXBDptWmT7FwuEzG3ryjH4ktypQSAewRiNMjANTtpgP4mLTj34bhnZX7UiM"

        m0h1_1_2h_2 = m0h1_1_2h.subkey(i=2)
        assert m0h1_1_2h_2.wallet_key(
        ) == "xpub6FHa3pjLCk84BayeJxFW2SP4XRrFd1JYnxeLeU8EqN3vDfZmbqBqaGJAyiLjTAwm6ZLRQUMv1ZACTj37sR62cfN7fe5JnJ7dh8zL4fiyLHV"
        assert m0h1_1_2h_2.wallet_key(
            as_private=True
        ) == "xprvA2JDeKCSNNZky6uBCviVfJSKyQ1mDYahRjijr5idH2WwLsEd4Hsb2Tyh8RfQMuPh7f7RtyzTtdrbdqqsunu5Mm3wDvUAKRHSC34sJ7in334"

        m0h1_1_2h_2_1000000000 = m0h1_1_2h_2.subkey(i=1000000000)
        assert m0h1_1_2h_2_1000000000.wallet_key(
        ) == "xpub6H1LXWLaKsWFhvm6RVpEL9P4KfRZSW7abD2ttkWP3SSQvnyA8FSVqNTEcYFgJS2UaFcxupHiYkro49S8yGasTvXEYBVPamhGW6cFJodrTHy"
        assert m0h1_1_2h_2_1000000000.wallet_key(
            as_private=True
        ) == "xprvA41z7zogVVwxVSgdKUHDy1SKmdb533PjDz7J6N6mV6uS3ze1ai8FHa8kmHScGpWmj4WggLyQjgPie1rFSruoUihUZREPSL39UNdE3BBDu76"

        # Vector 2

        m = Wallet.from_master_secret(
            bytes.fromhex(
                "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542"
            ))

        assert m.wallet_key(
        ) == "xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB"
        assert m.wallet_key(
            as_private=True
        ) == "xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U"

        m0 = m.subkey()
        assert m0.wallet_key(
        ) == "xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH"
        assert m0.wallet_key(
            as_private=True
        ) == "xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt"

        m0_2147483647p = m0.subkey(i=2147483647, is_hardened=True)
        assert m0_2147483647p.wallet_key(
        ) == "xpub6ASAVgeehLbnwdqV6UKMHVzgqAG8Gr6riv3Fxxpj8ksbH9ebxaEyBLZ85ySDhKiLDBrQSARLq1uNRts8RuJiHjaDMBU4Zn9h8LZNnBC5y4a"
        assert m0_2147483647p.wallet_key(
            as_private=True
        ) == "xprv9wSp6B7kry3Vj9m1zSnLvN3xH8RdsPP1Mh7fAaR7aRLcQMKTR2vidYEeEg2mUCTAwCd6vnxVrcjfy2kRgVsFawNzmjuHc2YmYRmagcEPdU9"

        m0_2147483647p_1 = m0_2147483647p.subkey(i=1)
        assert m0_2147483647p_1.wallet_key(
        ) == "xpub6DF8uhdarytz3FWdA8TvFSvvAh8dP3283MY7p2V4SeE2wyWmG5mg5EwVvmdMVCQcoNJxGoWaU9DCWh89LojfZ537wTfunKau47EL2dhHKon"
        assert m0_2147483647p_1.wallet_key(
            as_private=True
        ) == "xprv9zFnWC6h2cLgpmSA46vutJzBcfJ8yaJGg8cX1e5StJh45BBciYTRXSd25UEPVuesF9yog62tGAQtHjXajPPdbRCHuWS6T8XA2ECKADdw4Ef"

        m0_2147483647p_1_2147483646p = m0_2147483647p_1.subkey(
            i=2147483646, is_hardened=True)
        assert m0_2147483647p_1_2147483646p.wallet_key(
        ) == "xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL"
        assert m0_2147483647p_1_2147483646p.wallet_key(
            as_private=True
        ) == "xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc"

        m0_2147483647p_1_2147483646p_2 = m0_2147483647p_1_2147483646p.subkey(
            i=2)
        assert m0_2147483647p_1_2147483646p_2.wallet_key(
        ) == "xpub6FnCn6nSzZAw5Tw7cgR9bi15UV96gLZhjDstkXXxvCLsUXBGXPdSnLFbdpq8p9HmGsApME5hQTZ3emM2rnY5agb9rXpVGyy3bdW6EEgAtqt"
        assert m0_2147483647p_1_2147483646p_2.wallet_key(
            as_private=True
        ) == "xprvA2nrNbFZABcdryreWet9Ea4LvTJcGsqrMzxHx98MMrotbir7yrKCEXw7nadnHM8Dq38EGfSh6dqA9QWTyefMLEcBYJUuekgW4BYPJcr9E7j"

    def test_rfc6979(self):
        """
        Performs a test of the reference wallet's RFC6979 signatures against test vectors.
        """

        # Test vectors for RFC 6979 ECDSA (secp256k1, SHA-256).
        # Thanks to the Haskoin developer for these fully formed vectors.

        # (private key hex, private key WIF, message, r || r as hex, sig as DER)
        test_vectors = [
            (0x0000000000000000000000000000000000000000000000000000000000000001,
             "KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn",
             "Everything should be made as simple as possible, but not simpler.",
             "33a69cd2065432a30f3d1ce4eb0d59b8ab58c74f27c41a7fdb5696ad4e6108c96f807982866f785d3f6418d24163ddae117b7db4d5fdf0071de069fa54342262",
             "3044022033a69cd2065432a30f3d1ce4eb0d59b8ab58c74f27c41a7fdb5696ad4e6108c902206f807982866f785d3f6418d24163ddae117b7db4d5fdf0071de069fa54342262"
             ),
            (0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140,
             "L5oLkpV3aqBjhki6LmvChTCV6odsp4SXM6FfU2Gppt5kFLaHLuZ9",
             "Equations are more important to me, because politics is for the present, but an equation is something for eternity.",
             "54c4a33c6423d689378f160a7ff8b61330444abb58fb470f96ea16d99d4a2fed07082304410efa6b2943111b6a4e0aaa7b7db55a07e9861d1fb3cb1f421044a5",
             "3044022054c4a33c6423d689378f160a7ff8b61330444abb58fb470f96ea16d99d4a2fed022007082304410efa6b2943111b6a4e0aaa7b7db55a07e9861d1fb3cb1f421044a5"
             ),
            (0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140,
             "L5oLkpV3aqBjhki6LmvChTCV6odsp4SXM6FfU2Gppt5kFLaHLuZ9",
             "Not only is the Universe stranger than we think, it is stranger than we can think.",
             "ff466a9f1b7b273e2f4c3ffe032eb2e814121ed18ef84665d0f515360dab3dd06fc95f5132e5ecfdc8e5e6e616cc77151455d46ed48f5589b7db7771a332b283",
             "3045022100ff466a9f1b7b273e2f4c3ffe032eb2e814121ed18ef84665d0f515360dab3dd002206fc95f5132e5ecfdc8e5e6e616cc77151455d46ed48f5589b7db7771a332b283"
             ),
            (0x0000000000000000000000000000000000000000000000000000000000000001,
             "KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn",
             "How wonderful that we have met with a paradox. Now we have some hope of making progress.",
             "c0dafec8251f1d5010289d210232220b03202cba34ec11fec58b3e93a85b91d375afdc06b7d6322a590955bf264e7aaa155847f614d80078a90292fe205064d3",
             "3045022100c0dafec8251f1d5010289d210232220b03202cba34ec11fec58b3e93a85b91d3022075afdc06b7d6322a590955bf264e7aaa155847f614d80078a90292fe205064d3"
             ),
            (0x69ec59eaa1f4f2e36b639716b7c30ca86d9a5375c7b38d8918bd9c0ebc80ba64,
             "KzmcSTRmg8Gtoq8jbBCwsrvgiTKRrewQXniAHHTf7hsten8MZmBB",
             "Computer science is no more about computers than astronomy is about telescopes.",
             "7186363571d65e084e7f02b0b77c3ec44fb1b257dee26274c38c928986fea45d0de0b38e06807e46bda1f1e293f4f6323e854c86d58abdd00c46c16441085df6",
             "304402207186363571d65e084e7f02b0b77c3ec44fb1b257dee26274c38c928986fea45d02200de0b38e06807e46bda1f1e293f4f6323e854c86d58abdd00c46c16441085df6"
             ),
            (0x00000000000000000000000000007246174ab1e92e9149c6e446fe194d072637,
             "KwDiBf89QgGbjEhKnhXJwe1E2mCa8asowBrSKuCaBV6EsPYEAFZ8",
             "...if you aren't, at any given time, scandalized by code you wrote five or even three years ago, you're not learning anywhere near enough",
             "fbfe5076a15860ba8ed00e75e9bd22e05d230f02a936b653eb55b61c99dda4870e68880ebb0050fe4312b1b1eb0899e1b82da89baa5b895f612619edf34cbd37",
             "3045022100fbfe5076a15860ba8ed00e75e9bd22e05d230f02a936b653eb55b61c99dda48702200e68880ebb0050fe4312b1b1eb0899e1b82da89baa5b895f612619edf34cbd37"
             ),
            (0x000000000000000000000000000000000000000000056916d0f9b31dc9b637f3,
             "KwDiBf89QgGbjEhKnhXJuH7LrciVrZiib5S9h4knkymNojPUVsWN",
             "The question of whether computers can think is like the question of whether submarines can swim.",
             "cde1302d83f8dd835d89aef803c74a119f561fbaef3eb9129e45f30de86abbf906ce643f5049ee1f27890467b77a6a8e11ec4661cc38cd8badf90115fbd03cef",
             "3045022100cde1302d83f8dd835d89aef803c74a119f561fbaef3eb9129e45f30de86abbf9022006ce643f5049ee1f27890467b77a6a8e11ec4661cc38cd8badf90115fbd03cef"
             )
        ]

        for (secret_exponent, _, message, _, expected_sig) in test_vectors:

            h = hashlib.sha256(message.encode('utf-8')).digest()
            val = intbytes.from_bytes(h)

            # This will use deterministic values of k based on 'val'
            r, s = ecdsa.sign(secp256k1.generator_secp256k1, secret_exponent,
                              val)

            # Ensure that 's' is even to prevent attacks - see https://bitcointalk.org/index.php?topic=285142.msg3295518#msg3295518
            if s > (secp256k1.generator_secp256k1.order() / 2):
                s = secp256k1.generator_secp256k1.order() - s

            sig = der.sigencode_der(r, s)

            assert sig == bytes.fromhex(
                expected_sig
            ), "ECDSA signature using RFC 6979 failed\nExpected: " + expected_sig + "\nActual:   " + self.hexstr(
                sig)

    def test_txhash(self):
        """
        Performs a test of the reference wallet's tx hashing against a known blockchain tx.
        """

        # 's' and 'expected' are from:
        # https://blockchain.info/rawtx/a8196acaf3938b988f9816ae3e9da1df5a04afff0b5b460e4c1dc4a08dd52109?format=hex

        s = ("0100000002bca066b9cfe1eb81e667f219a442acdc5c5e2e470610659a314"
             "74dfb5e29c552000000008c493046022100b2857170045d5e59112e0d5200"
             "4a8f65d18945e52d42c2eb12f7d3c2314600b802210098bdab40dfe38b5d4"
             "fe02e1fa3057ada3e0a982a5c7979eabff86395e2a911e8014104a8075344"
             "0c651f7191f46085411679545486f1dc6bf34cdaba453966c5fe7cc34f3dd"
             "c15ae321974f426807faa34b3fc10034e129222067ec053c409a6ac1f30ff"
             "ffffffe047c65f9e560d799580f6a965c12a059ca1e82cebc3e220659ba29"
             "ee31c8d0a010000008b48304502206b7eaa2dec17b53022b57a55b48ac245"
             "6f1c22d87b0170aa969de04146b80bbc022100d6700f6eb9bde89c35b0545"
             "588c06dcfed95e0502941d79786b5ea24eafc2cfe01410435d1d08c6f5296"
             "0d056e60c3b5c858e5299c1a688395b589dbde6b58861b20fdd7ee58832b3"
             "528845973765038cafc1c81280dc635ee202ce06aa4a373db012fffffffff"
             "0200f2052a010000001976a914315bfd9ee07d6779e44b8e07229650f039f"
             "0942788aced931600000000001976a91484004861f9a742fc83ad4ab83c42"
             "e709b512df1888ac00000000")

        expected = "a8196acaf3938b988f9816ae3e9da1df5a04afff0b5b460e4c1dc4a08dd52109"
        expected = bytes.fromhex(expected)

        actual = encoding.double_sha256(bytes.fromhex(s))

        # Reverse the bytes to flow lsb -> msb
        actual = actual[::-1]

        assert actual == expected, "tx hash calculation mismatch\n" + "Expected: " + self.hexstr(
            expected) + "\nActual:   " + self.hexstr(actual)

    #
    # Utilities
    #
    def fw_update(self, fwfile):
        """
        Updates device firmware.
       
        fwfile  - Path to and name of the firmware file to use.
        """

        self.polly.send_fw_download(fwfile)

    def create_prev_tx(self, win, in_keynum, sources_per_input, wout,
                       out_keynum_satoshi, fees_satoshi):
        """
        Creates and returns a supporting 'previous' tx of 100KB or less
    
        win                 - wallet to use for input addresses
        in_keynum           - key nums from win
        sources_per_input   - how many sources are used to fund each input
        wout                - wallet to use for output addresses
        out_keynum_satoshi  - list of key nums from wout and satoshis to spend in tuples of (num, satoshis)
        
        Returns a bytes object containing the previous tx.
        """

        # Calculate the total output
        payables = []
        total_spent = 0

        for (out_key_id, out_satoshi) in out_keynum_satoshi:

            address = wout.subkey(out_key_id).bitcoin_address()
            payables.append((address, out_satoshi))

            total_spent += out_satoshi

        # Split the total to spend across all of the inputs
        spendables = []
        total_value = 0

        satoshi_per_input = int(total_spent + fees_satoshi) / len(in_keynum)

        for keynum in in_keynum:

            # Grab the address for the current key num
            addr = win.subkey(keynum, as_private=True).bitcoin_address()

            # Generate fake sources for funding the input coin
            spendables.extend(
                self.fake_sources_for_address(addr, sources_per_input,
                                              satoshi_per_input))

            total_value += satoshi_per_input

        # Calculate the fee
        tx_fee = total_value - total_spent

        assert tx_fee >= 0, "fee < 0: " + str(tx_fee)

        # Create and 'sign' the transaction
        unsigned_tx = create_tx(spendables, payables, tx_fee)
        signed_tx = self.__sign_fake(unsigned_tx)

        return self.get_tx_bytes(signed_tx)

    def fake_sources_for_address(self, addr, num_sources, total_satoshi):
        """
        Returns a fake list of funding sources for a bitcoin address.
        
        Note: total_satoshi will be split evenly by num_sources
        
        addr          - bitcoin address to fund
        num_sources   - number of sources to fund it with
        total_satoshi - total satoshis to fund 'addr' with 
    
        Returns a list of Spendable objects
        """

        spendables = []
        satoshi_left = total_satoshi
        satoshi_per_tx = satoshi_left / num_sources
        satoshi_per_tx = int(satoshi_per_tx)

        # Create the output script for the input to fund
        script = standard_tx_out_script(addr)

        while satoshi_left > 0:
            if satoshi_left < satoshi_per_tx:
                satoshi_per_tx = satoshi_left

            # Create a random hash value
            rand_hash = bytes([random.randint(0, 0xFF) for _ in range(0, 32)])

            # Create a random output index
            # This field is 32 bits, but typically transactions dont have that many, limit to 0xFF
            rand_output_index = random.randint(0, 0xFF)

            # Append the new fake source
            spend = Spendable(satoshi_per_tx, script, rand_hash,
                              rand_output_index)

            spendables.append(spend)

            satoshi_left -= satoshi_per_tx

        assert satoshi_left == 0, "incorrect funding"

        return spendables

    def get_tx_bytes(self, tx):
        """
        Takes a Tx object and returns a bytes object containing the tx bytes.
        """

        s = io.BytesIO()
        tx.stream(s)
        return s.getvalue()

    def gen_wordlist(self, seed):
        """
        Generates a polly mnemonic wordlist from a seed, including the checksum.
        
        seed -  a string of 24 hex bytes (for a strength of 192 bits)
        
        Returns a space separated string of 18 words from the wordlist.
        """

        assert len(seed) == 24, "incorrect seed length, expecting 24 bytes"

        return self.mnemonic.to_mnemonic(seed)

    def hexstr(self, data):
        """
        Takes a bytes object and returns a packed hex string.
        """

        # Hexlify the bytes object and strip off the leading b' and trailing '
        return str(binascii.hexlify(data))[2:-1]

    def txstr(self, tx):
        """
        Takes a tx bytes object and prints out its details field by field.
        """
        def hexy(tag, data):
            print("{0:<20s} : {1}".format(tag, self.hexstr(data)))

        print("\n[tx details]\n")

        s = 0

        hexy("version", tx[s:s + 4])
        s += 4

        in_count = ord(tx[s:s + 1])
        hexy("in count", tx[s:s + 1])
        s += 1

        for _ in range(0, in_count):

            print(" -------------------")

            hexy(" prev out hash", tx[s:s + 32])
            s += 32

            hexy(" prev out index", tx[s:s + 4])
            s += 4

            scriptlen = ord(tx[s:s + 1])
            hexy(" scriptlen", tx[s:s + 1])
            s += 1

            hexy(" script", tx[s:s + scriptlen])
            s += scriptlen

            hexy(" sequence", tx[s:s + 4])
            s += 4

        print()
        out_count = ord(tx[s:s + 1])
        hexy("out count", tx[s:s + 1])
        s += 1

        for _ in range(0, out_count):

            print(" -------------------")

            hexy(" value", tx[s:s + 8])
            s += 8

            scriptlen = ord(tx[s:s + 1])
            hexy(" pk scriptlen", tx[s:s + 1])
            s += 1

            hexy(" pk script", tx[s:s + scriptlen])
            s += scriptlen

        print()
        hexy("lock time", tx[s:s + 4])
        s += 4

    #
    # Private
    #

    def __outok(self):
        """
        Creates a standard successful completion string for Polly operations
        """
        return "ok (" + self.polly.get_cmd_time() + ")"

    def __sign_fake(self, tx):
        """
        Sign a transaction using a fake randomly generated signature.
        """

        # Create a fake ecdsa signature from 0x68 - 0x6b bytes
        rand_script = bytes([
            random.randint(0, 0xFF)
            for _ in range(0, random.randint(0x68, 0x6b))
        ])

        tx.check_unspents()
        for idx, tx_in in enumerate(tx.txs_in):
            if tx.unspents[idx]:
                tx_in.script = rand_script

        return tx