20180823

Bitcoin : un coup d'oeil coup le capot

Un post pour traduire un article de 2017 sur le fonctionnement des transactions Bitcoin

Pour une fois, un article en français. comme on ne peut pas toujours se contenter de la surface, cette fois-ci, on plonge plus en profondeur...  L'article original a été écrit par Sam Lewis et se trouve ici.

Bitcoin est vraiment cool. Bien sûr il y a des remarques à faire à son sujet, qu'il s'agisse de savoir si c'est une technologie interessante, si nous sommes dans une bulle des cryptomonnaies ou bien si le problème de gouvernance auquel nous sommes confrontés va trouver une solution. Mais d'un point de vue purement technique, le mystique Satoshi Nakamoto a créé une technologie impressionnante.

Malheureusement, bien qu'il y ait de nombreuses ressources résumant comment Bitcoin fonctionne à un haut niveau (je recommanderai par exemple la fantastique vidéo de Anders Brownworth, Blockchain visual 101), il n'y a pas beaucoup d'informations sur ce qui se passe dans les couches basses et, à mon avis, on ne peut pas toujours se contenter de la vue à 10 000 pieds d'altitude.

En tant que néophyte, j'ai eu beaucoup de mal à comprendre la mécanique du foncitonnement de Bitcoin. Heureusement, parce que bitcoin est décentralisé et pair à pair par nature, tout à chacun est capable de développer un client respectant le protocole. Afin de mieux comprendre la façon dont bitcoin foncionne, j'ai donc décidé d'écrire mon propre petit client Bitcoin, capable de publier une transaction sur la Blockchain Bitcoin.

Cet article décrit le processus de création d'un client minimal, capable de créer une transaction, de la soumettre au réseau pair à pair Bitcoin afin qu'il soit inscrit dans sa Blockchain. Si vous préférez juste lire le code brut, vous pouvez sans problème vous rendre sur mon repo GITHUB.

Génération d'une adresse

Pour participer au réseau bitcoin, il faut disposer d'une adresse à partir de laquelle il est possible d'envoyer et de recevoir des fonds. Bitcoin utilise la cryptographie à clé publique et une adresse est simplement un hash de la clé publique qui, elle-même, dérive de la clé privée. Etonnamment, et contrairement à la plupart des autres systèmes de cryptographie à clé publique, la clé publique est égalment gardée secrète jusqu'à ce que les fonds soient dépenses depuis cette adresse - mais nous décrirons cela plus tard.

En aparté, un point de terminologie : Le terme de "porte-monnaie (wallet)" est utilisé par les clients Bitcoin pour désigner une liste d'adresses. Ce concept de porte-monnaie n'existe pas dans le protocole technique qui ne connait que des adresses.
Bitcoin utilise la cryptographie à courbe elliptique pour définir ses adresses. Au niveau le plus fin, la cryptographie à courbe elliptique sert à générer une clé publique à partir d'une clé privée, un peu comme le ferait RSA, mais avec une empreinte plus légère. Si vous souhaitez appronfondir l'aspect mathématique de ce fonctionnement, Cloudflare's primer est une ressource fantastique.

Le schéma ci-dessous montre le processus de génération d'une adresse Bitcoin à partir d'une clé privée de 256 bits.

Sous Python, j'utilise la bibliothèque ecsda pour faire le gros du travail de cryptographie à courbe elliptique. Le bout de code suivant crée une clé publique à partir de la très mémorable (et très peu sûre) clé privée 0xFEEDB0BDEADBEEF (En ajoutant assez de zéros à l'avant pour faire 64 caractères hexadécimaux, soit 256 bis). Utilisez une méthode de création de clé privée plus sûre si vous souhaitez stocker de vrai montants sur une adresse !
Aparté amusante. J'avais initialemment créé une adresse en utilisant la clé privé OxFACEBEFF en y stockant 0.0005 BTC. Après seulement un mois quelqu'un avait volé mes 0.0005 BTC ! J'imagine que certains doivent de temps en temps aller à la pêche sur les adresses issues des clés privées les plus simples/usuelles. Vous devez vraiment utiliser une technique fiable pour définir vos clés privées !

      from ecdsa import SECP256k1, SigningKey

        def get_private_key(hex_string):
            return bytes.fromhex(hex_string.zfill(64)) # pad the hex string to the required 64 characters
          
        def get_public_key(private_key):
            # this returns the concatenated x and y coordinates for the supplied private address
            # the prepended 04 is used to signify that it's uncompressed
            return (bytes.fromhex("04") + SigningKey.from_string(private_key, curve=SECP256k1).verifying_key.to_string())
          
        private_key = get_private_key("FEEDB0BDEADBEEF")
        public_key = get_public_key(private_key)

Ce code produit la clé privée suivante (en hexadécimal) :

0000000000000000000000000000000000000000000000000feedb0bdeadbeef

Et la clé publique (toujours en hexadécimal) :

04d077e18fd45c031e0d256d75dfa8c3c21c589a861c4c33b99e64cf613113fcff9fc9d90a9d81346bcac64d3c01e6e0ef0828543edad73c0e257b845812cc8d28

Le 0x04 au début de la clé publique indique qu'il s'agit d'une clé publiquenon compressée, cela signifie que les coordonnées x et le y sur la courbe ECSDA sont simplement concaténés. De part la structure de la courbe ECSDA, si vous connaissez la valeur de l'abcisse x, l'ordonnée y ne peut prendre que deux valeurs, uen paire et une impaire. En utilisant cette caractéristique, il est possible d'exprimer une clé publique avec seulement son abcisse et la polarité de son ordonnée. Cela réduit la taille de la clé de 65 à 33 bits, la clé (et par conséquent l'adresse qui en découle) est dite compressée. Pour les clés publiques compressées, la valeur de début est 0x02 ou 0x03 en fonction de la polarité de l'abcisse. Bitcoin utilise principalement les clés non compressées, c'est donc ce que nous ferons également par la suite.

A partir de là, afin de générer l'adresse Bitcoin depuis la clé publique, cette dernière est "hashée" par sha256 puis par "ripemd160". Cette double empreinte produit une sur-couche de sécurité et le hash ripemd160 réduit la longueur de l'adresse, qui passe de 256 bits à 160 bits. Une conséquence remarquable est qu'il alors possible que deux clés publiques produisent la même adresse ! Toutefois, avec 2^160 adresses possibles, ce n'est pas susceptible de se produire de si tôt.

      import hashlib
        def get_public_address(public_key):
            address = hashlib.sha256(public_key).digest()
            h = hashlib.new('ripemd160')
            h.update(address)
            address = h.digest()
            return address
        public_address = get_public_address(public_key)

Le code ci-dessus génère l'adresse c8db639c24f6dc026378225e40459ba8a9e54d1a. Ceci est parfois appelé l'adresse hash 160.

Comme évoqué plus tôt, un point intéressant est que les conversions de la clé privée en clé publique puis de la clé publique vers l'adresse ne sont pas réversibles. Si vous diposez d'une adresse la seule possibilité de remonter à la clé publique est de résoudre le hash sha256. C'est un peu différent de la plupart des cryptographies à clés publiques dans lesquelles votre clé publique est connue et votre clé privée cachée. Dans ce cas, les deux clés sont cachées et seule l'adresse (qui est une empreinte de la clé publique) est publiée.

Les clés publiques sont cachées pour uen bonne raison. Bien qu'il soit normalement impossible de remonter d'une clé publique vers sa clé privée, si la méthode de génération de clé privée est corrompue, alors l'accès aux clés publiques permet un peu plus facilement de déduire la clé privée. En 2013, c'est malheureusement arrivé dans les porte-monnaies Bitcoin pour Androïd. Androïd avait une faiblesse critique dans sa génération de nombres aléatoires, créant ainsi un vecteur d'attaque pour trouver les clés privées depuis les clés publiques. C'est également la raison pour laquelle la réutilisation d'adresses publiques n'est pas encouragée. La signature d'une transaction oblige à produire sa clé publique. si vous ne réutilisez pas une clé après l'envoi d'une transaction depuis son adresse, vous n'avez pas à vous inquiéter d'avoir exposé cette clé publique.

La façon standard d'exrpimer une adresse Bitcoin est de l'encoder via Base58Check. Cet encodage n'est qu'une représentation de l'adresse (il peut être décodé). Base58Check génère des adresses de la forme 1661HxZpSy5jhcJ2k6av2dxuspa8aafDac. L'encodage Base58Check produit des adresses plus courtes et embarque une somme de contrôle, cela permet de détecter les adresses malformées. Dans à peu près tous les clients Bitcoin les adresses encodées en Base58Check sont les seules que vous verrez. Base58Check comporte également un numéro de version, que je positionne à 0 dans le code suivant pour indiquer qu'il s'agit du hash d'une clé publique.

        # 58 character alphabet used
        BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
        
        def base58_encode(version, public_address):
            """
            Gets a Base58Check string
            See https://en.bitcoin.it/wiki/base58Check_encoding
            """
            version = bytes.fromhex(version)
            checksum = hashlib.sha256(hashlib.sha256(version + public_address).digest()).digest()[:4]
            payload = version + public_address + checksum
        
            result = int.from_bytes(payload, byteorder="big")
        
            print(result)
        
            # count the leading 0s
            padding = len(payload) - len(payload.lstrip(b'\0'))
            encoded = []
        
            while result != 0:
                result, remainder = divmod(result, 58)
                encoded.append(BASE58_ALPHABET[remainder])
        
            return padding*"1" + "".join(encoded)[::-1]
        
        bitcoin_address = base58_encode("00", public_address)

Au final, à partir de ma clé privée feedb0bdeadbeef (préfixée par des zéros), je suis arrivé à l'adresse Bitcoin 1KK2xni6gmTtdnSGRiuAf94jciFgRjDj7W !

Muni d'une adresse, il est maintenant possible d'obtenir queqlues bitcoins ! Pour ce faire, je me suis acheté sur 0.0045 BTC (à peu près 11 USD au moment où j'écris) depuis btcmarkets en utilisant des dollars australiens. Depuis le portail de trading de btcmarkets, je les ai transférés sur l'adresse ci-dessus, perdant 0.0005 BTC de frais durant la procédure. Vous pouvez voir ce mouvement sur la blockchain à la transaction 95855ba9f46c6936d7b5ee6733c81e715ac92199938ce30ac3e1214b8c2cd8d7

Se connecter au réseau P2P

Maintenant que j'ai une adresse avec quelques bitcoinc dessus, les choses deviennent plus intéressantes. Pour envoyer ces bitcoins ailleurs, il est nécessaire de se connecter au réseau P2P de Bitcoin.

Bootstrapping

Une des difficultés que j'ai eu à la première approche de Bitcoin, étant donné la nature décentralisée du réseau, fût de déterminer comment les pairs du réseau trouvaient les autres pairs. Sans autorité centrale, comment un client peut-il démarrer et commencer le dialogue avec le reste du réseau ?

Il s'est avéré que l'idéalism se soumet au réalisme et qu'il s'avère subsister un reliquat de centralisation dans le processus de découverte initiale des autres pairs. La principale manière pour un nouveau client de trouver des pairs est d'utiliser une requête DNS vers un certain nombre de serveurs DNS graines maintenus par des membres de la communauté Bitcoin.

Il s'avère que le DNS est bien équipé pour ce type d'initialisation, ce protocole, qui utilise UDP et est trés léger, est difficilement sujet aux attauqes par Déni de Service. Précédemment, c'est IRC qui était utilisé pour cette phase, mais cela a cessé justement pour sa faiblesse aux attaques DDOS.

Les graines sont codées en dutr dans le source du noyau Bitcoin mais peuvent être changés par le noyau de développeurs.

Le code Python ci-dessous se connecte à une graine DNS et choisit arbitrairement le premier des pairs pour se connecter. Il utilise la bibliothèque socket, et réalise un nslookup pour retourner l'adresse IPV4 du premier résultat sur la requête vers la graine seed.bitcoin.sipa.be.

        import socket
          # use a dns request to a seed bitcoin DNS server to find a node
          nodes = socket.getaddrinfo("seed.bitcoin.sipa.be", None)
          
          # arbitrarily choose the first node
          node = nodes[0][4][0]

Après avoir lancé ceci, l'adresse retournée fût 208.67.251.126 qui semble un pair amical auquel je peux me connecter !

Dire coucou à mon nouvel ami

Les connexions bitcoin entre pairs se font via TCP. Afin de se connecter à pair, la poignée de main initiale du protocole Bitcoin est un message de type Version. Tant que les pairs n'ont pas échangé un message de type Version, aucun autre message ne sera accepté.

Les messages Bitcoin sont bien documentés dans La référence du développeur Bitcoin En utilisant cette référence comme un guide, le message de type version peut être construit en Python comme le bout de code suivant le montre. La plupart des données sont des données administratives peu intéressantes qui sont utilisées pour établir la connexion. si vous souhaitez plus de détails que ceux présents dans les commentaires, consultez la référence du développeur.

        version = 70014
        services = 1 # not a full node, cant provide any data
        timestamp = int(time.time())
        addr_recvservices = 1
        addr_recvipaddress = socket.inet_pton(socket.AF_INET6, "::ffff:127.0.0.1") #ip address of receiving node in big endian
        addr_recvport = 8333
        addr_transservices = 1
        addr_transipaddress = socket.inet_pton(socket.AF_INET6, "::ffff:127.0.0.1")
        addr_transport = 8333
        nonce = 0
        user_agentbytes = 0
        start_height = 329167
        relay = 0

En utilisant la bibliothèque struct de Python les données sont empaquetées dans le bon format, en prenant une attention particulière sur la taille en octets des données et à leur sens de lecture (endianness). Le bon empaquetage des données est important, dans le cas contraire, l'interlocuteur ne sera pas capable de comprendre les octets bruts qu'il reçoit.

        payload = struct.pack("<I", version)
        payload += struct.pack("<Q", services)
        payload += struct.pack("<Q", timestamp)
        payload += struct.pack("<Q", addr_recvservices)
        payload += struct.pack("16s", addr_recvipaddress)
        payload += struct.pack(">H", addr_recvport)
        payload += struct.pack("<Q", addr_transservices)
        payload += struct.pack("16s", addr_transipaddress)
        payload += struct.pack(">H", addr_transport)
        payload += struct.pack("<Q", nonce)
        payload += struct.pack("<H", user_agentbytes)
        payload += struct.pack("<I", start_height)

A nouveau, on trouvera dans le guide du développeur la description de la façon dont les données doivent être empaquetées. Enfin, chaque donnée transmise sur le réseau doit être préfixée par une entête, qui contient la longueur des données, une somme de contrôle et le type de message dont il s'agit. L'entête contient également la constante magique 0xF9BEB4D9 qui doit être positionnée pour tous les messages du réseau principal de Bitcoin. La fonction suivante retourne un message Bitcoin contenant les données attachées à leur entête.

      def get_bitcoin_message(message_type, payload):
        header = struct.pack(">L", 0xF9BEB4D9)
        header += struct.pack("12s", bytes(message_type, 'utf-8'))
        header += struct.pack("<L", len(payload))
        header += hashlib.sha256(hashlib.sha256(payload).digest()).digest()[:4]
    
        return header + payload

Avec des données empaquetées dans le bon format et le header attaché, on peut les envoyer à notre pair !

        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.connect((node, 8333))
        s.send(get_bitcoin_message("version", payload))
        print(s.recv(1024))

Le protocole Bitcoin prévoit qu'en réponse à un message de type version, unpair doit répondre avec un message de type Verack. Comme je développe un petit client pour le fun, et parce que cela ne me sera pas préjudiciable si je ne le fait pas, j'ignorerai les messages de type Version et je ne leur renverrai pas d'acquittement. Se connecter avec un message de type Version suffit pour m'autoriser à envoyer d'autres messages par la suite.

Après exécution, le code précédent affiche ce qui suit. Cela est sûrement prometteur. "Satoshi" et "Verack" sont de bons mots à retrouver dans le dump de sortie ! Si mon message de type Version avait été malformé, le pair ne m'aurait pas répondu du tout.

        b'\xf9\xbe\xb4\xd9version\x00\x00\x00\x00\x00f\x00\x00\x00\xf8\xdd\x9aL\x7f\x11\x01\x00
        \r\x00\x00\x00\x00\x00\x00\x00\xddR1Y\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
        \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xcb\xce\x1d\xfc\xe9j\r\x00\x00\x00
        \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
        \x00\x06\xb8>*\x88@I\x8e\x10/Satoshi:0.14.0/t)\x07\x00\x01\xf9\xbe\xb4\xd9verack\x00
        \x00\x00\x00\x00\x00\x00\x00\x00\x00]\xf6\xe0\xe2'


