見出し画像

Symbolテスト・ネットワーク構築の手引き

この記事はSymbol/NEMの技術者であるWayonさんによって書かれた記事「A Guide to Creating a Symbol Test Network」を機械翻訳したものです。


Symbol mainnetに変更が加えられる前、または新しいプロジェクトがブロックチェーン上で開始される前に、まずテストバージョンがtestnetにデプロイされます。Symbolテストネットワーク(testnet)は、Symbolメインネットワークをシミュレートし、開発者やコミュニティが実際の資産を使用する前に機能をテストする機会を提供します。

Sai Testnetは、現在Symbolのテストネットです。ノードを追加して、Faucetからテストトークンを取得することができます。Faucetでは、1つのアカウントで最大10kのテストトークンを要求することができます。テストや投票ノードの運営にさらにトークンが必要な場合は、DiscordのSymbolのhelp deskに依頼することができます。現実世界での価値はないものの、ちょっと贅沢をするために300万や400万のtest xymを持っていると楽しいかもしれません。

なぜ独自の開発テストネットを作る必要があるのか?

Symbol テストネットの作成と維持には、証明書や投票鍵の更新などの作業が必要ですが、なぜわざわざ自分でセットアップするのでしょうか?このような理由から、独自のテストネットを構築することをお勧めします。

テストシナリオ - あなたのアプリケーションのために特定のテストシナリオを実行したいかもしれません。例えば、ネットワークをフォークしてファイナライズを停止させ、アプリケーションが入出金の受け付けも停止することを検証したいとします。

永続性 - テストネットがリセットされないという保証はありません。シンボルテストネットはストレージを低く保つために、少なくとも年に一度はリセットされます。しかし、主要な機能が動作しなくなった場合にもリセットされることがあります。もし、あなたのアプリケーションがより長い時間データを保持することを必要とするならば、開発用テストネットの方が良いでしょう。

資金の管理 - あなたのユースケースは、5億xymのような大きな資金を必要とするかもしれません。Faucetがあるとはいえ、それだけのトークンを要求するのは開発者用テストネットで行うのがベストです。

ハードウェア要件 - Symbol testnetを使用するには、あなたのノードがブロックチェーンと同期する必要があります。ストレージの要件は、単に自分たちでローリングするよりも高くなります。

注:ブロックチェーンでは一番最初のブロック高をジェネシスブロックといいますが、SymbolとNEMではネメシスブロック(Nemesis block)と呼んでいるので、その事に留意して、以下を呼んでください。

Symbol testnetの新規作成

各Symbolテストネットワークには、config-network.propertiesファイルに1つ以上のプロパティがあり、一意であることが必要です。すべてのネットワークプロパティは、Symbolのドキュメントサイトに掲載されています。

  • generationHashSeed: すべてのtxに署名するために使用されるネメシス世代のハッシュシード。これは新しいネットワークで一意であるべきです。

  • nemesisSignerPublicKey。ネメシス シグナーの公開鍵。

  • currencyMosaicIdとharvestingMosaicIdは、ネメシス シグナーから生成される。

  • harvestNetworkFeeSinkAddressとharvestNetworkFeeSinkAddressV1:ハーベストネットワークフィーシンクアカウントのアドレス。

  • mosaicRentalFeeSinkAddressとmosaicRentalFeeSinkAddressV1。モザイクレンタル料シンクアカウントのアドレス。

  • namespaceRentalFeeSinkAddressとnamespaceRentalFeeSinkAddress。名前空間レンタル料シンクアカウントのアドレス。

開発用テストネットのセットアップを自動化するために必要なツール

  • catapult.tools.nemgen - docker イメージで利用可能です。

  • Symbol Python SDK 3.x

  • Symbol ノードリソース - nemgenが使用します。

テストネット ジェネシス シードの作成

宿敵ブロックには、ネットワークを開始するために必要な初期アカウントとアセットが含まれています。これらのアカウントはブロックを作成するためのハーベスタになるものもあれば、最終的な投票に参加するためのボータになるものもあります。以下は、ネメシス内のアカウントを記述したネットワーク・テンプレートである。
注:ホスト情報は、ネメシスシードを作成する際には必要ありません。

network: testnet
nodes:
  - host: host001.test.net
    friendly_name: host001
    mode: dual
    amount: 3000000000000
    roles:
      - harvester
      - voter
  - host: host002.test.net
    friendly_name: host002
    mode: peer
    amount: 3000000000000
    roles:
      - voter
  - host: host003.test.net
    friendly_name: host003
    mode: dual
    amount: 100000000000
    roles:
      - harvester
accounts:
  - amount: 100000000000
    count: 5
  - amount: 3000000000000
    count: 5
total_supply: 7842928625000000

Nemgenは、nemesis seedを作成するために、アカウントアドレス、キーペア、アセットが必要です。Symbol Python SDKを使用してジェネレータを作成します。ネットワークジェネレーターは、ネットワークテンプレートファイルを読み込み、必要なキーペアとアセットを含む新しいyamlを作成して、ネメシスを作成します。
以下は、ネットワークジェネレーターの主なロジックです。主な秘密鍵/公開鍵であるキーペアは、アカウントごとに作成されます。アカウントに関連付けられた各ロールに対して、ハンドラが必要な他のメタデータを追加します。

def _create_keys(self):
    key_pair = self.facade.KeyPair(PrivateKey.random())
    return {
        'privatekey': str(key_pair.private_key),
        'publickey': str(key_pair.public_key),
        'address': str(self.facade.network.public_key_to_address(key_pair.public_key))
    }

def _add_keys(self, account, name):
    account[name] = self._create_keys()

def _create_account(self, amount, roles):
    account = self._create_keys()
    account.update({'amount': amount})
    if roles:
        account.update({'roles': roles})

    return account

def _create_node_account(self, node):
    node.update({'main': self._create_account(node['amount'], [])})

    if 'harvester' in node['roles']:
        self._add_keys(node, 'remote')
        self._add_keys(node, 'vrf')

    if 'voter' in node['roles']:
        self._add_keys(node, 'voting')

    return node

def create_accounts(self, amount, count, roles):
    return [self._create_account(
        amount,
        roles
    ) for _ in range(0, count)]

ネットワークジェネレータは以下のようなyamlファイルを出力します。他のノードもキーペアが違うだけで同じようなものなので、最初のノードだけ追加しています。

network: testnet
generation_hash_seed: 15609F28EE0E282ABF2105A5475313DF6FBACEA22B99EDD351672DB9351B5121
total_supply: 7842928625000000
currency_mosaic_id: '0x1BE3064212EBCB90'
epoch_adjustment: 1671429771s
nemesis:
  privatekey: D3BBBBA1D4BDF7834ED92D275E09E97A69DADCB98ACABB9404E3AB3D01688663
  publickey: F4885DF48A71E634D2E0F3BFBB54EF3E7FF3D73EFBB7C7F10E641F8AD2AA99F1
  address: TDDKGFAET5VIRX6CVJR46C3CJVTTOWYCFD3WA3I
nodes:
- host: host001.test.net
  friendly_name: host001
  mode: dual
  amount: 3000000000000
  roles:
  - harvester
  - voter
  main:
    privatekey: BEF08CEFF5A2A847FBDD2A39CA6F813D926EB41BAB93F331C9703ACB284EC336
    publickey: 42F8F92259ECF5F4D5BA3F0E99C532DDDB90CFEABFBFB5F92F2DB7B2D6B98CA5
    address: TCI6CN5WGZAVLQBJKYBIHRMIPPWWD5FZMAI54HA
    amount: 3000000000000
  remote:
    privatekey: 94A0D302C2C70BEE0A324612211E27026BDBAEE7BE5201E3680AC8D249DAAAC0
    publickey: C1FF6A475ABBD86CA6BFC0BE3B654BAA0DDC69F9BF7B30B908B277F2EC329D2D
    address: TBZT2JZPVHJKYN6YLM54B2V5HBSNAAG6IVI2EBI
  vrf:
    privatekey: 4C6E3D32B0FB727652FA3D2FC86D587162B997766D64DE9DB0DDFF69A5808DE5
    publickey: E802474E4576596B411954F7995A44F8957FC6B955CABA0A7C644006FC9DED8B
    address: TDDYZBZZM5LZGOVIHFFHVA7BDDI6HO4XZX7RYNY
  voting:
    privatekey: 6A0150B41D4A9E795E2493C9E80788873287E817A5B14E85FBA7CCC9ADFB874B
    publickey: C3BDF8672762AB4FF706590A4C05DDF4BCD38FA927A7BCC672921D11D1575FE5
    address: TCO3IMSPGS3IDEJZIVK4SKOU4KH7ZAUF6HOURVA
