0から仮想通貨を作るために知っておくべき技術【第1回アドレス編】

はじめに

現在、趣味で0から独自の仮想通貨を開発しているのですが、仮想通貨の開発方法について調べるとEthereumプラットフォーム上のトークンに対する記事がほとんどで、その他仮想通貨を構成する技術であるブロックチェーンやP2P、マークルツリー等の仕組みをどの様に実装するのかを記した記事があまり見当たらないため、どうすれば独自の仮想通貨を作ることができるのか、そのために必要な技術は何かを備忘録を兼ねて複数回に渡りまとめたいと思います。

仮想通貨の基礎知識(アドレス、マイニング、ブロックチェーンなど)はある程度知っている事を前提に説明します。

本記事は理解しやすくするため、初期のBitcoinと同等のものを開発することを前提とし、C#によるサンプルコードを交え解説していきたいと思います。

また、サンプルコードはVisual Studio Community 2017 .Net Framework 4.7でコーディングされていますのでご注意ください。

仮想通貨に含まれる技術要素

仮想通貨に含まれる技術要素は大きく分けると

  • 公開鍵暗号(デジタル署名アルゴリズム)
  • ハッシュ
  • マークルツリー(ハッシュ木)
  • P2Pネットワーク

など現在様々な場所でよく使われている技術にブロックチェーンという新しい仕組みが組み合わさることで機能しています。Bitcoinなど基本的な仮想通貨に出てくるアドレスや送金・マイニングといった仕組みは上記の仕組みを上手くラッピングしたもので、これら技術の信頼の元成り立っています。なので、例えば公開鍵暗号技術が破られる事があれば現在普及している仮想通貨市場は完全に崩壊します。(その前にインターネット自体が崩壊しますが・・・)

それぞれの技術について、なぜ仮想通貨に必要なのか簡単に説明します。
今回は「公開鍵暗号」と「ハッシュ」について説明します。

公開鍵暗号

公開鍵暗号は、秘密鍵と公開鍵という2つの鍵を使った暗号のことで、1つの鍵を使う共通鍵暗号と対をなす暗号技術です。代表的なものにRSA暗号が挙げられます。通信を暗号化するSSLは公開鍵暗号と共通鍵暗号の両方を利用しています。
秘密鍵は自分以外に知られないようにし、公開鍵はオープンにしても問題ないという面白い暗号方式です。
公開鍵暗号は以下の性質を持ちます。

  • 公開鍵で暗号化した文は秘密鍵のみで復号可能
  • 秘密鍵で暗号化した文は公開鍵のみで復号可能(出来ない公開鍵暗号も存在)
  • 秘密鍵から公開鍵を知ることが可能、公開鍵から秘密鍵を知ることは不可能
  • 秘密鍵でサインした文は公開鍵で本人が作成した文であると証明可能(デジタル署名)

以上の性質を持つ公開鍵暗号ですが、仮想通貨ではアドレスの作成アドレスを持つ本人である証明(デジタル署名)に使われます。デジタル署名は、主に仮想通貨の送金時に本人が送金した証明に使われます。これがないと、誰かさんの仮想通貨を不正に送金し放題になり通貨として機能しません。なぜ不正送金できてしまうかは後で説明します。

仮想通貨ではBitcoinの慣例に倣い楕円曲線デジタル署名アルゴリズムECDSA(Elliptic Curve Digital Signature Algorithm)が用いられることが多いです。

公開鍵暗号の詳しい説明は「公開鍵暗号と電子署名の基礎知識」とか「「公開鍵暗号方式」と「共通鍵暗号方式」について」を参考にしてください。

ハッシュ

ハッシュはあらゆる入力データから固定長の小さな出力値を一意に求める仕組みのことを指します、この仕組みで得られた出力値をハッシュ値と言います。代表的なものにMD5SHA-1があります。
ハッシュの主な用途は受信したデータの破損や改ざんの検証公開鍵暗号のデジタル署名です。あと、パスワードをDBに保存するときも普通はパスワードにハッシュを施してから保存したりします。
ハッシュは以下の性質を持ちます。

  • あらゆる長さの入力データから固定長のハッシュ値を出力する
  • 同じ入力データからは必ず同じハッシュ値が得られる
  • 似た入力データからでも、まったく違うハッシュ値が得られる
  • ハッシュ値から入力データを求めることは出来ない
  • 違う入力データから同じハッシュ値が出力される(衝突する)ことはほぼあり得ないがごく稀にあり得る