Les transactions Bitcoin

Pour transférer des bitcoins, il faut diffuser une transaction sur le réseau Bitcoin.

D'une manière critique, l'idée la plus importante à comprendre est que la balance d'une adresse Bitcoin est seulement constituée par le nombre de sorties non dépensées (UTXO, "Unspent Transaction Outputs") que cette adresse possède. Lorsque Bob envoie des bitcoins à Alice, il ne fait en fait que créer une UTXO qu'Alice (et seulement Alice) pour utiliser plus tard pour créer une autre UTXO et envoyer des bitcoins dessus. La balance d'une adresse est donc le nombre de bitcoins qu'elle peut transférer à une autre adresse plutôt que le nombre de bitcoins qu'elle possède.

J'insite encore, quand quelqqu'un dit qu'il possède X bitcoins, il dit en réalité que la somme de ses UTXOs fait X bitcoins. La différence est subtile mais importante, la balance d'une adresse Bitcoin n'est enregistrée nulle part directement mais elle doit être trouvée en faisant la somme des UTXOs qu'elle peut encore dépenser. Quand j'ai réalisé cela, ce fut un moment de "Oh, c'est comme ça que ça marche !".

Un effet de bord est qu'une sortie ne peut être que entièrement dépensée ou non dépensée. Il n'est pas possible de ne dépenser qu'une moitié d'une sortie que l'on vous a envoyé, puis de dépenser le reste plus tard. Si vous souhaitez dépenser une partie d'une sortie que vous avez reçu, vous devez envoyer la partie que vous souhaitez dépenser tout en vous renvoyant le reste. Une version simplifiée de ceci est schématisée ci-dessous.


