14
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

nem / symbolAdvent Calendar 2023

Day 21

誰か勝手に投げてくれ

Last updated at Posted at 2023-12-20

アグリゲートボンデッド,正直使いづらくないですか

Symbolを利用されている人であれば主にマルチシグでアグリゲートボンデット(以下アグボン)を利用されている方は少なくないと思います。アグボンを投げるためには以下の手順を踏む必要があります。

  1. ハッシュロック,アナウンス
  2. ハッシュロックの承認を待つ
  3. ハッシュロックが承認されたらアグボンをアナウンスする
  4. アグボンの承認を待つ
  5. 連署する
  6. 連署を全部集めれば完了

更にこれをデスクトップウォレットでアグボンを発行する場合,1-3の手順を行っている間に次のことが要求されます。条件を満たさなかった場合。アグボンのアナウンスに失敗してハッシュロック分の10XYMを失います。

  • 1-3が完了するまでアプリを閉じないこと
  • 1-3が完了するまでネットの接続を切らないこと
  • トランザクションが正しく伝搬するノードに接続すること

10XYMを失う可能性があることはなんとなくわかっていただけた思います。少なくとも私は今までにアグボンのアナウンスを1-2回失敗し,ハッシュロック分を持っていかれた経験があります,とてもつらいですね。

使いやすくしよう

使いにくい理由は言うまでもなく,1-3において承認待ちまで待機することを必要としている点にあります。要はクライアントがノードにハッシュロックとアグボンを両方同時に投げた後。離脱できれば良いのです。がチェーンの仕様上そんなことはできません。そこで,クライアントがアグボンの送付から逃れる手法を3つほど考えてみました

  1. ハッシュロックとアグボンを代理botに送信する
  2. ノードそのものにアグボンの代理送付機能を付加する
  3. アグボンのペイロードを公開し,誰かに勝手に投げてもらう

1. ハッシュロックとアグボンを代理botに送信する

ウォレット側でなく,別のサーバーに立てたにてアグボンを投げる方法です。

今回はPythonV3を使ってみたかったのでPythonで書いてみました(そのおかげで結構雑なところが多いですが。。。)今回はトランザクションハッシュとペイロードを両方送付していますが,本来はペイロードからハッシュを生成できるはずなので不要では無いかと思います。もしよければ改善してみてください。

botにアグボンとハッシュロックを投げる

from symbolchain.facade.SymbolFacade import SymbolFacade
from symbolchain.symbol.KeyPair import KeyPair, PrivateKey
from binascii import unhexlify
import datetime, hashlib
import requests
import json
import math


EPOCH_ADJUSTMENT = 1667250467
NAMESPACE_FLAG = 1 << 63
PRIVATE_KEY = "***"

unhex_privatekey = unhexlify(PRIVATE_KEY)
privatekey = PrivateKey(unhex_privatekey)
key_pair = KeyPair(privatekey)
deadline = (int((datetime.datetime.today() + datetime.timedelta(hours=2)).timestamp()) - EPOCH_ADJUSTMENT) * 1000
agg_deadline = (int((datetime.datetime.today() + datetime.timedelta(hours=47)).timestamp()) - EPOCH_ADJUSTMENT) * 1000

def generate_mosaic_alias_id(fully_qualified_name):
    """Generates a mosaic id from a fully qualified mosaic alias name."""
    return generate_namespace_path(fully_qualified_name)[-1]


def is_valid_namespace_name(name):
    """Returns true if a name is a valid namespace name."""
    def is_alphanum(character):
        return 'a' <= character <= 'z' or '0' <= character <= '9'

    return name and is_alphanum(name[0]) and all(is_alphanum(ch) or ch in ['_', '-'] for ch in name)


def generate_namespace_path(fully_qualified_name):
    """Parses a fully qualified namespace name into a path."""
    path = []
    parent_namespace_id = 0
    for name in fully_qualified_name.split('.'):
        if not is_valid_namespace_name(name):
            raise ValueError(f'fully qualified name is invalid due to invalid part name ({fully_qualified_name})')

        path.append(generate_namespace_id(name, parent_namespace_id))
        parent_namespace_id = path[-1]

    return path

def generate_namespace_id(name, parent_namespace_id=0):
    """Generates a namespace id from a name and an optional parent namespace id."""
    hasher = hashlib.sha3_256()
    hasher.update(parent_namespace_id.to_bytes(8, 'little'))
    hasher.update(name.encode('utf8'))
    digest = hasher.digest()

    result = int.from_bytes(digest[0:8], 'little')
    return result | NAMESPACE_FLAG

