5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

nemAdvent Calendar 2021

Day 25

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

Last updated at Posted at 2022-06-23

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

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

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

using

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;

アカウント作成

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

アカウント復元

var alicePrivateKey = Utils.GetBytes("94ee0f4d7fe388ac4b04a6a6ae2ba969617879b83616e4d25710d688a89d80c7");
var alicePublicKey = new Ed25519PrivateKeyParameters(alicePrivateKey, 0).GeneratePublicKey().GetEncoded();
Console.WriteLine(Utils.ToHex(alicePrivateKey));
Console.WriteLine(Utils.ToHex(alicePublicKey));

アドレス導出

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);

トランザクション構築

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));

トランザクション署名

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();

トランザクションの通知

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);

確認

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));

検証プログラム

Appendix

Utils

    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

    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();
        }
    }

5
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?