Lorsqu'une sortie de transaction est créée, elle comporte une condition verrou qui autorisera quelqu'un à la dépenser dans le futur, à travers ce que l'on appelle un script de transaction. La plupart du temps, ce verrou est du type : "pour dépenser cette sortie, vous devez prouver que vous possédez la clé privée correspondant à une adresse particulière". C'est ce que l'on appelle un script "Pay-to-Public-Key-Hash". Toutefois rappelez vous que d'autres types de scripts Bitcoin sont possibles. Par exemple, une sortie peut être créée qui pourrait être dépensée par toute personne pouvant résoudre une certaine empreinte, ou une transaction peut être créée que que tout le monde pourrait dépenser.

A travers le langage Script, il est possible de créer des transactions basées sur des contrats simples. Script est un langage à pile rudimentaire avec un certain nombre d'opérations centrées sur le contrôle d'égalité d'empreinte ou de vérification de signatures. Script n'est pas un langage "Turing complet" et il ne propose pas la possibilité de faire des boucles. La cryptomonnaie concurrente Ethereum a été conçue pour être capable de traiter des "contrats intelligents", et possède un langage "Turing complet". On peut débattre longuement de l'utilité, la nécessité et la sécurité de disposer d'un langage "Turing complet" dans les cryptomonnaies, mais je laisse ce débat à d'autres !

