Commit 22d7e53b authored by Ralph Pichler's avatar Ralph Pichler Committed by GitHub

add cheque signing (#720)

parent b3607c89
......@@ -9,6 +9,7 @@ import (
"context"
"fmt"
"io/ioutil"
"os"
"os/signal"
"path/filepath"
......@@ -17,6 +18,7 @@ import (
"time"
"github.com/ethereum/go-ethereum/accounts/external"
"github.com/ethereum/go-ethereum/rpc"
"github.com/ethersphere/bee/pkg/crypto"
"github.com/ethersphere/bee/pkg/crypto/clef"
"github.com/ethersphere/bee/pkg/keystore"
......@@ -143,7 +145,12 @@ Welcome to the Swarm.... Bzzz Bzzzz Bzzzz
return err
}
signer, err = clef.NewSigner(externalSigner, crypto.Recover)
clefRPC, err := rpc.Dial(endpoint)
if err != nil {
return err
}
signer, err = clef.NewSigner(externalSigner, clefRPC, crypto.Recover)
if err != nil {
return err
}
......
......@@ -14,8 +14,10 @@ import (
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethersphere/bee/pkg/crypto"
"github.com/ethersphere/bee/pkg/crypto/eip712"
)
var (
......@@ -23,20 +25,26 @@ var (
clefRecoveryMessage = []byte("public key recovery message")
)
// ExternalSignerInterface is the interface for the clef client from go-ethereum
// ExternalSignerInterface is the interface for the clef client from go-ethereum.
type ExternalSignerInterface interface {
SignData(account accounts.Account, mimeType string, data []byte) ([]byte, error)
SignTx(account accounts.Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error)
Accounts() []accounts.Account
}
// Client is the interface for rpc.RpcClient.
type Client interface {
Call(result interface{}, method string, args ...interface{}) error
}
type clefSigner struct {
client Client // low-level rpc client to clef as ExternalSigner does not implement account_signTypedData
clef ExternalSignerInterface
account accounts.Account // the account this signer will use
pubKey *ecdsa.PublicKey // the public key for the account
}
// DefaultIpcPath returns the os-dependent default ipc path for clef
// DefaultIpcPath returns the os-dependent default ipc path for clef.
func DefaultIpcPath() (string, error) {
socket := "clef.ipc"
// on windows clef uses top level pipes
......@@ -58,9 +66,9 @@ func DefaultIpcPath() (string, error) {
return filepath.Join(home, ".clef", socket), nil
}
// NewSigner creates a new connection to the signer at endpoint
// As clef does not expose public keys it signs a test message to recover the public key
func NewSigner(clef ExternalSignerInterface, recoverFunc crypto.RecoverFunc) (signer crypto.Signer, err error) {
// NewSigner creates a new connection to the signer at endpoint.
// As clef does not expose public keys it signs a test message to recover the public key.
func NewSigner(clef ExternalSignerInterface, client Client, recoverFunc crypto.RecoverFunc) (signer crypto.Signer, err error) {
// get the list of available ethereum accounts
clefAccounts := clef.Accounts()
if len(clefAccounts) == 0 {
......@@ -83,29 +91,41 @@ func NewSigner(clef ExternalSignerInterface, recoverFunc crypto.RecoverFunc) (si
}
return &clefSigner{
client: client,
clef: clef,
account: account,
pubKey: pubKey,
}, nil
}
// PublicKey returns the public key recovered during creation
// PublicKey returns the public key recovered during creation.
func (c *clefSigner) PublicKey() (*ecdsa.PublicKey, error) {
return c.pubKey, nil
}
// SignData signs with the text/plain type which is the standard Ethereum prefix method
// SignData signs with the text/plain type which is the standard Ethereum prefix method.
func (c *clefSigner) Sign(data []byte) ([]byte, error) {
return c.clef.SignData(c.account, accounts.MimetypeTextPlain, data)
}
// SignTx signs an ethereum transaction
// SignTx signs an ethereum transaction.
func (c *clefSigner) SignTx(transaction *types.Transaction) (*types.Transaction, error) {
// chainId is nil here because it is set on the clef side
return c.clef.SignTx(c.account, transaction, nil)
}
// EthereumAddress returns the ethereum address this signer uses
// EthereumAddress returns the ethereum address this signer uses.
func (c *clefSigner) EthereumAddress() (common.Address, error) {
return c.account.Address, nil
}
// SignTypedData signs data according to eip712.
func (c *clefSigner) SignTypedData(typedData *eip712.TypedData) ([]byte, error) {
var sig hexutil.Bytes
err := c.client.Call(&sig, "account_signTypedData", c.account.Address, typedData)
if err != nil {
return nil, err
}
return sig, nil
}
......@@ -13,9 +13,11 @@ import (
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethersphere/bee/pkg/crypto"
"github.com/ethersphere/bee/pkg/crypto/clef"
"github.com/ethersphere/bee/pkg/crypto/eip712"
)
type mockClef struct {
......@@ -61,7 +63,7 @@ func TestNewClefSigner(t *testing.T) {
signature: testSignature,
}
signer, err := clef.NewSigner(mock, func(signature, data []byte) (*ecdsa.PublicKey, error) {
signer, err := clef.NewSigner(mock, nil, func(signature, data []byte) (*ecdsa.PublicKey, error) {
if !bytes.Equal(testSignature, signature) {
t.Fatalf("wrong data used for recover. expected %v got %v", testSignature, signature)
}
......@@ -102,7 +104,7 @@ func TestClefNoAccounts(t *testing.T) {
accounts: []accounts.Account{},
}
_, err := clef.NewSigner(mock, nil)
_, err := clef.NewSigner(mock, nil, nil)
if err == nil {
t.Fatal("expected ErrNoAccounts error if no accounts")
}
......@@ -110,3 +112,63 @@ func TestClefNoAccounts(t *testing.T) {
t.Fatalf("expected ErrNoAccounts error but got %v", err)
}
}
type mockRpc struct {
call func(result interface{}, method string, args ...interface{}) error
}
func (m *mockRpc) Call(result interface{}, method string, args ...interface{}) error {
return m.call(result, method, args...)
}
func TestClefTypedData(t *testing.T) {
key, err := crypto.GenerateSecp256k1Key()
if err != nil {
t.Fatal(err)
}
publicKey := &key.PublicKey
signature := common.FromHex("0xabcdef")
account := common.HexToAddress("21b26864067deb88e2d5cdca512167815f2910d3")
typedData := &eip712.TypedData{
PrimaryType: "MyType",
}
signer, err := clef.NewSigner(&mockClef{
accounts: []accounts.Account{
{
Address: account,
},
},
signature: make([]byte, 65),
}, &mockRpc{
call: func(result interface{}, method string, args ...interface{}) error {
if method != "account_signTypedData" {
t.Fatalf("called wrong method. was %s", method)
}
if args[0].(common.Address) != account {
t.Fatalf("called with wrong account. was %x, wanted %x", args[0].(common.Address), account)
}
if args[1].(*eip712.TypedData) != typedData {
t.Fatal("called with wrong data")
}
*result.(*hexutil.Bytes) = signature
return nil
},
}, func(signature, data []byte) (*ecdsa.PublicKey, error) {
return publicKey, nil
})
if err != nil {
t.Fatal(err)
}
s, err := signer.SignTypedData(typedData)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(s, signature) {
t.Fatalf("wrong signature. wanted %x, got %x", signature, s)
}
}
// Copyright 2020 The Swarm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package eip712
import (
"fmt"
"github.com/ethereum/go-ethereum/signer/core"
)
// type aliases to avoid importing "core" everywhere
type TypedData = core.TypedData
type TypedDataDomain = core.TypedDataDomain
type Types = core.Types
type Type = core.Type
type TypedDataMessage = core.TypedDataMessage
// EncodeForSigning encodes the hash that will be signed for the given EIP712 data
func EncodeForSigning(typedData *TypedData) ([]byte, error) {
domainSeparator, err := typedData.HashStruct("EIP712Domain", typedData.Domain.Map())
if err != nil {
return nil, err
}
typedDataHash, err := typedData.HashStruct(typedData.PrimaryType, typedData.Message)
if err != nil {
return nil, err
}
rawData := []byte(fmt.Sprintf("\x19\x01%s%s", string(domainSeparator), string(typedDataHash)))
return rawData, nil
}
// EIP712DomainType is the type description for the EIP712 Domain
var EIP712DomainType = []Type{
{
Name: "name",
Type: "string",
},
{
Name: "version",
Type: "string",
},
{
Name: "chainId",
Type: "uint256",
},
}
......@@ -12,6 +12,7 @@ import (
"github.com/btcsuite/btcd/btcec"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethersphere/bee/pkg/crypto/eip712"
)
var (
......@@ -19,28 +20,30 @@ var (
)
type Signer interface {
// Sign signs data with ethereum prefix (eip191 type 0x45)
// Sign signs data with ethereum prefix (eip191 type 0x45).
Sign(data []byte) ([]byte, error)
// SignTx signs an ethereum transaction
// SignTx signs an ethereum transaction.
SignTx(transaction *types.Transaction) (*types.Transaction, error)
// PublicKey returns the public key this signer uses
// SignTypedData signs data according to eip712.
SignTypedData(typedData *eip712.TypedData) ([]byte, error)
// PublicKey returns the public key this signer uses.
PublicKey() (*ecdsa.PublicKey, error)
// EthereumAddress returns the ethereum address this signer uses
// EthereumAddress returns the ethereum address this signer uses.
EthereumAddress() (common.Address, error)
}
// addEthereumPrefix adds the ethereum prefix to the data
// addEthereumPrefix adds the ethereum prefix to the data.
func addEthereumPrefix(data []byte) []byte {
return []byte(fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(data), data))
}
// hashWithEthereumPrefix returns the hash that should be signed for the given data
// hashWithEthereumPrefix returns the hash that should be signed for the given data.
func hashWithEthereumPrefix(data []byte) ([]byte, error) {
return LegacyKeccak256(addEthereumPrefix(data))
}
// Recover verifies signature with the data base provided.
// It is using `btcec.RecoverCompact` function
// It is using `btcec.RecoverCompact` function.
func Recover(signature, data []byte) (*ecdsa.PublicKey, error) {
if len(signature) != 65 {
return nil, ErrInvalidLength
......@@ -69,48 +72,36 @@ func NewDefaultSigner(key *ecdsa.PrivateKey) Signer {
}
}
// PublicKey returns the public key this signer uses
// PublicKey returns the public key this signer uses.
func (d *defaultSigner) PublicKey() (*ecdsa.PublicKey, error) {
return &d.key.PublicKey, nil
}
// Sign signs data with ethereum prefix (eip191 type 0x45)
// Sign signs data with ethereum prefix (eip191 type 0x45).
func (d *defaultSigner) Sign(data []byte) (signature []byte, err error) {
hash, err := hashWithEthereumPrefix(data)
if err != nil {
return nil, err
}
signature, err = btcec.SignCompact(btcec.S256(), (*btcec.PrivateKey)(d.key), hash, true)
if err != nil {
return nil, err
}
// Convert to Ethereum signature format with 'recovery id' v at the end.
v := signature[0]
copy(signature, signature[1:])
signature[64] = v
return signature, nil
return d.sign(hash, true)
}
// SignTx signs an ethereum transaction
// SignTx signs an ethereum transaction.
func (d *defaultSigner) SignTx(transaction *types.Transaction) (*types.Transaction, error) {
hash := (&types.HomesteadSigner{}).Hash(transaction).Bytes()
// isCompressedKey is false here so we get the expected v value (27 or 28)
signature, err := btcec.SignCompact(btcec.S256(), (*btcec.PrivateKey)(d.key), hash, false)
signature, err := d.sign(hash, false)
if err != nil {
return nil, err
}
// Convert to Ethereum signature format with 'recovery id' v at the end.
v := signature[0]
copy(signature, signature[1:])
// v value needs to be adjusted by 27 as transaction.WithSignature expects it to be 0 or 1
signature[64] = v - 27
signature[64] -= 27
return transaction.WithSignature(&types.HomesteadSigner{}, signature)
}
// EthereumAddress returns the ethereum address this signer uses
// EthereumAddress returns the ethereum address this signer uses.
func (d *defaultSigner) EthereumAddress() (common.Address, error) {
publicKey, err := d.PublicKey()
if err != nil {
......@@ -124,3 +115,56 @@ func (d *defaultSigner) EthereumAddress() (common.Address, error) {
copy(ethAddress[:], eth)
return ethAddress, nil
}
// SignTypedData signs data according to eip712.
func (d *defaultSigner) SignTypedData(typedData *eip712.TypedData) ([]byte, error) {
rawData, err := eip712.EncodeForSigning(typedData)
if err != nil {
return nil, err
}
sighash, err := LegacyKeccak256(rawData)
if err != nil {
return nil, err
}
return d.sign(sighash, false)
}
// sign the provided hash and convert it to the ethereum (r,s,v) format.
func (d *defaultSigner) sign(sighash []byte, isCompressedKey bool) ([]byte, error) {
signature, err := btcec.SignCompact(btcec.S256(), (*btcec.PrivateKey)(d.key), sighash, false)
if err != nil {
return nil, err
}
// Convert to Ethereum signature format with 'recovery id' v at the end.
v := signature[0]
copy(signature, signature[1:])
signature[64] = v
return signature, nil
}
// RecoverEIP712 recovers the public key for eip712 signed data.
func RecoverEIP712(signature []byte, data *eip712.TypedData) (*ecdsa.PublicKey, error) {
if len(signature) != 65 {
return nil, errors.New("invalid length")
}
// Convert to btcec input format with 'recovery id' v at the beginning.
btcsig := make([]byte, 65)
btcsig[0] = signature[64]
copy(btcsig[1:], signature)
rawData, err := eip712.EncodeForSigning(data)
if err != nil {
return nil, err
}
sighash, err := LegacyKeccak256(rawData)
if err != nil {
return nil, err
}
p, _, err := btcec.RecoverCompact(btcec.S256(), btcsig, sighash)
return (*ecdsa.PublicKey)(p), err
}
......@@ -5,6 +5,7 @@
package crypto_test
import (
"bytes"
"encoding/hex"
"errors"
"math/big"
......@@ -14,6 +15,7 @@ import (
"github.com/ethereum/go-ethereum/common/math"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethersphere/bee/pkg/crypto"
"github.com/ethersphere/bee/pkg/crypto/eip712"
)
func TestDefaultSigner(t *testing.T) {
......@@ -122,3 +124,90 @@ func TestDefaultSignerSignTx(t *testing.T) {
t.Fatalf("wrong s value. expected %x, got %x", expectedS, s)
}
}
var testTypedData = &eip712.TypedData{
Domain: eip712.TypedDataDomain{
Name: "test",
Version: "1.0",
},
Types: eip712.Types{
"EIP712Domain": {
{
Name: "name",
Type: "string",
},
{
Name: "version",
Type: "string",
},
},
"MyType": {
{
Name: "test",
Type: "string",
},
},
},
Message: eip712.TypedDataMessage{
"test": "abc",
},
PrimaryType: "MyType",
}
func TestDefaultSignerTypedData(t *testing.T) {
data, err := hex.DecodeString("634fb5a872396d9693e5c9f9d7233cfa93f395c093371017ff44aa9ae6564cdd")
if err != nil {
t.Fatal(err)
}
privKey, err := crypto.DecodeSecp256k1PrivateKey(data)
if err != nil {
t.Fatal(err)
}
signer := crypto.NewDefaultSigner(privKey)
sig, err := signer.SignTypedData(testTypedData)
if err != nil {
t.Fatal(err)
}
expected, err := hex.DecodeString("60f054c45d37a0359d4935da0454bc19f02a8c01ceee8a112cfe48c8e2357b842e897f76389fb96947c6d2c80cbfe081052204e7b0c3cc1194a973a09b1614f71c")
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(expected, sig) {
t.Fatalf("wrong signature. expected %x, got %x", expected, sig)
}
}
func TestRecoverEIP712(t *testing.T) {
data, err := hex.DecodeString("634fb5a872396d9693e5c9f9d7233cfa93f395c093371017ff44aa9ae6564cdd")
if err != nil {
t.Fatal(err)
}
privKey, err := crypto.DecodeSecp256k1PrivateKey(data)
if err != nil {
t.Fatal(err)
}
expected, err := hex.DecodeString("60f054c45d37a0359d4935da0454bc19f02a8c01ceee8a112cfe48c8e2357b842e897f76389fb96947c6d2c80cbfe081052204e7b0c3cc1194a973a09b1614f71c")
if err != nil {
t.Fatal(err)
}
pubKey, err := crypto.RecoverEIP712(expected, testTypedData)
if err != nil {
t.Fatal(err)
}
if privKey.PublicKey.X.Cmp(pubKey.X) != 0 {
t.Fatalf("recovered wrong public key. wanted %x, got %x", privKey.PublicKey, pubKey)
}
if privKey.PublicKey.Y.Cmp(pubKey.Y) != 0 {
t.Fatalf("recovered wrong public key. wanted %x, got %x", privKey.PublicKey, pubKey)
}
}
......@@ -165,6 +165,11 @@ func NewBee(addr string, swarmAddress swarm.Address, keystore keystore.Service,
// print ethereum address so users know which address we need to fund
logger.Infof("using ethereum address %x", overlayEthAddress)
chainId, err := swapBackend.ChainID(p2pCtx)
if err != nil {
return nil, err
}
// TODO: factory address discovery for well-known networks (goerli for beta)
if o.SwapFactoryAddress == "" {
......@@ -178,6 +183,8 @@ func NewBee(addr string, swarmAddress swarm.Address, keystore keystore.Service,
return nil, err
}
chequeSigner := chequebook.NewChequeSigner(signer, chainId.Int64())
// initialize chequebook logic
// return value is ignored because we don't do anything yet after initialization. this will be passed into swap settlement.
chequebookService, err = chequebook.Init(p2pCtx,
......@@ -188,6 +195,7 @@ func NewBee(addr string, swarmAddress swarm.Address, keystore keystore.Service,
transactionService,
swapBackend,
overlayEthAddress,
chequeSigner,
chequebook.NewSimpleSwapBindings,
chequebook.NewERC20Bindings)
if err != nil {
......
// Copyright 2020 The Swarm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package chequebook
import (
"fmt"
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/math"
"github.com/ethersphere/bee/pkg/crypto"
"github.com/ethersphere/bee/pkg/crypto/eip712"
)
// Cheque represents a cheque for a SimpleSwap chequebook
type Cheque struct {
Chequebook common.Address
Beneficiary common.Address
CumulativePayout *big.Int
}
// SignedCheque represents a cheque together with its signature
type SignedCheque struct {
Cheque
Signature []byte
}
// chequebookDomain computes chainId-dependant EIP712 domain
func chequebookDomain(chainID int64) eip712.TypedDataDomain {
return eip712.TypedDataDomain{
Name: "Chequebook",
Version: "1.0",
ChainId: math.NewHexOrDecimal256(chainID),
}
}
// ChequeTypes are the needed type descriptions for cheque signing
var ChequeTypes = eip712.Types{
"EIP712Domain": eip712.EIP712DomainType,
"Cheque": []eip712.Type{
{
Name: "chequebook",
Type: "address",
},
{
Name: "beneficiary",
Type: "address",
},
{
Name: "cumulativePayout",
Type: "uint256",
},
},
}
// ChequeSigner signs cheque
type ChequeSigner interface {
// Sign signs a cheque
Sign(cheque *Cheque) ([]byte, error)
}
type chequeSigner struct {
signer crypto.Signer // the underlying signer used
chainID int64 // the chainID used for EIP712
}
// NewChequeSigner creates a new cheque signer for the given chainID.
func NewChequeSigner(signer crypto.Signer, chainID int64) ChequeSigner {
return &chequeSigner{
signer: signer,
chainID: chainID,
}
}
// eip712DataForCheque converts a cheque into the correct TypedData structure.
func eip712DataForCheque(cheque *Cheque, chainID int64) *eip712.TypedData {
return &eip712.TypedData{
Domain: chequebookDomain(chainID),
Types: ChequeTypes,
Message: eip712.TypedDataMessage{
"chequebook": cheque.Chequebook.Hex(),
"beneficiary": cheque.Beneficiary.Hex(),
"cumulativePayout": cheque.CumulativePayout.String(),
},
PrimaryType: "Cheque",
}
}
// Sign signs a cheque.
func (s *chequeSigner) Sign(cheque *Cheque) ([]byte, error) {
return s.signer.SignTypedData(eip712DataForCheque(cheque, s.chainID))
}
func (cheque *Cheque) String() string {
return fmt.Sprintf("Contract: %x Beneficiary: %x CumulativePayout: %v", cheque.Chequebook, cheque.Beneficiary, cheque.CumulativePayout)
}
// Copyright 2020 The Swarm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package chequebook_test
import (
"bytes"
"encoding/hex"
"math/big"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethersphere/bee/pkg/crypto"
"github.com/ethersphere/bee/pkg/crypto/eip712"
"github.com/ethersphere/bee/pkg/settlement/swap/chequebook"
)
func TestSignCheque(t *testing.T) {
chequebookAddress := common.HexToAddress("0x8d3766440f0d7b949a5e32995d09619a7f86e632")
beneficiaryAddress := common.HexToAddress("0xb8d424e9662fe0837fb1d728f1ac97cebb1085fe")
signature := common.Hex2Bytes("abcd")
cumulativePayout := big.NewInt(10)
chainId := int64(1)
cheque := &chequebook.Cheque{
Chequebook: chequebookAddress,
Beneficiary: beneficiaryAddress,
CumulativePayout: cumulativePayout,
}
signer := &signerMock{
signTypedData: func(data *eip712.TypedData) ([]byte, error) {
if data.Message["beneficiary"].(string) != beneficiaryAddress.Hex() {
t.Fatal("signing cheque with wrong beneficiary")
}
if data.Message["chequebook"].(string) != chequebookAddress.Hex() {
t.Fatal("signing cheque for wrong chequebook")
}
if data.Message["cumulativePayout"].(string) != cumulativePayout.String() {
t.Fatal("signing cheque with wrong cumulativePayout")
}
return signature, nil
},
}
chequeSigner := chequebook.NewChequeSigner(signer, chainId)
result, err := chequeSigner.Sign(cheque)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(result, signature) {
t.Fatalf("returned wrong signature. wanted %x, got %x", signature, result)
}
}
func TestSignChequeIntegration(t *testing.T) {
chequebookAddress := common.HexToAddress("0xfa02D396842E6e1D319E8E3D4D870338F791AA25")
beneficiaryAddress := common.HexToAddress("0x98E6C644aFeB94BBfB9FF60EB26fc9D83BBEcA79")
cumulativePayout := big.NewInt(500)
chainId := int64(1)
data, err := hex.DecodeString("634fb5a872396d9693e5c9f9d7233cfa93f395c093371017ff44aa9ae6564cdd")
if err != nil {
t.Fatal(err)
}
privKey, err := crypto.DecodeSecp256k1PrivateKey(data)
if err != nil {
t.Fatal(err)
}
signer := crypto.NewDefaultSigner(privKey)
cheque := &chequebook.Cheque{
Chequebook: chequebookAddress,
Beneficiary: beneficiaryAddress,
CumulativePayout: cumulativePayout,
}
chequeSigner := chequebook.NewChequeSigner(signer, chainId)
result, err := chequeSigner.Sign(cheque)
if err != nil {
t.Fatal(err)
}
// computed using ganache
expectedSignature, err := hex.DecodeString("171b63fc598ae2c7987f4a756959dadddd84ccd2071e7b5c3aa3437357be47286125edc370c344a163ba7f4183dfd3611996274a13e4b3496610fc00c0e2fc421c")
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(result, expectedSignature) {
t.Fatalf("returned wrong signature. wanted %x, got %x", expectedSignature, result)
}
}
......@@ -7,25 +7,29 @@ package chequebook
import (
"context"
"errors"
"fmt"
"math/big"
"strings"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethersphere/bee/pkg/storage"
"github.com/ethersphere/sw3-bindings/v2/simpleswapfactory"
)
// Service is the main interface for interacting with the nodes chequebook
// Service is the main interface for interacting with the nodes chequebook.
type Service interface {
// Deposit starts depositing erc20 token into the chequebook. This returns once the transactions has been broadcast.
Deposit(ctx context.Context, amount *big.Int) (hash common.Hash, err error)
// WaitForDeposit waits for the deposit transaction to confirm and verifies the result
// WaitForDeposit waits for the deposit transaction to confirm and verifies the result.
WaitForDeposit(ctx context.Context, txHash common.Hash) error
// Balance returns the token balance of the chequebook
// Balance returns the token balance of the chequebook.
Balance(ctx context.Context) (*big.Int, error)
// Address returns the address of the used chequebook contract
// Address returns the address of the used chequebook contract.
Address() common.Address
// Issue a new cheque for the beneficiary with an cumulativePayout amount higher than the last.
Issue(beneficiary common.Address, amount *big.Int) (*SignedCheque, error)
}
type service struct {
......@@ -40,10 +44,13 @@ type service struct {
erc20Address common.Address
erc20ABI abi.ABI
erc20Instance ERC20Binding
store storage.StateStorer
chequeSigner ChequeSigner
}
// New creates a new chequebook service for the provided chequebook contract
func New(backend Backend, transactionService TransactionService, address, erc20Address, ownerAddress common.Address, simpleSwapBindingFunc SimpleSwapBindingFunc, erc20BindingFunc ERC20BindingFunc) (Service, error) {
// New creates a new chequebook service for the provided chequebook contract.
func New(backend Backend, transactionService TransactionService, address, erc20Address, ownerAddress common.Address, store storage.StateStorer, chequeSigner ChequeSigner, simpleSwapBindingFunc SimpleSwapBindingFunc, erc20BindingFunc ERC20BindingFunc) (Service, error) {
chequebookABI, err := abi.JSON(strings.NewReader(simpleswapfactory.ERC20SimpleSwapABI))
if err != nil {
return nil, err
......@@ -74,10 +81,12 @@ func New(backend Backend, transactionService TransactionService, address, erc20A
erc20Address: erc20Address,
erc20ABI: erc20ABI,
erc20Instance: erc20Instance,
store: store,
chequeSigner: chequeSigner,
}, nil
}
// Address returns the address of the used chequebook contract
// Address returns the address of the used chequebook contract.
func (s *service) Address() common.Address {
return s.address
}
......@@ -117,14 +126,14 @@ func (s *service) Deposit(ctx context.Context, amount *big.Int) (hash common.Has
return txHash, nil
}
// Balance returns the token balance of the chequebook
// Balance returns the token balance of the chequebook.
func (s *service) Balance(ctx context.Context) (*big.Int, error) {
return s.chequebookInstance.Balance(&bind.CallOpts{
Context: ctx,
})
}
// WaitForDeposit waits for the deposit transaction to confirm and verifies the result
// WaitForDeposit waits for the deposit transaction to confirm and verifies the result.
func (s *service) WaitForDeposit(ctx context.Context, txHash common.Hash) error {
receipt, err := s.transactionService.WaitForReceipt(ctx, txHash)
if err != nil {
......@@ -135,3 +144,44 @@ func (s *service) WaitForDeposit(ctx context.Context, txHash common.Hash) error
}
return nil
}
// Issue issues a new cheque.
func (s *service) Issue(beneficiary common.Address, amount *big.Int) (*SignedCheque, error) {
storeKey := fmt.Sprintf("chequebook_last_issued_cheque_%x", beneficiary)
var cumulativePayout *big.Int
var lastCheque Cheque
err := s.store.Get(storeKey, &lastCheque)
if err != nil {
if err != storage.ErrNotFound {
return nil, err
}
cumulativePayout = big.NewInt(0)
} else {
cumulativePayout = lastCheque.CumulativePayout
}
// increase cumulativePayout by amount
cumulativePayout = cumulativePayout.Add(cumulativePayout, amount)
cheque := Cheque{
Chequebook: s.address,
CumulativePayout: cumulativePayout,
Beneficiary: beneficiary,
}
sig, err := s.chequeSigner.Sign(&cheque)
if err != nil {
return nil, err
}
err = s.store.Put(storeKey, cheque)
if err != nil {
return nil, err
}
return &SignedCheque{
Cheque: cheque,
Signature: sig,
}, nil
}
......@@ -31,6 +31,8 @@ func newTestChequebook(
address,
erc20address,
ownerAdress,
nil,
nil,
func(addr common.Address, b bind.ContractBackend) (chequebook.SimpleSwapBinding, error) {
if addr != address {
t.Fatalf("initialised binding with wrong address. wanted %x, got %x", address, addr)
......
......@@ -6,12 +6,14 @@ package chequebook_test
import (
"context"
"crypto/ecdsa"
"math/big"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethersphere/bee/pkg/crypto/eip712"
"github.com/ethersphere/bee/pkg/settlement/swap/chequebook"
"github.com/ethersphere/sw3-bindings/v2/simpleswapfactory"
)
......@@ -103,6 +105,10 @@ func (m *simpleSwapBindingMock) Balance(o *bind.CallOpts) (*big.Int, error) {
return m.balance(o)
}
func (m *simpleSwapBindingMock) Issuer(*bind.CallOpts) (common.Address, error) {
return common.Address{}, nil
}
type erc20BindingMock struct {
balanceOf func(*bind.CallOpts, common.Address) (*big.Int, error)
}
......@@ -110,3 +116,28 @@ type erc20BindingMock struct {
func (m *erc20BindingMock) BalanceOf(o *bind.CallOpts, a common.Address) (*big.Int, error) {
return m.balanceOf(o, a)
}
type signerMock struct {
signTx func(transaction *types.Transaction) (*types.Transaction, error)
signTypedData func(*eip712.TypedData) ([]byte, error)
}
func (*signerMock) EthereumAddress() (common.Address, error) {
return common.Address{}, nil
}
func (*signerMock) Sign(data []byte) ([]byte, error) {
return nil, nil
}
func (m *signerMock) SignTx(transaction *types.Transaction) (*types.Transaction, error) {
return m.signTx(transaction)
}
func (*signerMock) PublicKey() (*ecdsa.PublicKey, error) {
return nil, nil
}
func (m *signerMock) SignTypedData(d *eip712.TypedData) ([]byte, error) {
return m.signTypedData(d)
}
......@@ -23,15 +23,15 @@ var (
ErrNotDeployedByFactory = errors.New("chequebook not deployed by factory")
)
// Factory is the main interface for interacting with the chequebook factory
// Factory is the main interface for interacting with the chequebook factory.
type Factory interface {
// ERC20Address returns the token for which this factory deploys chequebooks
// ERC20Address returns the token for which this factory deploys chequebooks.
ERC20Address(ctx context.Context) (common.Address, error)
// Deploy deploys a new chequebook and returns once confirmed
// Deploy deploys a new chequebook and returns once confirmed.
Deploy(ctx context.Context, issuer common.Address, defaultHardDepositTimeoutDuration *big.Int) (common.Address, error)
// VerifyBytecode checks that the factory is valid
// VerifyBytecode checks that the factory is valid.
VerifyBytecode(ctx context.Context) error
// VerifyChequebook checks that the supplied chequebook has been deployed by this factory
// VerifyChequebook checks that the supplied chequebook has been deployed by this factory.
VerifyChequebook(ctx context.Context, chequebook common.Address) error
}
......@@ -44,7 +44,7 @@ type factory struct {
instance SimpleSwapFactoryBinding
}
// NewFactory creates a new factory service for the provided factory contract
// NewFactory creates a new factory service for the provided factory contract.
func NewFactory(backend Backend, transactionService TransactionService, address common.Address, simpleSwapFactoryBindingFunc SimpleSwapFactoryBindingFunc) (Factory, error) {
ABI, err := abi.JSON(strings.NewReader(simpleswapfactory.SimpleSwapFactoryABI))
if err != nil {
......@@ -65,7 +65,7 @@ func NewFactory(backend Backend, transactionService TransactionService, address
}, nil
}
// Deploy deploys a new chequebook and returns once confirmed
// Deploy deploys a new chequebook and returns once confirmed.
func (c *factory) Deploy(ctx context.Context, issuer common.Address, defaultHardDepositTimeoutDuration *big.Int) (common.Address, error) {
callData, err := c.ABI.Pack("deploySimpleSwap", issuer, big.NewInt(0).Set(defaultHardDepositTimeoutDuration))
if err != nil {
......@@ -98,7 +98,7 @@ func (c *factory) Deploy(ctx context.Context, issuer common.Address, defaultHard
return chequebookAddress, nil
}
// parseDeployReceipt parses the address of the deployed chequebook from the receipt
// parseDeployReceipt parses the address of the deployed chequebook from the receipt.
func (c *factory) parseDeployReceipt(receipt *types.Receipt) (address common.Address, err error) {
if receipt.Status != 1 {
return common.Address{}, ErrTransactionReverted
......@@ -118,7 +118,7 @@ func (c *factory) parseDeployReceipt(receipt *types.Receipt) (address common.Add
return address, nil
}
// VerifyBytecode checks that the factory is valid
// VerifyBytecode checks that the factory is valid.
func (c *factory) VerifyBytecode(ctx context.Context) (err error) {
code, err := c.backend.CodeAt(ctx, c.address, nil)
if err != nil {
......@@ -132,7 +132,7 @@ func (c *factory) VerifyBytecode(ctx context.Context) (err error) {
return nil
}
// VerifyChequebook checks that the supplied chequebook has been deployed by this factory
// VerifyChequebook checks that the supplied chequebook has been deployed by this factory.
func (c *factory) VerifyChequebook(ctx context.Context, chequebook common.Address) error {
deployed, err := c.instance.DeployedContracts(&bind.CallOpts{
Context: ctx,
......@@ -146,7 +146,7 @@ func (c *factory) VerifyChequebook(ctx context.Context, chequebook common.Addres
return nil
}
// ERC20Address returns the token for which this factory deploys chequebooks
// ERC20Address returns the token for which this factory deploys chequebooks.
func (c *factory) ERC20Address(ctx context.Context) (common.Address, error) {
erc20Address, err := c.instance.ERC20Address(&bind.CallOpts{
Context: ctx,
......
......@@ -15,7 +15,7 @@ import (
const chequebookKey = "chequebook"
// Init initialises the chequebook service
// Init initialises the chequebook service.
func Init(
ctx context.Context,
chequebookFactory Factory,
......@@ -25,6 +25,7 @@ func Init(
transactionService TransactionService,
swapBackend Backend,
overlayEthAddress common.Address,
chequeSigner ChequeSigner,
simpleSwapBindingFunc SimpleSwapBindingFunc,
erc20BindingFunc ERC20BindingFunc) (chequebookService Service, err error) {
// verify that the supplied factory is valid
......@@ -60,7 +61,7 @@ func Init(
return nil, err
}
chequebookService, err = New(swapBackend, transactionService, chequebookAddress, erc20Address, overlayEthAddress, simpleSwapBindingFunc, erc20BindingFunc)
chequebookService, err = New(swapBackend, transactionService, chequebookAddress, erc20Address, overlayEthAddress, stateStore, chequeSigner, simpleSwapBindingFunc, erc20BindingFunc)
if err != nil {
return nil, err
}
......@@ -81,7 +82,7 @@ func Init(
logger.Infof("deposited to chequebook %x in transaction %x", chequebookAddress, depositHash)
}
} else {
chequebookService, err = New(swapBackend, transactionService, chequebookAddress, erc20Address, overlayEthAddress, simpleSwapBindingFunc, erc20BindingFunc)
chequebookService, err = New(swapBackend, transactionService, chequebookAddress, erc20Address, overlayEthAddress, stateStore, chequeSigner, simpleSwapBindingFunc, erc20BindingFunc)
if err != nil {
return nil, err
}
......
......@@ -67,6 +67,10 @@ func (s *Service) Address() common.Address {
return common.Address{}
}
func (s *Service) Issue(beneficiary common.Address, amount *big.Int) (*chequebook.SignedCheque, error) {
return nil, errors.New("Error")
}
// Option is the option passed to the mock Chequebook service
type Option interface {
apply(*Service)
......
......@@ -22,13 +22,13 @@ var (
ErrTransactionReverted = errors.New("transaction reverted")
)
// Backend is the minimum of blockchain backend functions we need
// Backend is the minimum of blockchain backend functions we need.
type Backend interface {
bind.ContractBackend
TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error)
}
// TxRequest describes a request for a transaction that can be executed
// TxRequest describes a request for a transaction that can be executed.
type TxRequest struct {
To common.Address // recipient of the transaction
Data []byte // transaction data
......@@ -39,9 +39,9 @@ type TxRequest struct {
// TransactionService is the service to send transactions. It takes care of gas price, gas limit and nonce management.
type TransactionService interface {
// Send creates a transaction based on the request and sends it
// Send creates a transaction based on the request and sends it.
Send(ctx context.Context, request *TxRequest) (txHash common.Hash, err error)
// WaitForReceipt waits until either the transaction with the given hash has been mined or the context is cancelled
// WaitForReceipt waits until either the transaction with the given hash has been mined or the context is cancelled.
WaitForReceipt(ctx context.Context, txHash common.Hash) (receipt *types.Receipt, err error)
}
......@@ -52,7 +52,7 @@ type transactionService struct {
sender common.Address
}
// NewTransactionService creates a new transaction service
// NewTransactionService creates a new transaction service.
func NewTransactionService(logger logging.Logger, backend Backend, signer crypto.Signer) (TransactionService, error) {
senderAddress, err := signer.EthereumAddress()
if err != nil {
......@@ -66,7 +66,7 @@ func NewTransactionService(logger logging.Logger, backend Backend, signer crypto
}, nil
}
// Send creates and signs a transaction based on the request and sends it
// Send creates and signs a transaction based on the request and sends it.
func (t *transactionService) Send(ctx context.Context, request *TxRequest) (txHash common.Hash, err error) {
tx, err := prepareTransaction(ctx, request, t.sender, t.backend)
if err != nil {
......@@ -86,7 +86,7 @@ func (t *transactionService) Send(ctx context.Context, request *TxRequest) (txHa
return signedTx.Hash(), nil
}
// WaitForReceipt waits until either the transaction with the given hash has been mined or the context is cancelled
// WaitForReceipt waits until either the transaction with the given hash has been mined or the context is cancelled.
func (t *transactionService) WaitForReceipt(ctx context.Context, txHash common.Hash) (receipt *types.Receipt, err error) {
for {
receipt, err := t.backend.TransactionReceipt(ctx, txHash)
......@@ -108,7 +108,7 @@ func (t *transactionService) WaitForReceipt(ctx context.Context, txHash common.H
}
}
// prepareTransaction creates a signable transaction based on a request
// prepareTransaction creates a signable transaction based on a request.
func prepareTransaction(ctx context.Context, request *TxRequest, from common.Address, backend Backend) (tx *types.Transaction, err error) {
var gasLimit uint64
if request.GasLimit == 0 {
......
......@@ -7,7 +7,6 @@ package chequebook_test
import (
"bytes"
"context"
"crypto/ecdsa"
"io/ioutil"
"math/big"
"testing"
......@@ -19,24 +18,6 @@ import (
"github.com/ethersphere/bee/pkg/settlement/swap/chequebook"
)
type signerMock struct {
signTx func(transaction *types.Transaction) (*types.Transaction, error)
}
func (*signerMock) EthereumAddress() (common.Address, error) {
return common.Address{}, nil
}
func (*signerMock) Sign(data []byte) ([]byte, error) {
return nil, nil
}
func (m *signerMock) SignTx(transaction *types.Transaction) (*types.Transaction, error) {
return m.signTx(transaction)
}
func (*signerMock) PublicKey() (*ecdsa.PublicKey, error) {
return nil, nil
}
func TestTransactionSend(t *testing.T) {
logger := logging.New(ioutil.Discard, 0)
recipient := common.HexToAddress("0xabcd")
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment