mirror of https://github.com/poanetwork/quorum.git
Quorum private transaction support for Abigen (#819)
Support private transaction for abigen and update private abigen docs
This commit is contained in:
parent
e1278520d0
commit
26bab38682
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
//
|
||||
|
|
|
@ -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 opts.Signer == nil {
|
||||
return nil, errors.New("no signer to authorize the transaction with")
|
||||
}
|
||||
signedTx, err := opts.Signer(types.HomesteadSigner{}, opts.From, rawTx)
|
||||
|
||||
// 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
|
||||
}
|
||||
if err := c.transactor.SendTransaction(ensureContext(opts.Context), signedTx); err != nil {
|
||||
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")
|
||||
}
|
||||
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, 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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
|
@ -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"
|
||||
|
@ -35,6 +37,7 @@ import (
|
|||
// Client defines typed wrappers for the Ethereum RPC API.
|
||||
type Client struct {
|
||||
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,13 +513,27 @@ 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
|
||||
}
|
||||
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{} {
|
||||
arg := map[string]interface{}{
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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{})
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue