こんにちは。SKILL Inc. CTOの田坂です。
コンソーシアムのPoCにおけるウォレット管理とTXサイン実装例という表題に関して溜まった知見を一部アウトプットしたいと思います
注意
この記事はSKILL Inc.としての記事であり、紐づいているOrganizationは関連ありません。
1. Dappを作りPoCをする上で気になること
- UX
- Gas代
- ウォレット管理
- 基本的なセキュリティ
- 仮説検証を成り立たせるためのサイズ感(これは今度書く)
Dappを作るとき外部ウォレットを使うと現状ブラウザ拡張を入れたり、専用ブラウザを使ったりとUX的ハードルがあることは周知の事実だと思います。
今後技術の発展でウォレットやブロックチェーンを支える技術はより使いやすいものになって行く思いますが、今のタイミングでUXの障壁にならずDappと必要最低限なサービス仮説検証をガンガン行って行きたいと思っており。微力ながら知見を共有できればと思い書いていきます。
2. 全体像
Ethereum環境はAzure Ethereum PoA
を利用しています。
具体的な技術選択に関する記事は別途書こうと思っていますので、ここではあまり触れません。
サマるとPoCでスモールにやるならPoA使ってGas代ゼロにした方が色々面倒なことを気にせず仮説検証ポイント絞れていいですよって話です。もちろんLayer1,2周りの技術キャッチアップは並行して行っています。一つのPoCで多くのことを検証しようとすると破綻するので、切り分けが大事だと思っています。
追記
ネットワークアクセスをIP制限しきながら、Ethノードへの接続もVPNでアクセス制御することで
よりセキュアに実証実験ができます。
3. ウォレット
僕たちのPoCではリアルマネーは現状BC上で扱わず、基本的なWebサービスにBCのバリューを加える形になっています。
なので既存ウォレットは使える必要がない前提の話で進めていきます。
ウォレットはサーバで生成管理せず、クライアントで保管しています。
また、別のブラウザなどでも同じウォレットを使えるように、サーバに暗号化したウォレットを保管しています
ウォレットの暗号化は1Passwordのセキュリティ実装を参考に、秘密鍵とマスターキーのセットで二回暗号化しています。
3.1. ウォレット生成し、暗号化したものをサーバに保管
3.2. ユーザの暗号化されたウォレットがサーバにあればDLして復号化
下の説明でBCで使う秘密鍵とそれを暗号化する秘密鍵二つあり、混乱するので
BCで使う秘密鍵を ウォレット秘密鍵
、暗号化のキーをウォレット暗号化秘密鍵
と表現しています。
3.1. ウォレット生成し、暗号化したものをサーバに保管
ウォレット暗号化秘密鍵
複雑な文字列。ブラウザにキャッシュしておく。
キャッシュにない場合はユーザに入力してもらう。
→ 基本新しいブラウザを使い始める時以外入力しない。
例 SK-AAAA-AAAA-AAAA-AAAA-AAAA-AAAA
prefixは正しいものをわかりやすくするため
マスタキーサンプル
上と比較すると簡単な文字列
ブラウザでキャッシュ。セッション切れるとユーザに再入力してもらう。
例 password1234&&
手順
-
ethereumjs-wallet
でウォレット生成 -
ウォレット暗号化秘密鍵
とマスターキー
でウォレット秘密鍵
を暗号化 - サーバに暗号化された
ウォレット秘密鍵
を保管 - ブラウザのキャッシュに各種キーを保管
実装例
- ウォレット暗号化秘密鍵
walletPrivateKey
- マスターキー
password
const Wallet = require("ethereumjs-wallet")
const CryptoJS = require("crypto-js")
const prefix = "SK"
let password = encodeURI(passwordString)
let wallet = Wallet.generate()
let address = wallet.getAddressString()
let privateKey = wallet.getPrivateKeyString()
let walletPrivateKey =
prefix +
"-" +
code() +
"-" +
code() +
"-" +
code() +
"-" +
code() +
"-" +
code() +
"-" +
code()
var ciperPrivateKey1 = CryptoJS.AES.encrypt(
privateKey,
password
).toString()
var ciperPrivateKey2 = CryptoJS.AES.encrypt(
ciperPrivateKey1,
walletPrivateKey
).toString()
3.2 ユーザの暗号化されたウォレットがサーバにあればDLして復号化
手順
-
ウォレット暗号化秘密鍵
で復号 - 1の復号データを
マスターキー
で復号 ← web3で使えるウォレット秘密鍵
になる
let masterKey = encodeURI('****')
var privateKey = null
var address = null
let response = await this.$axios.get("/v1/wallets/mine", {})
if(response.data != null) {
address = response.data.wallet.address
let encodedPrivateKey = response.data.wallet.encoded_private_key
let a1 = CryptoJS.AES.decrypt(encodedPrivateKey, masterKey).toString(
CryptoJS.enc.Utf8
)
privateKey = CryptoJS.AES.decrypt(a1, password).toString(
CryptoJS.enc.Utf8
)
}
4. 別ブラウザでの利用方法
この方法は議論中で使いやす形がまだ定まっていませんが
1Passwordのエマージェンシーキットが運用実績があり、参考事例としてあげさせて頂きます。
1Passwordではアカウントを作成するとき、マスターパスワードを任意で入力が求められます。
同時に秘密鍵が生成され、秘密鍵とマスターキー以外のログイン情報がPDFで生成され、ユーザが保管します。
ユーザは別環境で利用するときそのエマージェンシーキットを元に、自分が保管した情報を復号化する仕組みになっています。
詳しくは以下のページなどわかりやすいと思います
5. TXサイン
TXはクライアントでサインするが、RPC endpoint直接叩かない。
多分言葉で書くよりコードの方がわかりやすいと思うので実装ほぼそのまま掲載
5.1 トランザクション呼び出し部分
コントラクトのメソッドそのまま呼びだすが、返り値をmakeTransactionでRawTransactionに変換する
let txHash = await this.$makeTransaction(
this,
this.$contract.methods.makeReference(
this.summary.registered_summary_id,
response.reference.hash
)
).catch(e => {
console.log(e)
throw "TransactionError"
})
5.2 トランザクションのサインとAPIに投げる部分
- addressに対する、transactionカウントを取得
- transactionに署名し、RawTransactionに変換する
- APIサーバにRawTransactionを投げる
- RawTransactionを検証し、署名した人がサービスにいていい人かチェックする。またDDoSなどはAPIサーバで弾く
以下Nuxt.jsの実装例
default
abiをstaticファイルから引っ張ってきて、 Web3.Contract
インスタンスを生成
getTransactionCount
APIサーバ経由でtransactionカウントを取得する関数
sendTransaction
APIサーバ経由でRawTransactionを実行する
import abi from "~/static/contracts/dapp_contract_v1.json"
const getTransactionCount = async (app, address) => {
const res = await app.$axios.get("/v1/transactions/count?address=" + address)
return res.data
}
const sendTransaction = async (app, rawTransaction, address) => {
const res = await app.$axios.post("/v1/transactions", {
raw_transaction: rawTransaction,
address: address
})
return res.data
}
async function send(app, transaction) {
let privateKey = app.$store.state.wallet.privateKey
let address = app.$store.state.wallet.address
let nonceRes = await getTransactionCount(app, address)
let options = {
to: transaction._parent._address,
data: transaction.encodeABI(),
gas: 4700000,
chainId: process.env.NUXT_ENV_ETH_NETWORK_ID,
nonce: nonceRes.nonce,
gasPrice: "0x0"
}
let signedTransaction = await window.web3.eth.accounts.signTransaction(
options,
privateKey
)
let res = await sendTransaction(
app,
signedTransaction.rawTransaction,
address
)
return res.tx_id
}
export default ({ store }, inject) => {
var Web3 = require("web3")
var web3 = new Web3()
var FakeProvider = require("web3-fake-provider")
web3.setProvider(new FakeProvider())
window.web3 = web3
const contract = new window.web3.eth.Contract(
abi,
process.env.NUXT_ENV_DAPP_CONTRACT_ADDRESS,
{
gasPrice: "0x00",
gas: 4700000
}
)
inject("contract", contract)
inject("makeTransaction", send)
}
Gas
gasPriceは0にし、gasの単位は設定する
6. おわりに
今回コンソーシアムにおけるPoCでのウォレット管理とサイン周りの話を書きました。
PoCにおける仮説検証するときに多くのことを検証しすぎて、検証にめっちゃお金かかったり時間かかったり
そもそもコアなバリュー検証できたんだっけ?という状況って全然あるなと思っていて、検証ポイントをはっきりさせて
価値ある知見を世の中に残していければと思っています。
インプットとかまさかりとかあれば是非よろしくお願いします!