仮想通貨では、お馴染みブロックチェーンアドレスの作成取引(トランザクション)の検証マークルルートの作成そしてマイニングなど様々なところで利用されますので、確実に理解してください。

仮想通貨でよく使われるのはSHA-256(出力32byte)やRIPEMD-160(出力20byte)です。

ハッシュの詳しい説明は「ハッシュ関数とハッシュアルゴリズム ~PKI基礎④~」とか「ハッシュ関数」など、調べればわんさか出てきます。

アドレスの作成

仮想通貨の口座ともいえるアドレスは公開鍵と秘密鍵のペアから様々な手順を施して作成されます。なぜ公開鍵暗号から作成されるかというとアドレスを持つ本人の確認のために都合が良いからです。
アドレス1つに対し公開鍵と秘密鍵のペアも一緒に存在すると思ってください。
仮想通貨のウォレットは1つのウォレットで複数のアドレス、公開鍵、秘密鍵を保持している感じですね。
アドレスは以下の手順で作成されます。

  1. 公開鍵と秘密鍵のペアをECDSAで自動生成する
  2. 公開鍵の先頭に非圧縮を示すプレフィックス0x04を付加する
  3. 2をSHA-256でハッシュ化する
  4. 3をRIPEMD-160でハッシュ化する
  5. 4の先頭にアドレスのバージョンを示すプレフィックス0x0Fを付加する(仮想通貨ごとに独自、Bitcoinは0x00)
  6. 5をSHA-256でダブルハッシュ化する
  7. 5の末尾に6の先頭4バイトをチェックサムとして付加する
  8. 7をBase58でエンコードしたらアドレスの出来上がり!

長いですね。
この様な手順を踏んでアドレスを作成することで公開鍵暗号から正当な手順を踏んで作成されたアドレスかどうか検証が出来ます。適当なアドレスを受信しても誰だお前知らんと一蹴できるのです。
ちなみにアドレスは「73APNAsEnArENxeTPUKJi81bzzbCb1dij2」みたいな先頭が7のアドレスが作成されます。

アドレス作成の分かりやすい図解(Bitcoin)を以下に転載しておきます。
Generate Address

サンプルコード

アドレス作成のサンプルコードは以下のようになります。
Base58の説明をしていませんが、それらのコードも載せときます。

Program.cs
using System;
using System.Linq;
using System.Security.Cryptography;

namespace GenerateAddressTest
{
    class Program
    {
        static void Main(string[] args)
        {
            // 1.公開鍵と秘密鍵のペアをECDsaで自動生成する
            var dsa = ECDsa.Create("ECDsaCng");
            var curve = ECCurve.CreateFromFriendlyName("secp256k1");
            dsa.GenerateKey(curve);
            var param = dsa.ExportParameters(true);

            // 秘密鍵(32byte)
            var privateKey = param.D;

            // 公開鍵(64byte)
            var publicKey = param.Q.X.Concat(param.Q.Y).ToArray();

            // 公開鍵からアドレスを作成
            var address = Address.GenerateFromPublicKey(publicKey);

            // ランダムに作成したアドレスを出力
            Console.WriteLine($"Address: {address.String}");
            Console.ReadLine();
        }
    }
}
Address.cs
using System.Linq;

namespace GenerateAddressTest
{
    class Address
    {
        public static readonly byte[] Pref_NotCompressed = new byte[] { 0x04 };
        public static readonly byte[] Pref_MainNet = new byte[] { 0x0F };

        public byte[] Bytes { get; set; }

        // 8. 7をBase58でエンコードしたらアドレスの出来上がり!
        public string String => Convert.ToBase58String(Bytes);

        public Address(byte[] bytes) => Bytes = bytes;

