Undefined


Discover Bitcoin #1 : Création d'une adresse bitcoin

L'objectif de cette suite d'article est de mieux comprendre les aspects techniques de la blockchain en s'appuyant sur la blockchain la plus populaire : Bitcoin. Dans cette première partie, voyons comment créer une adresse valide et utilisable sur la blockchain Bitcoin.

Les sources du projet sont disponibles sur GitHub : adrien-chinour/discover-blockchain

Introduction

Une adresse en cryptomonnaie correspond à un RIB. Cela vous permet d'identifier le portefeuille (le compte). Vous pouvez donc la partager a n'importe qui pour recevoir de l'argent.

Transfer/receive transactions of Bitcoins (Cryptocurrency, BTC) can be performed via address like the work with e-mail messages. (https://bitcoinwiki.org/wiki/address#Purpose_and_opportunities)

Il y a différents types d'adresses, comme présenté dans cet article : Bitcoin address types compared: P2PKH, P2SH, P2WPKH, and more.

Les types les plus utilisés sont :

  • P2PKH : Pay-To-Public-Key-Hash (43%)
  • P2WPKH : Pay-to-Witness-Public-Key-Hash (20%)
  • P2SH : Pay-To-Script-Hash (24%)
  • P2WSH : Pay-to-Witness-Script-Hash (4%)

Voyons d'abord le format, encore aujourd'hui, le plus commun : P2PKH ; même s'il tend à être remplacé par son évolution P2WPKH.

Génération d'une adresse P2PKH

Ce type d'adresse est le plus commun est fonctionne sur le mécanisme de clé publique et clé privée. La clé publique est partagée et permet de valider une transaction alors que notre clé privée permet de signer une transaction.

On peut y lire qu'une adresse utilise ECDSA comme algorithme de génération de notre couple clé publique et clé privée :

Bitcoin uses the Elliptic Curve Digital Signature Algorithm (ECDSA) with the secp256k1 curve; secp256k1 private keys are 256 bits of random data. https://developer.bitcoin.org/devguide/transactions.html

Dans les grandes lignes parce que l'objectif n'est pas de rentrer dans le détail de ECDSA, on va fournir en entrée un entier aléatoire, nommé k, dans l'intervalle [1, n - 1 ] avec n égal à 2^256 pour secp256k1.

L'algorithme va ensuite nous donner les coordonnées x et y de notre point k x G sur la courbe elliptique. G étant le point initial de la courbe. (x,y) correspond à notre clé publique. Et la complexité de cet algorithme réside dans la difficulté à obtenir k sachant (x,y).

Passons au code, on va utiliser les fonctions crypto déjà fournies en go pour calculer notre clé privée et notre clé publique :

Ce qu'il faut savoir et qui est très important, c'est que le seed que l'on va fournir contient le seul secret qui importe dans notre clé. La clé privée générée à partir de celui-ci sera toujours identique et par conséquent la clé publique aussi.

Passons au code, ici, on va utiliser les fonctions crypto déjà fournies en go pour calculer notre clé privée et notre clé publique :

func main() {
	// Generate a private key based on secp256k1 ellipsis curve
	privateKey, err := ecdsa.GenerateKey(btcec.S256(), rand.Reader)
	if err != nil {
		fmt.Println("Cannot generate private key.", err)
	}

	// Display private key as hexadecimal string
	fmt.Printf("Private Key: %s\n", hex.EncodeToString(privateKey.D.Bytes()))

	// Display public key as hexadecimal string
	publicKey := append(privateKey.X.Bytes(), privateKey.Y.Bytes()...)
	fmt.Printf("Public Key: %s\n", hex.EncodeToString(publicKey))
}

Mais on n'a pas encore fini parce que notre clé publique ne correspond pas à une adresse Bitcoin. Pour ça il nous manque quelques étapes :

PubKeyToAddr

L'avantage de cette opération est de sécuriser encore plus notre clé publique en fournissant un hash de celle-ci et surtout d'économiser de la place dans la blockchain en réduisant le nombre de bit par adresse. https://privacypros.io/btc-faq/how-many-btc-addresses

func hash(input []byte, hash crypto.Hash) []byte {
	hasher := hash.New()
	hasher.Write(input)

	return hasher.Sum(nil)
}

func publicKeyToAddress(publicKey ecdsa.PublicKey) (string, error) {
	publicKeyBytes := append(publicKey.X.Bytes(), publicKey.Y.Bytes()...)

	network := byte(0x00)
	fingerprint := hash(hash(publicKeyBytes, crypto.SHA256), crypto.RIPEMD160)

	address := append([]byte{network}, fingerprint...)
	checksum := hash(hash(fingerprint, crypto.SHA256), crypto.SHA256)[:4]

	return base58.Encode(append(address, checksum...)), nil
}

0x00 correspond a notre type d'adresse sur le réseau principal : MainNet. Il existe un réseau de test : TestNet. Dans le TestNet il faudra utiliser 0x6f.

Le détail de l'algorithme est détaillé dans cette documentation : https://en.bitcoin.it/wiki/Technical_background_of_version_1_Bitcoin_addresses

Et pour valider que notre adresse est utilisable et vérifier l'état du portefeuille on peut utiliser ce site : https://www.blockchain.com/explorer.

Passons a l'évolution de cette première génération d'adresse : P2WPKH.

Génération d'une adresse P2WPKH

Tout d'abord, il faut comprendre pourquoi ce nouveau terme : "Witness".

SegWit (abréviation de Segregated Witness) est une mise à niveau rétrocompatible du protocole Bitcoin qui modifie en profondeur la structure des transactions en déplaçant les données de signature (le témoin ou witness) dans une base de données séparée (segregated). https://cryptoast.fr/segwit-bitcoin-explication-definition/

Il s'agit d'un changement dans le protocole qui permet d'accélérer le débit des transactions en intégrant un plus grand nombre de transactions au sein d'un même bloc. Et tout ça, simplement en réduisant la taille d'une transaction.

Au niveau de notre adresse, cela ne change pas énormément. En utilisant la documentation suivante https://en.bitcoin.it/wiki/Bech32, on peut écrire le code suivant :

func P2WPKH() BitcoinAddress {
   privateKey, _ := ecdsa.GenerateKey(btcec.S256(), rand.Reader)

   // Compressed public key based on Elliptic-Curve-Point-to-Octet-String Conversion (https://www.secg.org/sec1-v2.pdf)
   publicKey := elliptic.MarshalCompressed(privateKey.Curve, privateKey.PublicKey.X, privateKey.PublicKey.Y)

   // Witness version, current is 0
   version := byte(0x00)

   // Hash public key with SHA256 then RIPEMD160
   hashedKey := hash(hash(publicKey, crypto.SHA256), crypto.RIPEMD160) 

   // 8-bit unsigned integer to 5-bit unsigned integers
   fingerprint, _ := bech32.ConvertBits(hashedKey, 8, 5, true)

   // Encode to bech32 format with network (bc = main network) and checksum calculated
   address, _ := bech32.Encode("bc", append([]byte{version}, fingerprint...))

   return BitcoinAddress{address: address, privateKey: hex.EncodeToString(privateKey.D.Bytes())}
}

Pas beaucoup de changement, simplement des améliorations techniques par rapport à notre version P2PKH.

Maintenant que l'on a vu le fonctionnement d'une adresse par clé publique, voyons les différences avec les adresses par script.

Génération d'une adresse P2SH

Les deux formats P2SH et son évolution P2WSH repose sur l'exécution d'un script qui renverra True ou False pour valider ou non une transaction.

Le script permet plus de flexibilité dans la signature d'une transaction. On retrouve plusieurs usages comme :

  1. Les transactions multi-signature : plusieurs clés publiques doivent être utilisées pour autoriser une transaction
  2. Le dépôt fiduciaire : service dans lequel une tierce partie, appelée escrow, retient temporairement les fonds pendant que les parties impliquées dans la transaction satisfont aux conditions convenues.
  3. Transactions Atomic Swaps : permettent l'échange atomique de cryptomonnaies entre deux parties sans nécessiter de tiers de confiance. Qu'est-ce qu'un Atomic Swap ?

Un article en français qui parle des différents usages : Les smart contracts avec Bitcoin

Pour un exemple assez simple, je vais générer une adresse qui nécessite la signature de 2 clés publiques. Pour ça je me base sur la documentation présente ici : https://en.bitcoin.it/wiki/Script et qui me donne les instructions que je peux définir dans mon script :

OP_2 <publicKey1> <publicKey2> OP_2 OP_CHECKMULTISIG

On peut générer notre adresse avec le code suivant :

func AddressPubKey() []byte {
	privateKey, _ := ecdsa.GenerateKey(btcec.S256(), rand.Reader)
	return elliptic.MarshalCompressed(privateKey.Curve, privateKey.PublicKey.X, privateKey.PublicKey.Y)
}

// MultiSignAddress demonstrate P2SH address
func MultiSignAddress() {
	// In this exemple, we use 2 initial P2PK addresses
	address1, _ := btcutil.NewAddressPubKey(AddressPubKey(), &chaincfg.MainNetParams)
	address2, _ := btcutil.NewAddressPubKey(AddressPubKey(), &chaincfg.MainNetParams)
	pubKeys := []*btcutil.AddressPubKey{address1, address2}

	// Create MultiSign Script
	// OP_2 <publicKey1> <publicKey2> OP_2 OP_CHECKMULTISIG
	script, _ := txscript.MultiSigScript(pubKeys, 2)

	// 0x05 for MainNet, Oxc4 for TestNet
	network := byte(0x05)

	hashedScript := hash(hash(script, crypto.SHA256), crypto.RIPEMD160)

	address := append([]byte{network}, hashedScript...)
	checksum := hash(hash(address, crypto.SHA256), crypto.SHA256)[:4]

	print(base58.Encode(append(address, checksum...)))

La méthodologie est assez proche de ce que l'on a pour P2PKH, à la différence qu'on ne part pas d'une clé mais d'un script. Pour générer une adresse P2WSH, on applique bech32 sur notre script.

En conclusion

On a vu les principaux types d'adresses disponibles sur la blockchain Bitcoin. Rendez-vous dans la deuxième partie pour voir comment utiliser notre adresse. Et en bonus voici un résumé graphique de cette génération d'adresse Bitcoin.

Address map

Source : https://en.bitcoin.it/wiki/File:Address_map.jpg


  • Blockchain