...
accounts:
- privatekey: 83D0155BCAD2BB2896B36C59DD6064277AEEEBDE785F673BB960A36A4D1612E3
  publickey: 37F826D581C1397F01290C8400B1DF4AA04EB54267A1F82B3596955FE2A3BFF5
  address: TAZUD3FGGMNQ4MRU6AIN6GSHU52DBJB2J5PD5EI
  amount: 100000000000
- privatekey: C68E1B16E347CF549524B72F5FD30E862233FF72CEC684A0A255311841528006
  publickey: B03BB1FB9F361EDCBF82195DE2AC45DB3AF6D8235944D77080C41E97686D6819
  address: TCDU332HMCQAWP3LIUIYL24D5EBUBXAWTKONEGQ
  amount: 100000000000
...

nemgenツールを実行してnemesisファイルを作成する前に、もう2つ必要なことがあります。

  1. nemgenの設定用プロパティファイル

  2. ノード設定に必要なトランザクションファイル(リモート、vrf、投票リンク)。

  3. Symbol ノードリソースフォルダの config-network.properties ファイルを更新します。

nemgenの設定ファイルを作成

Network.yaml には、nemgen の設定ファイルを作成するために必要なすべての情報が含まれています。以下のコードでは、Network yaml を configuration オブジェクトに読み込んでいます。このオブジェクトは nemgen 設定ファイルを生成するために使用されます。

	def save_nemesis_configuration(self, output_filepath):
		accounts = '\n'.join(
			[f'{account["main"]["address"]} = {format_number_single_quote(account["amount"])}' for account in self.configuration['nodes']])
		accounts += '\n'
		accounts += '\n'.join(
			[f'{account["address"]} = {format_number_single_quote(account["amount"])}' for account in self.configuration['accounts']])
		configuration = f'''
[nemesis]

networkIdentifier = {self.configuration['network']}
nemesisGenerationHashSeed = {self.configuration['generation_hash_seed']}
nemesisSignerPrivateKey = {self.configuration['nemesis']['privatekey']}

[cpp]

cppFileHeader =

[output]

cppFile =
binDirectory = ./seed

[transactions]
transactionsDirectory = ./transactions

[namespaces]

symbol = true
symbol.xym = true

[namespace>symbol]

duration = 0

[mosaics]

symbol:xym = true

[mosaic>symbol:xym]

divisibility = 6
duration = 0
supply = {format_number_single_quote(self.configuration['total_supply'])}
isTransferable = true
isSupplyMutable = false
isRestrictable = false

[distribution>symbol:xym]
{accounts}
		'''

		self._save_configuration_file(output_filepath, configuration)

これで、以下のようなnemgenの構成が出来上がります。ここでの主な注意点は、distributionセクション以下の全てのアドレスが、作成された各アカウントのメインキーペアであることです。これらのアドレスは、nemesisで指定された金額の転送トランザクションに変換されます。

[nemesis]

networkIdentifier = testnet
nemesisGenerationHashSeed = 15609F28EE0E282ABF2105A5475313DF6FBACEA22B99EDD351672DB9351B5121
nemesisSignerPrivateKey = D3BBBBA1D4BDF7834ED92D275E09E97A69DADCB98ACABB9404E3AB3D01688663

[cpp]

cppFileHeader =

[output]

cppFile =
binDirectory = ./seed

[transactions]
transactionsDirectory = ./transactions

[namespaces]

symbol = true
symbol.xym = true

[namespace>symbol]

duration = 0

[mosaics]

symbol:xym = true

[mosaic>symbol:xym]

divisibility = 6
duration = 0
supply = 7'842'928'625'000'000
isTransferable = true
isSupplyMutable = false
isRestrictable = false