Dans la terminologie standard, une transaction Bitcoin est faite d'entrées et de sorties. Une entrée est une UTXO (une sortie maintenant dépensée) et une sortie est une nouvelle UTXO. Il peut y avoir plusieurs sorties pour une entrée mais une sortie doit être complètement dépensée dans uen trasaction. Toute partie non dépensée d'une entrée est considérée comme un pourboire de minage par les mineurs.

Pour mon petit client, je souhaite être capable d'envoyer les bitcoins précédemment transférés depuis une place de marché vers mon adresse FEEDB0BDEADBEEF. En utilisant la même procédure que précédemment, j'ai généré une autre adresse à partir de la clé privée BADCAFEFABC0FFEE. Cette adresse est 1QGNXLzGXhWTKF3HTSjuBMpQyUYFkWfgVC.

Création d'une transaction brute

La création d'une transaction est une affaire d'empaquetage d'une "transaction brute", puis de signature de cette transaction brute. Encore une fois, la référence du développeur contient une description de ce que doit contenir uen transaction. Ce qui constitue une transaction est montré ci-dessous, mais tout d'abord :

  • Le vocabulaire Bitcoin utilise les termes de "signature script" et "pubkey script" que je trouve un peu confus. Le script de signature est utilisé pour définir les conditions de l'UTXO que l'on souhaite utiliser dans la transaction, et le script de clé publique est utilisé pour donner les conditions nécessaires pour dépenser l'UTXO que nous sommes en train de créer. Il serait préférable d'appeler le script de signature le script de déverrouillage et d'appeler le le script de clé publique le script de verrouillage.
  • Le montant d'une transaction Bitcoin est donnée en Satoshis. Le Satoshi représente la plus petite partie non divisible d'un bitcoin, soit le cent millionnième d'un bitcoin.


Afin de rester simple, ce qui est indiqué ci-dessous correspond à une transaction comportant une entrée et une sortie. Des transactions plus complexes, avec plusieurs entrées et plusieurs sorties, sont créées de la même façon.

ChampDescription
Version Transaction version (actuellement toujours 1)
Number of inputs Nombre d'entrées à dépenser
Transaction ID ID de la transaction à dépenser
Output number Numéro de la sortie à dépenser
Signature script length Longueur en octets du script de signature
Signature script Script de signature dans le langage Script
Sequence number Toujours 0xffffffff sauf si vous souhaitez positionner un Lock Time
Number of outputs Nombre de sorties à créer
Value Montant à dépenser en Satoshis
Pubkey script length Longueur en octets du script de clé publique
Pubkey script Script de clé publique en langage Script
Lock time Prochaine date ou numéro de bloc où il sera possible d'inclure la transaction dans la blockchain


Exceptés les script de signatur et de clé publique, il est assez facile de voir ce qui doit aller dans les autres champs de la trasaction brute. Pour envoyer mes fonds de mon adresse FEEDB0BDEADBEEF à mon adresse BADCAFEFABC0FFEE, j'ai recherché la transaction qui avait été crée par la place de marché. Cela m'a donné :

  • L'ID de la transaction est 95855ba9f46c6936d7b5ee6733c81e715ac92199938ce30ac3e1214b8c2cd8d7.
  • La sortie qui était envoyée à mon adresse était la deuxième sortie, soit la sortie 1 (le tableau des sorties commence à 0)
  • Le nombre de sortie est 1, car je souhaite tout transférer de FEEDB0BDEADBEEF à BADCAFEFABC0FFEE
  • Le montant doit être au maximum de 400 000 Satoshis.
    Cela doit être moins que cela pour accorder un pourboire.
    J'accorde 20 000 Satoshis à être pris en tant que pourboire, le montant sera donc fixé à 380 000 Satoshis.
  • Le locktime sera mis à 0, autorisant la transaction a être prise en compte n'importe quand.


Le script de clé publique sera un script Pay to Pubkey hash (ou p2pk). Ce script garantit que la seule personne capable de dépenser la sortie qui sera créée sera celle qui connait la clé publique dont l'empreinte coïncide avec l'adresse Bitcoin donnée et qui détient également la clé privée correspondant à cette la clé publique.

Pour débloquer une transaction verrouillée par un script P2PK, l'utilisateur doit fournir sa clé publique ainsi qu'une signature de l'empreinte de la transaction brute. L'empreinte de la clé publique est comparée à l'adresse avec laquelle le script a été créé et la signature est vérifiée avec la clé publique fournie. Si l'empreinte coïncide et que la signature est vérifiée, la sortie peut être dépensée.

Avec les opérateurs de script Bitcoin, le script P2PK ressemble à ce qui suit.

OP_DUP
OP_HASH160
-- Longueur en octets de l'adresse --
-- adresse Bitcoin --
OP_EQUALVERIFY
OP_CHECKSIG

