Commit 8cb1d7c1 authored by Ralph Pichler's avatar Ralph Pichler Committed by GitHub

feat: add tx cancel (#2212)

parent 93fee3ce
......@@ -736,6 +736,31 @@ paths:
$ref: "SwarmCommon.yaml#/components/responses/500"
default:
description: Default response
delete:
summary: Cancel existing transaction
parameters:
- in: path
name: txHash
schema:
$ref: "SwarmCommon.yaml#/components/schemas/TransactionHash"
required: true
description: Hash of the transaction
- $ref: "SwarmCommon.yaml#/components/parameters/GasPriceParameter"
tags:
- Transaction
responses:
"200":
description: Hash of the transaction
content:
application/json:
schema:
$ref: "SwarmCommon.yaml#/components/schemas/TransactionResponse"
"404":
$ref: "SwarmCommon.yaml#/components/responses/404"
"500":
$ref: "SwarmCommon.yaml#/components/responses/500"
default:
description: Default response
"/stamps":
get:
......
......@@ -175,8 +175,9 @@ func (s *Service) newRouter() *mux.Router {
"GET": http.HandlerFunc(s.transactionListHandler),
})
router.Handle("/transactions/{hash}", jsonhttp.MethodHandler{
"GET": http.HandlerFunc(s.transactionDetailHandler),
"POST": http.HandlerFunc(s.transactionResendHandler),
"GET": http.HandlerFunc(s.transactionDetailHandler),
"POST": http.HandlerFunc(s.transactionResendHandler),
"DELETE": http.HandlerFunc(s.transactionCancelHandler),
})
router.Handle("/tags/{id}", jsonhttp.MethodHandler{
......
......@@ -6,6 +6,7 @@ package debugapi
import (
"errors"
"math/big"
"net/http"
"time"
......@@ -13,6 +14,7 @@ import (
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethersphere/bee/pkg/bigint"
"github.com/ethersphere/bee/pkg/jsonhttp"
"github.com/ethersphere/bee/pkg/sctx"
"github.com/ethersphere/bee/pkg/transaction"
"github.com/gorilla/mux"
)
......@@ -115,7 +117,7 @@ func (s *Service) transactionResendHandler(w http.ResponseWriter, r *http.Reques
hash := mux.Vars(r)["hash"]
txHash := common.HexToHash(hash)
err := s.transaction.ResendTransaction(txHash)
err := s.transaction.ResendTransaction(r.Context(), txHash)
if err != nil {
s.logger.Debugf("debug api: transactions: resend %x: %v", txHash, err)
s.logger.Errorf("debug api: transactions: can't resend transaction %x", txHash)
......@@ -133,3 +135,37 @@ func (s *Service) transactionResendHandler(w http.ResponseWriter, r *http.Reques
TransactionHash: txHash,
})
}
func (s *Service) transactionCancelHandler(w http.ResponseWriter, r *http.Request) {
hash := mux.Vars(r)["hash"]
txHash := common.HexToHash(hash)
ctx := r.Context()
if price, ok := r.Header[gasPriceHeader]; ok {
p, ok := big.NewInt(0).SetString(price[0], 10)
if !ok {
s.logger.Error("debug api: transactions: cancel: bad gas price")
jsonhttp.BadRequest(w, errBadGasPrice)
return
}
ctx = sctx.SetGasPrice(ctx, p)
}
txHash, err := s.transaction.CancelTransaction(ctx, txHash)
if err != nil {
s.logger.Debugf("debug api: transactions: cancel %x: %v", txHash, err)
s.logger.Errorf("debug api: transactions: can't cancel transaction %x", txHash)
if errors.Is(err, transaction.ErrUnknownTransaction) {
jsonhttp.NotFound(w, errUnknownTransaction)
} else if errors.Is(err, transaction.ErrAlreadyImported) {
jsonhttp.BadRequest(w, errAlreadyImported)
} else {
jsonhttp.InternalServerError(w, errCantResendTransaction)
}
return
}
jsonhttp.OK(w, transactionHashResponse{
TransactionHash: txHash,
})
}
......@@ -5,6 +5,7 @@
package debugapi_test
import (
"context"
"errors"
"math/big"
"net/http"
......@@ -216,7 +217,7 @@ func TestTransactionResend(t *testing.T) {
t.Run("ok", func(t *testing.T) {
testServer := newTestServer(t, testServerOptions{
TransactionOpts: []mock.Option{
mock.WithResendTransactionFunc(func(txHash common.Hash) error {
mock.WithResendTransactionFunc(func(ctx context.Context, txHash common.Hash) error {
return nil
}),
},
......@@ -232,7 +233,7 @@ func TestTransactionResend(t *testing.T) {
t.Run("unknown transaction", func(t *testing.T) {
testServer := newTestServer(t, testServerOptions{
TransactionOpts: []mock.Option{
mock.WithResendTransactionFunc(func(txHash common.Hash) error {
mock.WithResendTransactionFunc(func(ctx context.Context, txHash common.Hash) error {
return transaction.ErrUnknownTransaction
}),
},
......@@ -249,7 +250,7 @@ func TestTransactionResend(t *testing.T) {
t.Run("already imported", func(t *testing.T) {
testServer := newTestServer(t, testServerOptions{
TransactionOpts: []mock.Option{
mock.WithResendTransactionFunc(func(txHash common.Hash) error {
mock.WithResendTransactionFunc(func(ctx context.Context, txHash common.Hash) error {
return transaction.ErrAlreadyImported
}),
},
......@@ -266,7 +267,7 @@ func TestTransactionResend(t *testing.T) {
t.Run("other error", func(t *testing.T) {
testServer := newTestServer(t, testServerOptions{
TransactionOpts: []mock.Option{
mock.WithResendTransactionFunc(func(txHash common.Hash) error {
mock.WithResendTransactionFunc(func(ctx context.Context, txHash common.Hash) error {
return errors.New("err")
}),
},
......
......@@ -23,8 +23,9 @@ type transactionServiceMock struct {
watchSentTransaction func(txHash common.Hash) (chan types.Receipt, chan error, error)
call func(ctx context.Context, request *transaction.TxRequest) (result []byte, err error)
pendingTransactions func() ([]common.Hash, error)
resendTransaction func(txHash common.Hash) error
resendTransaction func(ctx context.Context, txHash common.Hash) error
storedTransaction func(txHash common.Hash) (*transaction.StoredTransaction, error)
cancelTransaction func(ctx context.Context, originalTxHash common.Hash) (common.Hash, error)
}
func (m *transactionServiceMock) Send(ctx context.Context, request *transaction.TxRequest) (txHash common.Hash, err error) {
......@@ -62,9 +63,9 @@ func (m *transactionServiceMock) PendingTransactions() ([]common.Hash, error) {
return nil, errors.New("not implemented")
}
func (m *transactionServiceMock) ResendTransaction(txHash common.Hash) error {
func (m *transactionServiceMock) ResendTransaction(ctx context.Context, txHash common.Hash) error {
if m.resendTransaction != nil {
return m.resendTransaction(txHash)
return m.resendTransaction(ctx, txHash)
}
return errors.New("not implemented")
}
......@@ -76,6 +77,13 @@ func (m *transactionServiceMock) StoredTransaction(txHash common.Hash) (*transac
return nil, errors.New("not implemented")
}
func (m *transactionServiceMock) CancelTransaction(ctx context.Context, originalTxHash common.Hash) (common.Hash, error) {
if m.send != nil {
return m.cancelTransaction(ctx, originalTxHash)
}
return common.Hash{}, errors.New("not implemented")
}
func (m *transactionServiceMock) Close() error {
return nil
}
......@@ -119,7 +127,7 @@ func WithPendingTransactionsFunc(f func() ([]common.Hash, error)) Option {
})
}
func WithResendTransactionFunc(f func(txHash common.Hash) error) Option {
func WithResendTransactionFunc(f func(ctx context.Context, txHash common.Hash) error) Option {
return optionFunc(func(s *transactionServiceMock) {
s.resendTransaction = f
})
......
......@@ -18,6 +18,7 @@ import (
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethersphere/bee/pkg/crypto"
"github.com/ethersphere/bee/pkg/logging"
"github.com/ethersphere/bee/pkg/sctx"
"github.com/ethersphere/bee/pkg/storage"
"golang.org/x/net/context"
)
......@@ -34,6 +35,7 @@ var (
ErrTransactionReverted = errors.New("transaction reverted")
ErrUnknownTransaction = errors.New("unknown transaction")
ErrAlreadyImported = errors.New("already imported")
ErrGasPriceTooLow = errors.New("gas price too low")
)
// TxRequest describes a request for a transaction that can be executed.
......@@ -76,9 +78,11 @@ type Service interface {
StoredTransaction(txHash common.Hash) (*StoredTransaction, error)
// PendingTransactions retrieves the list of all pending transaction hashes
PendingTransactions() ([]common.Hash, error)
// Resend resends a previously sent transaction
// ResendTransaction resends a previously sent transaction
// This operation can be useful if for some reason the transaction vanished from the eth networks pending pool
ResendTransaction(txHash common.Hash) error
ResendTransaction(ctx context.Context, txHash common.Hash) error
// CancelTransaction cancels a previously sent transaction by double-spending its nonce with zero-transfer one
CancelTransaction(ctx context.Context, originalTxHash common.Hash) (common.Hash, error)
}
type transactionService struct {
......@@ -195,8 +199,9 @@ func (t *transactionService) waitForPendingTx(txHash common.Hash) {
if !errors.Is(err, ErrTransactionCancelled) {
t.logger.Errorf("error while waiting for pending transaction %x: %v", txHash, err)
return
} else {
t.logger.Warningf("pending transaction %x cancelled", txHash)
}
t.logger.Warningf("pending transaction %x cancelled", txHash)
} else {
t.logger.Tracef("pending transaction %x confirmed", txHash)
}
......@@ -371,7 +376,7 @@ func (t *transactionService) PendingTransactions() ([]common.Hash, error) {
return txHashes, nil
}
func (t *transactionService) ResendTransaction(txHash common.Hash) error {
func (t *transactionService) ResendTransaction(ctx context.Context, txHash common.Hash) error {
storedTransaction, err := t.StoredTransaction(txHash)
if err != nil {
return err
......@@ -415,6 +420,61 @@ func (t *transactionService) ResendTransaction(txHash common.Hash) error {
return nil
}
func (t *transactionService) CancelTransaction(ctx context.Context, originalTxHash common.Hash) (common.Hash, error) {
storedTransaction, err := t.StoredTransaction(originalTxHash)
if err != nil {
return common.Hash{}, err
}
gasPrice := sctx.GetGasPrice(ctx)
if gasPrice == nil {
gasPrice = new(big.Int).Add(storedTransaction.GasPrice, big.NewInt(1))
} else if gasPrice.Cmp(storedTransaction.GasPrice) <= 0 {
return common.Hash{}, ErrGasPriceTooLow
}
signedTx, err := t.signer.SignTx(types.NewTransaction(
storedTransaction.Nonce,
t.sender,
big.NewInt(0),
21000,
gasPrice,
[]byte{},
), t.chainID)
if err != nil {
return common.Hash{}, err
}
err = t.backend.SendTransaction(t.ctx, signedTx)
if err != nil {
return common.Hash{}, err
}
txHash := signedTx.Hash()
err = t.store.Put(storedTransactionKey(txHash), StoredTransaction{
To: signedTx.To(),
Data: signedTx.Data(),
GasPrice: signedTx.GasPrice(),
GasLimit: signedTx.Gas(),
Value: signedTx.Value(),
Nonce: signedTx.Nonce(),
Created: time.Now().Unix(),
Description: fmt.Sprintf("%s (cancellation)", storedTransaction.Description),
})
if err != nil {
return common.Hash{}, err
}
err = t.store.Put(pendingTransactionKey(txHash), struct{}{})
if err != nil {
return common.Hash{}, err
}
t.waitForPendingTx(txHash)
return txHash, err
}
func (t *transactionService) Close() error {
t.cancel()
t.wg.Wait()
......
......@@ -7,6 +7,7 @@ package transaction_test
import (
"bytes"
"context"
"errors"
"fmt"
"io/ioutil"
"math/big"
......@@ -18,6 +19,7 @@ import (
"github.com/ethersphere/bee/pkg/crypto"
signermock "github.com/ethersphere/bee/pkg/crypto/mock"
"github.com/ethersphere/bee/pkg/logging"
"github.com/ethersphere/bee/pkg/sctx"
storemock "github.com/ethersphere/bee/pkg/statestore/mock"
"github.com/ethersphere/bee/pkg/transaction"
"github.com/ethersphere/bee/pkg/transaction/backendmock"
......@@ -475,8 +477,154 @@ func TestTransactionResend(t *testing.T) {
}
defer transactionService.Close()
err = transactionService.ResendTransaction(signedTx.Hash())
err = transactionService.ResendTransaction(context.Background(), signedTx.Hash())
if err != nil {
t.Fatal(err)
}
}
func TestTransactionCancel(t *testing.T) {
logger := logging.New(ioutil.Discard, 0)
recipient := common.HexToAddress("0xbbbddd")
chainID := big.NewInt(5)
nonce := uint64(10)
data := []byte{1, 2, 3, 4}
gasPrice := big.NewInt(1)
gasLimit := uint64(100000)
value := big.NewInt(0)
store := storemock.NewStateStore()
defer store.Close()
signedTx := types.NewTransaction(nonce, recipient, value, gasLimit, gasPrice, data)
err := store.Put(transaction.StoredTransactionKey(signedTx.Hash()), transaction.StoredTransaction{
Nonce: nonce,
To: &recipient,
Data: data,
GasPrice: gasPrice,
GasLimit: gasLimit,
Value: value,
})
if err != nil {
t.Fatal(err)
}
t.Run("ok", func(t *testing.T) {
cancelTx := types.NewTransaction(
nonce,
recipient,
big.NewInt(0),
21000,
new(big.Int).Add(gasPrice, big.NewInt(1)),
[]byte{},
)
transactionService, err := transaction.NewService(logger,
backendmock.New(
backendmock.WithSendTransactionFunc(func(ctx context.Context, tx *types.Transaction) error {
if tx != cancelTx {
t.Fatal("not sending signed transaction")
}
return nil
}),
),
signerMockForTransaction(cancelTx, recipient, chainID, t),
store,
chainID,
monitormock.New(),
)
if err != nil {
t.Fatal(err)
}
defer transactionService.Close()
cancelTxHash, err := transactionService.CancelTransaction(context.Background(), signedTx.Hash())
if err != nil {
t.Fatal(err)
}
if cancelTx.Hash() != cancelTxHash {
t.Fatalf("returned wrong hash. wanted %v, got %v", cancelTx.Hash(), cancelTxHash)
}
})
t.Run("custom gas price", func(t *testing.T) {
customGasPrice := big.NewInt(5)
cancelTx := types.NewTransaction(
nonce,
recipient,
big.NewInt(0),
21000,
customGasPrice,
[]byte{},
)
transactionService, err := transaction.NewService(logger,
backendmock.New(
backendmock.WithSendTransactionFunc(func(ctx context.Context, tx *types.Transaction) error {
if tx != cancelTx {
t.Fatal("not sending signed transaction")
}
return nil
}),
),
signerMockForTransaction(cancelTx, recipient, chainID, t),
store,
chainID,
monitormock.New(),
)
if err != nil {
t.Fatal(err)
}
defer transactionService.Close()
ctx := sctx.SetGasPrice(context.Background(), customGasPrice)
cancelTxHash, err := transactionService.CancelTransaction(ctx, signedTx.Hash())
if err != nil {
t.Fatal(err)
}
if cancelTx.Hash() != cancelTxHash {
t.Fatalf("returned wrong hash. wanted %v, got %v", cancelTx.Hash(), cancelTxHash)
}
})
t.Run("too low gas price", func(t *testing.T) {
customGasPrice := big.NewInt(0)
cancelTx := types.NewTransaction(
nonce,
recipient,
big.NewInt(0),
21000,
customGasPrice,
[]byte{},
)
transactionService, err := transaction.NewService(logger,
backendmock.New(
backendmock.WithSendTransactionFunc(func(ctx context.Context, tx *types.Transaction) error {
if tx != cancelTx {
t.Fatal("not sending signed transaction")
}
return nil
}),
),
signerMockForTransaction(cancelTx, recipient, chainID, t),
store,
chainID,
monitormock.New(),
)
if err != nil {
t.Fatal(err)
}
defer transactionService.Close()
ctx := sctx.SetGasPrice(context.Background(), customGasPrice)
_, err = transactionService.CancelTransaction(ctx, signedTx.Hash())
if !errors.Is(err, transaction.ErrGasPriceTooLow) {
t.Fatalf("returned wrong error. wanted %v, got %v", transaction.ErrGasPriceTooLow, err)
}
})
}
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