Solidityのecrecoverの動作確認

IDとか署名とか調べていくなかで、メッセージと署名から、署名したアカウントのアドレスを復元するといった、基本的なタイプの署名について実装してみました。solidity関数のecrecoverの使い方を確認しています。

  1. メッセージどアドレスから署名を生成します (server.js)
  2. 生成したメッセージと署名を使い、スマートコントラクトの関数を呼ぶ (server.js)
  3. アカウントのアドレスを回復 (Verifier.sol)

署名については、dApp側で行うことを想定して、上記の通りJavaScriptとしてserver.js内で実装しています。復元についてはスマートコントラクト側で確認することを想定しています。コードはGitHubで公開しています。

動作確認は、当該フォルダ"lessons/signature"にて、

$ truffle develop
truffle (develop)> migrate --reset

としてローカルのチェーンを立ち上げた後、

npm run server

として行えます。

今回はテスト用のサーバー側スクリプトとして、以下のものを作成しました。

import Verifier from '../build/contracts/Verifier.json';
import Config from './config.json';
import Web3 from 'web3';

import express from 'express';
import { toHex } from 'web3-utils';

let config = Config['localhost'];
console.log(config);
let web3 = new Web3(new Web3.providers.HttpProvider(config.url));
let verifier = new web3.eth.Contract(Verifier.abi, config.appAddress);
let msg = 'Hello, signature';
let hash = web3.utils.sha3(msg);

console.log("message: " + msg);
console.log("message hash: " + hash);

web3.eth.getAccounts(async (error, accounts) => {
  let signature = await web3.eth.sign(hash, accounts[0]);
  console.log("account: " + accounts[0]);
  signature = signature.substr(2); //remove 0x
  const r = '0x' + signature.slice(0, 64);
  const s = '0x' + signature.slice(64, 128);
  let v = '0x' + signature.slice(128, 130);
  if (v == '0x00') {
    v = '0x1b';
  } else if (v1 == '0x01') {
    v = '0x1c';
  }

console.log("Signature");
console.log("r: " + r);
console.log("s: " + s);
console.log("v: " + v);
const v_decimal = web3.utils.toDecimal(v);
console.log("v_decimal: " + v_decimal);
let a = await verifier.methods.recoverAddr(hash, v, r, s).send({
  from: accounts[0],
  gas: 4712388,
  gasPrice: 100000000000
});
console.log(a);
});

const app = express();
app.get('/api', (req, res) => {
  res.send({
    message: 'An API for use with your Dapp!'
  })
})

export default app;

また、アカウントアドレスの復元を行うスマートコントラクトとして、以下を実装しています。
セキュリティ対策なども削ぎ落とした、機能確認のためだけの最小限のコードにしています。

pragma solidity >=0.5.0 <0.6.0;
contract Verifier {
    event evRecoveredAddress(address);
    function recoverAddr(
            bytes32 msgHash,
            uint8 v,
            bytes32 r,
            bytes32 s)
        public
        returns (address) {
        bytes memory garbagePrefix = "\x19Ethereum Signed Message:\n32";
        bytes32 hash = keccak256(
            abi.encodePacked(
                garbagePrefix,
                msgHash));
        address addr = ecrecover(hash, v, r, s);
        //emit evRecoveredAddress(addr);
        return addr;
    }
}

今回、トリッキーに思えた点が2つありました。

最初の1つが、ecrecoverにメッセージを渡す際に、"\x19Ethereum Signed Message:\n32"を先頭につけて、keccak256でハッシュを求める必要があった点です。この接頭辞などなくても良いのでは?とも思ったりもしますが、おまじないみたいなものとして、処理を挟んでおく必要があります。

        bytes memory garbagePrefix = "\x19Ethereum Signed Message:\n32";
        bytes32 hash = keccak256(
            abi.encodePacked(
                garbagePrefix,
                msgHash));

もう1つは、サーバー側でr, s, vが生成されますが、vについてはecrecoverに渡すためには、以下の処理を加えておく必要がある点です。

  if (v == '0x00') {
    v = '0x1b';
  } else if (v1 == '0x01') {
    v = '0x1c';
  }

この処理を知らなくて、数時間はまりました。

サーバーのログ出力は以下のようになっています。

{ url: 'http://localhost:8545',
  appAddress: '0xE5c08D0bFC2a90298a238B4Cb0a1c810eaC9a6B1' }