def add_embedded_transfers(facade, public_key):
    # obtain recipient from public_key, so direct all transfers to 'self'
    embedded_transactions = []
    recipient_address = facade.network.public_key_to_address(public_key)
    for i in range(2):
        message = "hoge"
        embedded_transaction = facade.transaction_factory.create_embedded({
            'type': 'transfer_transaction_v1',
            'signer_public_key': public_key,
            'recipient_address': recipient_address,
            'message': bytes(1) + message.encode('utf8'),
            
        })

        embedded_transactions.append(embedded_transaction)

    return embedded_transactions


def main():

    facade = SymbolFacade('testnet')

    embedded_transactions = add_embedded_transfers(facade, key_pair.public_key)
    merkle_hash = facade.hash_embedded_transactions(embedded_transactions)
    aggregate_transaction = facade.transaction_factory.create({
        'type': 'aggregate_bonded_transaction_v2',
        'signer_public_key': key_pair.public_key,
        'fee':  int(math.pow(10, 6)*1),
        'deadline': agg_deadline,
        'transactions_hash': merkle_hash,
        'transactions': embedded_transactions
    })
    

    signature = facade.sign_transaction(key_pair, aggregate_transaction)
    payload = facade.transaction_factory.attach_signature(aggregate_transaction, signature)

    bonded_hash = facade.hash_transaction(aggregate_transaction)
    hash_transaction = facade.transaction_factory.create({
        'signer_public_key': key_pair.public_key,
        'deadline': deadline,

        'type': 'hash_lock_transaction_v1',
        'mosaic': {'mosaic_id': generate_mosaic_alias_id('symbol.xym'), 'amount': 10000000},
        'fee': int(math.pow(10, 6)*1),
        'duration': 100,
        'hash': bonded_hash
    })
    
    hash_signature = facade.sign_transaction(key_pair, hash_transaction)
    hash_payload = facade.transaction_factory.attach_signature(hash_transaction, hash_signature)
    print(f'HASHLOCK_Hash: {facade.hash_transaction(hash_transaction)}\n')
    print(f'AGGREGATE_Hash: {facade.hash_transaction(aggregate_transaction)}\n')

    hash_lock = json.loads(hash_payload)
    aggregate = json.loads(payload)

    req = {
        "hashLock": {
            "payload":hash_lock,
            "hash":str(facade.hash_transaction(hash_transaction))
        },
        "aggregate":{
            "payload":aggregate,
            "hash":str(facade.hash_transaction(aggregate_transaction))
        }
    }

    data = json.dumps(req)
    headers = {'content-type': 'application/json'}
    res =  requests.post("http://localhost:8080", data= data, headers=headers)
    print(res)
    print(res.text)

if __name__ == '__main__':
    main()

ハッシュロックをアナウンスし,結果を返す
main.py

import json
from http.server import BaseHTTPRequestHandler, HTTPServer
import requests
import subprocess

NODE = "https://201-sai-dual.symboltest.net:3001"

class BondedHandler(BaseHTTPRequestHandler):
    
    def do_POST(self):
        err = []
        try:
            content_len=int(self.headers.get('content-length'))
            body = json.loads(self.rfile.read(content_len).decode('utf-8'))
            if not "hashLock"in body:
                err.append("hashLockは必須です")
            if not "aggregate" in body:
                err.append("aggregateは必須です")
            

            if len(err) == 0:
                print("err = 0")
                hashLock_payload = body["hashLock"]["payload"]
                hashLock_txhash = body["hashLock"]["hash"]
                aggregate_payload = body["aggregate"]["payload"]
                aggregate_txhash = body["aggregate"]["hash"]
                
                headers = {'content-type': 'application/json'}
                data = json.dumps(hashLock_payload)
                hash_req =  requests.put(NODE+"/transactions", data=data, headers=headers)
                response = hash_req.json()
                self.send_response(hash_req.status_code)
                self.send_header('Content-type', 'application/json')
                self.end_headers()
                responseBody = json.dumps(response)
                self.wfile.write(responseBody.encode('utf-8'))
                
                command = ["python","call.py",hashLock_txhash, json.dumps(aggregate_payload)]
                subprocess.Popen(command)

            else:
                raise ValueError("invalid param") 
        except Exception as e:
            print("An error occured")
            print(e)
            response = { 'status' : 500,
                         'msg' : 'An error occured' }

            self.send_response(500)
            self.send_header('Content-type', 'application/json')
            self.end_headers()
            responseBody = json.dumps(response)

            self.wfile.write(responseBody.encode('utf-8'))


def main(server_class=HTTPServer, handler_class=BondedHandler, server_name='localhost', port=8080):
    server = server_class((server_name, port), handler_class)
    server.serve_forever()