Après conversion des opérateurs dans leur valeurs (fournies par le WIKI) et en rajoutant l'adresse publique (avant encodage en Base58Check), cela donne le script hexadecimal suivant.

0x76
0xA9
0x14
0xFF33195EC053D6E58D5FD3CC67747D3E1C71B280
0x88
0xAC

L'adresse a été trouvée en utilisant le bout de code montré précédemment pour dérivé une adresse depuis une clé privée, en l'appliquant à la clé privée de destination, 0xBADCAFEFABC0FFEE.

Signer la transaction

Il y a 2 usages séparés, mais quelque part reliés, du script de signature dans une transaction P2PK.

  • Le script vérifie (déverrouille) l'UTXO que l'on essaie de dépenser en fournissant notre clé publique qui créée l'empreinte de l'adresse à laquelle l'UTXO a été envoyée.
  • Le sicript effectue également une signature de ce nous transmettons au réseau, de façon à ce personne ne puisse modifier la transaction sans corrompre la signature.

Ainsi, la transaction brute contient le script de signature qui contient uen signature de la transaction brute ! Ce problème de l'oeuf et la poule est résolu en plaçant le script de clé publique de notre UTXO dans l'emplacement du script de signature avant de signer la transaction brute. Pour autant, il ne semble pas de bonne raison pour utiliser l'emplacement du script de clé publique, cela pourrait être arbitraimenent n'importe quelle autre donnée.

Avant de calculer l'empreinte de la transaction brute, on doit d'abord ajouter la valeur du type d'empreinte (Hashtype). Le type d'empreinte le plus commun est SIGHASH_ALL, qui signe toute la structure pour qu'aucune entrée ni aucune sortie ne puisse être modifiée. Le lien vers le WIKI liste les autres types d'empreinte, qui peuvent autoriser la combinaison de la modification des entrées et des sorties après que la transaction ait été signée.

Les fonctions Python ci-dessous placent ensemble le répertoire des valeurs de transaction brute

def get_p2pkh_script(pub_key):
          """
          This is the standard 'pay to pubkey hash' script
          """
          # OP_DUP then OP_HASH160 then 20 bytes (pub address length)
          script = bytes.fromhex("76a914")
      
          # The address to pay to
          script += pub_key
      
          # OP_EQUALVERIFY then OP_CHECKSIG
          script += bytes.fromhex("88ac")
      
          return script
      
      def get_raw_transaction(from_addr, to_addr, transaction_hash, output_index, satoshis_spend):
          """
          Gets a raw transaction for a one input to one output transaction
          """
          transaction = {}
          transaction["version"] = 1
          transaction["num_inputs"] = 1
      
          # transaction byte order should be reversed:
          # https://bitcoin.org/en/developer-reference#hash-byte-order
          transaction["transaction_hash"] = bytes.fromhex(transaction_hash)[::-1]
          transaction["output_index"] = output_index
      
          # temporarily make the signature script the old pubkey script
          # this will later be replaced. I'm assuming here that the previous
          # pubkey script was a p2pkh script here
          transaction["sig_script_length"] = 25
          transaction["sig_script"] = get_p2pkh_script(from_addr)
      
          transaction["sequence"] = 0xffffffff
          transaction["num_outputs"] = 1
          transaction["satoshis"] = satoshis_spend
          transaction["pubkey_length"] = 25
          transaction["pubkey_script"] = get_p2pkh_script(to_addr)
          transaction["lock_time"] = 0
          transaction["hash_code_type"] = 1
      
          return transaction

Appeler ce code avec les valeurs ci-dessous crée la transaction brute que je me suis fixé.

        private_key = address_utils.get_private_key("FEEDB0BDEADBEEF")
        public_key = address_utils.get_public_key(private_key)
        from_address = address_utils.get_public_address(public_key)
        to_address = address_utils.get_public_address(address_utils.get_public_key(address_utils.get_private_key("BADCAFEFABC0FFEE")))
        
        transaction_id = "95855ba9f46c6936d7b5ee6733c81e715ac92199938ce30ac3e1214b8c2cd8d7"  
        satoshis = 380000
        output_index = 1
        
        raw = get_raw_transaction(from_address, to_address, transaction_id, output_index, satoshis)        

Il peut paraître perturbant d'utliser la clé privée pour générer la valeur to_address. C'est fait uniquement par commodité et pour montrer comment ce champ est trouvé. Lorsque vous faites une transaction avec un tiers, vous devez lui demander son adresse et y effectuer le transfert, sans en connaître la clé privée.

Pour être capable de signer, et éventuellement de transmettre cette transaction au réseau, la trasaction brute doit être empaquetée correctement. Ceci est implémenté dans la focntion get_packed_transaction que je ne vais pas copier ici, puisqu'il s'agit essentiellement d'un nouvel usage de la bibliothèque struct. Si cela vous intéresse, voyus pouvez la trouver dans le fichier Python bitcoin_transaction_utils.py de mon dépôt GITHUB.