        public static Address GenerateFromPublicKey(byte[] publicKey)
        {
            if (publicKey == null || publicKey.Length != 64) return null;

            // 2. 公開鍵の先頭に非圧縮を示すプレフィックス0x04を付加する
            var kPref = Pref_NotCompressed.Concat(publicKey).ToArray();

            // 3. 2をSHA-256でハッシュ化する
            var kHash = Hash.SHA256(kPref);

            // 4. 3をRIPEMD-160でハッシュ化する
            kHash = Hash.RIPEMD160(kHash);

            // 5. 4の先頭にアドレスのバージョンを示すプレフィックスを付加する
            var ad = Pref_MainNet.Concat(kHash).ToArray();

            // 6. 5をSHA-256でダブルハッシュ化する
            var adHash = Hash.SHA256(ad, 2);

            // 7. 5の末尾に6の先頭4バイトをチェックサムとして付加する
            return new Address(ad.Concat(adHash.Take(4)).ToArray());
        }
    }
}
Hash.cs
using System.Security.Cryptography;
using System.Text;

namespace GenerateAddressTest
{
    static class Hash
    {
        #region SHA-256
        private static SHA256 sha256 = new SHA256CryptoServiceProvider();

        public static byte[] SHA256(string plane, uint times = 1)
        {
            return SHA256(Encoding.UTF8.GetBytes(plane), times);
        }

        public static byte[] SHA256(byte[] buf, uint times = 1)
        {
            if (buf == null) return null;
            byte[] hash = buf;
            for (uint i = 0; i < times; i++)
            {
                hash = sha256.ComputeHash(hash);
            }
            return hash;
        }
        #endregion

        #region RIPEMD-160
        private static RIPEMD160 ripemd160 = new RIPEMD160Managed();

        public static byte[] RIPEMD160(string plane, uint times = 1)
        {
            return RIPEMD160(Encoding.UTF8.GetBytes(plane), times);
        }

        public static byte[] RIPEMD160(byte[] buf, uint times = 1)
        {
            if (buf == null) return null;
            byte[] hash = buf;
            for (uint i = 0; i < times; i++)
            {
                hash = ripemd160.ComputeHash(hash);
            }
            return hash;
        }
        #endregion
    }
}
Convert.cs
using System;
using System.Linq;
using System.Numerics;
using System.Text;

namespace GenerateAddressTest
{
    static class Convert
    {
        private static readonly string Base58Digits = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";

        public static string ToBase58String(byte[] array)
        {
            BigInteger data = 0;
            for (var i = 0; i < array.Length; i++)
            {
                data = data * 256 + array[i];
            }

            var sb = new StringBuilder();
            while (data > 0)
            {
                var index = (int)(data % 58);
                data /= 58;
                sb.Insert(0, Base58Digits[index]);
            }

            for (var i = 0; i < array.Length && array[i] == 0; i++)
            {
                sb.Insert(0, Base58Digits[0]);
            }

            return sb.ToString();
        }

        public static byte[] FromBase58String(string str)
        {
            BigInteger data = 0;
            for (var i = 0; i < str.Length; i++)
            {
                var index = Base58Digits.IndexOf(str[i]);
                if (index < 0)
                    throw new FormatException(string.Format("Invalid Base58 character `{0}` at position {1}", str[i], i));
                data = data * 58 + index;
            }

            var zeroCount = str.TakeWhile(c => c == Base58Digits[0]).Count();
            var zeros = Enumerable.Repeat((byte)0, zeroCount);
            var bytes = data.ToByteArray().Reverse().SkipWhile(b => b == 0);

            return zeros.Concat(bytes).ToArray();
        }

    }
}

上記コードで実行するたびにランダムなアドレスが生成されます。

簡単に説明すると、
まず、Program.csで公開鍵と秘密鍵をECDSAで自動生成します。Elliptic Curveの種類はBitcoinと同じsecp256k1です。そしてAddress.csのGenerateFromPublicKeyで公開鍵からアドレスを作成し、最後にBae58でコンソール出力します。
Addressクラスはアドレス値として25byteのバイト配列を保持し、Base58文字列はその都度変換するようにしておきます。
Hash.csにはSHA-256とRIPEMD-160のハッシュ値を求めるメソッドを定義し、Convert.csにはbyte配列をBase58文字列に変換、Base58文字列をbyte配列に変換するメソッドを定義しています。
この様に予め定義しておくと後で利用するときにとても楽です。

