From 26bab38682c4e53e9f734b5d40a7db6128a9f302 Mon Sep 17 00:00:00 2001 From: Zhou Zhiyao Date: Wed, 2 Oct 2019 00:49:26 +0800 Subject: [PATCH] Quorum private transaction support for Abigen (#819) Support private transaction for abigen and update private abigen docs --- accounts/abi/bind/backend.go | 4 +- accounts/abi/bind/backends/simulated.go | 7 +- accounts/abi/bind/base.go | 47 +++++++++++- accounts/abi/bind/util_test.go | 2 +- cmd/faucet/faucet.go | 4 +- docs/private-abigen.md | 9 +++ ethclient/ethclient.go | 37 ++++++++-- ethclient/ethclient_test.go | 29 ++++++++ ethclient/privateTransactionManagerClient.go | 71 +++++++++++++++++++ .../privateTransactionManagerClient_test.go | 53 ++++++++++++++ internal/ethapi/api.go | 5 +- mobile/ethclient.go | 4 +- 12 files changed, 259 insertions(+), 13 deletions(-) create mode 100644 docs/private-abigen.md create mode 100644 ethclient/privateTransactionManagerClient.go create mode 100644 ethclient/privateTransactionManagerClient_test.go diff --git a/accounts/abi/bind/backend.go b/accounts/abi/bind/backend.go index ca60cc1b4..e8f5b1faf 100644 --- a/accounts/abi/bind/backend.go +++ b/accounts/abi/bind/backend.go @@ -81,7 +81,9 @@ type ContractTransactor interface { // for setting a reasonable default. EstimateGas(ctx context.Context, call ethereum.CallMsg) (gas uint64, err error) // SendTransaction injects the transaction into the pending pool for execution. - SendTransaction(ctx context.Context, tx *types.Transaction) error + SendTransaction(ctx context.Context, tx *types.Transaction, args PrivateTxArgs) error + // PreparePrivateTransaction send the private transaction to Tessera/Constellation's /storeraw API using HTTP + PreparePrivateTransaction(data []byte, privateFrom string) ([]byte, error) } // ContractFilterer defines the methods needed to access log events using one-off diff --git a/accounts/abi/bind/backends/simulated.go b/accounts/abi/bind/backends/simulated.go index fe06cb70a..18468fb8a 100644 --- a/accounts/abi/bind/backends/simulated.go +++ b/accounts/abi/bind/backends/simulated.go @@ -293,7 +293,7 @@ func (b *SimulatedBackend) callContract(ctx context.Context, call ethereum.CallM // SendTransaction updates the pending block to include the given transaction. // It panics if the transaction is invalid. -func (b *SimulatedBackend) SendTransaction(ctx context.Context, tx *types.Transaction) error { +func (b *SimulatedBackend) SendTransaction(ctx context.Context, tx *types.Transaction, args bind.PrivateTxArgs) error { b.mu.Lock() defer b.mu.Unlock() @@ -319,6 +319,11 @@ func (b *SimulatedBackend) SendTransaction(ctx context.Context, tx *types.Transa return nil } +// PreparePrivateTransaction dummy implementation +func (b *SimulatedBackend) PreparePrivateTransaction(data []byte, privateFrom string) ([]byte, error) { + return data, nil +} + // FilterLogs executes a log filter operation, blocking during execution and // returning all the results in one batch. // diff --git a/accounts/abi/bind/base.go b/accounts/abi/bind/base.go index 83ad1c8ae..c53a77a2f 100644 --- a/accounts/abi/bind/base.go +++ b/accounts/abi/bind/base.go @@ -34,6 +34,13 @@ import ( // sign the transaction before submission. type SignerFn func(types.Signer, common.Address, *types.Transaction) (*types.Transaction, error) +// Quorum +// +// Additional arguments in order to support transaction privacy +type PrivateTxArgs struct { + PrivateFor []string `json:"privateFor"` +} + // CallOpts is the collection of options to fine tune a contract call request. type CallOpts struct { Pending bool // Whether to operate on the pending state or the last known one @@ -54,6 +61,10 @@ type TransactOpts struct { GasLimit uint64 // Gas limit to set for the transaction execution (0 = estimate) Context context.Context // Network context to support cancellation and timeouts (nil = no timeout) + + // Quorum + PrivateFrom string // The public key of the Tessera/Constellation identity to send this tx from. + PrivateFor []string // The public keys of the Tessera/Constellation identities this tx is intended for. } // FilterOpts is the collection of options to fine tune filtering for events @@ -231,16 +242,36 @@ func (c *BoundContract) transact(opts *TransactOpts, contract *common.Address, i } else { rawTx = types.NewTransaction(nonce, c.address, value, gasLimit, gasPrice, input) } + + // If this transaction is private, we need to substitute the data payload + // with the hash of the transaction from tessera/constellation. + if opts.PrivateFor != nil { + var payload []byte + payload, err = c.transactor.PreparePrivateTransaction(rawTx.Data(), opts.PrivateFrom) + if err != nil { + return nil, err + } + rawTx = c.createPrivateTransaction(rawTx, payload) + } + + // Choose signer to sign transaction if opts.Signer == nil { return nil, errors.New("no signer to authorize the transaction with") } - signedTx, err := opts.Signer(types.HomesteadSigner{}, opts.From, rawTx) + var signedTx *types.Transaction + if rawTx.IsPrivate() { + signedTx, err = opts.Signer(types.QuorumPrivateTxSigner{}, opts.From, rawTx) + } else { + signedTx, err = opts.Signer(types.HomesteadSigner{}, opts.From, rawTx) + } if err != nil { return nil, err } - if err := c.transactor.SendTransaction(ensureContext(opts.Context), signedTx); err != nil { + + if err := c.transactor.SendTransaction(ensureContext(opts.Context), signedTx, PrivateTxArgs{PrivateFor: opts.PrivateFor}); err != nil { return nil, err } + return signedTx, nil } @@ -340,6 +371,18 @@ func (c *BoundContract) UnpackLog(out interface{}, event string, log types.Log) return parseTopics(out, indexed, log.Topics[1:]) } +// createPrivateTransaction replaces the payload of private transaction to the hash from Tessera/Constellation +func (c *BoundContract) createPrivateTransaction(tx *types.Transaction, payload []byte) *types.Transaction { + var privateTx *types.Transaction + if tx.To() == nil { + privateTx = types.NewContractCreation(tx.Nonce(), tx.Value(), tx.Gas(), tx.GasPrice(), payload) + } else { + privateTx = types.NewTransaction(tx.Nonce(), c.address, tx.Value(), tx.Gas(), tx.GasPrice(), payload) + } + privateTx.SetPrivate() + return privateTx +} + // ensureContext is a helper method to ensure a context is not nil, even if the // user specified it as such. func ensureContext(ctx context.Context) context.Context { diff --git a/accounts/abi/bind/util_test.go b/accounts/abi/bind/util_test.go index 8f4092971..d9ec08d58 100644 --- a/accounts/abi/bind/util_test.go +++ b/accounts/abi/bind/util_test.go @@ -76,7 +76,7 @@ func TestWaitDeployed(t *testing.T) { }() // Send and mine the transaction. - backend.SendTransaction(ctx, tx) + backend.SendTransaction(ctx, tx, bind.PrivateTxArgs{}) backend.Commit() select { diff --git a/cmd/faucet/faucet.go b/cmd/faucet/faucet.go index 2ffe12276..18503b7ed 100644 --- a/cmd/faucet/faucet.go +++ b/cmd/faucet/faucet.go @@ -41,6 +41,8 @@ import ( "sync" "time" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/common" @@ -483,7 +485,7 @@ func (f *faucet) apiHandler(conn *websocket.Conn) { continue } // Submit the transaction and mark as funded if successful - if err := f.client.SendTransaction(context.Background(), signed); err != nil { + if err := f.client.SendTransaction(context.Background(), signed, bind.PrivateTxArgs{}); err != nil { f.lock.Unlock() if err = sendError(conn, err); err != nil { log.Warn("Failed to send transaction transmission error to client", "err", err) diff --git a/docs/private-abigen.md b/docs/private-abigen.md new file mode 100644 index 000000000..a6d595972 --- /dev/null +++ b/docs/private-abigen.md @@ -0,0 +1,9 @@ +# Abigen with Quorum + +### Overview + +Abigen is a source code generator that converts smart contract ABI definitions into type-safe Go packages. In addition to the original capabilities provided by Ethereum described [here](https://github.com/ethereum/go-ethereum/wiki/Native-DApps:-Go-bindings-to-Ethereum-contracts). Quorum Abigen also supports private transactions. + +### Implementation + +`PrivateFrom` and `PrivateFor` fields have been added to the `bind.TransactOpts` which allows users to specify the public keys of the transaction manager (Tessera/Constellation) used to send and receive private transactions. The existing `ethclient` has been extended with a private transaction manager client to support sending `/storeraw` request. diff --git a/ethclient/ethclient.go b/ethclient/ethclient.go index f3163e19b..38a9c80fd 100644 --- a/ethclient/ethclient.go +++ b/ethclient/ethclient.go @@ -24,6 +24,8 @@ import ( "fmt" "math/big" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" @@ -34,7 +36,8 @@ import ( // Client defines typed wrappers for the Ethereum RPC API. type Client struct { - c *rpc.Client + c *rpc.Client + pc privateTransactionManagerClient // Tessera/Constellation client } // Dial connects a client to the given URL. @@ -52,7 +55,19 @@ func DialContext(ctx context.Context, rawurl string) (*Client, error) { // NewClient creates a client that uses the given RPC client. func NewClient(c *rpc.Client) *Client { - return &Client{c} + return &Client{c, nil} +} + +// Quorum +// +// provides support for private transactions +func (ec *Client) WithPrivateTransactionManager(rawurl string) (*Client, error) { + var err error + ec.pc, err = newPrivateTransactionManagerClient(rawurl) + if err != nil { + return nil, err + } + return ec, nil } func (ec *Client) Close() { @@ -498,12 +513,26 @@ func (ec *Client) EstimateGas(ctx context.Context, msg ethereum.CallMsg) (uint64 // // If the transaction was a contract creation use the TransactionReceipt method to get the // contract address after the transaction has been mined. -func (ec *Client) SendTransaction(ctx context.Context, tx *types.Transaction) error { +func (ec *Client) SendTransaction(ctx context.Context, tx *types.Transaction, args bind.PrivateTxArgs) error { data, err := rlp.EncodeToBytes(tx) if err != nil { return err } - return ec.c.CallContext(ctx, nil, "eth_sendRawTransaction", common.ToHex(data)) + if args.PrivateFor != nil { + return ec.c.CallContext(ctx, nil, "eth_sendRawPrivateTransaction", common.ToHex(data), bind.PrivateTxArgs{PrivateFor: args.PrivateFor}) + } else { + return ec.c.CallContext(ctx, nil, "eth_sendRawTransaction", common.ToHex(data)) + } +} + +// Quorum +// +// Retrieve encrypted payload hash from the private transaction manager if configured +func (ec *Client) PreparePrivateTransaction(data []byte, privateFrom string) ([]byte, error) { + if ec.pc == nil { + return nil, errors.New("missing private transaction manager client configuration") + } + return ec.pc.storeRaw(data, privateFrom) } func toCallArg(msg ethereum.CallMsg) interface{} { diff --git a/ethclient/ethclient_test.go b/ethclient/ethclient_test.go index 3e8bf974c..96968b1a7 100644 --- a/ethclient/ethclient_test.go +++ b/ethclient/ethclient_test.go @@ -22,6 +22,8 @@ import ( "reflect" "testing" + "github.com/stretchr/testify/assert" + "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" ) @@ -150,3 +152,30 @@ func TestToFilterArg(t *testing.T) { }) } } + +func TestClient_PreparePrivateTransaction_whenTypical(t *testing.T) { + testObject := NewClient(nil) + + _, err := testObject.PreparePrivateTransaction([]byte("arbitrary payload"), "arbitrary private from") + + assert.Error(t, err) +} + +func TestClient_PreparePrivateTransaction_whenClientIsConfigured(t *testing.T) { + expectedData := []byte("arbitrary data") + testObject := NewClient(nil) + testObject.pc = &privateTransactionManagerStubClient{expectedData} + + actualData, err := testObject.PreparePrivateTransaction([]byte("arbitrary payload"), "arbitrary private from") + + assert.NoError(t, err) + assert.Equal(t, expectedData, actualData) +} + +type privateTransactionManagerStubClient struct { + expectedData []byte +} + +func (s *privateTransactionManagerStubClient) storeRaw(data []byte, privateFrom string) ([]byte, error) { + return s.expectedData, nil +} diff --git a/ethclient/privateTransactionManagerClient.go b/ethclient/privateTransactionManagerClient.go new file mode 100644 index 000000000..0d4d6470b --- /dev/null +++ b/ethclient/privateTransactionManagerClient.go @@ -0,0 +1,71 @@ +package ethclient + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "net/http" + "net/url" +) + +type privateTransactionManagerClient interface { + storeRaw(data []byte, privateFrom string) ([]byte, error) +} + +type privateTransactionManagerDefaultClient struct { + rawurl string + httpClient *http.Client +} + +// Create a new client to interact with private transaction manager via a HTTP endpoint +func newPrivateTransactionManagerClient(endpoint string) (privateTransactionManagerClient, error) { + _, err := url.Parse(endpoint) + if err != nil { + return nil, err + } + return &privateTransactionManagerDefaultClient{ + rawurl: endpoint, + httpClient: &http.Client{}, + }, nil +} + +type storeRawReq struct { + Payload string `json:"payload"` + From string `json:"from,omitempty"` +} + +type storeRawResp struct { + Key string `json:"key"` +} + +func (pc *privateTransactionManagerDefaultClient) storeRaw(data []byte, privateFrom string) ([]byte, error) { + storeRawReq := &storeRawReq{ + Payload: base64.StdEncoding.EncodeToString(data), + From: privateFrom, + } + reqBodyBuf := new(bytes.Buffer) + if err := json.NewEncoder(reqBodyBuf).Encode(storeRawReq); err != nil { + return nil, err + } + resp, err := pc.httpClient.Post(pc.rawurl+"/storeraw", "application/json", reqBodyBuf) + if err != nil { + return nil, fmt.Errorf("unable to invoke /storeraw due to %s", err) + } + defer func() { + _ = resp.Body.Close() + }() + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("server returns %s", resp.Status) + } + // parse response + var storeRawResp storeRawResp + if err := json.NewDecoder(resp.Body).Decode(&storeRawResp); err != nil { + return nil, err + } + encryptedPayloadHash, err := base64.StdEncoding.DecodeString(storeRawResp.Key) + if err != nil { + return nil, err + } + return encryptedPayloadHash, nil +} diff --git a/ethclient/privateTransactionManagerClient_test.go b/ethclient/privateTransactionManagerClient_test.go new file mode 100644 index 000000000..8b4e3b764 --- /dev/null +++ b/ethclient/privateTransactionManagerClient_test.go @@ -0,0 +1,53 @@ +package ethclient + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +const ( + arbitraryBase64Data = "YXJiaXRyYXJ5IGRhdGE=" // = "arbitrary data" +) + +func TestPrivateTransactionManagerClient_storeRaw(t *testing.T) { + // mock tessera client + arbitraryServer := newStoreRawServer() + defer arbitraryServer.Close() + testObject, err := newPrivateTransactionManagerClient(arbitraryServer.URL) + assert.NoError(t, err) + + key, err := testObject.storeRaw([]byte("arbitrary payload"), "arbitrary private from") + + assert.NoError(t, err) + assert.Equal(t, "arbitrary data", string(key)) +} + +func newStoreRawServer() *httptest.Server { + arbitraryResponse := fmt.Sprintf(` +{ + "key": "%s" +} +`, arbitraryBase64Data) + mux := http.NewServeMux() + mux.HandleFunc("/storeraw", func(w http.ResponseWriter, req *http.Request) { + if req.Method == "POST" { + // parse request + var storeRawReq storeRawReq + if err := json.NewDecoder(req.Body).Decode(&storeRawReq); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + // send response + _, _ = fmt.Fprintf(w, "%s", arbitraryResponse) + } else { + http.Error(w, "Invalid request method", http.StatusMethodNotAllowed) + } + + }) + return httptest.NewServer(mux) +} diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 9faea5827..9c5f1ab03 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -29,9 +29,10 @@ import ( "encoding/json" "net/http" - "github.com/davecgh/go-spew/spew" "sync" + "github.com/davecgh/go-spew/spew" + "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/common" @@ -1592,7 +1593,7 @@ func (s *PublicTransactionPoolAPI) Resend(ctx context.Context, sendArgs SendTxAr } newTx := sendArgs.toTransaction() // set v param to 37 to indicate private tx before submitting to the signer. - if len(sendArgs.PrivateFor) > 0 { + if sendArgs.PrivateFor != nil { newTx.SetPrivate() } signedTx, err := s.sign(sendArgs.From, newTx) diff --git a/mobile/ethclient.go b/mobile/ethclient.go index 662125c4a..32cd8b199 100644 --- a/mobile/ethclient.go +++ b/mobile/ethclient.go @@ -21,6 +21,8 @@ package geth import ( "math/big" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethclient" ) @@ -312,5 +314,5 @@ func (ec *EthereumClient) EstimateGas(ctx *Context, msg *CallMsg) (gas int64, _ // If the transaction was a contract creation use the TransactionReceipt method to get the // contract address after the transaction has been mined. func (ec *EthereumClient) SendTransaction(ctx *Context, tx *Transaction) error { - return ec.client.SendTransaction(ctx.context, tx.tx) + return ec.client.SendTransaction(ctx.context, tx.tx, bind.PrivateTxArgs{}) }