Cela me permet de définir la fonction qui va produire le script de signature. Une fois le script de signature généré, il va remplacer le contenu du champs signature script.

        def get_transaction_signature(transaction, private_key):
        """
            Gets the sigscript of a raw transaction
            private_key should be in bytes form
            """
        packed_raw_transaction = get_packed_transaction(transaction)
        hash = hashlib.sha256(hashlib.sha256(packed_raw_transaction).digest()).digest()
        public_key = address_utils.get_public_key(private_key)
        key = SigningKey.from_string(private_key, curve=SECP256k1)
        signature = key.sign_digest(hash, sigencode=util.sigencode_der)
        signature += bytes.fromhex("01") #hash code type
    
        sigscript = struct.pack("<B", len(signature))
        sigscript += signature
        sigscript += struct.pack("<B", len(public_key))
        sigscript += public_key
    
        return sigscript

Pour l'essentiel, le script de signature est fourni comme une entrée du script de clé publique de la transaction précédente que je tente d'utiliser, de cetet façon je peux prouver que je suis autorisé à en dépenser la sortie que j'utilise maintenant comme entrée. La mécanique de focnitonnement est montrée ci-dessous, extrait du WIKI Bitcoin. Cela se lit de haut en bas, chaque ligne est une itération du script. Cela décrit le script P2PK qui comme je l'ai déjà indiqué, est le plus utilisé. C'est aussi celui que j'utilise pour ma transaction ainsi que pour la transaction dont j'utilise la sortie.

Stack Script                  Description
Empty signature
publicKey
OP_DUP
OP_HASH160
pubKeyHash
OP_EQUALVERIFY
OP_CHECKSIG
La signature et la clé publique du script de signature sont combinées avec le script de clé publique.
publicKey
signature
OP_DUP
OP_HASH160
pubKeyHash
OP_EQUALVERIFY
OP_CHECKSIG
La signature et la clé publique sont ajouées sur la pile.
publicKey
publicKey
signature
OP_HASH160
pubKeyHash
OP_EQUALVERIFY
OP_CHECKSIG
L'item du haut de la pile est dupliqué par OP_DUP
pubHashA
publicKey
signature
pubKeyHash
OP_EQUALVERIFY
OP_CHECKSIG
On calcule l'empreinte de l'item du haut de la pile (publicKey) via OP_HASH160, le résultat pubHashA remplace le haut de la pile.
pubHash
pubHashA
publicKey
signature
OP_EQUALVERIFY
OP_CHECKSIG
pubKeyHash est ajouté au haut de la pile.
publicKey
signature
OP_CHECKSIG Contrôle de l'égalité entre pubHashA et pubKeyHash. Le script s'arrête si ce n'est pas égal.
True La publicKey est utiliséee pour signer la transaction, le résultat est comparé à la signature fournie par le script de signature.


Ce script échouera si la clé fournie n'a pas une empreinte équivalente à celle fournie dans le script ou si la signature donnée ne correspond pas à la clé publique. Cela garantit que seule la personne détenant la clé privée pour l'adresse donnée est capable de dépenser la sortie.

Vous pouvez voir que c'est ici la première fois que j'ai à produire ma clé publique. Jusqu'ici, seule mon adresse avait été publiée. Ici, il est nécessaire de montrer sa clé publique pour permettre de vérifier la signature utilisée dans la transaction.

En utilisant la fonction get_transaction_signature, nous pouvons maintenant signer et empaqueter notre transaction pour la transmettre ! Cela implique de remplacer l'emplacement du script de signature par le vraie script de signature et d'enlever le hash_code_type de la transaction comme montré ci-dessous.

        signature = get_transaction_signature(raw, private_key )

        raw["sig_script_length"] = len(signature)
        raw["sig_script"] = signature
        del raw["hash_code_type"]
        
        transaction = get_packed_transaction(raw)
Publier la transaction

