19
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 3 years have passed since last update.

nemAdvent Calendar 2021

Day 11

アグリゲート自動連署botでトランザクションをチェックする

Last updated at Posted at 2021-12-10

#連署botでトランザクションの確認を行う
Symbolでサービスを展開する場合,トランザクションをシステムが発行し,リアルタイムに処理を行えるようにする必要があります.この際,セキュリティを高めるため手段の一つとしてアグリゲートボンデッドを利用して複数のシステムの承認を得てからトランザクションを実行させる手段があります.しかし,人間がトランザクションを一つ一つチェックするのは大変です.そこで今回は簡単な自動連署botを作成してみました.

#トランザクションへの連署
##まず,連署してみる
まず,トランザクションに連署してみましょう.変数aggregateTxにアグリゲートトランザクションがある場合,連署は次のように行います.

const xym = require('symbol-sdk');
const node = 'https://sym-test-09.opening-line.jp:3001';
const repo = new xym.RepositoryFactoryHttp(node);
const transactionHttp = repo.createTransactionRepository();
const account = xym.Account.createFromPrivateKey(
    '*******',
    xym.NetworkType.TEST_NET
);
const cosignatureTx = xym.CosignatureTransaction.create(aggregateTx);//連署TXを作る
const signedTx = account.signCosignatureTransaction(cosignatureTx);

アグリゲートボンデッドの場合,次のようにして連署をネットワークにアナウンスできます.コンプリートの場合はトランザクション作成者に何らかの手段で返してあげる必要があります.

transactionHttp.announceAggregateBondedCosignature(signedTx).subscribe(//通常のアナウンスとは異なるので注意
      (x) => console.log(x),
      (er) => console.error(er),
);

##ネットワークからからアグリゲートトランザクションを取得して連署する
アグリゲートボンデッドの場合は連署するためのトランザクションをネットワークから取得できネットワークから取得したトランザクションに連署してみましょう.

const transactions = await transactionHttp.search({ address: account.address, group: xym.TransactionGroup.Partial }).toPromise();
for (var data of transactions.data) {
    const cosignatureTx = xym.CosignatureTransaction.create(data);
    const signedTx = account.signCosignatureTransaction(cosignatureTx);
    transactionHttp.announceAggregateBondedCosignature(signedTx).subscribe(
            (x) => console.log(x),
            (er) => console.error(er),
    );
}

これでアグリゲートボンデッドの連署ができました.ブロック生成を監視していつでも連署を行えるようにします.

注意:以下のプログラムは全てのパーシャルトランザクションに署名します.スパムアグリゲートボンデッドの署名を防ぐため,絶対にメインネットで使わないでください.

const xym = require('symbol-sdk');
const WebSocket = require('ws');

const node = 'https://sym-test-09.opening-line.jp:3001';
const nsHttp = new xym.NamespaceHttp(node);
const wsEndpoint = node.replace('http', 'ws') + "/ws";
const listener = new xym.Listener(wsEndpoint,nsHttp,WebSocket);
const repo = new xym.RepositoryFactoryHttp(node);
const transactionHttp = repo.createTransactionRepository();
const cosigAccount = xym.Account.createFromPrivateKey(
    '*******',
    xym.NetworkType.TEST_NET
);
websocketConnection();
async function signAllPartialTransaction(account){//partial状態であれば全て署名するので注意
    const transactions = await transactionHttp.search({ address: account.address, group: xym.TransactionGroup.Partial }).toPromise();
    txs: for (var data of transactions.data) {
        for(signature of data.cosignatures){
            if(signature.signer.address.address === account.address.address)break txs;
        }
        const cosignatureTx = xym.CosignatureTransaction.create(data);
        const signedTx = account.signCosignatureTransaction(cosignatureTx);
        transactionHttp.announceAggregateBondedCosignature(signedTx).subscribe(
            (x) => console.log(x),
            (er) => console.error(er),
        );
    }
}
function websocketConnection(){
listener.open().then(() => {
    listener.newBlock()
    .subscribe(block =>{
        console.log("ブロック高: "+block.height);
        signAllPartialTransaction(cosigAccount);
        
    },err=>{
      console.log("失敗しました");
    });

});
}

これで自動連署botが完成しました.あとはトランザクションの内容を確認して署名するように改良しましょう.

#トランザクションをチェックする

注意 実際に運用する際は更なる検証が必要な場合があります.また,プログラムに抜けやバグがある場合があります.このプログラムを使用して発生した損害については責任を負いません.

今回は2of2のマルチシグアドレスから特定のアドレスに100XYMの送金が行うトランザクションがあるとして,片方の連署者が生成し,これを確認して自動で連署するとします.

必要な条件をまとめてみます
・宛先が特定のアドレスである
・100XYMの送信元がマルチシグアドレスである
・トランザクションの作成者がマルチシグの連署者である
・100XYMの送金である
・関係のないトランザクションが含まれていない

マルチシグのように,トランザクションの作成者が決まっている場合は連署者であるか確認を行うだけで殆どの悪意のあるアグリゲートボンデッドから守ることができます.ただし,マルチシグの連署者の流出に備えて他の条件も必要になります.

async function checkTx(account, page = 1){
  const transactions = await transactionHttp.search({ 
      address: account.address, 
      group: xym.TransactionGroup.Partial ,
      pageNumber:page,
      pageSize:20
  }).toPromise();
  if(transactions.isLastPage === false )matchTx(account,page+1);
  console.log("pages",page);
  for (var tx of transactions.data) {
      if(tx.signer.address.address === "TDYUNBSZGVIP5AJPLYPS3SIGVJ3COXTHXLAQE4A"){//TXの作成者は連署者か
          const aggTx = await transactionHttp.getTransactionsById([tx.transactionInfo.hash],xym.TransactionGroup.Partial).toPromise();
          const innerTransferTx = aggTx[0].innerTransactions[0];
          if(aggTx[0].innerTransactions.length === 1 &&
            innerTransferTx.type === xym.TransactionType.TRANSFER &&
            innerTransferTx.mosaics[0]?.id.toHex() === currencyMosaicId.toHex() &&
            innerTransferTx.mosaics[0]?.amount.compact() === 10*1000000 &&
            innerTransferTx.mosaics?.length === 1 &&
            innerTransferTx.recipientAddress.address === receiptAddress.address &&
            innerTransferTx.signer.address.address  === vaultPublicAccount.address.address
          ){
              console.log("confirm");
              const cosignatureTx = xym.CosignatureTransaction.create(tx);
              const signedTx = account.signCosignatureTransaction(cosignatureTx);
              transactionHttp.announceAggregateBondedCosignature(signedTx).subscribe(
                  (x) => console.log(x),
                  (er) => console.error(er),
              );
          }else{
              console.log("invalid tx");
              
          }
      }else{
          console.log("invalid signer");
      }

  }
}

あとはこの判定を行うbotをアグリゲートボンデッドを作成する場所と切り離された安全な環境で動作させることで,100XYMを特定のアドレスにしか送信できないように制限することができます.以下にアグリゲートボンデッドのアナウンスから連署までを一括実行するサンプルプログラムを置いて置きます.連署の条件を変更したりして,条件にマッチしないと動作しないことをご確認下さい.

const xym = require('symbol-sdk');
const WebSocket = require('ws');
var networkGenerationHash ='';
var epochAdjustment = '';
var repo;
var transactionHttp;
var receiptRepo;
var currencyMosaicId;
var node;

var networkType =  xym.NetworkType.TEST_NET;
var feemultiplier;

var nsHttp;
var wsEndpoint;
var listener;
var transactionService;


console.log("***************************************************************");
console.log(Date());
console.log("***************************************************************");


const txMakerAccount = xym.Account.createFromPrivateKey(
  '***',
  networkType,
);

const autoCosigAccount = xym.Account.createFromPrivateKey(
    '***',
    networkType,
);

const vaultPublicAccount = xym.PublicAccount.createFromPublicKey(
    '***',
    networkType,
);

const receiptAddress = xym.Address.createFromRawAddress(
    '***',
    networkType,
);

(async()=>{

    node = 'https://sym-test-09.opening-line.jp:3001';
    nsHttp = new xym.NamespaceHttp(node);
    wsEndpoint = node.replace('http', 'ws') + "/ws";
    listener = new xym.Listener(wsEndpoint,nsHttp,WebSocket);
    repo = new xym.RepositoryFactoryHttp(node);
    transactionHttp = repo.createTransactionRepository();
    receiptRepo = repo.createReceiptRepository();
    transactionStatusRepo = repo.createTransactionStatusRepository();
    transactionService = new xym.TransactionService(transactionHttp,receiptRepo);
    websocketConnection();
    await getInfo();
    const aggTx = await createAggregateBonded(txMakerAccount);
    const hashLockTx = createHashLock(aggTx,txMakerAccount);
    const res = await transactionService.announceHashLockAggregateBonded(hashLockTx,aggTx,listener).toPromise();
    console.log(res);
    
})();

async function checkPartialTx(account, page = 1){
  const transactions = await transactionHttp.search({ 
      address: account.address, 
      group: xym.TransactionGroup.Partial ,
      pageNumber:page,
      pageSize:20
  }).toPromise();
  if(transactions.isLastPage === false )checkPartialTx(account,page+1);
  console.log("pages",page);
  for (var tx of transactions.data) {
      if(tx.signer.address.address === txMakerAccount.address.address){//作成者確認
          const aggTx = await transactionHttp.getTransactionsById(
              [tx.transactionInfo.hash],
              xym.TransactionGroup.Partial
              ).toPromise();
          const innerTransferTx = aggTx[0].innerTransactions[0];
          if(aggTx[0].innerTransactions.length === 1 &&//内部トランザクションの個数確認
            innerTransferTx.type === xym.TransactionType.TRANSFER &&//トランザクションの種類を確認
            innerTransferTx.mosaics[0]?.id.toHex() === currencyMosaicId.toHex() &&//モザイクIDを確認
            innerTransferTx.mosaics[0]?.amount.compact() === 10*1000000 &&//送金量を確認
            innerTransferTx.mosaics?.length === 1 &&//送金モザイクの数を確認
            innerTransferTx.recipientAddress.address === receiptAddress.address &&//受け取り先確認
            innerTransferTx.signer.address.address  === vaultPublicAccount.address.address//送信元確認
          ){
              console.log("confirm");//全ての条件を満たした場合
              const cosignatureTx = xym.CosignatureTransaction.create(tx);
              const signedTx = account.signCosignatureTransaction(cosignatureTx);
              transactionHttp.announceAggregateBondedCosignature(signedTx).subscribe(
                  (x) => console.log(x),
                  (er) => console.error(er),
              );
          }else{
              console.log("invalid tx");
              
          }
      }else{
          console.log("invalid signer");
          console.log(tx.signer.address);
      }

  }

}


function websocketConnection(){
listener.open().then(() => {
    listener.newBlock()
    .subscribe(block =>{
        console.log("ブロック高: "+block.height);
        checkPartialTx(autoCosigAccount,1);//毎ブロック確認させる
        
    },err=>{
      console.log("失敗しました");
    });

});
}

async function createAggregateBonded(account){

  const xymMosaic = new xym.Mosaic(
    currencyMosaicId,
    xym.UInt64.fromUint(10 * 1000000)//送金量*10^(可分性)(xymは6)
  );

    const vaulttoGetTx = xym.TransferTransaction.create(
    xym.Deadline.create(epochAdjustment),
    receiptAddress,//受取人アドレス
    [xymMosaic],//モザイク
    xym.PlainMessage.create('xym'),//メッセージ
    networkType,//ネットワークタイプ
  );

  const aggregateTx = xym.AggregateTransaction.createBonded(
    xym.Deadline.create(epochAdjustment,48),//有効期限,アグリゲートボンデッドは2日後まで受け入れられる
    [
        vaulttoGetTx.toAggregate(vaultPublicAccount),
    ],
    networkType,
    [],
).setMaxFeeForAggregate(100,1);

  const signedTx = account.sign(
      aggregateTx,
      networkGenerationHash
      );//payloadを作成する

return signedTx;
}


function createHashLock(signedTx,account){
    const lockXym = new xym.Mosaic(
        currencyMosaicId,
        xym.UInt64.fromUint(10 * 1000000)//10XYMロック
    );
    const hashLockTx = xym.HashLockTransaction.create(
        xym.Deadline.create(epochAdjustment),
        lockXym,
        xym.UInt64.fromUint(2 * 2880),//最大2日(2*2880ブロック)留め置きできる
        signedTx,
        networkType,
    ).setMaxFee(feemultiplier);

    const signed = account.sign(hashLockTx,networkGenerationHash);

    return signed;
}

async function getInfo(){
  feemultiplier = 100;
  epochAdjustment = await repo.getEpochAdjustment().toPromise();
  networkGenerationHash = await repo.getGenerationHash().toPromise();
  currencyMosaicId = new xym.MosaicId(
      (await repo.createNetworkRepository()
      .getNetworkProperties().toPromise())
      .chain.currencyMosaicId.replace(/0x/,"")
      .replace(/'/g,"")
      );
}

#連署botが使えそうな場面
・アドレスの1日の送金量を制限したい場合
・特定の時間にのみトランザクションを承認する場合(営業時間のみ署名のような)
・特定アドレスへの送金を禁止したい場合
・従業員の作成するトランザクションを制御したい場合
・不審なアグリゲートボンデッドに誤って署名した場合の砦
など....
結構使える場面があると思います.連署bot自体は0XYM(手数料不要)で可能なため,空の状態で動作させるのが良いと思います.万一連署botの秘密鍵や署名権が盗まれたとしても,もう一方の連署アカウントが盗まれなければ被害を被ることはありません.連署botを複数かつ環境を全て別の場所に置くことでセキュリティを向上させるといった使い方もできると思います.

19
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
19
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?