Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

eth/client: Sign message #1228

Merged
merged 1 commit into from
Oct 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 27 additions & 4 deletions client/asset/eth/eth.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import (
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/crypto/secp256k1"
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/p2p"
)
Expand Down Expand Up @@ -141,6 +143,7 @@ type ethFetcher interface {
refund(opts *bind.TransactOpts, netID int64, secretHash [32]byte) (*types.Transaction, error)
swap(ctx context.Context, from *accounts.Account, secretHash [32]byte) (*swap.ETHSwapSwap, error)
unlock(ctx context.Context, pw string, acct *accounts.Account) error
signData(addr common.Address, data []byte) ([]byte, error)
}

// Check that ExchangeWallet satisfies the asset.Wallet interface.
Expand Down Expand Up @@ -630,10 +633,30 @@ func (*ExchangeWallet) Redeem(form *asset.RedeemForm) ([]dex.Bytes, asset.Coin,
}

// SignMessage signs the message with the private key associated with the
// specified funding Coin. A slice of pubkeys required to spend the Coin and a
// signature for each pubkey are returned.
func (*ExchangeWallet) SignMessage(coin asset.Coin, msg dex.Bytes) (pubkeys, sigs []dex.Bytes, err error) {
return nil, nil, asset.ErrNotImplemented
// specified funding Coin. Only a coin that came from the address this wallet
// is initialized with can be used to sign.
func (e *ExchangeWallet) SignMessage(coin asset.Coin, msg dex.Bytes) (pubkeys, sigs []dex.Bytes, err error) {
ethCoin, err := decodeCoinID(coin.ID())
if err != nil {
return nil, nil, err
}

if !bytes.Equal(ethCoin.id.Address.Bytes(), e.acct.Address.Bytes()) {
return nil, nil, fmt.Errorf("SignMessage: coin address: %v != wallet address: %v",
ethCoin.id.Address, e.acct.Address)
}

sig, err := e.node.signData(e.acct.Address, msg)
if err != nil {
return nil, nil, fmt.Errorf("SignMessage: error signing data: %v", err)
}

pubKey, err := secp256k1.RecoverPubkey(crypto.Keccak256(msg), sig)
if err != nil {
return nil, nil, fmt.Errorf("SignMessage: error recovering pubkey %v", err)
}

return []dex.Bytes{pubKey}, []dex.Bytes{sig}, nil
}

// AuditContract retrieves information about a swap contract on the
Expand Down
123 changes: 106 additions & 17 deletions client/asset/eth/eth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package eth

import (
"context"
"crypto/ecdsa"
"encoding/hex"
"errors"
"math/big"
Expand All @@ -23,6 +24,8 @@ import (
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/crypto/secp256k1"
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/p2p"
)
Expand All @@ -33,23 +36,25 @@ var (
)

type testNode struct {
connectErr error
bestHdr *types.Header
bestHdrErr error
bestBlkHash common.Hash
bestBlkHashErr error
blk *types.Block
blkErr error
blkNum uint64
blkNumErr error
syncProg *ethereum.SyncProgress
syncProgErr error
peerInfo []*p2p.PeerInfo
peersErr error
bal *big.Int
balErr error
initGas uint64
initGasErr error
connectErr error
bestHdr *types.Header
bestHdrErr error
bestBlkHash common.Hash
bestBlkHashErr error
blk *types.Block
blkErr error
blkNum uint64
blkNumErr error
syncProg *ethereum.SyncProgress
syncProgErr error
peerInfo []*p2p.PeerInfo
peersErr error
bal *big.Int
balErr error
initGas uint64
initGasErr error
signDataErr error
privKeyForSigning *ecdsa.PrivateKey
}

func (n *testNode) connect(ctx context.Context, node *node.Node, addr *common.Address) error {
Expand Down Expand Up @@ -127,6 +132,17 @@ func (n *testNode) peers(ctx context.Context) ([]*p2p.PeerInfo, error) {
func (n *testNode) estimateGas(ctx context.Context, callMsg ethereum.CallMsg) (uint64, error) {
return n.initGas, n.initGasErr
}
func (n *testNode) signData(addr common.Address, data []byte) ([]byte, error) {
if n.signDataErr != nil {
return nil, n.signDataErr
}

if n.privKeyForSigning == nil {
return nil, nil
}

return crypto.Sign(crypto.Keccak256(data), n.privKeyForSigning)
}

func TestLoadConfig(t *testing.T) {
tests := []struct {
Expand Down Expand Up @@ -1019,3 +1035,76 @@ func TestMaxOrder(t *testing.T) {
}
}
}

func TestSignMessage(t *testing.T) {
node := &testNode{}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
privKey, _ := crypto.HexToECDSA("9447129055a25c8496fca9e5ee1b9463e47e6043ff0c288d07169e8284860e34")
node.privKeyForSigning = privKey
address := "2b84C791b79Ee37De042AD2ffF1A253c3ce9bc27"
account := accounts.Account{
Address: common.HexToAddress(address),
}
eth := &ExchangeWallet{
node: node,
ctx: ctx,
log: tLogger,
acct: &account,
}

msg := []byte("msg")

// Error due to coin with unparsable ID
var badCoin badCoin
_, _, err := eth.SignMessage(&badCoin, msg)
if err == nil {
t.Fatalf("expected error for signing message with bad coin")
}

// Error due to coin from with account than wallet
differentAddress := common.HexToAddress("345853e21b1d475582E71cC269124eD5e2dD3422")
nonce := [8]byte{}
coinDifferentAddress := coin{
id: dexeth.AmountCoinID{
Address: differentAddress,
Amount: 100,
Nonce: nonce,
},
}
_, _, err = eth.SignMessage(&coinDifferentAddress, msg)
if err == nil {
t.Fatalf("expected error for signing message with different address than wallet")
}

coin := coin{
id: dexeth.AmountCoinID{
Address: account.Address,
Amount: 100,
Nonce: nonce,
},
}

// SignData error
node.signDataErr = errors.New("")
_, _, err = eth.SignMessage(&coin, msg)
if err == nil {
t.Fatalf("expected error due to error in rpcclient signData")
}
node.signDataErr = nil

// Test no error
pubKeys, sigs, err := eth.SignMessage(&coin, msg)
if err != nil {
t.Fatalf("unexpected error signing message: %v", err)
}
if len(pubKeys) != 1 {
t.Fatalf("expected 1 pubKey but got %v", len(pubKeys))
}
if len(sigs) != 1 {
t.Fatalf("expected 1 signature but got %v", len(sigs))
}
if !secp256k1.VerifySignature(pubKeys[0], crypto.Keccak256(msg), sigs[0][:len(sigs[0])-1]) {
t.Fatalf("failed to verify signature")
}
}
14 changes: 14 additions & 0 deletions client/asset/eth/rpcclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,20 @@ func (c *rpcclient) wallet(acct accounts.Account) (accounts.Wallet, error) {
return wallet, nil
}

// signData uses the private key of the address to sign a piece of data.
// The address must have been imported and unlocked to use this function.
func (c *rpcclient) signData(addr common.Address, data []byte) ([]byte, error) {
account := accounts.Account{Address: addr}
wallet, err := c.wallet(account)
if err != nil {
return nil, err
}

// The mime type argument to SignData is not used in the keystore wallet in geth.
// It treats any data like plain text.
return wallet.SignData(account, accounts.MimetypeTextPlain, data)
}

func (c *rpcclient) addSignerToOpts(txOpts *bind.TransactOpts, netID int64) error {
wallet, err := c.wallet(accounts.Account{Address: txOpts.From})
if err != nil {
Expand Down
69 changes: 48 additions & 21 deletions client/asset/eth/rpcclient_harness_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ package eth
import (
"bytes"
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/sha256"
"encoding/hex"
"errors"
Expand All @@ -40,16 +42,16 @@ import (
"decred.org/dcrdex/client/asset"
"decred.org/dcrdex/dex"
"decred.org/dcrdex/dex/encode"
dexeth "decred.org/dcrdex/dex/networks/eth"
swap "decred.org/dcrdex/dex/networks/eth"
"decred.org/dcrdex/internal/eth/reentryattack"
"decred.org/dcrdex/server/asset/eth"
"github.com/davecgh/go-spew/spew"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/crypto/secp256k1"
)

const (
Expand All @@ -59,21 +61,23 @@ const (
)

var (
gasPrice = big.NewInt(82e9)
homeDir = os.Getenv("HOME")
contractAddrFile = filepath.Join(homeDir, "dextest", "eth", "contract_addr.txt")
testDir = filepath.Join(homeDir, "dextest", "eth", "client_rpc_tests")
alphaNodeDir = filepath.Join(homeDir, "dextest", "eth", "alpha", "node")
ethClient = new(rpcclient)
ctx context.Context
tLogger = dex.StdOutLogger("ETHTEST", dex.LevelTrace)
simnetAddr = common.HexToAddress("2b84C791b79Ee37De042AD2ffF1A253c3ce9bc27")
simnetAcct = &accounts.Account{Address: simnetAddr}
participantAddr = common.HexToAddress("345853e21b1d475582E71cC269124eD5e2dD3422")
participantAcct = &accounts.Account{Address: participantAddr}
contractAddr common.Address
simnetID = int64(42)
newTxOpts = func(ctx context.Context, from *common.Address, value *big.Int) *bind.TransactOpts {
gasPrice = big.NewInt(82e9)
homeDir = os.Getenv("HOME")
contractAddrFile = filepath.Join(homeDir, "dextest", "eth", "contract_addr.txt")
testDir = filepath.Join(homeDir, "dextest", "eth", "client_rpc_tests")
alphaNodeDir = filepath.Join(homeDir, "dextest", "eth", "alpha", "node")
ethClient = new(rpcclient)
ctx context.Context
tLogger = dex.StdOutLogger("ETHTEST", dex.LevelTrace)
simnetPrivKey = "9447129055a25c8496fca9e5ee1b9463e47e6043ff0c288d07169e8284860e34"
simnetAddr = common.HexToAddress("2b84C791b79Ee37De042AD2ffF1A253c3ce9bc27")
simnetAcct = &accounts.Account{Address: simnetAddr}
participantPrivKey = "0695b9347a4dc096ae5c6f1935380ceba550c70b112f1323c211bade4d11651a"
participantAddr = common.HexToAddress("345853e21b1d475582E71cC269124eD5e2dD3422")
participantAcct = &accounts.Account{Address: participantAddr}
contractAddr common.Address
simnetID = int64(42)
newTxOpts = func(ctx context.Context, from *common.Address, value *big.Int) *bind.TransactOpts {
return &bind.TransactOpts{
GasPrice: gasPrice,
GasLimit: 1e6,
Expand Down Expand Up @@ -237,8 +241,7 @@ func TestImportAccounts(t *testing.T) {
fmt.Println("Skipping TestImportAccounts because accounts are already imported.")
t.Skip()
}
// The address of this will be 2b84C791b79Ee37De042AD2ffF1A253c3ce9bc27.
privB, err := hex.DecodeString("9447129055a25c8496fca9e5ee1b9463e47e6043ff0c288d07169e8284860e34")
privB, err := hex.DecodeString(simnetPrivKey)
if err != nil {
t.Fatal(err)
}
Expand All @@ -247,8 +250,7 @@ func TestImportAccounts(t *testing.T) {
t.Fatal(err)
}
spew.Dump(acct)
// The address of this will be 345853e21b1d475582E71cC269124eD5e2dD3422.
privB, err = hex.DecodeString("0695b9347a4dc096ae5c6f1935380ceba550c70b112f1323c211bade4d11651a")
privB, err = hex.DecodeString(participantPrivKey)
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -938,3 +940,28 @@ func TestGetCodeAt(t *testing.T) {
t.Fatal("Contract on chain does not match one in code")
}
}

func TestSignMessage(t *testing.T) {
msg := []byte("test message")
err := ethClient.unlock(ctx, pw, simnetAcct)
if err != nil {
t.Fatalf("error unlocking account: %v", err)
}
signature, err := ethClient.signData(simnetAddr, msg)
if err != nil {
t.Fatalf("error signing text: %v", err)
}
pubKey, err := secp256k1.RecoverPubkey(crypto.Keccak256(msg), signature)
x, y := elliptic.Unmarshal(secp256k1.S256(), pubKey)
recoveredAddress := crypto.PubkeyToAddress(ecdsa.PublicKey{
Curve: secp256k1.S256(),
X: x,
Y: y,
})
if !bytes.Equal(recoveredAddress.Bytes(), simnetAcct.Address.Bytes()) {
t.Fatalf("recovered address: %v != simnet account address: %v", recoveredAddress, simnetAcct.Address)
}
if !secp256k1.VerifySignature(pubKey, crypto.Keccak256(msg), signature[:len(signature)-1]) {
t.Fatalf("failed to verify signature")
}
}