Avec une transaction empaquetée et signée, il s'agit de dire au réseau qu'elle existe. Utilisant les quelques fonctions définies dans cet article, placés dans bitcoin_p2p_message_utils.py, le bout de code suivant place l'entête Bitcoin sur la transmission et l'envoie à un pair. Comme indiqué précédemment, il est nécessaire d'envoyer un message de version au préalable à ce pair afin qu'il accepte les messages suivants.

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.connect((get_bitcoin_peer(), 8333))
        s.send(get_bitcoin_message("version", get_version_payload())
        s.send(get_bitcoin_message("tx", transaction)

Envoyer la transaction fût la partie la plus ennuyeuse de ce travail. Lorsque je soumettais une transaction qui était mal formée ou mal signée, souvent le pair se contentait de couper la connexion, ou dans le meilleur des cas, renvoyait un message d'erreur déroutant. L'un de ces (vraiment incompréhensible) messages était "La valeur S est inutilement haute" qui était dûe à la signature de l'empreinte de la transaction par un encodage suivant la methode ECSDA nommée sigencode_der. Bien que la signature soit valide, apparemment les mineurs Bitcoin n'aiment pas les sigantures ECSDA formatées de cette façon car elles autorisent le spam sur le réseau. La solution fût d'utiliser la function sigencode_der_canonize qui prend soin de formter la signature d'une autre façon. Un problème simple mais très difficile à débugguer !

Quoiqu'il en soit, j'ai fini par tout faire fonctionner et je fût trés exité lorsque je vis que ma transaction faisait son chemin dans la blockchain. Ce fût un grand sentiment d'accomplissement de savoir que ma ravissante petite transaction faite à la main ferait partie du registre Bitcoin à tout jamais.


Lorsque ma transaction fût soumise, le montant du pourboire était un peu faible par rapport à la moyenne (j'ai utilisé bitcoin fees pour vérifier) et ainsi il fallu 5 heures pour qu'un mineur se décide à l'inclure dans un bloc. J'ai contrôlé cela en regardant le nombre de confirmations que la transaction avait - c'est une mesure du nombre de blocs suivants celui qui contient la transaction. Au moment où j'écris ces lignes, il y a 190 confirmations, cela signifie que 190 blocs ont été produits depuis celui qui contient ma transaction. Cela peut être considéré comme relativement sûrement confirmée, puisqu'il faudrait une attaque impressionnante sur le réseau pour réécrire 190 blocs afin d'effacer ma transaction.

Conclusion

J'espère que vous avez gagné une petite idée de comment fonctionne Bitcoin en lisant cet article, pour ma part je suis certain de l'avoir fait pendant les mois que j'ai passé à mettre tout ceci ensemble ! Bien que la plupart des informations présentées ici ne sont pas applicables de façon pragmatiques - normalement vous utilisez un client qui fait tout cela pour vous - je pense que comprendre un peu mieux comment les choses fonctionnent sous la couverture ont fait de vous un utilisateur plus confiant dans la technologie.

Si vous souhaitez utiliser le code, ou bien vous amuser plus en avant avec le petit jouet, consultez mon dépôt GITHUB. Il y a de nombreuses pièces à découvrir dans le monde Bitcoin, je n'en ai exploré que les plus communes. Certaines pièces permettent probablement de faire des choses plus cool que le simple transfert de bitcoins entre deux adresses ! Je n'ai même pas effleuré le minnage, le processus pour ajouter des blocks dans la blockchain. Du travail et un nouveau trou de lapin en perspective ...

Si vous avez tout lu jusqu'ici, vous avez sans doute réalisé que les 380 000 Satoshis que j'ai transféré sur l'adresse 1QGNXLzGXhWTKF3HTSjuBMpQyUYFkWfgVC peuvent, si on est malin, être récupérés par tout à chacun ... puisque la clé privée de cette adresse est indiquée dans l'article. Je suis vraiment curieux de savoir combien de temps cela pour être transféré ailleurs et j'espère que celui qui le fera aura la décence de le faire en utilisant les techniques que j'ai décrit ici ! Je serai vraiment dommage que la clé privée soit juste chargée dans un porte-monnaie pour les prendre, mais je ne pense vous en empêcher ! Au moment où j'écris ces lignes, cela représente environ 10 USD, mais si Bitcoin "s'envole vers la lune", qui sait combien cela pourrait être plus ! (Edit : Et c'est déjà retiré ! Pris par 1KgoPFVDNcx7H2VY9bB2dxxP9yNM2Nar1N quelques heures après la publication de ces lignes. Bien joué !)

Au cas où vous chercheriez une adresse pour envoyer des bitcoins lorsque vous jouerez avec ce travail, ou bien si vous pensez que cet article était suffisemment interessant pour le récompenser, mon adresse 18uKa5c9S84tkN1ktuG568CR23vmeU7F5H sera heureuse de prendre de petits dons ! Par ailleurs, si vous souhaitez me crier dessus pour signaler des erreurs, j'en serai très heureux.

D'autres ressources

Si vous avez trouvé cet article intéressant, voici quelques ressources pour aller plus loin :

  • Le livre Mastering Bitcoin explique les détails techniques de Bitcoin. Je ne l'ai pas lu complètement mais il a l'air d'être riche en bonnes informations
  • L'artcile du blog de Ken Sheriff à l'air de couvrir le même sujet que mon article. Je ne l'ai malheureusement découvert que lorsque mon travail était déjà très avancé.Si vous ne comprenez pas quelque chose ici, consultez son article.
  • Déjà mentionnée, la fantastique vidéo d'Anders Brownworth est une excellente première approche de comment fonctionne les technologies Blockchain
  • A moins d'être complètement masochiste, je ne vous recommande pas de faire les choses à partir de rien, sauf dans un but pédagogique. La bibliothèque Python Pycoin vous économisera bien des maux de têtes
  • Pour économiser votre peine, il est sans doute préférable d'utiliser le réseau de test plutôt que le résau principal comme je l'ai fait. Ceci dit, c'est plus fun quand un code erroné vous fait perdre de l'argent réel !
  • Enfin je répète à nouveau que le code qui accompagne cet article est disponible sur mon dépôt GITHUB