Skip to content

Commit

Permalink
Quorum private transaction support for Abigen (#819)
Browse files Browse the repository at this point in the history
Support private transaction for abigen and update private abigen docs
  • Loading branch information
zzy96 authored and jpmsam committed Oct 1, 2019
1 parent e127852 commit 26bab38
Show file tree
Hide file tree
Showing 12 changed files with 259 additions and 13 deletions.
4 changes: 3 additions & 1 deletion accounts/abi/bind/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion accounts/abi/bind/backends/simulated.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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.
//
Expand Down
47 changes: 45 additions & 2 deletions accounts/abi/bind/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion accounts/abi/bind/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 3 additions & 1 deletion cmd/faucet/faucet.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down
9 changes: 9 additions & 0 deletions docs/private-abigen.md
Original file line number Diff line number Diff line change
@@ -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.
37 changes: 33 additions & 4 deletions ethclient/ethclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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.
Expand All @@ -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() {
Expand Down Expand Up @@ -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{} {
Expand Down
29 changes: 29 additions & 0 deletions ethclient/ethclient_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import (
"reflect"
"testing"

"github.com/stretchr/testify/assert"

"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
)
Expand Down Expand Up @@ -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
}
71 changes: 71 additions & 0 deletions ethclient/privateTransactionManagerClient.go
Original file line number Diff line number Diff line change
@@ -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
}
53 changes: 53 additions & 0 deletions ethclient/privateTransactionManagerClient_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading

0 comments on commit 26bab38

Please sign in to comment.