.Net Frameworkのバージョンにもよりますが、ECDSA、secp256k1、SHA-256、RIPEMD-160が標準で用意されているのでアドレス作成部分は比較的簡単に実装できてしまいますね。

アドレス作成の詳しい説明は「ビットコインアドレスを自分の手で作って理解する」や「Technical background of version 1 Bitcoin addresses」を参考にして下さい。

デジタル署名による本人の検証

本当はP2Pネットワークやトランザクションが絡んでくるのでもっと後に説明しようかと思ったのですが、アドレスをもつ本人確認は現時点でテスト実装できるので一応。

デジタル署名はコインの送金時にアドレスを持つ本人が送金したと証明するために利用されます。署名は秘密鍵で行われ、公開鍵で検証することが可能です。
公開鍵での検証が成功すれば、本人しか知らないはずである秘密鍵で署名が行われたことになるので、本人であると確認ができるわけです。

これだけだと分かりづらいと思いますので具体例を挙げます。(具体例なので仮想通貨の実装とは違います)

AさんはアドレスAとその秘密鍵Aと公開鍵Aを持っています。
アドレスAはコインを100枚持っているとします。
アドレスAからBさんのアドレスBにコイン50枚を送金したいと思います。
送金をするためにAさんは、まず「アドレスAからアドレスBにコイン50枚を送ります」という送金データAを作成します。
次にAさんが作成したと証明するため、送金データAと秘密鍵Aから署名Aを作成します。
送金することをみんなに知らせるため、Cさん、Dさん、Eさんに知らせるとします。
Aさんは、「送金データA・署名A・公開鍵A」をC,D,Eさんに送信します。
C,D,Eさんは受信した「送金データA」が本当にAさんが作成したものか確認するために、「署名A・公開鍵A」を利用します。
C,D,Eさんは送金データAと署名Aを公開鍵Aで検証し、成功すればAさんが送金データAを作成したと確認でき、これがブロックチェーンに記録されると送金が完了したとなるわけです。
もし検証に失敗したら誰かが改ざんしたことになるので、送金データAはゴミ箱にポイされます。

改ざんは検証できるわけですが、秘密鍵が他人に知れ渡ったらなりすまりは通ってしまいます。
秘密鍵がばれてしまったらもうどうすることもできません
理由は簡単で、悪者Zさんが「アドレスAからアドレスZにコイン100枚を送ります」という送金データZ作成し秘密鍵Aで署名した署名Zは、誰に送ってもAさんが送ったと証明されてしまうからです。これでAさんは無事Goxするわけです。

なので仮想通貨を実装する側も、秘密鍵は極力他人にばれないよう工夫して保存する必要があります。(ここは面倒なので触れません)

あと実は「送金データA・署名A・公開鍵A」はAさんから直接聞かなくてもまた聞きでも問題ありません。
これも理由は簡単で、「送金データA・署名A・公開鍵A」のどれか少しでも改ざんされていたらすぐに検証ではじかれるからです。

サンプルコード

デジタル署名による検証のサンプルコードです。
当たり前ですが署名の検証を確認するだけなので、使い物にはなりません。

Program.cs
using System;
using System.Linq;
using System.Security.Cryptography;
using System.Text;

namespace GenerateAddressTest
{
    class Program
    {
        static void Main(string[] args)
        {
            // 公開鍵と秘密鍵を自動生成
            var ds = DigitalSignature.Generate();

            // 送金データを作成
            var txA = Encoding.UTF8.GetBytes("アドレスAからアドレスBに50枚送ります");

            // 送金データの署名を作成(秘密鍵で署名)
            var signA = ds.Sign(txA);

            // --- 誰かが受け取ったとする ---

            // 検証用のデジタル署名インスタンスを作成
            var ds2 = DigitalSignature.FromKey(ds.PublicKey);

            // 送金データの検証(公開鍵で検証)
            var res = ds2.Verify(txA, signA);
            Console.WriteLine($"検証結果: {res}"); // true

            // ためしに改ざん送金データを作成
            var txZ = Encoding.UTF8.GetBytes("アドレスAからアドレスZに100枚送ります");

            // これは失敗
            res = ds2.Verify(txZ, signA);
            Console.WriteLine($"検証結果: {res}"); // false
            Console.ReadLine();
        }
    }
}
DigitalSignature.cs
using System.Linq;
using System.Security.Cryptography;

