如何操作 Hyperledger Besu 的 Private Raw Transaction
企業導入 Besu 私密交易會遇到什麼問題
前言
BSOS 曾在「企業以太坊解決了什麼問題?」這篇文章中,提到兩個比較知名的 EE(Enterprise Ethereum):
私密交易(Private transaction)是企業以太坊重要的技術環節,Besu 的 private raw transaction 符合 EEA 規範,其設計方式具有高度的參考價值,不過當我們要實際要把 Besu 導入企業應用時,要實現系統的高可用性(High Available),還是有一些工程面的問題需要解決。
本文將深入探討 Besu private raw transaction 的原理、實作方式,以及如何解決實際操作上會遇到的問題。
企業導入 Besu 私密交易會遇到什麼問題
Besu 所謂的 private raw transaction,就是將一份 private transaction 透過使用者私鑰簽名,而產生一個可在鏈上執行的私交易。
要在 Besu 上執行 private transaction,需先透過 PegaSys 所開發的 web3js-eea 套件將交易簽名。由於非所有企業都使用 Node.js 開發,且透過 web3js-eea 簽名時會揭露使用者私鑰。這樣一來,在系統架構上不論彈性、安全性與擴展性皆難以達到企業等級的要求。為了安全的在 Besu 發送私交易,至少需做到將簽名交易等關鍵流程嵌入企業安全性較高的架構 (如企業客製化的 Vault plugin),因此企業導入 Besu 的關鍵問題在於:
如何安全簽出合法可執行的 private raw transaction
Pegasys 同樣想到簽名安全性問題,因而開發 EthSigner,支援使用者將私鑰存在安全性較高的架構,透過 eea_Transaction 發送私交易。不過他將交易簽完名就直接丟給 Besu 執行,過程中並無 private raw transaction 產出;且雖然 EthSigner 支援使用者私鑰存放在 Vault,但 EthSigner 啟動時會指向 Vault 中的某把私鑰,無法做到多用戶的企業內使用。
$ ethsigner --chain-id=2018 --downstream-http-port=8590 hashicorp-signer --host=127.0.0.1 --port=8200 --auth-file=authFile --tls-enabled=false --signing-key-path=/v1/secret/data/ethsignerSigningKey
EthSigner 專案證明 private raw transaction 需從 web3js-eea 抽離的重要性。接下來,我們將打開 web3js-eea 潘朵拉的盒子,解釋一個 private raw transaction 是如何從無到有產生的,並展示透過 go-ethereum 做出一個可在 Besu 上執行的 private raw transaction。
工欲善其事,必先利其器:Besu 環境設定
為了實際執行 private raw transaction,我們必須先建立一套 Besu 環境。官方文件提供透過 docker-compose 方式快速建立 Besu 環境:
$ git clone https://github.com/PegaSysEng/besu-sample-networks.git $ cd besu-sample-networks $ ./run-privacy.sh -c ibft2
執行 run-privacy.sh 以實現可執行 private transaction 的節點環境,ibft2 是 Besu 的共識機制,也可以選擇 clique。另外 config/besu 目錄中有多個創世區塊設定檔可修改或嵌入合約,例如導入智能合約實現聯盟治理。
執行後會啟動多個 container,畫面如下:
$ docker ps
不論 Besu 或 Quorum,他們都會掛載一個 private transaction manager 來加密與存取私交易的內容。Besu 官方推薦使用 Pegasys 開發的 Orion;Quorum 則推薦使用 Tessera,使用者也可以自行掛載其他服務,如 Web3 Labs 開發的 Crux。
上圖可以看到除了 Besu 節點外,我們另外還起了三個 Orion 節點,以及其他的監控服務。可以透過執行 list.sh 看到有哪些服務可以用,例如 8545 port 用來呼叫 API;25000 port 是 blockchain explorer 服務。需要注意的是三個 Besu 節點的 8545 port 分別被導向 20000, 20001 與 20002 port。
$ ./list.sh
淺談 Besu 的 Private Transaction
我們先來看看 EEA 如何規範 private transaction:
The privateFrom and privateFor parameters in the eea_sendTransactionAsync and eea_sendTransaction calls specify the public keys of the sender and the intended recipients, respectively, of a private transaction. The private transaction type is specified using the restriction parameter. The two defined private transaction types are: 1. Restricted private transactions, where payload data is transmitted to and readable only by the parties to the transaction. 2. Unrestricted private transactions, where encrypted payload data is transmitted to all nodes in the Enterprise Ethereum blockchain, but readable only by the parties to the transaction.
由此可知,一個 private transaction 包含三個與 privacy 相關的參數:
- privateFrom,自己 private transaction manager 的公鑰
- privateFor,其他私交易參與者的 private transaction manager 的公鑰陣列
- restriction,限制私交易內容加密後是否同步到非參與者的節點
Besu 的一個 private transaction 會將上述三個 privacy 參數一併包進 tx 中簽名,產生 private raw transaction 後再透過 eea_sendRawTransaction 這個 API 發送私交易。實際操作如下:
$ curl -X POST --data '{"jsonrpc":"2.0","method":"eea_sendRawTransaction","params": ["0xf869018203e882520894f17f52151ebef6c7334fad080c5704d77216b732881bc16d674ec80000801ba02da1c48b670996dcb1f447ef9ef00b33033c48a4fe938f420bec3e56bfd24071a062e0aa78a81bf0290afbc3a9d8e9a068e6d74caa66c5e0fa8a46deaae96b0833"], "id":1}' http://127.0.0.1:8545
參數中看似複雜的 16 進位字串,就是本文不斷提及的 private raw transaction。這東西可是得來不易,除了 web3js-eea 有沒有更模組化的方式取得? 接下來我們將深度解析 private raw transaction 產生的方式,進而改由 Ethereum 原生的 go-ethereum 實作程式碼。
代誌不是那麼簡單…
同樣的問題,在 Quorum 中就不是個問題,原因是因為 Quorum 可以順理成章地使用 go-ethereum 的交易模型 (transaction model) 實作 private transaction,但 Besu 的情況可說是相當複雜…
以下是 go-ethereum 中 transaction model:
type Transaction struct { data txdata // caches hash atomic.Value size atomic.Value from atomic.Value } type txdata struct { AccountNonce uint64 `json:"nonce" gencodec:"required"` Price *big.Int `json:"gasPrice" gencodec:"required"` GasLimit uint64 `json:"gas" gencodec:"required"` Recipient *common.Address `json:"to" rlp:"nil"` // nil means contract creation Amount *big.Int `json:"value" gencodec:"required"` Payload []byte `json:"input" gencodec:"required"` // Signature values V *big.Int `json:"v" gencodec:"required"` R *big.Int `json:"r" gencodec:"required"` S *big.Int `json:"s" gencodec:"required"` // This is only used when marshaling to JSON. Hash *common.Hash `json:"hash" rlp:"-"` }
在 private transaction 中,比較 Quorum 與 Besu 的實作方式差異:
- Quorum 沒有 privateFrom 與 restriction 參數 (不符合 EEA 規範 94 狂),且 privateFor 不需被簽名在 private raw transaction,而是將一般的 raw transaction 加密雜湊後 (eth_getEncryptedHash) 再將 privateFor 作為參數發送私交易 (eth_sendRawPrivateTransaction)。
- Besu 的三種 privacy 參數 (privateFrom、privateFor 與 restriction) 皆需被簽名到交易中,因此無法直接使用 go-ethereum transaction model 實作 private transaction!
因此 Besu 的 private transaction model 需要特殊的設計,程式碼如下:
// Transaction . type Transaction struct { Data txdata } type txdata struct { AccountNonce uint64 `json:"nonce" gencodec:"required"` Price *big.Int `json:"gasPrice" gencodec:"required"` GasLimit uint64 `json:"gas" gencodec:"required"` Recipient *common.Address `json:"to" rlp:"nil"` // nil means contract creation Amount *big.Int `json:"value" gencodec:"required"` Payload []byte `json:"input" gencodec:"required"` V *big.Int `json:"v" gencodec:"required"` R *big.Int `json:"r" gencodec:"required"` S *big.Int `json:"s" gencodec:"required"` PrivateFrom []byte `json:"private_from" gencodec:"required"` PrivateFor [][]byte `json:"private_for" gencodec:"required"` Restriction string }
我們將三種 privacy 參數加入到 transaction model 中,再依照 Besu 的規則將交易簽名,理論上就可產生一個可在鏈上執行的 private raw transaction。
首先,我們先來瞧瞧 web3js-eea 是怎麼做的。
打開潘朵拉的盒子,深度解析 webjs-eea
在 web3js-eea 文件中提到如何執行一個 private transaction:
const createPrivateEmitterContract = privacyGroupId => { const contractOptions = { data: `0x${binary}`, privateFrom: orion.node1.publicKey, privacyGroupId, privateKey: besu.node1.privateKey }; return web3.eea.sendRawTransaction(contractOptions); };
筆者不禁好奇 contractOptions 會是怎麼樣的形式呢? 在深入研究 private raw transaction 前,先稍微分析一下 contractOptions 中的參數:
- data: 相當於前述 go-ethereum transaction model 中的 Payload,代表要執行的交易內容,例如發布一個智能合約。
- privateFrom: 發起交易節點的 Orion 公鑰
- privacyGroupId: private transaction 所有參與方組成,可透過 Besu API 產生。
- privateKey: 發起交易帳號的私鑰
接下來趕緊來安裝部署 web3js-eea 看看吧:
$ git clone https://github.com/PegaSysEng/web3js-eea.git $ cd web3js-eea $ npm install $ node example/eventEmitter.js # 此範例透過 private transaction 發布智能合約,並操作此合約
在 example/keys.js 中可取得三個節點的 orion 公鑰,以及三組私鑰:
orion: { node1: { publicKey: "A1aVtMxLCUHmBVHXoZzzBgPbW/wj5axDpW9X8l91SGo=" }, node2: { publicKey: "Ko2bVqD+nNlNYL5EE7y3IdOnviftjiizpjRt+HTuFBs=" }, node3: { publicKey: "k2zXEin4Ip/qBGlRkJejnGWdP9cjkK+DAvKNW31L2C8=" } }, besu: { node1: { url: "http://localhost:20000", privateKey: "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63" }, node2: { url: "http://localhost:20002", privateKey: "c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3" }, node3: { url: "http://localhost:20004", privateKey: "ae6ae8e5ccbfb04590405997ee2d52d2b330726137b875053c36d94e974d162f" } }
接下來,我們將從 EEAClient 主程式 src/index.js 來觀察 private raw transaction 產生流程,然後深入解析 src/privateTransaction.js 如何對一個 private transaction 簽名。
- private transaction 初始化
tx = new PrivateTransaction()
2. 匯入發起交易的 account
const from = `0x${privateToAddress(privateKeyBuffer).toString("hex")}`;
3. 計算 private nonce
web3.priv.getTransactionCount({ from, privateFrom: options.privateFrom, privateFor: options.privateFor, privacyGroupId: options.privacyGroupId })
4. 製作 private transaction
tx.nonce = options.nonce || transactionCount; tx.gasPrice = GAS_PRICE; tx.gasLimit = GAS_LIMIT; tx.to = options.to; tx.value = 0; tx.data = options.data; // eslint-disable-next-line no-underscore-dangle tx._chainId = chainId; tx.privateFrom = options.privateFrom; if (options.privateFor) { tx.privateFor = options.privateFor; } if (options.privacyGroupId) { tx.privacyGroupId = options.privacyGroupId; } tx.restriction = "restricted"; // Besu 只支援 restricted
5. 交易簽名
tx.sign(privateKeyBuffer);
6. 取得 private raw transaction!
const signedRlpEncoded = tx.serialize().toString("hex");
7. 執行 private raw transaction
result = web3.privInternal.sendRawTransaction(signedRlpEncoded);
看到這邊,我們已經了解到從無到有製作一個 private raw transaction 的流程。與 Quorum 和 Ethereum 不同的是,第三步的 private nonce 代表某 account 在某個 privacy group 中的 nonce,可使用 Besu API priv_getTransactionCount 取得。第六步完成後便可取得我們朝思暮想的 private raw transaction,因此重點會落在第五步實際是如何對一個 private transaction 簽名的。
詳見 src/privateTransaction.js,可分成 hash 與 sign:
- 將交易 hash
- 初始化 transaction 的 V, R, S 值
- 判斷 privacy 參數,privateFor 與 privacyGroupId 不能都是空值
hash(includeSignature) { // 還沒簽名,所以會給 false 值 if (includeSignature === undefined) includeSignature = true; let items; if (includeSignature) { items = this.raw; } else if (this._chainId > 0) { const raw = this.raw.slice(); // v, r, s 在簽完名後會給定值,這邊是做初始化 this.v = this._chainId; this.r = 0; this.s = 0; items = this.raw; this.raw = raw; } else { items = this.raw.slice(0, 6); } const arr = items.slice(); if (items[10][0].length !== 0 && items[11].length === 32) { throw Error( "privacyGroupId and privateFor fields are mutually exclusive" ); } if (items[11].length === 32) { // 這邊在判斷 privacyGroupId 是否有值 arr.splice(10, 1); } else { arr.splice(11, 1); } // create hash return ethUtil.rlphash(arr); }
2. sign
- 使用 ecsign 簽名,與原生的 web3.js 相同,go-ethereum 也有對應的簽名方式。
- 簽名後會改變 transaction 的 V, R, S 值
- Besu 的 V 值需做二次運算:sig.v += this._chainId * 2 + 8;
sign(privateKey) { const msgHash = this.hash(false); const sig = ethUtil.ecsign(msgHash, privateKey); if (this._chainId > 0) { sig.v += this._chainId * 2 + 8; } Object.assign(this, sig); }
以上解析 web3js-eea 產生一個簽名後可在 Besu 執行的 private raw transaction 流程,接下來我們就可以將相同邏輯透過 go-ethereum 實作了!
牛刀小試,透過 go-ethereum 實作產生 Besu Private Raw Transaction
本段使用 go-ethereum 產生 Besu private raw transaction,所有使用參數前段 web3js-eea 相同,以下範例是發佈一個 private 的智能合約。
1. 前面有提到,Besu 無法直接使用 go-ethereum 的 transaction model 直接實作 private transaction,因此設計了 Besu 專屬的 private transaction model,加上 PrivateFrom, PrivateFor 與 Restriction 等 privacy 相關參數,並將 private transaction manager public key 型態訂為 []byte。
// Transaction . type Transaction struct { Data txdata } type txdata struct { AccountNonce uint64 `json:"nonce" gencodec:"required"` Price *big.Int `json:"gasPrice" gencodec:"required"` GasLimit uint64 `json:"gas" gencodec:"required"` Recipient *common.Address `json:"to" rlp:"nil"` // nil means contract creation Amount *big.Int `json:"value" gencodec:"required"` Payload []byte `json:"input" gencodec:"required"` V *big.Int `json:"v" gencodec:"required"` R *big.Int `json:"r" gencodec:"required"` S *big.Int `json:"s" gencodec:"required"` PrivateFrom []byte `json:"private_from" gencodec:"required"` PrivateFor [][]byte `json:"private_for" gencodec:"required"` Restriction string }
2. 定義各節點 Orion 公鑰,為方便演示直接用字串方式傳入,並透過 base64 方式解碼成 []byte。
privateFromString := "A1aVtMxLCUHmBVHXoZzzBgPbW/wj5axDpW9X8l91SGo=" privateFor1String := "Ko2bVqD+nNlNYL5EE7y3IdOnviftjiizpjRt+HTuFBs=" privateFor2String := "k2zXEin4Ip/qBGlRkJejnGWdP9cjkK+DAvKNW31L2C8=" privateFrom, _ := base64.StdEncoding.DecodeString(privateFromString) privateFor1, _ := base64.StdEncoding.DecodeString(privateFor1String) privateFor2, _ := base64.StdEncoding.DecodeString(privateFor2String) _ = privateFor2
3. 設定 Besu client 連線與交易發起人,並定義交易參數與內容
rpcClient, _ := rpc.Dial("http://host:20000") ethClient := ethclient.NewClient(rpcClient) privateKey, _ := crypto.HexToECDSA("8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63") publicKey := privateKey.Public() publicKeyECDSA, _ := publicKey.(*ecdsa.PublicKey) fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA) gasLimit := uint64(3000000) networkID, _ := ethClient.NetworkID(context.TODO()) bytecode := "0x608060405234801561001057600080fd5b50336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550610221806100606000396000f300608060405260043610610057576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680633fa4f2451461005c5780636057361d1461008757806367e404ce146100b4575b600080fd5b34801561006857600080fd5b5061007161010b565b6040518082815260200191505060405180910390f35b34801561009357600080fd5b506100b260048036038101908080359060200190929190505050610115565b005b3480156100c057600080fd5b506100c96101cb565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b6000600254905090565b7fc9db20adedc6cf2b5d25252b101ab03e124902a73fcb12b753f3d1aaa2d8f9f53382604051808373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020018281526020019250505060405180910390a18060028190555033600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555050565b6000600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff169050905600a165627a7a723058208efaf938851fb2d235f8bf9a9685f149129a30fe0f4b20a6c1885dc02f639eba0029" data, _ := hexutil.Decode(bytecode)
4. 取得 privacy group,若不存在則需創建一個 privacy group。可使用 Besu API priv_findPrivacyGroup 尋找已存在 privacy group,或是 priv_createPrivacyGroup 建立一個新的 privacy group。
var findPrivacyGroupRsp []map[string]interface{} rpcClient.CallContext(context.TODO(), &findPrivacyGroupRsp, "priv_findPrivacyGroup", []string{privateFromString, privateFor1String}) var privacyGroupID string if len(findPrivacyGroupRsp) == 0 { // no existent private group, make it! createPrivacyGroup := getCreatePrivacyGroupArgs([]string{privateFromString, privateFor1String}, "KEVIN GROUP") var createPrivacyGroupRsp interface{} rpcClient.CallContext(context.TODO(), &createPrivacyGroupRsp, "priv_createPrivacyGroup", createPrivacyGroup) privacyGroupID = createPrivacyGroupRsp.(string) } else { privacyGroupID = findPrivacyGroupRsp[0]["privacyGroupId"].(string) } ... func getCreatePrivacyGroupArgs(addresses []string, name string) map[string]interface{} { result := make(map[string]interface{}) result["addresses"] = addresses result["name"] = name return result }
5. 取得 private nonce,可使用 Besu API priv_getTransactionCount
var getTransactionCountRsp interface{} rpcClient.CallContext(context.TODO(), &getTransactionCountRsp, "priv_getTransactionCount", fromAddress.Hex(), privacyGroupID) privateNonce, _ := hexutil.DecodeUint64(getTransactionCountRsp.(string))
6. 發起一個 private transaction。與 Quorum 和 Ethereum 不同的是 AccountNonce 需帶入 private nonce,且 Besu Restriction 目前只支援 restricted (不會將加密後的私交易內容同步到 privacy group 以外的 privacy group)。
besutx := newTransaction(privateNonce, nil, nil, gasLimit, big.NewInt(0), data, privateFrom, [][]byte{privateFor1}) ... func newTransaction(nonce uint64, to *common.Address, amount *big.Int, gasLimit uint64, gasPrice *big.Int, data []byte, privateFrom []byte, privateFor [][]byte) *Transaction { if len(data) > 0 { data = common.CopyBytes(data) } d := txdata{ AccountNonce: nonce, Recipient: to, Payload: data, Amount: new(big.Int), GasLimit: gasLimit, Price: new(big.Int), PrivateFrom: privateFrom, PrivateFor: privateFor, Restriction: "restricted", V: new(big.Int), R: new(big.Int), S: new(big.Int), } if amount != nil { d.Amount.Set(amount) } if gasPrice != nil { d.Price.Set(gasPrice) } return &Transaction{Data: d} }
7. 交易簽名,實作 web3js-eea 中的 hash 與 sign。
besuSignedTx, _ := signTx(besutx, networkID, privateKey) ... func signTx(tx *Transaction, chainID *big.Int, prv *ecdsa.PrivateKey) (*Transaction, error) { h := hash(tx, chainID) sig, err := crypto.Sign(h[:], prv) if err != nil { return nil, err } return withSignature(tx, sig, chainID) } func hash(tx *Transaction, chainID *big.Int) common.Hash { h := rlpHash([]interface{}{ tx.Data.AccountNonce, tx.Data.Price, tx.Data.GasLimit, tx.Data.Recipient, tx.Data.Amount, tx.Data.Payload, chainID, uint(0), uint(0), tx.Data.PrivateFrom, tx.Data.PrivateFor, tx.Data.Restriction, }) return h } func rlpHash(x interface{}) (h common.Hash) { hw := sha3.NewLegacyKeccak256() rlp.Encode(hw, x) hw.Sum(h[:0]) return h } func withSignature(tx *Transaction, sig []byte, chainID *big.Int) (*Transaction, error) { r, s, v, err := signatureValues(tx, sig) if err != nil { return nil, err } newV := v.Uint64() + chainID.Uint64()*2 + 8 // hack from web3js-eea cpy := &Transaction{Data: tx.Data} cpy.Data.R, cpy.Data.S, cpy.Data.V = r, s, new(big.Int).SetUint64(newV) return cpy, nil } func signatureValues(tx *Transaction, sig []byte) (r, s, v *big.Int, err error) { if len(sig) != crypto.SignatureLength { panic(fmt.Sprintf("wrong size for signature: got %d, want %d", len(sig), crypto.SignatureLength)) } r = new(big.Int).SetBytes(sig[:32]) s = new(big.Int).SetBytes(sig[32:64]) v = new(big.Int).SetBytes([]byte{sig[64] + 27}) return r, s, v, nil }
8. 產生 private raw transaction
besuRawTxData, _ := rlp.EncodeToBytes(besuSignedTx) besuRawTxData = append(besuRawTxData[:1], besuRawTxData[4:]...) // remove redundant dust fmt.Println(hexutil.Encode(besuRawTxData))
這邊使用 rlp.EncodeToBytes 出來會產生一些冗餘需去除。最後成功印出我們需要的 private raw transaction ,可使用 Besu API eea_sendRawTransaction 執行這筆 private raw transaction,得到 transaction hash。
完整程式碼如下
package main import ( "context" "crypto/ecdsa" "encoding/base64" "fmt" "math/big" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rpc" "golang.org/x/crypto/sha3" ) // Transaction . type Transaction struct { Data txdata } type txdata struct { AccountNonce uint64 `json:"nonce" gencodec:"required"` Price *big.Int `json:"gasPrice" gencodec:"required"` GasLimit uint64 `json:"gas" gencodec:"required"` Recipient *common.Address `json:"to" rlp:"nil"` // nil means contract creation Amount *big.Int `json:"value" gencodec:"required"` Payload []byte `json:"input" gencodec:"required"` V *big.Int `json:"v" gencodec:"required"` R *big.Int `json:"r" gencodec:"required"` S *big.Int `json:"s" gencodec:"required"` PrivateFrom []byte `json:"private_from" gencodec:"required"` PrivateFor [][]byte `json:"private_for" gencodec:"required"` Restriction string } func main() { privateFromString := "A1aVtMxLCUHmBVHXoZzzBgPbW/wj5axDpW9X8l91SGo=" privateFor1String := "Ko2bVqD+nNlNYL5EE7y3IdOnviftjiizpjRt+HTuFBs=" privateFor2String := "k2zXEin4Ip/qBGlRkJejnGWdP9cjkK+DAvKNW31L2C8=" privateFrom, _ := base64.StdEncoding.DecodeString(privateFromString) privateFor1, _ := base64.StdEncoding.DecodeString(privateFor1String) privateFor2, _ := base64.StdEncoding.DecodeString(privateFor2String) _ = privateFor2 bytecode := "0x608060405234801561001057600080fd5b50336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550610221806100606000396000f300608060405260043610610057576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680633fa4f2451461005c5780636057361d1461008757806367e404ce146100b4575b600080fd5b34801561006857600080fd5b5061007161010b565b6040518082815260200191505060405180910390f35b34801561009357600080fd5b506100b260048036038101908080359060200190929190505050610115565b005b3480156100c057600080fd5b506100c96101cb565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b6000600254905090565b7fc9db20adedc6cf2b5d25252b101ab03e124902a73fcb12b753f3d1aaa2d8f9f53382604051808373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020018281526020019250505060405180910390a18060028190555033600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555050565b6000600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff169050905600a165627a7a723058208efaf938851fb2d235f8bf9a9685f149129a30fe0f4b20a6c1885dc02f639eba0029" data, _ := hexutil.Decode(bytecode) rpcClient, _ := rpc.Dial("http://host:20000") ethClient := ethclient.NewClient(rpcClient) privateKey, _ := crypto.HexToECDSA("8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63") publicKey := privateKey.Public() publicKeyECDSA, _ := publicKey.(*ecdsa.PublicKey) fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA) gasLimit := uint64(3000000) networkID, _ := ethClient.NetworkID(context.TODO()) // get private nonce // 1. find private group var findPrivacyGroupRsp []map[string]interface{} rpcClient.CallContext(context.TODO(), &findPrivacyGroupRsp, "priv_findPrivacyGroup", []string{privateFromString, privateFor1String}) var privacyGroupID string if len(findPrivacyGroupRsp) == 0 { // no existent private group, make it! createPrivacyGroup := getCreatePrivacyGroupArgs([]string{privateFromString, privateFor1String}, "KEVIN GROUP") var createPrivacyGroupRsp interface{} rpcClient.CallContext(context.TODO(), &createPrivacyGroupRsp, "priv_createPrivacyGroup", createPrivacyGroup) privacyGroupID = createPrivacyGroupRsp.(string) } else { privacyGroupID = findPrivacyGroupRsp[0]["privacyGroupId"].(string) } // 2. get private nonce var getTransactionCountRsp interface{} rpcClient.CallContext(context.TODO(), &getTransactionCountRsp, "priv_getTransactionCount", fromAddress.Hex(), privacyGroupID) privateNonce, _ := hexutil.DecodeUint64(getTransactionCountRsp.(string)) besutx := newTransaction(privateNonce, nil, nil, gasLimit, big.NewInt(0), data, privateFrom, [][]byte{privateFor1}) besuSignedTx, _ := signTx(besutx, networkID, privateKey) besuRawTxData, _ := rlp.EncodeToBytes(besuSignedTx) besuRawTxData = append(besuRawTxData[:1], besuRawTxData[4:]...) // KEVIN hack, remove redundant dust fmt.Println(hexutil.Encode(besuRawTxData)) } func signTx(tx *Transaction, chainID *big.Int, prv *ecdsa.PrivateKey) (*Transaction, error) { h := hash(tx, chainID) sig, err := crypto.Sign(h[:], prv) if err != nil { return nil, err } return withSignature(tx, sig, chainID) } func hash(tx *Transaction, chainID *big.Int) common.Hash { h := rlpHash([]interface{}{ tx.Data.AccountNonce, tx.Data.Price, tx.Data.GasLimit, tx.Data.Recipient, tx.Data.Amount, tx.Data.Payload, chainID, uint(0), uint(0), tx.Data.PrivateFrom, tx.Data.PrivateFor, tx.Data.Restriction, }) return h } func rlpHash(x interface{}) (h common.Hash) { hw := sha3.NewLegacyKeccak256() rlp.Encode(hw, x) hw.Sum(h[:0]) return h } func withSignature(tx *Transaction, sig []byte, chainID *big.Int) (*Transaction, error) { r, s, v, err := signatureValues(tx, sig) if err != nil { return nil, err } newV := v.Uint64() + chainID.Uint64()*2 + 8 // KEVIN hack from web3js-eea cpy := &Transaction{Data: tx.Data} cpy.Data.R, cpy.Data.S, cpy.Data.V = r, s, new(big.Int).SetUint64(newV) return cpy, nil } func signatureValues(tx *Transaction, sig []byte) (r, s, v *big.Int, err error) { if len(sig) != crypto.SignatureLength { panic(fmt.Sprintf("wrong size for signature: got %d, want %d", len(sig), crypto.SignatureLength)) } r = new(big.Int).SetBytes(sig[:32]) s = new(big.Int).SetBytes(sig[32:64]) v = new(big.Int).SetBytes([]byte{sig[64] + 27}) return r, s, v, nil } func newTransaction(nonce uint64, to *common.Address, amount *big.Int, gasLimit uint64, gasPrice *big.Int, data []byte, privateFrom []byte, privateFor [][]byte) *Transaction { if len(data) > 0 { data = common.CopyBytes(data) } d := txdata{ AccountNonce: nonce, Recipient: to, Payload: data, Amount: new(big.Int), GasLimit: gasLimit, Price: new(big.Int), PrivateFrom: privateFrom, PrivateFor: privateFor, Restriction: "restricted", V: new(big.Int), R: new(big.Int), S: new(big.Int), } if amount != nil { d.Amount.Set(amount) } if gasPrice != nil { d.Price.Set(gasPrice) } return &Transaction{Data: d} } func getCreatePrivacyGroupArgs(addresses []string, name string) map[string]interface{} { result := make(map[string]interface{}) result["addresses"] = addresses result["name"] = name return result }
交易成功,取得發票 (transaction receipt)
在 Besu中,成功完成一筆交易後,可以拿到一個 transaction hash 交易證明。transaction hash 可在 blockchain explorer 上查詢交易明細,或可透過 Besu API 取得 transaction receipt。Besu 的 private transaction 有eth_getTransactionReceipt 與 priv_getTransactionReceipt 兩種 transaction receipt。
- eth_getTransactionReceipt
{ "jsonrpc": "2.0", "id": 1, "result": { "blockHash": "0xe222c7967a2afb632d0012c627cb5e9f8e4fa59bf194e76d8ecba3a761c800d0", "blockNumber": "0x2a15a", "contractAddress": null, "cumulativeGasUsed": "0x5a88", "from": "0x1342b05e52d223489be6cc1335a781989bf6b375", "gasUsed": "0x5a88", "logs": [], "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "status": "0x1", "to": "0x000000000000000000000000000000000000007e", "transactionHash": "0x8b6f02d07075c044f93788753ab02b8ca878713480f7e89398a33c9e33b83ef2", "transactionIndex": "0x0" } }
- priv_getTransactionReceipt
{ "jsonrpc": "2.0", "id": 1, "result": { "contractAddress": "0xc8eb6ead541ecfef384b1b4c02e02a27694e6e82", "from": "0xfe3b557e8fb62b89f4916b721be55ceb828dbd73", "output": "0x608060405260043610610057576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680633fa4f2451461005c5780636057361d1461008757806367e404ce146100b4575b600080fd5b34801561006857600080fd5b5061007161010b565b6040518082815260200191505060405180910390f35b34801561009357600080fd5b506100b260048036038101908080359060200190929190505050610115565b005b3480156100c057600080fd5b506100c96101cb565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b6000600254905090565b7fc9db20adedc6cf2b5d25252b101ab03e124902a73fcb12b753f3d1aaa2d8f9f53382604051808373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020018281526020019250505060405180910390a18060028190555033600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555050565b6000600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff169050905600a165627a7a723058208efaf938851fb2d235f8bf9a9685f149129a30fe0f4b20a6c1885dc02f639eba0029", "commitmentHash": "0x8b6f02d07075c044f93788753ab02b8ca878713480f7e89398a33c9e33b83ef2", "transactionHash": "0x813b7ac79bf55944917f82160b1338f5f0fb2095fd4aa7fcca4de30926e03c66", "privateFrom": "A1aVtMxLCUHmBVHXoZzzBgPbW/wj5axDpW9X8l91SGo=", "privateFor": [ "Ko2bVqD+nNlNYL5EE7y3IdOnviftjiizpjRt+HTuFBs=" ], "status": "0x1", "logs": [] } }
問題還沒完!謎樣的 Privacy Group
在 Besu API 文件中,提供對 privacy group 的 CRD 操作:
- priv_createPrivacyGroup
- priv_findPrivacyGroup
- priv_deletePrivacyGroup
其中 priv_findPrivacyGroup 回傳是一個陣列,言下之意,一群相同的私交易參與者之間,可存在多個 privacy group。筆者心中立刻冒出疑問:private nonce 怎麼算?
秉持著實驗精神,我們使用 priv_deletePrivacyGroup 刪除第一個 privacy group,再使用 priv_createPrivacyGroup 建立多個 privacy group,然後前段的 go-etheruem 程式就崩潰了,錯誤回傳 nonce too low,使用 priv_getTransactionCount 在任一個現有的 privacy group 中取得的 nonce 皆是 0。
我們找到 web3js-eea 中 src/privacyGroup.js
const participants = _.chain(options.privateFor || []) .concat(options.privateFrom) .uniq() .map(publicKey => { const buffer = Buffer.from(publicKey, "base64"); let result = 1; buffer.forEach(value => { // eslint-disable-next-line no-bitwise console.log(value) console.log(result) result = (31 * result + ((value << 24) >> 24)) & 0xffffffff; }); return { b64: publicKey, buf: buffer, hash: result }; }) .sort((a, b) => { return a.hash - b.hash; }) .map(x => { return x.buf; }) .value(); const rlp = RLP.encode(participants); return Buffer.from(keccak256(rlp)).toString("base64");
private nonce 還真的是 “算” 出來的,privacy group ID 會先將所有參與者的 Orion public key 經過複雜的排序,RLP 編碼後取得 keccak256 再回傳。不同程式語言在計算 Hash 時可能造成溢位,需特別注意。使用 go-ethereum 實作取得 private nonce 如下:
// PrivateNonceByParticipants . func (p *Privacy) PrivateNonceByParticipants(account common.Address, participants []*PublicKey) (uint64, error) { rootGroup := p.FindRootPrivacyGroup(participants) return p.PrivateNonce(account, rootGroup) } // FindRootPrivacyGroup . func (p *Privacy) FindRootPrivacyGroup(participants []*PublicKey) *Group { sortParticipants := p.sort(participants) hash := rlpHash(sortParticipants) return &Group{ ID: base64.StdEncoding.EncodeToString(hash.Bytes()), } } // Hash . func (pub PublicKey) Hash() int { result := int(1) for _, v := range pub { result = int(int32((31*result + int((int32(v)<<24)>>24)) & 0xffffffff)) } return result } func (p *Privacy) sort(participants []*PublicKey) []*PublicKey { hashMap := make(map[int]*PublicKey) for i := range participants { hashMap[participants[i].Hash()] = participants[i] } var keys []int for k := range hashMap { keys = append(keys, k) } sort.Ints(keys) var output []*PublicKey for _, v := range keys { output = append(output, hashMap[v]) } return output } func rlpHash(x interface{}) (h common.Hash) { hw := sha3.NewLegacyKeccak256() rlp.Encode(hw, x) hw.Sum(h[:0]) return h }
取得正確的 private nonce 後,透過 priv_findPrivacyGroup 找到的任一個 privacy group 皆可以被正常使用在 private transaction!
整理 Besu private transaction 一些需要注意的小細節
- 本文重點在邏輯展示,將 private raw transaction 產生方式模組化。由於原生 Ethereum 使用 Go 開發,故本文實作採 go-ethereum ,讀者也可以使用者熟悉的程式語言。
- 由於 privateFrom、privateFor 等參數會一起被簽名,需重新自定義 private transaction model。
- private transaction nonce 要帶入 private nonce,代表某 account 在某 group 中的 nonce,必須算出正確的 privacy group ID,搭配 account 透過 Besu API priv_getTransactionCount 取得。
- Besu private transaction 簽名後的 V 值需被改寫。
newV := v.Uint64() + chainID.Uint64()*2 + 8
- private transaction Rlp encode 後的 raw transaction data 需要處理,移除第 2 - 4 位置。
besuRawTxData, _ := rlp.EncodeToBytes(besuSignedTx) besuRawTxData = append(besuRawTxData[:1], besuRawTxData[4:]...)
- 執行完 private transaction 後,account 的 public nonce 不會改變,因此外人不知道此 account 發交易,具有高隱私性。
- eth_getTransactionReceipt 取得的 public receipt,blockHash 與 blockNumber 有參考價值,而 contractAddress、from 與 to 則被遮罩,確保雙方與合約資訊不洩漏。
- priv_getTransactionReceipt 取得的 private receipt,包含正確的 contractAddress 與 from 值。透過 privateFrom 與 privateFor 資訊,知道 group 成員與發起交易者,這點 Besu 比 Quorum 做得更好。
結語
本文解析 Besu 在 web3js-eea 產生 private raw transaction 方式,並為了增加安全性,將私交易簽名邏輯抽離,透過 go-ethereum 實作 private raw transaction 的全流程。
如果你對 Quorum有研究,看完 Besu 後,你會發現 Besu 在隱私交易和聯盟治理的實作上,更遵守 EEA 的規範,因此將來在互操作性(Interoperability)上會有更好的表現。筆者任職的 BSOS 是台灣少數專注在聯盟鏈及企業應用的區塊鏈公司,BSOS 的核心技術 BridgeX 是區塊鏈中間層(Middleware)微服務架構,對於 EE 有高度的支援,除了 Quorum 之外( BSOS 是 J.P. Morgan Quorum 在台唯一的技術大使),Besu 也被完美整合到了 BridgeX 內。我們將持續關注國際「企業區塊鏈/聯盟鏈」的技術發展,並透過文章與大家分享交流。這塊領域還在快速發展中,歡迎志同道合的朋友們,多多與我們聯繫指教。
參考資料
https://medium.com/bsos-taiwan/what-problems-has-entriprise-ethereum-solved-b4fde233342f
https://docs.ethsigner.pegasys.tech/en/latest/HowTo/Transactions/Make-Transactions
https://besu.hyperledger.org/en/stable/Tutorials/Examples/Privacy-Example
https://github.com/PegaSysEng/besu-sample-networks.git
https://besu.hyperledger.org/en/stable/Reference/API-Methods
https://entethalliance.github.io/client-spec/spec.html
https://besu.hyperledger.org/en/stable/Reference/web3js-eea-Methods
喜欢我的作品吗?别忘了给予支持与赞赏,让我知道在创作的路上有你陪伴,一起延续这份热忱!
- 来自作者