この記事は株式会社Gincoのテックブログとして書いています。
今回は弊社プロダクトのGinco Enterprise Walletで取り扱っているPolkadotについて、ウォレットを作成する上で重要なアドレスとトランザクションの仕様について解説したいと思います。サンプルコードはGolangになります。
Polkadotの概要
Polkadotは、ブロックチェーン同士を相互運用可能な形で結びつけるための分散型システムです。これにより、異なるブロックチェーン間での情報の交換や資産の移動が容易になります。
異なるブロックチェーン同士を接続するためにPolkadotでは「パラチェーン」と呼ばれるサイドチェーンが存在します。Polkadot側がセキュリティを担保する仕組みのため、パラチェーンはセキュリティリスクを気にせず開発を行えるという利点があります。パラチェーンにはASTARやEfinity、Moonbeamなどがあります。
またPolkadotのコンセンサスアルゴリズムは、PoS(Proof of Stake)プロトコルを採用しており、エネルギー消費量が少なく、スケーラブルという特徴があります。
Substrate
Substrateは、開発者が容易に新しいブロックチェーンを作成し、他のブロックチェーンと結合することができるように開発されたフレームワークです。Polkadot自身もこのフレームワークによって構築されています。
特徴としてはハードフォークなしにプロトコルアップデートを行うことができるという点があります。
アドレスの仕様
SS58
アドレスフォーマットはSubstrate上で定義されているSS58という仕様に定義されています。
まずアドレスの長さについてですが、固定ではなく多くの種類があります。一番長い35のアドレスが一番使われていますのでこのアドレスについて解説します。
Address Length | AddressType | Raw account | Checksum |
---|---|---|---|
3 | 1 | 1 | 1 |
4 | 1 | 2 | 1 |
5 | 1 | 2 | 2 |
6 | 1 | 4 | 1 |
7 | 1 | 4 | 2 |
8 | 1 | 4 | 3 |
9 | 1 | 4 | 4 |
10 | 1 | 8 | 1 |
11 | 1 | 8 | 2 |
12 | 1 | 8 | 3 |
13 | 1 | 8 | 4 |
14 | 1 | 8 | 5 |
15 | 1 | 8 | 6 |
16 | 1 | 8 | 7 |
17 | 1 | 8 | 8 |
35 | 1 | 32 | 2 |
SS58のフォーマットはBitcoinで使われているBase58Check形式に基づいていますが、Substrate ベースのチェーンに合わせていくつかの変更が加えられています。
アドレスの生成方法は下記になります。
base58encode ( concat ( <address-type>, <address>, <checksum> ) )
<address-type>はチェーン毎に区別をつけるための値となります。例えば、Polkadotは0x0(0)、テストネットの場合は0x2a(42)となります。
<address>はドキュメント上でaddressと記載されていますが公開鍵が入ります。
<checksum>は、”SS58PRE”という文字列をprefixとして、<address-type>と<address>を結合したものをプリイメージとしてblake2b-512でハッシュした先頭2バイトがchecksumとなります。
concat ("SS58PRE", <address-type>, <address> )
上記に沿ったGolangでの実装例が下記になります。
func EncodeAddress(pubKey []byte, isTestnet bool) string { var raw []byte addressType := []byte{0x00} if isTestnet { // Westend is 42 addressType = []byte{0x2A} } raw = append(addressType, pubKey...) checksum := blake2b.Sum512(append(prefix, raw...)) return base58.NewBase58(base58.BITCOIN).Encode(append(raw, checksum[0:2]...)) }
トランザクションの構築手順
トランザクションの構築手順は下記の通りになります。
- 未署名トランザクションを構築し、SCALEエンコードしてペイロードを生成
- ペイロードに署名をし、署名済トランザクションを構築
- 署名済トランザクションをSCALEエンコードし、rawtxを作成しネットワークに送金
- 事前にトランザクションハッシュを求める場合はrawtxをハッシュして生成
未署名トランザクションの構築
ペイロードを作成するための未署名トランザクションのフォーマットは下記になります。
type UnsignedTransaction struct { Method Method `scale:"-"` EraPeriod uint Nonce uint Tip uint SpecVersion uint32 TransactionVersion uint32 GenesisHash [32]byte BlockHash [32]byte }
Methodはトランザクションで実行する内容を決めるもので送金の場合はBalances PalletのTransferを使用します。
EraPeriodはトランザクションの有効期限を決める項目でオプショナルです。NonceはEthereumと同じで処理される順番を設定します。
Tipは手数料とは別に優先的に取り込まれるように設定するものですが、現状のPolkadotは混雑していないので設定の必要はありません。
SpecVersionにはRunTimeのバージョンを、TransactionVersionはRunTimeで定義されたトランザクションのバージョンを 設定する項目です。GenesisHashは名称通りの値で、BlockHashは有効期限(EraPeriod)を設定する場合はトランザクション作成時点のブロックのハッシュを設定します。
未署名トランザクションの構築方法は下記になります。
func NewUnsignedTransaction(method Method, eraPeriod, nonce uint, specVersion, transactionVersion uint32, genesisHash, currentBlockHash string) (UnsignedTransaction, error) { var ( gHash [32]byte cHash [32]byte ) g, err := hex.DecodeString(strings.TrimPrefix(genesisHash, "0x")) if err != nil { return UnsignedTransaction{}, err } copy(gHash[:], g) if eraPeriod == 0 { cHash = gHash } else { c, err := hex.DecodeString(strings.TrimPrefix(currentBlockHash, "0x")) if err != nil { return UnsignedTransaction{}, err } copy(cHash[:], c) } return UnsignedTransaction{ Method: method, EraPeriod: eraPeriod, Nonce: nonce, Tip: 0, SpecVersion: specVersion, TransactionVersion: transactionVersion, GenesisHash: gHash, BlockHash: cHash, }, nil }
ペイロードの生成
ペイロードの生成方法方は未署名トランザクションをSCALEエンコードするだけになりますが、エンコード結果が256バイトよりも大きい場合はblake2b256ハッシュでペイロード短くしてあげる必要があります。
Transfer Methodでは256バイトを超えませんが、マルチシグ関連のトランザクションの場合は超える場合があるため注意が必要です。
func GenerateSigningPayload(method Method, eraPeriod, nonce uint, specVersion, transactionVersion uint32, genesisHash, currentBlockHash string) (string, error) { unsignedTx, err := NewUnsignedTransaction(method, eraPeriod, nonce, specVersion, transactionVersion, genesisHash, currentBlockHash) if err != nil { return "", err } e, err := unsignedTx.Encode() if err != nil { return "", err } if len(e) > 256 { h := blake2b.Sum256(e) e = h[:] } return hex.EncodeToString(e), nil }
署名アルゴリズム
Substrateは3種類のスキームを採用しています。
ECDSA
BitcoinとEthereumと同じsecp256k1曲線を使用した署名スキームです。
Ed25519
Ed25519 は、 Curve25519を使用した EdDSA 署名スキームです。セキュリティを損なうことなく非常に高速に動作するように、設計と実装のいくつかのレベルで慎重に設計されています。
SR25519
SR25519 はEd25519と同じ楕円曲線に基づいています。ただし、EdDSA スキームの代わりに Schnorr 署名を使用します。
実際に署名した後は署名結果に対して、署名スキーマ別に決まった値をprefixにつける必要があります。
ed25519は0x00を、sr25519は0x01を、ecdsaは0x02をprefixとして追加します。
func Sign(key []byte, payload[]byte) string { signature := sign(key, payload) // signature = scheme + signature. ed25519: 0x00, sr25519: 0x01, ecdsa: 0x02 return fmt.Sprintf("0x00%x", signature) }
署名済みトランザクションのフォーマット
rawtxを作成するための署名済みトランザクションのフォーマットは下記になります。
type SignedTransaction struct { SignedFlag *big.Int // signing bit set FromMultiAddress uint8 FromPubKey [32]byte Signature [65]byte // signType(1 byte) + signature(64 byte) ed25519:0, sr25519:1 EraPeriod uint Nonce uint Tip uint Method Method scale:"-" }
SignedFlagはV4の場合33を指定します。FromMultiAddressのMultiAddressは正確にはmulti-format addressの意味ですが今回は使用しないため0を入れます。FromPubKeyは送金元の公開鍵です。他は未署名トランザクションで使用した値を設定します。
構築した署名済みトランザクションのエンコード方法は下記になります。気をつける点としては署名済みトランザクションのエンコード結果のlengthをrawtxの先頭に設定する必要があります。
func (s *SignedTransaction) Encode() ([]byte, error) { e, err := scale.Marshal(s) if err != nil { return nil, err } m, err := s.Method.Encode() if err != nil { return nil, err } e = append(e, m...) length := len(e) l, err := scale.Marshal(big.NewInt(int64(length))) if err != nil { return nil, err } return append(l, e...), nil }
まとめ
この記事ではPolakdot(Substrate)でのアドレスとトランザクションの生成方法についてまとめました。
ウォレットを作るにはアドレスやトランザクションの詳細な仕様について完璧に把握しないと作れないためかなりドキュメントを読み込みます。しかし、ドキュメントはウォレットを作ることを想定していないため仕様の詳細が記載されていない場合が多く、実際のソースコードを読んで理解しないといけないなど結構ハードルが高い一面もあります。次回はマルチシグのプロトコルについて解説したいと思います。
株式会社Gincoではブロックチェーンを学びたい方、ウォレットについて詳しくなりたい方を募集していますので下記リンクから是非ご応募ください。
髙妻智一
最新記事 by 髙妻智一 (全て見る)
- Polkadot(Substrate)のアドレスとトランザクションについて - 2023-03-09
- 【無料公開】「Goで始めるBitcoin」3章 Bitcoinノードとの通信 技術書典8 - 2020-03-08
- エンジニアがゼロから技術ブログを書くための方法をまとめました - 2019-05-25
コメントを残す