namespace GenerateAddressTest
{
    class DigitalSignature
    {
        private ECDsa dsa;
        public byte[] PrivateKey { get; private set; }
        public byte[] PublicKey { get; private set; }

        private DigitalSignature(byte[] privateKey, byte[] publicKey)
        {
            PrivateKey = privateKey;
            PublicKey = publicKey;
        }

        public static DigitalSignature Generate()
        {
            var dsa = ECDsa.Create("ECDsaCng");
            dsa.GenerateKey(ECCurve.CreateFromFriendlyName("secp256k1"));
            var param = dsa.ExportParameters(true);

            var ds = new DigitalSignature(param.D, param.Q.X.Concat(param.Q.Y).ToArray());
            ds.dsa = dsa;

            return ds;
        }

        public static DigitalSignature FromKey(byte[] publicKey)
        {
            if (publicKey == null || publicKey.Length != 64) return null;

            var param = new ECParameters()
            {
                Curve = ECCurve.CreateFromFriendlyName("secp256k1"),
                Q = new ECPoint()
                {
                    X = publicKey.Take(32).ToArray(),
                    Y = publicKey.Skip(32).Take(32).ToArray(),
                },
            };

            var ds = new DigitalSignature(null, publicKey);

            try
            {
                ds.dsa = ECDsa.Create(param);
            }
            catch
            {
                return null;
            }

            return ds;
        }

        public static DigitalSignature FromKey(byte[] privateKey, byte[] publicKey)
        {
            if (privateKey == null || privateKey.Length != 32) return null;
            if (publicKey == null || publicKey.Length != 64) return null;

            var param = new ECParameters()
            {
                Curve = ECCurve.CreateFromFriendlyName("secp256k1"),
                D = privateKey,
                Q = new ECPoint()
                {
                    X = publicKey.Take(32).ToArray(),
                    Y = publicKey.Skip(32).Take(32).ToArray(),
                },
            };

            var ds = new DigitalSignature(privateKey, publicKey);

            try
            {
                ds.dsa = ECDsa.Create(param);
            }
            catch
            {
                return null;
            }

            return ds;
        }


        public byte[] Sign(byte[] data)
        {
            return dsa.SignData(data, HashAlgorithmName.SHA256);
        }

        public bool Verify(byte[] data, byte[] sign)
        {
            return dsa.VerifyData(data, sign, HashAlgorithmName.SHA256);
        }
    }
}

とりあえず、DigitalSignature.csのように公開鍵、秘密鍵、デジタル署名の生成、検証を一つのクラスにまとめておくと、あとあと楽です。

本来なら、P2Pで誰かさんから送られてきたトランザクションを検証するので、もっともっと複雑なコードになります。
例えば今回はアドレスが出てきていませんが、本来ならトランザクションにアドレスが含まれていて、それが公開鍵から作られた正しいアドレスであるかとか確認しなければなりません。

デジタル署名の詳しい説明は「Digital Signature」を参照してください。

まとめ

公開鍵暗号とハッシュの簡単な説明と、アドレス作成、デジタル署名のサンプルコードを記述しました。
深い説明はしていませんがどの場面でどの技術要素が必要かを理解しておくと、何一つ手が出せないかというと全くそんなことはないと分かるかと思います。
bitcoinやLitecoinのソースコードと睨めっこしなくても要所を理解すれば0から作れますので(私はまだ途中ですが…)、気力のある方はぜひチャレンジしてみてください。

今回のサンプルコードはGithubにあげておきますので、気になる方はどうぞ
https://github.com/yoship1639/Cryptocurrency

次はブロックチェーン編の予定です。

参考リンク

公開鍵暗号

ハッシュ

アドレス作成

デジタル署名

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.