Adresse Bitcoin ?

Une adresse Bitcoin, ou simplement une adresse, est un identifiant de 26 à 35 caractères alphanumériques, commençant par le chiffre 1 ou 3, qui représente une destination possible pour un paiement bitcoin. Les adresses peuvent être générées gratuitement par n’importe quel utilisateur de Bitcoin. Par exemple, en utilisant Bitcoin Core, vous pouvez cliquer sur “New Address” et créer une adresse. Il est également possible d’obtenir une adresse Bitcoin en utilisant un compte auprès d’un service d’échange ou de portefeuille en ligne. [Source: bitcoin wiki]

Il y a actuellement trois formats d’adresses utilisés:

Pour voir la liste complètes des prefixes, veuillez visiter la documentation de Bitcoin

Alors comment créer une adresse Bitcoin ?

Voici une implémentation de l’algorithme de Base58 et du processus de génération des addresses Bitcoin en utilisantion Python3:

base58.py:

# Base58 implementation using Python3
# Author: Chiheb Nexus - 2018
# License: GPLv3
#

import binascii

class Base58:
	'''Base58 implementation'''
	def __init__(self):
		self.B58_DIGITS = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'

	def encode(self, var, n=0):
		'''Encode Bytes/int/string to padded base58 encoded string'''
		res, czero, pad = [], 0, 0

		if isinstance(var, bytes):
			# Convert bytes into hex then into an integer
			n = int('0x0' + binascii.hexlify(var).decode('utf8'), 16)
		elif isinstance(var, int):
			# Convert integer into a bytes
			n, var = var, bytes(str(var), 'utf8')
		elif isinstance(var, str):
			# convert string into an integer
			n = int(''.join(map(str, (ord(k) for k in var))))
		else:
			raise Exception('Byte not valid: {0}'.format(n))

		while n > 0:
			n, r = divmod(n, 58)
			res.append(self.B58_DIGITS[r])

		res_final = ''.join(res[::-1])

		for c in var:
			if c == czero:
				pad += 1
			else:
				break

		return self.B58_DIGITS[0] * pad + res_final

	def decode(self, s):
		'''Decode padded base58 encoded string into bytes'''
		if not s:
			return b''

		n = 0
		for c in s:
			n *= 58
			if c not in self.B58_DIGITS:
				raise Exception('Not valid base58: {0}'.format(c))

			digit = self.B58_DIGITS.index(c)
			n += digit

		# Convert n into hex
		h = '{:x}'.format(n)
		if len(h) % 2:
			h = '0' + h

		res = binascii.unhexlify(h.encode('utf8'))

		pad = 0
		for c in s[:-1]:
			if c == self.B58_DIGITS[0]:
				pad += 1
			else:
				break

		return b'\x00' * pad + res

btc_addresses.py:

# Générer des clés privées et publiques de Bitcoin
# Author: Chiheb Nexus - 2018
# License: GPLv3
#########
# Livre: 
#	http://chimera.labs.oreilly.com/books/1234000001802/ch04.html#_implementing_keys_and_addresses_in_python
# Liste des prefix de Bitcoin: 
#	https://en.bitcoin.it/wiki/List_of_address_prefixes
# Exemple: Private key => WIF: 
#	https://en.bitcoin.it/wiki/Wallet_import_format
# Exemple: WIF => Adresse publique: 
#	https://en.bitcoin.it/wiki/Technical_background_of_version_1_Bitcoin_addresses
#########

import os
import hashlib
import binascii
import ecdsa
from base58 import Base58

class BTCAddress:
    def __init__(self):
        self.base58 = Base58()
        # 2 ^ 256 + 1
        self.N = (1 << 256) + 1

    def generate_privkey(self, bits=32):
        '''Générer un random 32 bit seed'''
        while True:
            # Générer une séqunce de bits aléatoires
            # https://docs.python.org/3/library/os.html#os.urandom
            priv_hex = binascii.hexlify(os.urandom(bits)).decode('utf8')
            priv_int = int(priv_hex, 16)
            # une clé privée de bitcoin est de l'ordre 2^256 bit = 32 bytes
            if 0 < priv_int < self.N:
                return priv_hex

    def priv_toWIF(self, key='', prefix='80'):
        '''Générer un WIF depuis un seed'''
        # remplacer le premier byte par le prefix
        # enlever le char "L" et ajouter des 0 à gauche pour assurer la longueur
        # https://www.tutorialspoint.com/python/string_zfill.htm
        step1 = prefix + hex(int(key, 16))[2:].strip('L').zfill(64)
        # double hash sha256
        step2 = hashlib.sha256(binascii.unhexlify(step1)).hexdigest()
        step3 = hashlib.sha256(binascii.unhexlify(step2)).hexdigest()
        # step1 + 8 bits de step3 => convertir en entier
        step4 = int(step1 + step3[:8] , 16)
        # convertir l'entier de ste4 en base58
        return self.base58.encode(step4)

    def check_wif(self, wif='', privkey=''):
        '''Valider un WIF généré'''
        # décoder le WIF et le convertir en hexadécimal
        b58 = self.base58.decode(wif).hex()
        # enlever les 2 premiers bits et les 8 derniers bits
        # et comparer avec la clé privée
        assert b58[2:-8].upper() == privkey.upper() 

    def wif_to_priv(self, wif=''):
        '''Avoir un seed depuis le WIF'''
        # décoder le WIF et le convertir en hexadécimal
        # enlever les 2 premiers bits et les 8 derniers bits
        return self.base58.decode(wif).hex()[2:-8]      

    def priv_to_public(self, privkey='', compressed=False):
        '''Avoir une clé publique depuis un seed'''
        # voir ce lien: https://en.bitcoin.it/wiki/Protocol_documentation#Addresses
        sk = ecdsa.SigningKey.from_string(binascii.unhexlify(privkey), curve=ecdsa.SECP256k1)
        vk = sk.get_verifying_key()
        # Les prefixes: https://en.bitcoin.it/wiki/List_of_address_prefixes
        prefix = '04' if not compressed else ('03' if vk.pubkey.point.y() % 2 == 1 else '02')
        return prefix + binascii.hexlify(vk.to_string()).decode()

    def ripemd160(self, var):
        '''Fonction de hash ripemd160'''
        rmd = hashlib.new('ripemd160')
        rmd.update(var)
        return rmd

    def make_pub_address(self, pubkey, prefix='00'):
        '''Créer une addresse bitcoin depuis une clé publique'''
        # appliquer ripemd160 sur la hash sha256 du byte array du clé publique
        hash160 = self.ripemd160(hashlib.sha256(binascii.unhexlify(pubkey)).digest()).digest()
        # ajouter le prefix en byte au byte du hash160
        post_pub_addr = binascii.unhexlify(bytes(prefix.encode())) + hash160
        # double hash sha256 et extraire les 4 derniers bits => checksum
        checksum = hashlib.sha256(hashlib.sha256(post_pub_addr).digest()).digest()[:4]
        # encoder en base58
        return self.base58.encode(post_pub_addr + checksum)

test.py

# Tester l'implémentation de génération des addresses de bitcoin
# Author: Chiheb Nexus - 2018
# License: GPLv3
# l'exemple introduit par: 
# https://en.bitcoin.it/wiki/Wallet_import_format
#   et
# https://en.bitcoin.it/wiki/Technical_background_of_version_1_Bitcoin_addresses
# Aussi penser à valider les résultats avec le site: https://www.bitaddress.org
#

import unittest
from btc_addresses import BTCAddress

class TestBTCAddress(unittest.TestCase):
    btc_addresses = BTCAddress()
    privkey = '0C28FCA386C7A227600B2FE50B7CAE11EC86D3BF1FBE471BE89827E19D72AA1D'
    wif_example = "5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ"
    addr_example = "1GAehh7TsJAHuUAeKZcXf5CnwuGuGgyX2S"

    def test_wif(self):
        '''Tester le WIF généré avec le WIF de l'exemple'''
        priv = self.btc_addresses.priv_toWIF(self.privkey)
        self.assertEqual(priv, self.wif_example)

    def test_addr(self):
        '''Tester l'adresse générée avec l'adresse de l'exemple'''
        pub = self.btc_addresses.priv_to_public(self.privkey)
        addr = self.btc_addresses.make_pub_address(pub)
        self.assertEqual(addr, self.addr_example)

# Test
if __name__ == '__main__':
    unittest.main()

Output

┌─[Chiheb@NeXus][~/blog/tutos/generate_btc_addresses]
└──╼ [>] python3 test.py 
..
----------------------------------------------------------------------
Ran 2 tests in 0.112s

OK

Cheers :D !