[distribution>symbol:xym]
TCI6CN5WGZAVLQBJKYBIHRMIPPWWD5FZMAI54HA = 3'000'000'000'000
TARZLLBVHJZB3XYKDXMS6JOAAM645PR2NPL34TA = 3'000'000'000'000
TCHHP5GF2AGHCTRTPQUYVYZDMNZHEMMQ6ZX5JPI = 100'000'000'000
TAZUD3FGGMNQ4MRU6AIN6GSHU52DBJB2J5PD5EI = 100'000'000'000
TCDU332HMCQAWP3LIUIYL24D5EBUBXAWTKONEGQ = 100'000'000'000
TCTIOEMNEJAJXWF75QGGQV6JM4DAL6YENMOXG2Y = 100'000'000'000
TBVLDD5ZY4776JPF7AVJZRQYFPIKJDZJTXBKOLY = 100'000'000'000
TACPMJ7J5RASCMFQT4RCXXADHPCS5X7ICGHE5XA = 100'000'000'000
TDQBOFCN7P7NNKZKULQ4UMBXDNKGDW2VFJZH3FA = 3'000'000'000'000
TAJGUDFCC6UI2OAVVUFWICWR7FR5NHEQKQMRIDQ = 3'000'000'000'000
TD4ZRFEQABZP2DPXOKCKQYKGNIW5HPKTFV3LW4I = 3'000'000'000'000
TD5S6LJEF7S6JFB7APNDYZSUMMG7A3GHTA5NZNA = 3'000'000'000'000
TAQ6RQCME4DWVLOI4AWCLKEK5S5GLEAQLTHGZLA = 3'000'000'000'000
TC44DTLHL43XSCCAJR5QI6B4J35G3QQ4WWCJU2Y = 0
TCHVSKDSYAEPITMHBFECE7HF3Y65IACRPQDQHPQ = 0
TAYKKEU6VR7IXQV536VQZLULI6FWSUF5G65UMDQ = 0
TBMQUG2N7B4DU7G7VUDTDL6EZTPNNXHASFCBG6I = 7'821'328'625'000'000

注:transactionsDirectoryは、ノードを設定するための追加トランザクションが格納されるフォルダーです。

ノードの設定トランザクションの作成

各ノードをセットアップするのに必要なすべての追加トランザクション(リモート、vrf、投票)を作成し、それらをtransactionsDirectoryに配置しましょう。ここでのトリックは、すべてのトランザクションが新しいネットワークの生成ハッシュシードで署名される必要があることである。Symbolのためのファサードを作成した後、ネットワーク情報を更新する必要がある。

self.facade = SymbolFacade(self.configuration['network'])
self.facade.network = Network(
    'testnet',
    0x98,
    Hash256(self.configuration['generation_hash_seed'])
)

SymbolFacadeが作成され、新しいネットワーク世代のハッシュシードを使用するように更新されたので、ネメシス用の追加トランザクションを作成することは、Symbolブロックチェーンに現在のノードを設定することと変わりません。
注:ネメシスでのトランザクションの場合、期限は1、手数料は0です。

def _create_vrf_transaction(self, signer_public_key, account_descriptor):
    vrf_key_pair = self._get_key_pair_from_private_key(account_descriptor['vrf']['privatekey'])
    return self.facade.transaction_factory.create({
        'signer_public_key': signer_public_key,
        'deadline': 1,
        'type': 'vrf_key_link_transaction',
        'linked_public_key': vrf_key_pair.public_key,
        'link_action': 'link'
    })

def _create_account_key_link_transaction(self, signer_public_key, account_descriptor):
    remote_key_pair = self._get_key_pair_from_private_key(account_descriptor['remote']['privatekey'])
    return self.facade.transaction_factory.create({
        'signer_public_key': signer_public_key,
        'deadline': 1,
        'type': 'account_key_link_transaction',
        'linked_public_key': remote_key_pair.public_key,
        'link_action': 'link'
    })

def _create_voting_key_link_transaction(self, signer_public_key, account_descriptor):
    voting_key_pair = self._get_key_pair_from_private_key(account_descriptor['voting']['privatekey'])
    return self.facade.transaction_factory.create({
        'signer_public_key': signer_public_key,
        'deadline': 1,
        'type': 'voting_key_link_transaction',
        'linked_public_key': voting_key_pair.public_key,
        'start_epoch': 1,
        'end_epoch': 720,
        'link_action': 'link'
    })

def _create_transaction(self, transaction_type, account_descriptor, output_path):
    signer_key_pair = self._get_key_pair_from_private_key(account_descriptor['main']['privatekey'])
    transaction_factory = {
        'vrf': self._create_vrf_transaction,
        'remote': self._create_account_key_link_transaction,
        'voting': self._create_voting_key_link_transaction
    }

    transaction = transaction_factory[transaction_type](signer_key_pair.public_key, account_descriptor)
    transaction.fee = Amount(0)
    signature = self.facade.sign_transaction(signer_key_pair, transaction)
    self.facade.transaction_factory.attach_signature(transaction, signature)
    transaction_buffer = transaction.serialize()
    transaction_hash = self.facade.hash_transaction(transaction)
    file_path = Path(output_path) / f'{transaction_type}_{transaction_hash}.bin'
    self._save_transaction_file(file_path, transaction_buffer)

