Widok z Zamku w Bolkowie

Protokół Bitcoina od środka: Generujemy adresy kont

Aby wykonać dowolną transakcję w sieci Bitcoin, tak jak w przypadku klasycznych przelewów bankowych, musimy posługiwać się adresami kont. Korzystając z dowolnego klienta sieci, możemy wygenerować cały zestaw takich adresów. Możemy też poznać ich budowę i zrobić to całkowicie sami. Zobaczmy jak wygląda generowanie adresów kont w sieci Bitcoin w praktyce 🙂

Czym jest adres w sieci Bitcoin?

W poprzednich wpisach z cyklu o Bitcoinie, opisywałem jak możemy wykorzystać protokół do budowy własnej aplikacji, która komunikuje się z siecią. Kolejnym krokiem rozszerzającym powstałą koncepcję będzie implementacja wiadomości odpowiedzialnej za ogłaszanie transakcji. Zanim do tego dojedziemy, przyda nam się moduł do generowania adresów, których będziemy mogli pełnoprawnie używać wykonując transakcje. Okazuje się, że nie jest to wcale trudne, a procedurę generowania adresu możemy wykonać całkowicie offline.

W dużym uproszczeniu można powiedzieć, że adres w sieci Bitcoin to nic innego jak para kluczy: prywatny i publiczny. Publiczny to adres, który ogłaszamy światu i służy on do przyjmowania wpłat, zaś prywatny jest znany tylko nam i wykorzystujemy go do potwierdzania, że jesteśmy właścicielami adresu, z którego środki chcemy wydać.

Cała potęga w adresach sieci Bitcoin tkwi w fakcie, że można je wygenerować aż na 2^160 sposobów. Daje nam to dokładnie 1 461 501 637 330 902 918 203 684 832 716 283 019 655 932 542 976 możliwości. Prawdopodobieństwo wygenerowania adresu, który już istnieje (poza pewnymi sytuacjami, o których wspomnę w dalszej części wpisu) jest tak małe, że prawie nieprawdopodobne.

Receptura

Przepis na adres Bitcoin jest następujący:

1. Wybierz dowolną liczbę z przedziału [0;2^256]

W praktyce jest to dowolny ciąg 256 bitów. W przykładowym kodzie będziemy go reprezentować jako tablicę 32 bajtów. Dla ułatwienia, aby znaleźć losowy ciąg, można na przykład zastosować na czymkolwiek algorytm SHA-256. Wybrana liczba zostaje tajnym kluczem prywatnym.

Postawmy na łatwą do zapamiętania liczbę w reprezentacji szesnastkowej:

0x00 00 11 11 22 22 33 33 44 44 55 55 66 66 77 77 88 88 99 99 AA AA BB BB CC CC DD DD EE EE FF FF

2. Utwórz do wybranego klucza prywatnego odpowiadający mu klucz publiczny

Do wygenerowania klucza publicznego na podstawie prywatnego wykorzystuje się kryptografię krzywych eliptycznych. Jeżeli interesuje Ciebie podłoże matematyczne tego procesu – odsyłam do przystępnego artykułu o tytule „Matematyka kryjąca się za Bitcoin”. Do wygenerowanego klucza publicznego dodaje się na samym początku 1 bajt o wartości 0x04.

Implementację wspomnianego algorytmu możemy znaleźć w NuGetowej paczce BouncyCastle (link). Poniżej przykład w C# generowania klucza publicznego na podstawie ciągu 32 bajtów:

public static byte[] CreatePublicKeyFromPrivate(byte[] privateKey)
{
  privateKey = (new byte[] { 0x00 }).Concat(privateKey).ToArray();

  var secp256K1Algorithm = SecNamedCurves.GetByName("secp256k1");
  var privateKeyInteger = new BigInteger(privateKey);

  var multiplication = secp256K1Algorithm.G.Multiply(privateKeyInteger);
  var publicKey = new byte[65];

  var y = multiplication.Y.ToBigInteger().ToByteArray();
  Array.Copy(y, 0, publicKey, 64 - y.Length + 1, y.Length);

  var x = multiplication.X.ToBigInteger().ToByteArray();
  Array.Copy(x, 0, publicKey, 32 - x.Length + 1, x.Length);

  publicKey[0] = 0x04;

  return publicKey;
}

Po zastosowaniu powyższej funkcji otrzymujemy klucz publiczny o wartości:

0x04 0D 47 56 8A 5E 51 70 67 A2 83 6C 38 23 FB C5 81 69 A7 66 2B FA E9 34 A4 D4 1D A3 E2 3C 98 D8 16 E7 20 2D D7 02 FF E0 38 14 7F 78 AE E4 97 3A 58 19 72 96 0A 14 60 31 2F FB 6F 3F 0F 13 D4 A5 2C

3. Wylicz z powstałego klucza skrót SHA-256, a następnie RIPEMD-160

W celu wprowadzenia dodatkowego zamieszania i skrócenia długości klucza publicznego, stosuje się na nim kolejno dwie funkcje skrótu. Najpierw SHA-256, następnie RIPEMD-160. W wyniku tej operacji powstaje ciąg o długości 160 bitów. Dla naszego przykładu będzie to:

0xE9 57 AE CB B4 5D B8 82 48 EC 03 E5 9E 57 60 26 D6 2C AD 78

Kod odpowiedzialny za tę przemianę prezentuje się następująco:

public static byte[] Create160BitHashFromPublicKey(byte[] publicKey)
{
  var sha256Algorithm = new SHA256Managed();
  var ripemd160Algorithm = new RIPEMD160Managed();

  var sha256Hash = sha256Algorithm.ComputeHash(publicKey);
  var ripemd160Hash = ripemd160Algorithm.ComputeHash(sha256Hash);

  return ripemd160Hash;
}		

4. Dodaj do powstałej wartości identyfikator sieci, wylicz sumę kontrolną i przedstaw całość w formie czytelnej dla człowieka

Na koniec procesu generowania potrzebujemy jeszcze nieco dodatkowej magii. Do wyliczonej wartości, na jej początku doklejamy bajt 0x00 w celu późniejszej identyfikacji, czy adres pochodzi z głównej sieci, czy testowej (w przypadku sieci testowej wartość ta wynosi 0x6f). Następnie, z powstałej wartości liczymy sumę kontrolną. Tak jak przy innych miejscach, w których wyliczana jest suma kotrolna, stosujemy podwójnie funkcję skrótu SHA-256, a z powstałego ciągu bajtów bierzemy jedynie pierwsze cztery. Tak wygenerowany skrót doklejamy na koniec wartości, z której go liczyliśmy. Ostatnim etapem jest przekształcenie wyniku do formy czytelnej dla człowieka. Używa się do tego algorytmu Base58Check. Algorytm ten przekształca ciąg bajtów na tekst w taki sposób, aby zminimalizować ryzyko pomyłki przy wpisywaniu go z klawiatury (na przykład poprzez eliminację znaków, które mogą wyglądać podobnie – „O”, „0”, „1”, „I”, czy zastosowanie sumy kontrolnej).

Tak przekształcona dotychczas tablica bajtów zamienia nam się na adres:

1NGoV1EGZrwM7yvUYqRC7TMBMj7ftpjR2B

Kod odpowiadający opisanym przekształceniom może wyglądać następująco:

public static string CreateBitcoinAddressFrom160BitHash(
  byte[] publicKeyHash)
{
  byte[] firstHashByte = { 0x00 };

  var sha256Managed = new SHA256Managed();

  var extendedHash = firstHashByte.Concat(publicKeyHash).ToArray();

  var addressChecksum = sha256Managed.ComputeHash(
                        sha256Managed.ComputeHash(extendedHash))
                        .Take(4).ToArray();

  var extendedAddress = extendedHash.Concat(addressChecksum).ToArray();

  return Base58CheckEncode(extendedAddress);
}

		
public static string Base58CheckEncode(byte[] bytes)
{
  const string base58CheckCodeString = 
    "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";

  var result = string.Empty;
  var leadingZeros = 0;

  do
  {
    if (bytes[leadingZeros] == 0x00) {
      ++leadingZeros;
    }
                   

  } while (bytes[leadingZeros] == 0x00 && leadingZeros < bytes.Length);

  bytes = bytes.Reverse()
            .Take(bytes.Length - leadingZeros)
            .Concat(new byte[] { 0x00 })
            .ToArray();

  var bigInteger = new System.Numerics.BigInteger(bytes);

  while (bigInteger > 0)
  {
    var remainder = bigInteger % 58;

    result += base58CheckCodeString[(int)remainder];
    bigInteger = bigInteger / 58;
  }

  for (var i = 0; i < leadingZeros; ++i) {
    result += base58CheckCodeString[0];
  }

  return new string(result.ToCharArray().Reverse().ToArray());
}

6. Przekształć klucz prywatny do formy czytelnej dla człowieka

Klucz prywatny także przekształca się do formy, którą łatwo zapisać na kartce. Adres w takiej formie nazywa się WIF (Wallet import format). Procedura jest identyczna do poprzedniej, z tą różnicą, że na początku ciągu bajtów doklejamy wartość 0x80 identyfikujący ciąg jako prywatny klucz. Poniżej kod przedstawiający opisaną konwersję:

public static string ConvertPrivateKeyToWif(byte[] privateKey)
{
  byte[] firstByte = { 0x80 };

  var sha256Managed = new SHA256Managed();

  var extendedPrivateKey = firstByte.Concat(privateKey).ToArray();
  var checksum = sha256Managed.ComputeHash(
                 sha256Managed.ComputeHash(
                   extendedPrivateKey));

  extendedPrivateKey = extendedPrivateKey
                         .Concat(checksum.Take(4).ToArray())
                         .ToArray();

  return Base58CheckEncode(extendedPrivateKey);
}

Kolizja adresów

Prawdopodobieństwo wylosowania adresu, który już istnieje jest bardzo mało prawopodobne. Jeżeli jednak chcemy generować je na własną rękę, musimy podejść ostrożnie do wyboru wartości klucza prywatnego. Wystarczy, że posłużymy się łatwym wzorcem i możemy trafić na adres, który już istnieje. Na 99.(9)% na koncie tym nie będzie żadnych środków, a zaraz po przelaniu swoich Bitcoinów na to konto – znikną bezpowrotnie. Jestem gotów zaryzykować stwierdzenie, że gdzieś po sieci latają automaty sprawdzające konta powstałe właśnie w ten sposób 🙂

Nie trzeba daleko szukać, aby znaleźć przykłady:

Klucz prywatny: 0xAA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA
Numer konta: 19YCGbqXnsaTNTinE7EzjYS2shhv7BSQw1
Transakcje: https://blockchain.info/pl/address/19YCGbqXnsaTNTinE7EzjYS2shhv7BSQw1

Klucz prywatny: 0xFF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
Numer konta: 12M4QznuNZH2BRVbLK8SKvNqGTPJpCpST7
Transakcje: https://blockchain.info/pl/address/12M4QznuNZH2BRVbLK8SKvNqGTPJpCpST7

Inną metodą wyboru klucza prywatnego jest wyliczenie skrótu SHA-256 np. dla znanego dla siebie słowa, zdania, pliku itp. Do tego typu podejścia także trzeba podejść ostrożnie:

Klucz prywatny: SHA256(„bitcoin”) = 0x6B 88 C0 87 24 7A A2 F0 7E E1 C5 95 6B 8E 1A 9F 4C 7F 89 2A 70 E3 24 F1 BB 3D 16 1E 05 CA 10 7B
Numer konta: 1E984zyYbNmeuumzEdqT8VSL8QGJi3byAD
Transakcje: https://blockchain.info/pl/address/1E984zyYbNmeuumzEdqT8VSL8QGJi3byAD

Można natrafić także na akcenty polskie 🙂
Klucz prywatny: SHA256(„dupa”) = 0x60 A5 D3 E4 10 0F E8 AF A5 EE 01 03 73 9A 45 71 1D 50 D7 F3 BA 72 80 D8 A9 5B 51 F5 D0 4A A4 B8
Numer konta: 16VHSo4f2bTK1YSG4SV95ZsBRqGfMLVeVJ
Transakcje: https://blockchain.info/pl/address/16VHSo4f2bTK1YSG4SV95ZsBRqGfMLVeVJ

Tytułem zakończenia

Adresy w sieci Bitcoin to ciekawa koncepcja, która pokazuje jak stworzyć coś (prawie) unikalnego bez konieczności weryfikacji tej unikalności online. Zaimplementowano w nich także mechanizmy zabezpieczające przed przypadkowym przekłamaniem bitów, a nawet zminimalizowano ryzyko pomyłki przy ręcznym przepisywaniu adresu przez człowieka. W kolejnych wpisach na temat Bitcoina wykorzystamy wiedzę na temat adresów, która przyda się do generowania transakcji i ogłaszania ich w sieci.


Źródła, z których korzystałem przygotowując powyższy wpis:
1. https://en.bitcoin.it/wiki/Address
2. https://en.bitcoin.it/wiki/Technical_background_of_version_1_Bitcoin_addresses