Bitcoin Forum
May 14, 2024, 05:02:13 PM *
News: Latest Bitcoin Core release: 27.0 [Torrent]
 
   Home   Help Search Login Register More  
Pages: [1]
  Print  
Author Topic: Transaction verification in Python (with M2Crypto)  (Read 3425 times)
titeuf_87 (OP)
Member
**
Offline Offline

Activity: 111
Merit: 10


View Profile
June 16, 2011, 09:19:53 PM
 #1

Hey everyone,

I've been playing around with Python and bitcoin and while there are some implementations of the network protocol floating around on the web, I haven't found anything that can parse and run scripts from transactions and verify if they're valid.

After messing around, I've made a (really horribly) script that, given two linked transactions, checks that the second transaction is allowed to redeem the referenced output from the first transaction.

Code:
import binascii
import hashlib
import M2Crypto
import structures

#Transaction 2 uses an output from Transaction 1
#Transaction 1: http://blockexplorer.com/tx/945691940e0ccd9f526ee1edd57a77ce170804915749702f5564c49b1f70f330
#Transaction 2: http://blockexplorer.com/tx/ff954e099764d192c5bb531c9c14c18c230b0c0a63f02cd168a4ea94548c890f#i325189

tx1raw= '\x01\x00\x00\x00\x02\x0f{\x7f\xb8mL\xf6F\x05\x8eA\xd3\xb0\x07\x18?\xdfysn\xd1\x9b*th\xab\xc5\xbd\x04\xb1n\x91\x00\x00\x00\x00\x8cI0F\x02!\x00\xb2\xee9\xd2\xfc\xc2\xe5TJW\xc3\x0f{NI\xcf\xb8""fm\x03O\xb9\x0e"4\x8e\x17\xe2\x8e\x0f\x02!\x00\xdb\x91\xc3\x19\x9c\xc7\xb4\x1dMz\xfc\xe0\xcc\xb4\xce\xb4$\xb9GmQ\xc0aBX=\xafS\xce\n\x9bf\x01A\x04\xc3"\x15\xa9\t0\x11\xbd<A(:\xce=\x00,f`w\xb2J`[<\xfc\x8fq\x01\x9a\x0fC\xdff\xf3\x89\xf3\xd9\xa6!\x88\xa4\x94\xb8i\xdc~_\x9d\xff\xc9\x8av\xd3\x08\x8a!\xe9\xb78\xec\x9e\xba\x98\xcb\xff\xff\xff\xff\x97\x00A%R\x8f{^\xd34e\xca\xaa\xe0!\xc0\xb8\x15\xf3\xe6\xa3pvA\xd5\xa0\xbc\xa4?\xc1II\x01\x00\x00\x00\x8aG0D\x02 3\xd0,.\x89o\x1a\x12RH\x8dSL\xfb\x08\xab\xf3\xe7\xea\x90\xab\xa7\xbaoW\xab\xf1\x89\xce\xf1\xd87\x02 \x05f\x8duP\x13\xb0\xe5\x9a*\xf5\x14_\x10\xef\xe6.\xa7\x16\xd33&\x8b\x0bZ>\xfb\xd8-\x149\xbe\x01A\x04\xc3"\x15\xa9\t0\x11\xbd<A(:\xce=\x00,f`w\xb2J`[<\xfc\x8fq\x01\x9a\x0fC\xdff\xf3\x89\xf3\xd9\xa6!\x88\xa4\x94\xb8i\xdc~_\x9d\xff\xc9\x8av\xd3\x08\x8a!\xe9\xb78\xec\x9e\xba\x98\xcb\xff\xff\xff\xff\x01\x00\xc2\xeb\x0b\x00\x00\x00\x00\x19v\xa9\x14\x02\xbfK(\x89\xc6\xad\xa8\x19\x0c%.p\xbd\xe1\xa1\x90\x9f\x96\x17\x88\xac\x00\x00\x00\x00'
tx2raw= "\x01\x00\x00\x00\x030\xf3p\x1f\x9b\xc4dU/pIW\x91\x04\x08\x17\xcewz\xd5\xed\xe1nR\x9f\xcd\x0c\x0e\x94\x91V\x94\x00\x00\x00\x00\x8cI0F\x02!\x00\xf5tk\x0b%OZ7\xe7RQE\x9cz#\xb6\xdf\xcb\x86\x8a\xc7F~\xdd\x9ao\xdd\x1d\x96\x98q\xbe\x02!\x00\x88\x94\x8a\xea)\xb6\x91a\xca4\x1cI\xc0&\x86\xa8\x1d\x8c\xbbs\x94\x0f\x91\x7f\xa0\xedqThm>[\x01A\x04G\xd4\x90V\x1f9l\x8a\x9e\xfc\x14Hk\xc1\x98\x88K\xa1\x83y\xbc\xac.\x0b\xe2\xd8RQ4\xabt/0\x1a\x9a\xca6`n])\xaa#\x8a\x9e)\x93\x001PB=\xf6\x92Ecd-J\xfe\x9b\xf4\xfe(\xff\xff\xff\xffr\x14+\xf7hl\xe9,m\xe5\xb73e\xbf\xb9\xd5\x9b\xb6\x0c,\x80\x98-YX\xc1\xe6\xa3\xb0\x8e\xa6\x89\x00\x00\x00\x00JI0F\x02!\x00\xbc\xe4:\xd3\xac\xbcy\xb0$~T\xc8\xc9\x1e\xac\x1c\xf9\x03u\x05\x00\x0e\x01\xd1\xfd\x81\x18T\xd8[\xc2\x1a\x02!\x00\x99*oo/\xebob\xd3po;\x9a\xaa\xb8\x8d\x9f\x112\x95j\x1d\xff\xa9&\xcdUn\xd5S`\xdf\x01\xff\xff\xff\xff\xd2\x81(\xbb\xb6 |\x1c=\nc\x0c\xc6\x19\xdc~{\xeaV\xac\x19\xa1\xda\xb1'\xc6,x\xfa\x1bc,\x00\x00\x00\x00IH0E\x02  \x97W6\x81aSw\x08\xfd)\xd8\x9b\xb1\xe9\xd6H\x00yI\xec\xfd\xedx\x9bQ\xa9c$\xcbe\x18\x02!\x00\xcd\x0f|0!9\x16H+n\x16m\x8aO+\x98\x1fw~\xb1\x84\xcd\x8aI_\x1b=6\x90\xfb\xbf-\x01\xff\xff\xff\xff\x01\x00\xa6\xf7_\x02\x00\x00\x00\x19v\xa9\x14\x9e5\xd9<w\x92\xbd\xca\xadV\x97\xdd\xeb\xf0CS\xd9\xa5\xe1\x96\x88\xac\x00\x00\x00\x00"