message: Hello, signature
message hash: 0x42a5ba7f9a12350d6a30ff714ae0247715106bea03bc57cb816bdd7269931c20
[HMR] Updated modules:
[HMR]  - ./src/server.js
[HMR] Update applied.
account: 0x5268F2232368DDbA43541AA7a16A9C569D0f9D4f
Signature
r: 0x90c2e483eac069bbbbe031510abd548128cfdf98b6fa7d3486ffe660839704bc
s: 0x74c466561fd5859b74bbe8f73ce4f4de0ce74b87b1fe1e49530db15822d87b7b
v: 0x1b
v_decimal: 27
{ transactionHash:
   '0x2c02cfb7bfdf187305133ecd29378e5503c209802a9235c76ce0c1c1fae6c9bc',
  transactionIndex: 0,
  blockHash:
   '0xbea56c96b0e3ad07abfb71f4b231b748dfe222407d6940113f6d0032fac5e459',
  blockNumber: 67,
  from: '0x5268f2232368ddba43541aa7a16a9c569d0f9d4f',
  to: '0xe5c08d0bfc2a90298a238b4cb0a1c810eac9a6b1',
  gasUsed: 32732,
  cumulativeGasUsed: 32732,
  contractAddress: null,
  status: true,
  logsBloom:
   '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
  v: '0x1c',
  r:
   '0xac5cbb8baafeca10a0d4270baba03b7eb6c00a0c7556bc9b29842ae9f51e7bf6',
  s:
   '0x6de80e2c4f0040979fe0b119c4620b26bcf1b90603da8ac317cc6e08546ff91c',
  events: {} }
truffle develop

で起動したコンソールからデバッグして、値を確認しました。

truffle(develop)> debug 0x2c02cfb7bfdf187305133ecd29378e5503c209802a9235c76ce0c1c1fae6c9bc
Starting Truffle Debugger...
✔ Compiling your contracts...
✔ Gathering information about your project and the transaction...

Addresses called: (not created)
 0x0000000000000000000000000000000000000001(UNKNOWN)
 0xE5c08D0bFC2a90298a238B4Cb0a1c810eaC9a6B1 - Verifier

Warning: The source code for one or more contracts could not be found.

Commands:
(enter) last command entered (step next)
(o) step over, (i) step into, (u) step out, (n) step next
(;) step instruction (include number to step multiple), (p) print instruction
(h) print this help, (q) quit, (r) reset
(t) load new transaction, (T) unload transaction
(b) add breakpoint, (B) remove breakpoint, (c) continue until breakpoint
(+) add watch expression (`+:<expr>`), (-) remove watch expression (-:<expr>)
(?) list existing watch expressions and breakpoints
(v) print variables and values, (:) evaluate expression - see `v`

Verifier.sol:

1: 
2: pragma solidity >=0.5.0 <0.6.0;
3: contract Verifier {
   ^^^^^^^^^^^^^^^^^^^

n

(と中略)

debug(develop:0x2c02cfb7...)> n

Verifier.sol:

17:         address addr = ecrecover(hash, v, r, s);
18:         //emit evRecoveredAddress(addr);
19:         return addr;
                   ^^^^ 

debug(develop:0x2c02cfb7...)> v

  garbagePrefix: '0x19457468657265756d205369676e6564204d6573736167653a0a3332'
           hash: '0x97749dabd410999325b0bf0ef1fedef74b30ed77f18d1559db49f816c2359c56'
           addr: '0x5268F2232368DDbA43541AA7a16A9C569D0f9D4f'
        msgHash: '0x42a5ba7f9a12350d6a30ff714ae0247715106bea03bc57cb816bdd7269931c20'
              v: 27
              r: '0x90c2e483eac069bbbbe031510abd548128cfdf98b6fa7d3486ffe660839704bc'
              s: '0x74c466561fd5859b74bbe8f73ce4f4de0ce74b87b1fe1e49530db15822d87b7b'
            msg: { data:
                    '0xe5df669f42a5ba7f9a12350d6a30ff714ae0247715106bea03bc57cb816bdd7269931c20000000000000000000000000000000000000000000000000000000000000001b90c2e483eac069bbbbe031510abd548128cfdf98b6fa7d3486ffe660839704bc74c466561fd5859b74bbe8f73ce4f4de0ce74b87b1fe1e49530db15822d87b7b',
                   sig: '0xe5df669f',
                   sender:
                    '0x5268F2232368DDbA43541AA7a16A9C569D0f9D4f',
                   value: 0 }
             tx: { origin:
                    '0x5268F2232368DDbA43541AA7a16A9C569D0f9D4f',
                   gasprice: 100000000000 }
          block: { coinbase:
                    '0x0000000000000000000000000000000000000000',
                   difficulty: 0,
                   gaslimit: 6721975,
                   number: 67,
                   timestamp: 1573795454 }
           this: 'Verifier(0xE5c08D0bFC2a90298a238B4Cb0a1c810eaC9a6B1)'
            now: 1573795454

アドレスに'0x5268F2232368DDbA43541AA7a16A9C569D0f9D4f'が代入され、署名とメッセージからアドレスを復元できることがわかりました。

コメント

タイトルとURLをコピーしました