if __name__ == '__main__':
    main()

ハッシュロックの承認を待ち,アグボンを投げる
call.py

import sys
from time import sleep
import json
import requests
MAX_RETRY = 20
NODE = "https://201-sai-dual.symboltest.net:3001"

headers = {'content-type': 'application/json'}
lock_hash, aggregate_payload = sys.argv[1], sys.argv[2]

for i in range(MAX_RETRY):
    confirm_req = requests.get(NODE+"/transactionStatus/"+lock_hash)
    confirm_res = confirm_req.json()
    if confirm_req.status_code == 200:
        if confirm_res["group"] == "confirmed":
            agg_req = requests.put(NODE+"/transactions", data=aggregate_payload, headers=headers)
            print("agg_res", agg_req.text)
            break
    elif confirm_req.status_code == 404:
        continue
    else:
        break
    if i == MAX_RETRY - 1:
        print("timeout lock_txhash:", lock_hash)
    sleep(15)

試してないので分からないのですが,AWSのLambdaなんかにこのサービスが立てられるのであれば結構いい感じになるような気がします。仮にbotが落ちたとしても,ペイロードを保存しておく機能のような物があればあとから投げ直すことができるので良いのでは無いかと思います。

2, ノードそのものにアグボンの代理送付機能を付与する

1.の手法をノードそのものに持たせてしまう手法です。アナウンス先のノードでbotを稼働させるだけで完結します。アグボンの承認期間中にノードのサーバがダウンしない限りはアナウンスが通ると思います。今回はPythonで書いてしまったのでRESTとの統合はできないのですが,JSで書き直してRESTに新たなエンドポイントを1個生やしてもいいのでは無いかと思います(RESTの負荷は心配ですが。。。)
1と中身は同じなのでコードは省略です。

3. アグボンのペイロードを公開し,誰かに勝手に投げてもらう

そもそも,アグボンはハッシュロックの成立後に公開するトランザクションであるので誰かが勝手にアナウンスしてくれればよいのです。以下のようにして,アグボンのpayloadをrawmessageとして送信し,誰でもアグボンをアナウンスできる環境を作ります。

    recipient_address = facade.network.public_key_to_address(key_pair.public_key)
    
    ag_t = facade.transaction_factory.create({
        'type': 'transfer_transaction_v1',
        'signer_public_key': key_pair.public_key,
        'recipient_address': recipient_address,
        'message': unhexlify(aggregate["payload"]),
        'fee': int(math.pow(10, 6)*1),
        'deadline':deadline
    })

    signature = facade.sign_transaction(key_pair, ag_t)
    ag_t_payload = facade.transaction_factory.attach_signature(ag_t, signature)

    ag_t_req =  requests.put(NODE+"/transactions", data=ag_t_payload, headers=headers)
    res = ag_t_req.json()
    print(f'AG_t_Hash: {facade.hash_transaction(ag_t)}\n')
    print(res)

あとはハッシュロックとこのトランザクションを投げるだけでアグボンのアナウンスを実現することができます。誰かがネットワーク上で承認済みトランザクションを監視し,ペイロードを投げるbotを稼働させるだけでアグボンの呪いから開放されるのです。アグボンの中に,botへのお礼のXYMなんかを入れてあげるとより確実性が上がるかもしれません。

ただ,現在のノードの仕様として, アグボンをハッシュロックの承認前にノードにアナウンスすると,ハッシュロック未承認としてエラーになり,そのノードにはハッシュロック承認後も有効なアグボンをアナウンスできなくなるようになっていた記憶があります。つまり,承認前にネットワーク上の全ノードに対して未承認のアグボンを送付することでアグボンの成立を阻害してしまうような妨害が可能になってしまうかもしれません。

この妨害はハッシュロックと署名済みペイロードをアグリゲートコンプリートとして集約してアナウンスすることで防御が可能です。ハッシュロックとペイロードを同タイミングで承認させることも可能です。この場合,トランザクションが承認された時点でハッシュロックが承認済みになっていることが保証されているため,ペイロードを投げると確実にアナウンスすることができます。(記事を書き上げた後に気づいたのでコードは省略させていただきます)

(追記)未承認TXとして承認される前に公開されるので防御できないことにあとから気づきました。信頼できるbotのみが解読できる暗号化メッセージで送信するべきですね。誰かに投げさせるって難しい...

おわりに

正直アグボンが成立するまでアプリを稼働させ続けるのはきついです。1回のリクエストだけで完結できるよう,誰かなんとかしてください。あとハッシュロックに失敗して消えたアグボンのXYMを返してください...

14
5
1

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
14
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?