tx1 = structures.Tx.deserialize(tx1raw)[0]
tx2 = structures.Tx.deserialize(tx2raw)[0]

#We're going to check that the first input of Tx2 is correctly redeemed

def run_script(script, stack):
    while script:
        opcode = ord(script.pop(0))
        if opcode <= 75:
            value = ""
            for x in xrange(0, opcode):
                value += script.pop(0)
            stack.append(value)
        
        elif opcode == 118:
            #OP_DUP
            stack.append(stack[-1])
        
        elif opcode == 169:
            #OP_HASH160
            #The input is hashed twice: first with SHA-256 and then with RIPEMD-160.
            value = stack.pop()
            value = hashlib.sha256(value).digest()
            ripemd = hashlib.new('ripemd160')
            ripemd.update(value)
            value = ripemd.digest()
            stack.append(value)
        
        elif opcode == 136:
            #OP_EQUALVERIFY
            v1 = stack.pop()
            v2 = stack.pop()
            if v1 != v2:
                raise Exception()
        
        elif opcode == 172:
            #OP_CHECKSIG

            #1. the public key and the signature are popped from the stack, in that order.
            pubkey = stack.pop()
            signature = stack.pop()
            
            #2. A new subscript is created from the instruction from the most recent OP_CODESEPARATOR to the end of the script. If there is no OP_CODESEPARATOR the entire script becomes the subscript (hereby referred to as subScript)
            #(we don't do the OP_CODESEPARATOR thing yet...)
            subscript = tx1.txout[0].script
            
            #3. the signature is deleted from subscript
            subscript = subscript.replace(chr(len(signature)) + signature, "")

            #4. The hashtype is removed from the last byte of the sig and stored
            hashtype = signature[-1]
            signature = signature[:-1]
            
            #5. A deep copy is made of the current transaction (hereby referred to txCopy)
            #(we do this by serializing and deserialing this transaction again...)
            txCopy = structures.Tx.deserialize(tx2.serialize())[0]
            
            #6. All OP_CODESEPARATORS are removed from subScript
            #(we don't do this yet...)
            
            #7. The scripts for all transaction inputs in txCopy are set to empty scripts
            for txin in txCopy.txins:
                txin.script = ""
                
            #8. The script for the current transaction input in txCopy is set to subScript
            #We're checking the first input
            txCopy.txins[0].script = subscript

            #An array of bytes is constructed from the serialized txCopy + four bytes for the hash type.
            #This array is sha256 hashed twice, then the public key is used to to check the supplied
            #signature against the hash.
            
            #Load the public key. I have no idea what this magical string does...
            pkey = pubkey[::-1] + "0042030a0004812b050601023dce48862a070610305630".decode("hex")
            pkey = M2Crypto.EC.pub_key_from_der(pkey[::-1])
            
            #Serialize the transaction and add the hashtype to the end as an int
            txhash = txCopy.serialize() + "\x01\x00\x00\x00"
            #And sha256 it twice
            txhash = hashlib.sha256(hashlib.sha256(txhash).digest()).digest()
            
            if pkey.verify_dsa_asn1(txhash, signature):
                stack.append(True)
            else:
                stack.append(False)
            
