From 0844bc752117184f342b92e31999db8fc4d01a93 Mon Sep 17 00:00:00 2001 From: Ralph Pichler Date: Tue, 7 Jan 2020 12:14:16 +0100 Subject: [PATCH 1/7] swap: refactor cashout logic to cashout processor --- contracts/swap/swap.go | 46 ++++++++++++---- swap/cashout.go | 118 +++++++++++++++++++++++++++++++++++++++++ swap/config.go | 3 +- swap/swap.go | 46 +++++----------- swap/swap_test.go | 22 ++++++-- 5 files changed, 186 insertions(+), 49 deletions(-) create mode 100644 swap/cashout.go diff --git a/contracts/swap/swap.go b/contracts/swap/swap.go index 3be7f3203c..ae9db1eab3 100644 --- a/contracts/swap/swap.go +++ b/contracts/swap/swap.go @@ -40,6 +40,7 @@ var ( type Backend interface { bind.ContractBackend TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) + TransactionByHash(ctx context.Context, txHash common.Hash) (*types.Transaction, bool, error) } // Contract interface defines the methods exported from the underlying go-bindings for the smart contract @@ -48,8 +49,10 @@ type Contract interface { Withdraw(auth *bind.TransactOpts, amount *big.Int) (*types.Receipt, error) // Deposit sends a raw transaction to the chequebook, triggering the fallback—depositing amount Deposit(auth *bind.TransactOpts, amout *big.Int) (*types.Receipt, error) - // CashChequeBeneficiary cashes the cheque by the beneficiary - CashChequeBeneficiary(auth *bind.TransactOpts, beneficiary common.Address, cumulativePayout *big.Int, ownerSig []byte) (*CashChequeResult, *types.Receipt, error) + // CashChequeBeneficiaryStart sends the transaction to a cash a cheque as the beneficiary + CashChequeBeneficiaryStart(opts *bind.TransactOpts, beneficiary common.Address, cumulativePayout *big.Int, ownerSig []byte) (*types.Transaction, error) + // CashChequeBeneficiaryResult processes the receipt from a CashChequeBeneficiary transaction + CashChequeBeneficiaryResult(receipt *types.Receipt) *CashChequeResult // LiquidBalance returns the LiquidBalance (total balance in ERC20-token - total hard deposits in ERC20-token) of the chequebook LiquidBalance(auth *bind.CallOpts) (*big.Int, error) //Token returns the address of the ERC20 contract, used by the chequebook @@ -139,17 +142,17 @@ func (s simpleContract) Deposit(auth *bind.TransactOpts, amount *big.Int) (*type return WaitFunc(auth.Context, s.backend, tx) } -// CashChequeBeneficiary cashes the cheque on the blockchain and blocks until the transaction is mined. -func (s simpleContract) CashChequeBeneficiary(opts *bind.TransactOpts, beneficiary common.Address, cumulativePayout *big.Int, ownerSig []byte) (*CashChequeResult, *types.Receipt, error) { +// CashChequeBeneficiaryStart sends the transaction to a cash a cheque as the beneficiary +func (s simpleContract) CashChequeBeneficiaryStart(opts *bind.TransactOpts, beneficiary common.Address, cumulativePayout *big.Int, ownerSig []byte) (*types.Transaction, error) { tx, err := s.instance.CashChequeBeneficiary(opts, beneficiary, cumulativePayout, ownerSig) if err != nil { - return nil, nil, err - } - receipt, err := WaitFunc(opts.Context, s.backend, tx) - if err != nil { - return nil, nil, err + return nil, err } + return tx, nil +} +// CashChequeBeneficiaryResult processes the receipt from a CashChequeBeneficiary transaction +func (s simpleContract) CashChequeBeneficiaryResult(receipt *types.Receipt) *CashChequeResult { result := &CashChequeResult{ Bounced: false, } @@ -170,7 +173,7 @@ func (s simpleContract) CashChequeBeneficiary(opts *bind.TransactOpts, beneficia } } - return result, receipt, nil + return result } // LiquidBalance returns the LiquidBalance (total balance in ERC20-token - total hard deposits in ERC20-token) of the chequebook @@ -237,3 +240,26 @@ func waitForTx(ctx context.Context, backend Backend, tx *types.Transaction) (*ty } return receipt, nil } + +// WaitForTransactionByHash waits for a transaction to by mined by hash +func WaitForTransactionByHash(ctx context.Context, backend Backend, txHash common.Hash) (*types.Receipt, error) { + tx, pending, err := backend.TransactionByHash(ctx, txHash) + if err != nil { + return nil, err + } + + var receipt *types.Receipt + if pending { + receipt, err = WaitFunc(ctx, backend, tx) + if err != nil { + return nil, err + } + } else { + receipt, err = backend.TransactionReceipt(ctx, txHash) + if err != nil { + return nil, err + } + } + + return receipt, nil +} diff --git a/swap/cashout.go b/swap/cashout.go new file mode 100644 index 0000000000..1b1aeeef37 --- /dev/null +++ b/swap/cashout.go @@ -0,0 +1,118 @@ +package swap + +import ( + "context" + "crypto/ecdsa" + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/metrics" + contract "github.com/ethersphere/swarm/contracts/swap" +) + +// CashoutProcessor holds all relevant fields needed for processing cashouts +type CashoutProcessor struct { + backend contract.Backend // ethereum backend to use + privateKey *ecdsa.PrivateKey // private key to use +} + +// CashoutRequest represents a request for a cashout operation +type CashoutRequest struct { + Cheque Cheque // cheque to be cashed + Destination common.Address // destination for the payout +} + +// ActiveCashout stores the necessary information for a cashout in progess +type ActiveCashout struct { + Request CashoutRequest // the request that caused this cashout + TransactionHash common.Hash // the hash of the current transaction for this request +} + +// newCashoutProcessor creates a new instance of CashoutLoop +func newCashoutProcessor(backend contract.Backend, privateKey *ecdsa.PrivateKey) *CashoutProcessor { + return &CashoutProcessor{ + backend: backend, + privateKey: privateKey, + } +} + +// cashCheque tries to cash the cheque specified in the request +// after the transaction is sent it waits on its success +func (c *CashoutProcessor) cashCheque(ctx context.Context, request *CashoutRequest) error { + cheque := request.Cheque + opts := bind.NewKeyedTransactor(c.privateKey) + opts.Context = ctx + + otherSwap, err := contract.InstanceAt(cheque.Contract, c.backend) + if err != nil { + return err + } + + tx, err := otherSwap.CashChequeBeneficiaryStart(opts, request.Destination, big.NewInt(int64(cheque.CumulativePayout)), cheque.Signature) + if err != nil { + return err + } + + // this blocks until the cashout has been successfully processed + return c.waitForAndProcessActiveCashout(&ActiveCashout{ + Request: *request, + TransactionHash: tx.Hash(), + }) +} + +// estimatePayout estimates the payout for a given cheque as well as the transaction cost +func (c *CashoutProcessor) estimatePayout(ctx context.Context, cheque *Cheque) (uint64, uint64, error) { + otherSwap, err := contract.InstanceAt(cheque.Contract, c.backend) + if err != nil { + return 0, 0, err + } + + paidOut, err := otherSwap.PaidOut(&bind.CallOpts{Context: ctx}, cheque.Beneficiary) + if err != nil { + return 0, 0, err + } + + gasPrice, err := c.backend.SuggestGasPrice(ctx) + if err != nil { + return 0, 0, err + } + + transactionCosts := gasPrice.Uint64() * 50000 // cashing a cheque is approximately 50000 gas + + if paidOut.Cmp(big.NewInt(int64(cheque.CumulativePayout))) > 0 { + return 0, transactionCosts, nil + } + + expectedPayout := cheque.CumulativePayout - paidOut.Uint64() + + return expectedPayout, transactionCosts, nil +} + +// waitForAndProcessActiveCashout waits for activeCashout to complete +func (c *CashoutProcessor) waitForAndProcessActiveCashout(activeCashout *ActiveCashout) error { + ctx, cancel := context.WithTimeout(context.Background(), DefaultTransactionTimeout) + defer cancel() + + receipt, err := contract.WaitForTransactionByHash(ctx, c.backend, activeCashout.TransactionHash) + if err != nil { + return err + } + + otherSwap, err := contract.InstanceAt(activeCashout.Request.Cheque.Contract, c.backend) + if err != nil { + return err + } + + result := otherSwap.CashChequeBeneficiaryResult(receipt) + + metrics.GetOrRegisterCounter("swap.cheques.cashed.honey", nil).Inc(result.TotalPayout.Int64()) + + if result.Bounced { + metrics.GetOrRegisterCounter("swap.cheques.cashed.bounced", nil).Inc(1) + swapLog.Warn("cheque bounced", "tx", receipt.TxHash) + } + + swapLog.Info("cheque cashed", "honey", activeCashout.Request.Cheque.Honey) + return nil +} diff --git a/swap/config.go b/swap/config.go index e465bdbf44..ab645d0a89 100644 --- a/swap/config.go +++ b/swap/config.go @@ -34,5 +34,6 @@ const ( // The smart-contract allows for setting this variable differently per beneficiary defaultHarddepositTimeoutDuration = 24 * time.Hour // Until we deploy swap officially, it's only allowed to be enabled under a specific network ID (use the --bzznetworkid flag to set it) - AllowedNetworkID = 5 + AllowedNetworkID = 5 + DefaultTransactionTimeout = 10 * time.Minute ) diff --git a/swap/swap.go b/swap/swap.go index d50812530e..f40a40667f 100644 --- a/swap/swap.go +++ b/swap/swap.go @@ -66,6 +66,7 @@ type Swap struct { contract contract.Contract // reference to the smart contract chequebookFactory contract.SimpleSwapFactory // the chequebook factory used honeyPriceOracle HoneyOracle // oracle which resolves the price of honey (in Wei) + cashoutProcessor *CashoutProcessor // processor for cashing out } // Owner encapsulates information related to accessing the contract @@ -144,6 +145,7 @@ func newSwapInstance(stateStore state.Store, owner *Owner, backend contract.Back chequebookFactory: chequebookFactory, honeyPriceOracle: NewHoneyPriceOracle(), chainID: chainID, + cashoutProcessor: newCashoutProcessor(backend, owner.privateKey), } } @@ -433,27 +435,14 @@ func (s *Swap) handleEmitChequeMsg(ctx context.Context, p *Peer, msg *EmitCheque return err } - otherSwap, err := contract.InstanceAt(cheque.Contract, s.backend) + expectedPayout, transactionCosts, err := s.cashoutProcessor.estimatePayout(context.TODO(), cheque) if err != nil { - log.Error("error getting contract", "err", err) return err } - gasPrice, err := s.backend.SuggestGasPrice(context.TODO()) - if err != nil { - return err - } - transactionCosts := gasPrice.Uint64() * 50000 // cashing a cheque is approximately 50000 gas - paidOut, err := otherSwap.PaidOut(nil, cheque.Beneficiary) - if err != nil { - return err - } // do a payout transaction if we get 2 times the gas costs - if (cheque.CumulativePayout - paidOut.Uint64()) > 2*transactionCosts { - opts := bind.NewKeyedTransactor(s.owner.privateKey) - opts.Context = ctx - // cash cheque in async, otherwise this blocks here until the TX is mined - go defaultCashCheque(s, otherSwap, opts, cheque) + if expectedPayout > 2*transactionCosts { + go defaultCashCheque(s, cheque) } return err @@ -489,26 +478,15 @@ func (s *Swap) handleConfirmChequeMsg(ctx context.Context, p *Peer, msg *Confirm // cashCheque should be called async as it blocks until the transaction(s) are mined // The function cashes the cheque by sending it to the blockchain -func cashCheque(s *Swap, otherSwap contract.Contract, opts *bind.TransactOpts, cheque *Cheque) { - // blocks here, as we are waiting for the transaction to be mined - result, receipt, err := otherSwap.CashChequeBeneficiary(opts, s.GetParams().ContractAddress, big.NewInt(int64(cheque.CumulativePayout)), cheque.Signature) - if err != nil { - // TODO: do something with the error - // and we actually need to log this error as we are in an async routine; nobody is handling this error for now - swapLog.Error("error cashing cheque", "err", err) - return - } - - metrics.GetOrRegisterCounter("swap.cheques.cashed.honey", nil).Inc(result.TotalPayout.Int64()) +func cashCheque(s *Swap, cheque *Cheque) { + err := s.cashoutProcessor.cashCheque(context.Background(), &CashoutRequest{ + Cheque: *cheque, + Destination: s.GetParams().ContractAddress, + }) - if result.Bounced { - metrics.GetOrRegisterCounter("swap.cheques.cashed.bounced", nil).Inc(1) - swapLog.Warn("cheque bounced", "tx", receipt.TxHash) - return - // TODO: do something here + if err != nil { + swapLog.Error(err.Error()) } - - swapLog.Debug("cash tx mined", "receipt", receipt) } // processAndVerifyCheque verifies the cheque and compares it with the last received cheque diff --git a/swap/swap_test.go b/swap/swap_test.go index 51a67eb25d..35a29e96f3 100644 --- a/swap/swap_test.go +++ b/swap/swap_test.go @@ -52,6 +52,7 @@ import ( "github.com/ethereum/go-ethereum/rpc" contractFactory "github.com/ethersphere/go-sw3/contracts-v0-2-0/simpleswapfactory" "github.com/ethersphere/swarm/contracts/swap" + contract "github.com/ethersphere/swarm/contracts/swap" cswap "github.com/ethersphere/swarm/contracts/swap" "github.com/ethersphere/swarm/p2p/protocols" "github.com/ethersphere/swarm/state" @@ -852,8 +853,8 @@ func TestRestoreBalanceFromStateStore(t *testing.T) { // During tests, because the cashing in of cheques is async, we should wait for the function to be returned // Otherwise if we call `handleEmitChequeMsg` manually, it will return before the TX has been committed to the `SimulatedBackend`, // causing subsequent TX to possibly fail due to nonce mismatch -func testCashCheque(s *Swap, otherSwap cswap.Contract, opts *bind.TransactOpts, cheque *Cheque) { - cashCheque(s, otherSwap, opts, cheque) +func testCashCheque(s *Swap, cheque *Cheque) { + cashCheque(s, cheque) // send to the channel, signals to clients that this function actually finished if stb, ok := s.backend.(*swapTestBackend); ok { if stb.cashDone != nil { @@ -1193,11 +1194,17 @@ func TestContractIntegration(t *testing.T) { log.Debug("cash-in the cheque") - cashResult, receipt, err := issuerSwap.contract.CashChequeBeneficiary(opts, beneficiaryAddress, big.NewInt(int64(cheque.CumulativePayout)), cheque.Signature) + tx, err := issuerSwap.contract.CashChequeBeneficiaryStart(opts, beneficiaryAddress, big.NewInt(int64(cheque.CumulativePayout)), cheque.Signature) + if err != nil { + t.Fatal(err) + } + receipt, err := contract.WaitForTransactionByHash(ctx, issuerSwap.backend, tx.Hash()) if err != nil { t.Fatal(err) } + + cashResult := issuerSwap.contract.CashChequeBeneficiaryResult(receipt) if receipt.Status != 1 { t.Fatalf("Bad status %d", receipt.Status) } @@ -1225,13 +1232,20 @@ func TestContractIntegration(t *testing.T) { } log.Debug("try to cash-in the bouncing cheque") - cashResult, receipt, err = issuerSwap.contract.CashChequeBeneficiary(opts, beneficiaryAddress, big.NewInt(int64(bouncingCheque.CumulativePayout)), bouncingCheque.Signature) + tx, err = issuerSwap.contract.CashChequeBeneficiaryStart(opts, beneficiaryAddress, big.NewInt(int64(bouncingCheque.CumulativePayout)), bouncingCheque.Signature) + if err != nil { + t.Fatal(err) + } + + receipt, err = contract.WaitForTransactionByHash(ctx, issuerSwap.backend, tx.Hash()) if err != nil { t.Fatal(err) } if receipt.Status != 1 { t.Fatalf("Bad status %d", receipt.Status) } + + cashResult = issuerSwap.contract.CashChequeBeneficiaryResult(receipt) if !cashResult.Bounced { t.Fatal("cheque did not bounce") } From 9d34553dc4338fd44c59fd27d8853043778cdfe0 Mon Sep 17 00:00:00 2001 From: Ralph Pichler Date: Tue, 7 Jan 2020 14:35:36 +0100 Subject: [PATCH 2/7] swap: create a test for cashCheque and move TestContractIntegration testDeployWithPrivateKey --- swap/cashout.go | 15 ++++++ swap/swap_test.go | 131 +++++++--------------------------------------- 2 files changed, 34 insertions(+), 112 deletions(-) diff --git a/swap/cashout.go b/swap/cashout.go index 1b1aeeef37..d05db830aa 100644 --- a/swap/cashout.go +++ b/swap/cashout.go @@ -1,3 +1,18 @@ +// Copyright 2020 The Swarm Authors +// This file is part of the Swarm library. +// +// The Swarm library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The Swarm library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the Swarm library. If not, see . package swap import ( diff --git a/swap/swap_test.go b/swap/swap_test.go index 35a29e96f3..ac34cabe27 100644 --- a/swap/swap_test.go +++ b/swap/swap_test.go @@ -1152,105 +1152,6 @@ func TestFactoryVerifySelf(t *testing.T) { } } -// TestContractIntegration tests a end-to-end cheque interaction. -// First a simulated backend is created, then we deploy the issuer's swap contract. -// We issue a test cheque with the beneficiary address and on the issuer's contract, -// and immediately try to cash-in the cheque -// afterwards it attempts to cash-in a bouncing cheque -func TestContractIntegration(t *testing.T) { - log.Debug("creating test swap") - - issuerSwap, clean := newTestSwap(t, ownerKey, nil) - defer clean() - - issuerSwap.owner.address = ownerAddress - issuerSwap.owner.privateKey = ownerKey - - log.Debug("deploy issuer swap") - - cheque := newTestCheque() - - ctx := context.TODO() - err := testDeploy(ctx, issuerSwap, big.NewInt(int64(cheque.CumulativePayout))) - if err != nil { - t.Fatal(err) - } - - log.Debug("deployed. signing cheque") - cheque.ChequeParams.Contract = issuerSwap.GetParams().ContractAddress - cheque.Signature, err = cheque.Sign(issuerSwap.owner.privateKey) - if err != nil { - t.Fatal(err) - } - - log.Debug("sending cheque...") - - // setup the wait for mined transaction function for testing - cleanup := setupContractTest() - defer cleanup() - - opts := bind.NewKeyedTransactor(beneficiaryKey) - opts.Context = ctx - - log.Debug("cash-in the cheque") - - tx, err := issuerSwap.contract.CashChequeBeneficiaryStart(opts, beneficiaryAddress, big.NewInt(int64(cheque.CumulativePayout)), cheque.Signature) - if err != nil { - t.Fatal(err) - } - - receipt, err := contract.WaitForTransactionByHash(ctx, issuerSwap.backend, tx.Hash()) - if err != nil { - t.Fatal(err) - } - - cashResult := issuerSwap.contract.CashChequeBeneficiaryResult(receipt) - if receipt.Status != 1 { - t.Fatalf("Bad status %d", receipt.Status) - } - if cashResult.Bounced { - t.Fatal("cashing bounced") - } - - // check state, check that cheque is indeed there - result, err := issuerSwap.contract.PaidOut(nil, beneficiaryAddress) - if err != nil { - t.Fatal(err) - } - if result.Uint64() != cheque.CumulativePayout { - t.Fatalf("Wrong cumulative payout %d", result) - } - log.Debug("cheques result", "result", result) - - // create a cheque that will bounce - bouncingCheque := newTestCheque() - bouncingCheque.ChequeParams.Contract = issuerSwap.GetParams().ContractAddress - bouncingCheque.CumulativePayout = bouncingCheque.CumulativePayout + 10000*RetrieveRequestPrice - bouncingCheque.Signature, err = bouncingCheque.Sign(issuerSwap.owner.privateKey) - if err != nil { - t.Fatal(err) - } - - log.Debug("try to cash-in the bouncing cheque") - tx, err = issuerSwap.contract.CashChequeBeneficiaryStart(opts, beneficiaryAddress, big.NewInt(int64(bouncingCheque.CumulativePayout)), bouncingCheque.Signature) - if err != nil { - t.Fatal(err) - } - - receipt, err = contract.WaitForTransactionByHash(ctx, issuerSwap.backend, tx.Hash()) - if err != nil { - t.Fatal(err) - } - if receipt.Status != 1 { - t.Fatalf("Bad status %d", receipt.Status) - } - - cashResult = issuerSwap.contract.CashChequeBeneficiaryResult(receipt) - if !cashResult.Bounced { - t.Fatal("cheque did not bounce") - } -} - // when testing, we don't need to wait for a transaction to be mined func testWaitForTx(ctx context.Context, backend cswap.Backend, tx *types.Transaction) (*types.Receipt, error) { @@ -1272,51 +1173,57 @@ func testWaitForTx(ctx context.Context, backend cswap.Backend, tx *types.Transac } // deploy for testing (needs simulated backend commit) -func testDeploy(ctx context.Context, swap *Swap, depositAmount *big.Int) (err error) { - opts := bind.NewKeyedTransactor(swap.owner.privateKey) +func testDeployWithPrivateKey(ctx context.Context, backend swap.Backend, privateKey *ecdsa.PrivateKey, ownerAddress common.Address, depositAmount *big.Int) (contract.Contract, error) { + opts := bind.NewKeyedTransactor(privateKey) opts.Context = ctx var stb *swapTestBackend var ok bool - if stb, ok = swap.backend.(*swapTestBackend); !ok { - return errors.New("not the expected test backend") + if stb, ok = backend.(*swapTestBackend); !ok { + return nil, errors.New("not the expected test backend") } factory, err := cswap.FactoryAt(stb.factoryAddress, stb) if err != nil { - return err + return nil, err } // setup the wait for mined transaction function for testing cleanup := setupContractTest() defer cleanup() - swap.contract, err = factory.DeploySimpleSwap(opts, swap.owner.address, big.NewInt(int64(defaultHarddepositTimeoutDuration))) + contract, err := factory.DeploySimpleSwap(opts, ownerAddress, big.NewInt(int64(defaultHarddepositTimeoutDuration))) if err != nil { - return err + return nil, err } stb.Commit() // send money into the new chequebook token, err := contractFactory.NewERC20Mintable(stb.tokenAddress, stb) if err != nil { - return err + return nil, err } - tx, err := token.Mint(bind.NewKeyedTransactor(ownerKey), swap.contract.ContractParams().ContractAddress, depositAmount) + tx, err := token.Mint(bind.NewKeyedTransactor(ownerKey), contract.ContractParams().ContractAddress, depositAmount) if err != nil { - return err + return nil, err } stb.Commit() receipt, err := stb.TransactionReceipt(ctx, tx.Hash()) if err != nil { - return err + return nil, err } if receipt.Status != 1 { - return errors.New("token transfer reverted") + return nil, errors.New("token transfer reverted") } - return nil + return contract, nil +} + +// deploy for testing (needs simulated backend commit) +func testDeploy(ctx context.Context, swap *Swap, depositAmount *big.Int) (err error) { + swap.contract, err = testDeployWithPrivateKey(ctx, swap.backend, swap.owner.privateKey, swap.owner.address, depositAmount) + return err } // newTestSwapAndPeer is a helper function to create a swap and a peer instance that fit together From c47db3f303e4efa0ab24b3b49486c7e6791ccde5 Mon Sep 17 00:00:00 2001 From: Ralph Pichler Date: Tue, 7 Jan 2020 14:45:26 +0100 Subject: [PATCH 3/7] swap: add missing test file --- swap/cashout_test.go | 194 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 swap/cashout_test.go diff --git a/swap/cashout_test.go b/swap/cashout_test.go new file mode 100644 index 0000000000..6d690c8faf --- /dev/null +++ b/swap/cashout_test.go @@ -0,0 +1,194 @@ +// Copyright 2020 The Swarm Authors +// This file is part of the Swarm library. +// +// The Swarm library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The Swarm library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the Swarm library. If not, see . + +package swap + +import ( + "context" + "crypto/ecdsa" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" + contract "github.com/ethersphere/swarm/contracts/swap" +) + +func newSignedTestCheque(testChequeContract common.Address, beneficiaryAddress common.Address, cumulativePayout *big.Int, signingKey *ecdsa.PrivateKey) (*Cheque, error) { + cheque := &Cheque{ + ChequeParams: ChequeParams{ + Contract: testChequeContract, + CumulativePayout: cumulativePayout.Uint64(), + Beneficiary: beneficiaryAddress, + }, + Honey: cumulativePayout.Uint64(), + } + + sig, err := cheque.Sign(signingKey) + if err != nil { + return nil, err + } + cheque.Signature = sig + return cheque, nil +} + +// TestContractIntegration tests a end-to-end cheque interaction. +// First a simulated backend is created, then we deploy the issuer's swap contract. +// We issue a test cheque with the beneficiary address and on the issuer's contract, +// and immediately try to cash-in the cheque +// afterwards it attempts to cash-in a bouncing cheque +func TestContractIntegration(t *testing.T) { + backend := newTestBackend(t) + reset := setupContractTest() + defer reset() + + payout := big.NewInt(42) + + chequebook, err := testDeployWithPrivateKey(context.Background(), backend, ownerKey, ownerAddress, payout) + if err != nil { + t.Fatal(err) + } + + cheque, err := newSignedTestCheque(chequebook.ContractParams().ContractAddress, beneficiaryAddress, payout, ownerKey) + if err != nil { + t.Fatal(err) + } + + opts := bind.NewKeyedTransactor(beneficiaryKey) + + tx, err := chequebook.CashChequeBeneficiaryStart(opts, beneficiaryAddress, payout, cheque.Signature) + if err != nil { + t.Fatal(err) + } + + receipt, err := contract.WaitForTransactionByHash(context.Background(), backend, tx.Hash()) + if err != nil { + t.Fatal(err) + } + + cashResult := chequebook.CashChequeBeneficiaryResult(receipt) + if receipt.Status != 1 { + t.Fatalf("Bad status %d", receipt.Status) + } + if cashResult.Bounced { + t.Fatal("cashing bounced") + } + + // check state, check that cheque is indeed there + result, err := chequebook.PaidOut(nil, beneficiaryAddress) + if err != nil { + t.Fatal(err) + } + if result.Uint64() != cheque.CumulativePayout { + t.Fatalf("Wrong cumulative payout %d", result) + } + log.Debug("cheques result", "result", result) + + // create a cheque that will bounce + bouncingCheque, err := newSignedTestCheque(chequebook.ContractParams().ContractAddress, beneficiaryAddress, payout.Add(payout, big.NewInt(int64(10000*RetrieveRequestPrice))), ownerKey) + if err != nil { + t.Fatal(err) + } + + tx, err = chequebook.CashChequeBeneficiaryStart(opts, beneficiaryAddress, big.NewInt(int64(bouncingCheque.CumulativePayout)), bouncingCheque.Signature) + if err != nil { + t.Fatal(err) + } + + receipt, err = contract.WaitForTransactionByHash(context.Background(), backend, tx.Hash()) + if err != nil { + t.Fatal(err) + } + if receipt.Status != 1 { + t.Fatalf("Bad status %d", receipt.Status) + } + + cashResult = chequebook.CashChequeBeneficiaryResult(receipt) + if !cashResult.Bounced { + t.Fatal("cheque did not bounce") + } + +} + +func TestCashCheque(t *testing.T) { + backend := newTestBackend(t) + reset := setupContractTest() + defer reset() + + cashoutProcessor := newCashoutProcessor(backend, ownerKey) + payout := big.NewInt(42) + + chequebook, err := testDeployWithPrivateKey(context.Background(), backend, ownerKey, ownerAddress, payout) + if err != nil { + t.Fatal(err) + } + + testCheque, err := newSignedTestCheque(chequebook.ContractParams().ContractAddress, ownerAddress, payout, ownerKey) + if err != nil { + t.Fatal(err) + } + + err = cashoutProcessor.cashCheque(context.Background(), &CashoutRequest{ + Cheque: *testCheque, + Destination: ownerAddress, + }) + if err != nil { + t.Fatal(err) + } + + paidOut, err := chequebook.PaidOut(nil, ownerAddress) + if err != nil { + t.Fatal(err) + } + + if paidOut.Cmp(big.NewInt(int64(testCheque.CumulativePayout))) != 0 { + t.Fatalf("paidOut does not equal the CumulativePayout: paidOut=%v expected=%v", paidOut, testCheque.CumulativePayout) + } +} + +func TestEstimatePayout(t *testing.T) { + backend := newTestBackend(t) + reset := setupContractTest() + defer reset() + + cashoutProcessor := newCashoutProcessor(backend, ownerKey) + payout := big.NewInt(42) + + chequebook, err := testDeployWithPrivateKey(context.Background(), backend, ownerKey, ownerAddress, payout) + if err != nil { + t.Fatal(err) + } + + testCheque, err := newSignedTestCheque(chequebook.ContractParams().ContractAddress, ownerAddress, payout, ownerKey) + if err != nil { + t.Fatal(err) + } + + expectedPayout, transactionCost, err := cashoutProcessor.estimatePayout(context.Background(), testCheque) + if err != nil { + t.Fatal(err) + } + + if expectedPayout != payout.Uint64() { + t.Fatalf("unexpected expectedPayout: got %d, wanted: %d", expectedPayout, payout.Uint64()) + } + + // the gas price in the simulated backend is 1 therefore the total transactionCost should be 50000 * 1 = 50000 + if transactionCost != 50000 { + t.Fatalf("unexpected transactionCost: got %d, wanted: %d", transactionCost, 0) + } +} From 096ee54de4401b6554b25a5ff269a73825d50a43 Mon Sep 17 00:00:00 2001 From: Ralph Pichler Date: Tue, 7 Jan 2020 14:46:56 +0100 Subject: [PATCH 4/7] swap: add comments for tests --- swap/cashout_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/swap/cashout_test.go b/swap/cashout_test.go index 6d690c8faf..045f36ce55 100644 --- a/swap/cashout_test.go +++ b/swap/cashout_test.go @@ -124,6 +124,7 @@ func TestContractIntegration(t *testing.T) { } +// TestCashCheque creates a valid cheque and feeds it to cashoutProcessor.cashCheque func TestCashCheque(t *testing.T) { backend := newTestBackend(t) reset := setupContractTest() @@ -160,6 +161,7 @@ func TestCashCheque(t *testing.T) { } } +// TestEstimatePayout creates a valid cheque and feeds it to cashoutProcessor.estimatePayout func TestEstimatePayout(t *testing.T) { backend := newTestBackend(t) reset := setupContractTest() From cb88fd29735f0e54fd8f41df28c6b3831493de49 Mon Sep 17 00:00:00 2001 From: Ralph Pichler Date: Thu, 9 Jan 2020 16:16:43 +0100 Subject: [PATCH 5/7] swap: address pr comments --- contracts/swap/swap.go | 2 +- swap/cashout.go | 8 ++++---- swap/swap_test.go | 6 ++---- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/contracts/swap/swap.go b/contracts/swap/swap.go index ae9db1eab3..a490d72784 100644 --- a/contracts/swap/swap.go +++ b/contracts/swap/swap.go @@ -142,7 +142,7 @@ func (s simpleContract) Deposit(auth *bind.TransactOpts, amount *big.Int) (*type return WaitFunc(auth.Context, s.backend, tx) } -// CashChequeBeneficiaryStart sends the transaction to a cash a cheque as the beneficiary +// CashChequeBeneficiaryStart sends the transaction to cash a cheque as the beneficiary func (s simpleContract) CashChequeBeneficiaryStart(opts *bind.TransactOpts, beneficiary common.Address, cumulativePayout *big.Int, ownerSig []byte) (*types.Transaction, error) { tx, err := s.instance.CashChequeBeneficiary(opts, beneficiary, cumulativePayout, ownerSig) if err != nil { diff --git a/swap/cashout.go b/swap/cashout.go index d05db830aa..ea7c3aef01 100644 --- a/swap/cashout.go +++ b/swap/cashout.go @@ -44,7 +44,7 @@ type ActiveCashout struct { TransactionHash common.Hash // the hash of the current transaction for this request } -// newCashoutProcessor creates a new instance of CashoutLoop +// newCashoutProcessor creates a new instance of CashoutProcessor func newCashoutProcessor(backend contract.Backend, privateKey *ecdsa.PrivateKey) *CashoutProcessor { return &CashoutProcessor{ backend: backend, @@ -77,7 +77,7 @@ func (c *CashoutProcessor) cashCheque(ctx context.Context, request *CashoutReque } // estimatePayout estimates the payout for a given cheque as well as the transaction cost -func (c *CashoutProcessor) estimatePayout(ctx context.Context, cheque *Cheque) (uint64, uint64, error) { +func (c *CashoutProcessor) estimatePayout(ctx context.Context, cheque *Cheque) (expectedPayout uint64, transactionCosts uint64, err error) { otherSwap, err := contract.InstanceAt(cheque.Contract, c.backend) if err != nil { return 0, 0, err @@ -93,13 +93,13 @@ func (c *CashoutProcessor) estimatePayout(ctx context.Context, cheque *Cheque) ( return 0, 0, err } - transactionCosts := gasPrice.Uint64() * 50000 // cashing a cheque is approximately 50000 gas + transactionCosts = gasPrice.Uint64() * 50000 // cashing a cheque is approximately 50000 gas if paidOut.Cmp(big.NewInt(int64(cheque.CumulativePayout))) > 0 { return 0, transactionCosts, nil } - expectedPayout := cheque.CumulativePayout - paidOut.Uint64() + expectedPayout = cheque.CumulativePayout - paidOut.Uint64() return expectedPayout, transactionCosts, nil } diff --git a/swap/swap_test.go b/swap/swap_test.go index ac34cabe27..c68140f675 100644 --- a/swap/swap_test.go +++ b/swap/swap_test.go @@ -51,8 +51,6 @@ import ( "github.com/ethereum/go-ethereum/p2p/simulations/adapters" "github.com/ethereum/go-ethereum/rpc" contractFactory "github.com/ethersphere/go-sw3/contracts-v0-2-0/simpleswapfactory" - "github.com/ethersphere/swarm/contracts/swap" - contract "github.com/ethersphere/swarm/contracts/swap" cswap "github.com/ethersphere/swarm/contracts/swap" "github.com/ethersphere/swarm/p2p/protocols" "github.com/ethersphere/swarm/state" @@ -512,7 +510,7 @@ func TestStartChequebookFailure(t *testing.T) { name: "with wrong pass in", configure: func(config *chequebookConfig) { config.passIn = common.HexToAddress("0x4405415b2B8c9F9aA83E151637B8370000000000") // address without deployed chequebook - config.expectedError = fmt.Errorf("contract validation for %v failed: %v", config.passIn.Hex(), swap.ErrNotDeployedByFactory) + config.expectedError = fmt.Errorf("contract validation for %v failed: %v", config.passIn.Hex(), cswap.ErrNotDeployedByFactory) }, check: func(t *testing.T, config *chequebookConfig) { // create SWAP @@ -1173,7 +1171,7 @@ func testWaitForTx(ctx context.Context, backend cswap.Backend, tx *types.Transac } // deploy for testing (needs simulated backend commit) -func testDeployWithPrivateKey(ctx context.Context, backend swap.Backend, privateKey *ecdsa.PrivateKey, ownerAddress common.Address, depositAmount *big.Int) (contract.Contract, error) { +func testDeployWithPrivateKey(ctx context.Context, backend cswap.Backend, privateKey *ecdsa.PrivateKey, ownerAddress common.Address, depositAmount *big.Int) (cswap.Contract, error) { opts := bind.NewKeyedTransactor(privateKey) opts.Context = ctx From b6dd9bb0722ff5fc65bf95b3c1560c96bbb9fb7d Mon Sep 17 00:00:00 2001 From: Ralph Pichler Date: Fri, 10 Jan 2020 12:08:43 +0100 Subject: [PATCH 6/7] swap: extract CashChequeBeneficiaryTransactionCost to a constant --- contracts/swap/swap.go | 2 +- swap/cashout.go | 5 ++++- swap/cashout_test.go | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/contracts/swap/swap.go b/contracts/swap/swap.go index a490d72784..535748bf14 100644 --- a/contracts/swap/swap.go +++ b/contracts/swap/swap.go @@ -49,7 +49,7 @@ type Contract interface { Withdraw(auth *bind.TransactOpts, amount *big.Int) (*types.Receipt, error) // Deposit sends a raw transaction to the chequebook, triggering the fallback—depositing amount Deposit(auth *bind.TransactOpts, amout *big.Int) (*types.Receipt, error) - // CashChequeBeneficiaryStart sends the transaction to a cash a cheque as the beneficiary + // CashChequeBeneficiaryStart sends the transaction to cash a cheque as the beneficiary CashChequeBeneficiaryStart(opts *bind.TransactOpts, beneficiary common.Address, cumulativePayout *big.Int, ownerSig []byte) (*types.Transaction, error) // CashChequeBeneficiaryResult processes the receipt from a CashChequeBeneficiary transaction CashChequeBeneficiaryResult(receipt *types.Receipt) *CashChequeResult diff --git a/swap/cashout.go b/swap/cashout.go index ea7c3aef01..fb5106d6dc 100644 --- a/swap/cashout.go +++ b/swap/cashout.go @@ -26,6 +26,9 @@ import ( contract "github.com/ethersphere/swarm/contracts/swap" ) +// CashChequeBeneficiaryTransactionCost is the expected gas cost of a CashChequeBeneficiary transaction +const CashChequeBeneficiaryTransactionCost = 50000 + // CashoutProcessor holds all relevant fields needed for processing cashouts type CashoutProcessor struct { backend contract.Backend // ethereum backend to use @@ -93,7 +96,7 @@ func (c *CashoutProcessor) estimatePayout(ctx context.Context, cheque *Cheque) ( return 0, 0, err } - transactionCosts = gasPrice.Uint64() * 50000 // cashing a cheque is approximately 50000 gas + transactionCosts = gasPrice.Uint64() * CashChequeBeneficiaryTransactionCost if paidOut.Cmp(big.NewInt(int64(cheque.CumulativePayout))) > 0 { return 0, transactionCosts, nil diff --git a/swap/cashout_test.go b/swap/cashout_test.go index 045f36ce55..cb0c369177 100644 --- a/swap/cashout_test.go +++ b/swap/cashout_test.go @@ -190,7 +190,7 @@ func TestEstimatePayout(t *testing.T) { } // the gas price in the simulated backend is 1 therefore the total transactionCost should be 50000 * 1 = 50000 - if transactionCost != 50000 { + if transactionCost != CashChequeBeneficiaryTransactionCost { t.Fatalf("unexpected transactionCost: got %d, wanted: %d", transactionCost, 0) } } From a14816168f4917a34acc9510e2684006ceb45b56 Mon Sep 17 00:00:00 2001 From: Ralph Pichler Date: Fri, 10 Jan 2020 12:13:11 +0100 Subject: [PATCH 7/7] swap: add metric for cheque cashing errors --- swap/swap.go | 1 + 1 file changed, 1 insertion(+) diff --git a/swap/swap.go b/swap/swap.go index f40a40667f..18b562ff9f 100644 --- a/swap/swap.go +++ b/swap/swap.go @@ -485,6 +485,7 @@ func cashCheque(s *Swap, cheque *Cheque) { }) if err != nil { + metrics.GetOrRegisterCounter("swap.cheques.cashed.errors", nil).Inc(1) swapLog.Error(err.Error()) } }