Skip to content
This repository has been archived by the owner on Oct 7, 2020. It is now read-only.

Add stateful checkTx #276

Merged
merged 27 commits into from
Oct 16, 2017
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ dist/
scripts/kubernetes/ethermint.yaml
ethstats/
coverage.txt

tests/integration/truffle/build
ethstats/
coverage.txt
tests/tendermint_data/**/*.bak
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
* no networking is started by the ethereum node
* performance optimisation
* doc updates
* block state in calls for checkTx
* allows multiple transactions from the same account

## 0.4.0
### Breaking
Expand Down
3 changes: 3 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# TODO

* Document performance numbers for Tendermint and Ethermint and write a blog post about it
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can just be an issue, no? TODO files in a repo are unnecessary IMO ... it confuses what there is to do with issues

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

alright

87 changes: 59 additions & 28 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"fmt"
"math/big"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/state"
ethTypes "github.com/ethereum/go-ethereum/core/types"
Expand All @@ -26,7 +25,9 @@ type EthermintApplication struct {
backend *ethereum.Backend // backend ethereum struct

// a closure to return the latest current state from the ethereum blockchain
currentState func() (*state.StateDB, error)
getCurrentState func() (*state.StateDB, error)

checkTxState *state.StateDB

// an ethereum rpc client we can forward queries to
rpcClient *rpc.Client
Expand All @@ -41,11 +42,18 @@ type EthermintApplication struct {
// #stable - 0.4.0
func NewEthermintApplication(backend *ethereum.Backend,
client *rpc.Client, strategy *emtTypes.Strategy) (*EthermintApplication, error) {

state, err := backend.Ethereum().BlockChain().State()
if err != nil {
return nil, err
}

app := &EthermintApplication{
backend: backend,
rpcClient: client,
currentState: backend.Ethereum().BlockChain().State,
strategy: strategy,
backend: backend,
rpcClient: client,
getCurrentState: backend.Ethereum().BlockChain().State,
checkTxState: state.Copy(),
strategy: strategy,
}

if err := app.backend.ResetWork(app.Receiver()); err != nil {
Expand All @@ -63,6 +71,9 @@ func (app *EthermintApplication) SetLogger(log tmLog.Logger) {

var bigZero = big.NewInt(0)

// maxTransactionSize is 32KB in order to prevent DOS attacks
const maxTransactionSize = 32768

// Info returns information about the last height and app_hash to the tendermint engine
// #stable - 0.4.0
func (app *EthermintApplication) Info() abciTypes.ResponseInfo {
Expand Down Expand Up @@ -109,11 +120,11 @@ func (app *EthermintApplication) InitChain(validators []*abciTypes.Validator) {
// #stable - 0.4.0
func (app *EthermintApplication) CheckTx(txBytes []byte) abciTypes.Result {
tx, err := decodeTx(txBytes)
app.logger.Debug("CheckTx: Received valid transaction", "tx", tx) // nolint: errcheck
if err != nil {
app.logger.Debug("CheckTx: Received invalid transaction", "tx", tx) // nolint: errcheck
return abciTypes.ErrEncodingError.AppendLog(err.Error())
}
app.logger.Debug("CheckTx: Received valid transaction", "tx", tx) // nolint: errcheck

return app.validateTx(tx)
}
Expand Down Expand Up @@ -164,6 +175,13 @@ func (app *EthermintApplication) Commit() abciTypes.Result {
app.logger.Error("Error getting latest ethereum state", "err", err) // nolint: errcheck
return abciTypes.ErrInternalError.AppendLog(err.Error())
}
state, err := app.getCurrentState()
if err != nil {
app.logger.Error("Error getting latest state", "err", err) // nolint: errcheck
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does it take for this to err? do we need to catch it properly?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could happen if the underlying ethereum state gets corrupted.

We might want to abort here.

For now it only affects the checktx state though.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

presumably state will be nil if there is an error and the call to Copy below will panic. So we should instead catch that and return an InternalError result so Tendermint knows something went wrong, logs the error, and halts

return abciTypes.ErrInternalError.AppendLog(err.Error())
}

app.checkTxState = state.Copy()
return abciTypes.NewResultOK(blockHash[:], "")
}

Expand Down Expand Up @@ -191,48 +209,52 @@ func (app *EthermintApplication) Query(query abciTypes.RequestQuery) abciTypes.R
// validateTx checks the validity of a tx against the blockchain's current state.
// it duplicates the logic in ethereum's tx_pool
func (app *EthermintApplication) validateTx(tx *ethTypes.Transaction) abciTypes.Result {
currentState, err := app.currentState()
if err != nil {
return abciTypes.ErrInternalError.AppendLog(err.Error())

// Heuristic limit, reject transactions over 32KB to prevent DOS attacks
if tx.Size() > maxTransactionSize {
return abciTypes.ErrInternalError.
AppendLog(core.ErrOversizedData.Error())
}

var signer ethTypes.Signer = ethTypes.FrontierSigner{}
if tx.Protected() {
signer = ethTypes.NewEIP155Signer(tx.ChainId())
}

// Make sure the transaction is signed properly
from, err := ethTypes.Sender(signer, tx)
if err != nil {
return abciTypes.ErrBaseInvalidSignature.
AppendLog(core.ErrInvalidSender.Error())
}

// Make sure the account exist. Non existent accounts
// haven't got funds and well therefor never pass.
// Transactions can't be negative. This may never happen using RLP decoded
// transactions but may occur if you create a transaction using the RPC.
if tx.Value().Sign() < 0 {
return abciTypes.ErrBaseInvalidInput.
AppendLog(core.ErrNegativeValue.Error())
}

currentState := app.checkTxState

// Make sure the account exist - cant send from non-existing account.
if !currentState.Exist(from) {
return abciTypes.ErrBaseUnknownAddress.
AppendLog(core.ErrInvalidSender.Error())
}

// Check for nonce errors
currentNonce := currentState.GetNonce(from)
if currentNonce > tx.Nonce() {
return abciTypes.ErrBadNonce.
AppendLog(fmt.Sprintf("Got: %d, Current: %d", tx.Nonce(), currentNonce))
}

// Check the transaction doesn't exceed the current block limit gas.
gasLimit := app.backend.GasLimit()
if gasLimit.Cmp(tx.Gas()) < 0 {
return abciTypes.ErrInternalError.AppendLog(core.ErrGasLimitReached.Error())
return abciTypes.ErrInternalError.
AppendLog(core.ErrGasLimitReached.Error())
}

// Transactions can't be negative. This may never happen
// using RLP decoded transactions but may occur if you create
// a transaction using the RPC for example.
if tx.Value().Cmp(common.Big0) < 0 {
return abciTypes.ErrBaseInvalidInput.
SetLog(core.ErrNegativeValue.Error())
// Check if nonce is not strictly increasing
nonce := currentState.GetNonce(from)
if nonce != tx.Nonce() {
return abciTypes.ErrBadNonce.
AppendLog(fmt.Sprintf("Nonce is not strictly increasing. Expected: %d; Got: %d", nonce, tx.Nonce()))
}

// Transactor should have enough funds to cover the costs
Expand All @@ -241,14 +263,23 @@ func (app *EthermintApplication) validateTx(tx *ethTypes.Transaction) abciTypes.
if currentBalance.Cmp(tx.Cost()) < 0 {
return abciTypes.ErrInsufficientFunds.
AppendLog(fmt.Sprintf("Current balance: %s, tx cost: %s", currentBalance, tx.Cost()))

}

intrGas := core.IntrinsicGas(tx.Data(), tx.To() == nil, true) // homestead == true
if tx.Gas().Cmp(intrGas) < 0 {
return abciTypes.ErrBaseInsufficientFees.
SetLog(core.ErrIntrinsicGas.Error())
AppendLog(core.ErrIntrinsicGas.Error())
}

// Update ether balances
// amount + gasprice * gaslimit
currentState.SubBalance(from, tx.Cost())
// tx.To() returns a pointer to a common address. It returns nil
// if it is a contract creation transaction.
if to := tx.To(); to != nil {
currentState.AddBalance(*to, tx.Value())
}
currentState.SetNonce(from, tx.Nonce()+1)

return abciTypes.OK
}
Loading