stack = []
run_script(list(tx2.txins[0].script), stack)
run_script(list(tx1.txout[0].script), stack)

print stack

This code needs structures.py which is my implementation of the (de)serialization of the various bitcoin messages.

The above script is really messy and more a quick proof-of-concept than anything else. Using it like that will be harmful for the bitcoin network as it is far from fully implemented and misses some small details.
The script checks that input 1 from this transaction is valid with the referenced transaction.

My biggest problem was with M2Crypto: it doesn't seem to implement o2i_ECPublicKey from openssl, which is used by bitcoin to set the public key. I also looked at using pyOpenssl and pycrypto, but it looks like neither of these libraries can handle elliptic curve cryptography...

To make M2Crypto work for this I had to use some magic, which I'm really not happy about:
Code:
pkey = pubkey[::-1] + "0042030a0004812b050601023dce48862a070610305630".decode("hex")
pkey = M2Crypto.EC.pub_key_from_der(pkey[::-1])
As you can see, I need to add a fixed string in order to load the public key. Without this it fails to load it and I get an exception (ValueError: Received a NULL pointer.)
This string I got by making new public/private key pairs with M2Crypto and printing them out: I noticed that this part is fixed and never changes. I assume this string defines the parameters used (NID_secp256k)

Anyways, I hope this is useful to someone. Feel free to use this code for anything, but keep in mind that it is horrible and needs lots of cleaning up.

And if anyone feels like improving it, go for it Smiley


Edit: I forgot to mention that this only validates standard transactions to another bitcoin address.

15kfBM3TQ4PGzL7cKncU3su2pH7ZJmiLtr
Martin P. Hellwig
Newbie
*
Offline Offline

Activity: 33
Merit: 0


View Profile
June 28, 2011, 09:24:49 PM
Last edit: June 28, 2011, 09:35:08 PM by hellwig
 #2

Hi,

Thanks for the snippet it is greatly appreciated!

My biggest problem was with M2Crypto: it doesn't seem to implement o2i_ECPublicKey from openssl, which is used by bitcoin to set the public key. I also looked at using pyOpenssl and pycrypto, but it looks like neither of these libraries can handle elliptic curve cryptography...

To make M2Crypto work for this I had to use some magic, which I'm really not happy about:
Code:
pkey = pubkey[::-1] + "0042030a0004812b050601023dce48862a070610305630".decode("hex")
pkey = M2Crypto.EC.pub_key_from_der(pkey[::-1])
As you can see, I need to add a fixed string in order to load the public key. Without this it fails to load it and I get an exception (ValueError: Received a NULL pointer.)
This string I got by making new public/private key pairs with M2Crypto and printing them out: I noticed that this part is fixed and never changes. I assume this string defines the parameters used (NID_secp256k)

Yes, that is correct, as you already figured out bitcoin only sends the xy position of the curve, however for openssl also needs to know which curve is used and of course what type of public key it is, since this is all DER encoded I opted for recreating the DER package. As you can see I used the same key to check if it works. Although in my code I only have a dependency on openssl directly (via subprocess) and thus do away with m2crypto or any other crypto wrapper, I do have a new dependency on pyasn1 http://sourceforge.net/projects/pyasn1/ though.

The relevant part of my code:
Code:
from pyasn1.codec.der import encoder
from pyasn1.type.univ import Sequence, ObjectIdentifier, BitString

# http://www.oid-info.com/get/1.2.840.10045.2.1
OID_EC_PUBLIC_KEY = "1.2.840.10045.2.1"
# http://www.oid-info.com/get/1.3.132.0.10
OID_SECP256K1 = "1.3.132.0.10"

def ec_public_key_in_der(xy_curve):
    "Create the DER public key part using the XY curve values"
    oid = Sequence()
    oid.setComponentByPosition(0, ObjectIdentifier(OID_EC_PUBLIC_KEY))
    oid.setComponentByPosition(1, ObjectIdentifier(OID_SECP256K1))
    
    xyc = BitString("'%s'H" % xy_curve.encode('hex'))
    
    tmp = Sequence()
    tmp.setComponentByPosition(0, oid)
    tmp.setComponentByPosition(1, xyc)
    return(encoder.encode(tmp))

if __name__ == '__main__':
    XY_CURVE = "0447d490561f396c8a9efc14486bc198884ba18379bcac2e0be2d8525134" +\
    "ab742f301a9aca36606e5d29aa238a9e2993003150423df6924563642d4afe9bf4fe28"
    XY_CURVE = XY_CURVE.decode('hex')
    
    PUBLIC_KEY = "3056301006072a8648ce3d020106052b8104000a0342000447d490561f" +\
    "396c8a9efc14486bc198884ba18379bcac2e0be2d8525134ab742f301a9aca36606e5d2" +\
    "9aa238a9e2993003150423df6924563642d4afe9bf4fe28"
    PUBLIC_KEY = PUBLIC_KEY.decode('hex')

    if ec_public_key_in_der(XY_CURVE) == PUBLIC_KEY:
        print(True)
    else:
        print(False)
titeuf_87 (OP)
Member
**
Offline Offline

Activity: 111
Merit: 10


View Profile
June 29, 2011, 07:38:12 AM
 #3

Awesome, thank you Smiley

If you post a bitcoin address I'll thank you with bitcoins too (when I get home, I don't take my wallet with me at work  Tongue)

15kfBM3TQ4PGzL7cKncU3su2pH7ZJmiLtr
Martin P. Hellwig
Newbie
*
Offline Offline

Activity: 33
Merit: 0


View Profile
June 29, 2011, 08:56:36 PM
 #4

12zhTAZg4R6DuV3TJpXTDE98ayFn9YL5qH

Most appreciated as I am not on exchanges and my laptop is a bit slow in mining :-)
genjix
Legendary
*
expert
Offline Offline

Activity: 1232
Merit: 1076


View Profile
June 29, 2011, 11:36:51 PM
 #5

This is awesome! ^^ I'm definitely using this in my project as a guide if anything.
titeuf_87 (OP)
Member
**
Offline Offline

Activity: 111
Merit: 10


View Profile
June 30, 2011, 06:29:55 AM
 #6

12zhTAZg4R6DuV3TJpXTDE98ayFn9YL5qH

Most appreciated as I am not on exchanges and my laptop is a bit slow in mining :-)

Sent, enjoy!

15kfBM3TQ4PGzL7cKncU3su2pH7ZJmiLtr
Martin P. Hellwig
Newbie
*
Offline Offline

Activity: 33
Merit: 0


View Profile
June 30, 2011, 05:24:55 PM
 #7


Sent, enjoy!
[/quote]

Thanks!
Pages: [1]
  Print  
 
Jump to:  

Powered by MySQL Powered by PHP Powered by SMF 1.1.19 | SMF © 2006-2009, Simple Machines Valid XHTML 1.0! Valid CSS!