C#でSymbolブロックチェーンのトランザクションを送信する


この記事は「自分の得意なプログラミング言語でSymbolブロックチェーンを動かす方法」をC#で実践したものです。

https://qiita.com/nem_takanobu/items/09b6b687f3cbe10af945

ほぼすべてのロジックを @Toshi_ma さん(Twitter:[toshiya_ma](https://twitter.com/toshiya_ma))に作成していただきました。ありがとうございます。

@Toshi_ma さんが公開されている、UnityでブロックチェーンであるSymbolを利用するためのアセット、Symnityもご参考ください。

https://github.com/0x070696E65/Symnity


### using

```cs
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using System.Numerics;
using System.Globalization;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Crypto.Signers;
using Org.BouncyCastle.Crypto.Digests;
using System.Net.Http;
using System.Net.Http.Headers;

```

### アカウント作成

```cs
var privateKey = Utils.RandomBytes(32);
var publicKey = new Ed25519PrivateKeyParameters(privateKey, 0).GeneratePublicKey().GetEncoded();
Console.WriteLine(Utils.ToHex(privateKey));
Console.WriteLine(Utils.ToHex(publicKey));
```



### アカウント復元
```cs
var alicePrivateKey = Utils.GetBytes("94ee0f4d7fe388ac4b04a6a6ae2ba969617879b83616e4d25710d688a89d80c7");
var alicePublicKey = new Ed25519PrivateKeyParameters(alicePrivateKey, 0).GeneratePublicKey().GetEncoded();
Console.WriteLine(Utils.ToHex(alicePrivateKey));
Console.WriteLine(Utils.ToHex(alicePublicKey));
```


### アドレス導出
```cs
var addressHasher = new Sha3Digest(256);
var publicKeyHash = new byte[addressHasher.GetDigestSize()];
addressHasher.BlockUpdate(alicePublicKey, 0, alicePublicKey.Length);
addressHasher.DoFinal(publicKeyHash, 0);
var addressBodyHasher = new RipeMD160Digest();
var addressBody = new byte[addressBodyHasher.GetDigestSize()];
addressBodyHasher.BlockUpdate(publicKeyHash, 0, publicKeyHash.Length);
addressBodyHasher.DoFinal(addressBody, 0);
var sumHasher = new Sha3Digest(256);
var preSumHash = new byte[sumHasher.GetDigestSize()];
sumHasher.BlockUpdate(Utils.GetBytes("98" + Utils.ToHex(addressBody)), 0, 21);
sumHasher.DoFinal(preSumHash, 0);
var sumHash = new byte[3];
Array.Copy(preSumHash, sumHash, 3);
var aliceAddress = Base32.ToBase32String(Utils.GetBytes("98" + Utils.ToHex(addressBody) + Utils.ToHex(sumHash))).Substring(0, 39);
Console.WriteLine(aliceAddress);
```

### トランザクション構築
```cs
var version = new byte[] { 1 };
var networkType = new byte[] { 152 };
var transactionType = BitConverter.GetBytes((ushort)16724);
var fee = BitConverter.GetBytes((ulong)1000000);
var deadlineDateTime = DateTime.Now.ToUniversalTime().AddHours(2).AddSeconds(-1637848847);
var unixOriginTime = new DateTime(1970, 1, 1, 0, 0, 0);
var deadline = BitConverter.GetBytes((ulong)Math.Floor((deadlineDateTime - unixOriginTime).TotalMilliseconds));
var recipientAddress = Base32.FromBase32String("TBIL6D6RURP45YQRWV6Q7YVWIIPLQGLZQFHWFEQ");
var mosaicCount = new byte[] { 1 };
var mosaicId = BitConverter.GetBytes((ulong)BigInteger.Parse("3A8416DB2D53B6C8", NumberStyles.HexNumber));
var mosaicAmount = BitConverter.GetBytes((ulong)100);
var message = Encoding.UTF8.GetBytes(("Hello C#! Welcome to Symbol world!"));
var messageSize = BitConverter.GetBytes((ushort)(message.Length + 1));
```

### トランザクション署名
```cs
var verifiableBody = Utils.ToHex(version)
    + Utils.ToHex(networkType)
    + Utils.ToHex(transactionType)
    + Utils.ToHex(fee)
    + Utils.ToHex(deadline)
    + Utils.ToHex(recipientAddress)
    + Utils.ToHex(messageSize)
    + Utils.ToHex(mosaicCount)
    + "00" + "00000000"
    + Utils.ToHex(mosaicId)
    + Utils.ToHex(mosaicAmount)
    + "00" + Utils.ToHex(message);

var verifiableString = "7fccd304802016bebbcd342a332f91ff1f3bb5e902988b352697be245f48e836"
    + verifiableBody;

var verifiableBuffer = Utils.GetBytes(verifiableString);
var signer = new Ed25519Signer();
signer.Init(true, new Ed25519PrivateKeyParameters(alicePrivateKey, 0));
signer.BlockUpdate(verifiableBuffer, 0, verifiableBuffer.Length);
var signature = signer.GenerateSignature();
```

### トランザクションの通知
```cs
var transactionSize = BitConverter.GetBytes((uint)Utils.GetBytes(verifiableBody).Length + 108);

var payloadString = Utils.ToHex(transactionSize)
    + "00000000"
    + Utils.ToHex(signature)
    + Utils.ToHex(alicePublicKey)
    + "00000000"
    + verifiableBody;

var httpClient = new HttpClient();
var payload = new StringContent("{ \"payload\" : \"" + payloadString + "\"}");
payload.Headers.ContentType = new MediaTypeHeaderValue("application/json");
var response = httpClient.PutAsync("https://sym-test-02.opening-line.jp:3001/transactions", payload).Result;
Console.WriteLine(response.Headers);
Console.WriteLine(response.RequestMessage);
```

### 確認
```cs
var hashableBuffer = Utils.GetBytes(
    Utils.ToHex(signature)
    + Utils.ToHex(alicePublicKey)
    + verifiableString
    );

var hasher = new Sha3Digest(256);
var transactionHash = new byte[hasher.GetDigestSize()];
hasher.BlockUpdate(hashableBuffer, 0, hashableBuffer.Length);
hasher.DoFinal(transactionHash, 0);

Console.WriteLine("transactionStatus: https://sym-test-02.opening-line.jp:3001/transactionStatus/" + Utils.ToHex(transactionHash));
Console.WriteLine("confirmed: https://sym-test-02.opening-line.jp:3001/transactions/confirmed/" + Utils.ToHex(transactionHash));
Console.WriteLine("explorer: https://testnet.symbol.fyi/transactions/" + Utils.ToHex(transactionHash));
```

### 検証プログラム

https://github.com/0x070696E65/cs_symbol_sampletransaction/blob/e7c83dc07bf8013063be581c7e0fb2a84dff9a31/Program.cs

# Appendix

### Utils
```cs
    class Utils
    {
        internal static byte[] GetBytes(string hexString)
        {
            var bs = new List<byte>();
            for (var i = 0; i < hexString.Length / 2; i++)
            {
                bs.Add(Convert.ToByte(hexString.Substring(i * 2, 2), 16));
            }
            return bs.ToArray();
        }

        internal static byte[] RandomBytes(byte length)
        {
            var rngCsp = new RNGCryptoServiceProvider();
            var randomBytes = new byte[length];
            rngCsp.GetBytes(randomBytes);
            return randomBytes;
        }

        internal static string ToHex(byte[] bytes)
        {
            var str = BitConverter.ToString(bytes);
            str = str.Replace("-", string.Empty);
            return str;
        }
    }

```

### Base32
```cs
    static class Base32
    {
        private static readonly char[] _digits = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".ToCharArray();
        private const int _mask = 31;
        private const int _shift = 5;

        internal static int CharToInt(char c)
        {
            switch (c)
            {
                case 'A': return 0;
                case 'B': return 1;
                case 'C': return 2;
                case 'D': return 3;
                case 'E': return 4;
                case 'F': return 5;
                case 'G': return 6;
                case 'H': return 7;
                case 'I': return 8;
                case 'J': return 9;
                case 'K': return 10;
                case 'L': return 11;
                case 'M': return 12;
                case 'N': return 13;
                case 'O': return 14;
                case 'P': return 15;
                case 'Q': return 16;
                case 'R': return 17;
                case 'S': return 18;
                case 'T': return 19;
                case 'U': return 20;
                case 'V': return 21;
                case 'W': return 22;
                case 'X': return 23;
                case 'Y': return 24;
                case 'Z': return 25;
                case '2': return 26;
                case '3': return 27;
                case '4': return 28;
                case '5': return 29;
                case '6': return 30;
                case '7': return 31;
            }

            return -1;
        }

        internal static byte[] FromBase32String(string encoded)
        {
            if (encoded == null)
                throw new ArgumentNullException(nameof(encoded));

            // Remove whitespace and padding. Note: the padding is used as hint 
            // to determine how many bits to decode from the last incomplete chunk
            // Also, canonicalize to all upper case
            encoded = encoded.Trim().TrimEnd('=').ToUpper();
            if (encoded.Length == 0)
                return new byte[0];

            var outLength = encoded.Length * _shift / 8;
            var result = new byte[outLength];
            var buffer = 0;
            var next = 0;
            var bitsLeft = 0;
            var charValue = 0;
            foreach (var c in encoded)
            {
                charValue = CharToInt(c);
                if (charValue < 0)
                    throw new FormatException("Illegal character: `" + c + "`");

                buffer <<= _shift;
                buffer |= charValue & _mask;
                bitsLeft += _shift;
                if (bitsLeft >= 8)
                {
                    result[next++] = (byte)(buffer >> (bitsLeft - 8));
                    bitsLeft -= 8;
                }
            }

            return result;
        }

        internal static string ToBase32String(byte[] data, bool padOutput = false)
        {
            return ToBase32String(data, 0, data.Length, padOutput);
        }

        internal static string ToBase32String(byte[] data, int offset, int length, bool padOutput = false)
        {
            if (data == null)
                throw new ArgumentNullException(nameof(data));

            if (offset < 0)
                throw new ArgumentOutOfRangeException(nameof(offset));

            if (length < 0)
                throw new ArgumentOutOfRangeException(nameof(length));

            if ((offset + length) > data.Length)
                throw new ArgumentOutOfRangeException();

            if (length == 0)
                return "";

            // SHIFT is the number of bits per output character, so the length of the
            // output is the length of the input multiplied by 8/SHIFT, rounded up.
            // The computation below will fail, so don't do it.
            if (length >= (1 << 28))
                throw new ArgumentOutOfRangeException(nameof(data));

            var outputLength = (length * 8 + _shift - 1) / _shift;
            var result = new StringBuilder(outputLength);

            var last = offset + length;
            int buffer = data[offset++];
            var bitsLeft = 8;
            while (bitsLeft > 0 || offset < last)
            {
                if (bitsLeft < _shift)
                {
                    if (offset < last)
                    {
                        buffer <<= 8;
                        buffer |= (data[offset++] & 0xff);
                        bitsLeft += 8;
                    }
                    else
                    {
                        int pad = _shift - bitsLeft;
                        buffer <<= pad;
                        bitsLeft += pad;
                    }
                }

                int index = _mask & (buffer >> (bitsLeft - _shift));
                bitsLeft -= _shift;
                result.Append(_digits[index]);
            }

            if (padOutput)
            {
                int padding = 8 - (result.Length % 8);
                if (padding > 0) result.Append('=', padding == 8 ? 0 : padding);
            }

            return result.ToString();
        }
    }

```