Quorum private transaction support for Abigen (#819)

Support private transaction for abigen and update private abigen docs
This commit is contained in:
Zhou Zhiyao 2019-10-02 00:49:26 +08:00 committed by Samer Falah
parent e1278520d0
commit 26bab38682
12 changed files with 259 additions and 13 deletions

View File

@ -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

View File

@ -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.
//

View File

@ -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 {

View File

@ -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 {

View File

@ -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)

9
docs/private-abigen.md Normal file
View File

@ -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.

View File

@ -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{} {

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)

View File

@ -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{})
}