def create_nemesis_transactions(self, output_path):
    mkdirs(output_path)
    for node_descriptor in self.configuration['nodes']:
        if 'harvester' in node_descriptor['roles']:
            self._create_transaction('vrf', node_descriptor, output_path)
            self._create_transaction('remote', node_descriptor, output_path)

        if 'voter' in node_descriptor['roles']:
            self._create_transaction('voting', node_descriptor, output_path)

Symbolノードのリソースを更新する

nemgenを実行する準備はほぼ整いました。最後のステップは、Symbolノードのリソースを更新することです。新しい testnet ネットワークの情報と一致するように config-network.properties ファイルを更新するパッチャーを作成する予定です。

config = RawConfigParser(comment_prefixes=None, empty_lines_in_values=False, allow_no_value=True)

config.optionxform = lambda option: option
filename = f'{resources_folder}/resources/config-network.properties'
config.read(filename)

config['network']['identifier'] = account_config['network']
config['network']['nemesisSignerPublicKey'] = account_config['nemesis']['publickey']
config['network']['generationHashSeed'] = account_config['generation_hash_seed']
config['network']['epochAdjustment'] = account_config['epoch_adjustment']

config['chain']['currencyMosaicId'] = account_config['currency_mosaic_id']
config['chain']['harvestingMosaicId'] = account_config['currency_mosaic_id']
config['chain']['initialCurrencyAtomicUnits'] = format_number_single_quote(account_config['total_supply'])
config['chain']['totalChainImportance'] = format_number_single_quote(account_config['total_supply'])

harvest_address = get_account_address(get_account_with_role(account_config, 'harvest_network_fee_sink'))
config['chain']['harvestNetworkFeeSinkAddressV1'] = harvest_address
config['chain']['harvestNetworkFeeSinkAddress'] = harvest_address

mosaic_address = get_account_address(get_account_with_role(account_config, 'mosaic_rental_fee_sink'))
config['plugin:catapult.plugins.mosaic']['mosaicRentalFeeSinkAddressV1'] = mosaic_address
config['plugin:catapult.plugins.mosaic']['mosaicRentalFeeSinkAddress'] = mosaic_address

namespace_address = get_account_address(get_account_with_role(account_config, 'namespace_rental_fee_sink'))
config['plugin:catapult.plugins.namespace']['namespaceRentalFeeSinkAddressV1'] = namespace_address
config['plugin:catapult.plugins.namespace']['namespaceRentalFeeSinkAddress'] = namespace_address

print(f'update network file: {filename}')
with open(filename, 'wt', encoding='utf8') as output_file:
    config.write(output_file)

注意:新しいテストネットのために更新が必要なのは、ネットワーク設定だけです。他のプロパティファイルの値を変更したい場合は、それが有効である限り、問題ありません。

ネメシスシードの作成実績

これで、nemgenツールを実行する準備ができました。catapultクライアントコードをゼロからビルドした場合、binフォルダにnemgenがあります。私は簡単な方法で、最新のdockerイメージからツールを実行することにします。

@staticmethod
def generate_seed(output_path, config_path):
    cmd = [
        'docker',
        'run',
        '--rm',
        f'--user={os.geteuid()}:{os.getgid()}',
        f'--volume={output_path}:/output',
        f'--volume={config_path}:/config',
        '-w=/output',
        '-e=LD_LIBRARY_PATH=/usr/catapult/lib:/usr/catapult/deps',
        'symbolplatform/symbol-server:gcc-1.0.3.5',
        '/usr/catapult/bin/catapult.tools.nemgen',
        '--resources=/config',
        '--nemesisProperties=/output/block-properties-file.properties',
        '--useTemporaryCacheDatabase'
    ]

    dispatch_subprocess(cmd)

これが完了すると、outputフォルダにnemesisのシードがあるはずです。これはテストネットワークをブートストラップするために使用できます。

次はどうする?

これで、テストネットワークをブートストラップするための3つのノードが揃いました。

各ノードには

  1. メインアカウント

  2. ハーベストまたは投票を可能にするネメシスでリンクされたトランザクション

あとは、各ノードの設定を生成するだけです。これを行うツールはshoestringのようにすでにあるので、ここでは詳細を説明しません。しかし、ステップのアイデアを与えるために

  1. 各ノード用のキーを使って各ノード用の設定を生成する

  2. 必要な投票ファイルの作成

  3. ピアp2pとapiのjsonファイル作成

  4. 各ノードに全てコピーして起動

テストネットは数秒で立ち上がるはずです。http://localhost:3000 に移動して確認することができます。

この記事が気に入ったらサポートをしてみませんか?