forked from cerc-io/laconicd-deprecated
R4R: CheckTx AnteHandler for Ethereum Tx Messages (#505)
* Rename Ethereum tx message * Use new tx decoder in the ethermint app * Update ante handler to prevent spam/dos * Update ethereum msg signing/verification logic * Implement secp256k1 key types * Remove pointer from To method * Move sig check to after inartistic gas check * Add comment on chainID parsing * Updated validateIntrinsicGas godoc * Implement Fee method on eth tx msg * Add reference to spec for recoverEthSig * Upgrade TM to v0.27.0
This commit is contained in:
+251
-45
@@ -1,29 +1,28 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/x/auth"
|
||||
"github.com/tendermint/tendermint/crypto/secp256k1"
|
||||
|
||||
"github.com/cosmos/ethermint/crypto"
|
||||
"github.com/cosmos/ethermint/types"
|
||||
evmtypes "github.com/cosmos/ethermint/x/evm/types"
|
||||
)
|
||||
|
||||
var dummySecp256k1Pubkey secp256k1.PubKeySecp256k1 // used for tx simulation
|
||||
ethcmn "github.com/ethereum/go-ethereum/common"
|
||||
ethcore "github.com/ethereum/go-ethereum/core"
|
||||
|
||||
tmcrypto "github.com/tendermint/tendermint/crypto"
|
||||
)
|
||||
|
||||
const (
|
||||
memoCostPerByte = 1
|
||||
maxMemoCharacters = 100
|
||||
secp256k1VerifyCost = 100
|
||||
memoCostPerByte sdk.Gas = 3
|
||||
secp256k1VerifyCost = 21000
|
||||
)
|
||||
|
||||
func init() {
|
||||
bz, _ := hex.DecodeString("035AD6810A47F073553FF30D2FCC7E0D3B1C0B74B61A1AAA2582344037151E143A")
|
||||
copy(dummySecp256k1Pubkey[:], bz)
|
||||
}
|
||||
|
||||
// NewAnteHandler returns an ante handelr responsible for attempting to route an
|
||||
// NewAnteHandler returns an ante handler responsible for attempting to route an
|
||||
// Ethereum or SDK transaction to an internal ante handler for performing
|
||||
// transaction-level processing (e.g. fee payment, signature verification) before
|
||||
// being passed onto it's respective handler.
|
||||
@@ -35,22 +34,130 @@ func NewAnteHandler(ak auth.AccountKeeper, fck auth.FeeCollectionKeeper) sdk.Ant
|
||||
ctx sdk.Context, tx sdk.Tx, sim bool,
|
||||
) (newCtx sdk.Context, res sdk.Result, abort bool) {
|
||||
|
||||
stdTx, ok := tx.(auth.StdTx)
|
||||
if !ok {
|
||||
return ctx, sdk.ErrInternal("transaction type invalid: must be StdTx").Result(), true
|
||||
switch castTx := tx.(type) {
|
||||
case auth.StdTx:
|
||||
return sdkAnteHandler(ctx, ak, fck, castTx, sim)
|
||||
|
||||
case *evmtypes.EthereumTxMsg:
|
||||
return ethAnteHandler(ctx, castTx, ak)
|
||||
|
||||
default:
|
||||
return ctx, sdk.ErrInternal(fmt.Sprintf("transaction type invalid: %T", tx)).Result(), true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// SDK Ante Handler
|
||||
|
||||
func sdkAnteHandler(
|
||||
ctx sdk.Context, ak auth.AccountKeeper, fck auth.FeeCollectionKeeper, stdTx auth.StdTx, sim bool,
|
||||
) (newCtx sdk.Context, res sdk.Result, abort bool) {
|
||||
|
||||
// Ensure that the provided fees meet a minimum threshold for the validator,
|
||||
// if this is a CheckTx. This is only for local mempool purposes, and thus
|
||||
// is only ran on check tx.
|
||||
if ctx.IsCheckTx() && !sim {
|
||||
res := auth.EnsureSufficientMempoolFees(ctx, stdTx)
|
||||
if !res.IsOK() {
|
||||
return newCtx, res, true
|
||||
}
|
||||
}
|
||||
|
||||
newCtx = auth.SetGasMeter(sim, ctx, stdTx)
|
||||
|
||||
// AnteHandlers must have their own defer/recover in order for the BaseApp
|
||||
// to know how much gas was used! This is because the GasMeter is created in
|
||||
// the AnteHandler, but if it panics the context won't be set properly in
|
||||
// runTx's recover call.
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
switch rType := r.(type) {
|
||||
case sdk.ErrorOutOfGas:
|
||||
log := fmt.Sprintf("out of gas in location: %v", rType.Descriptor)
|
||||
res = sdk.ErrOutOfGas(log).Result()
|
||||
res.GasWanted = stdTx.Fee.Gas
|
||||
res.GasUsed = newCtx.GasMeter().GasConsumed()
|
||||
abort = true
|
||||
default:
|
||||
panic(r)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if err := stdTx.ValidateBasic(); err != nil {
|
||||
return newCtx, err.Result(), true
|
||||
}
|
||||
|
||||
newCtx.GasMeter().ConsumeGas(memoCostPerByte*sdk.Gas(len(stdTx.GetMemo())), "memo")
|
||||
|
||||
signerAccs, res := auth.GetSignerAccs(newCtx, ak, stdTx.GetSigners())
|
||||
if !res.IsOK() {
|
||||
return newCtx, res, true
|
||||
}
|
||||
|
||||
// the first signer pays the transaction fees
|
||||
if !stdTx.Fee.Amount.IsZero() {
|
||||
signerAccs[0], res = auth.DeductFees(signerAccs[0], stdTx.Fee)
|
||||
if !res.IsOK() {
|
||||
return newCtx, res, true
|
||||
}
|
||||
|
||||
// TODO: Handle gas/fee checking and spam prevention. We may need two
|
||||
// different models for SDK and Ethereum txs. The SDK currently supports a
|
||||
// primitive model where a constant gas price is used.
|
||||
//
|
||||
// Ref: #473
|
||||
fck.AddCollectedFees(newCtx, stdTx.Fee.Amount)
|
||||
}
|
||||
|
||||
if ethTx, ok := isEthereumTx(stdTx); ethTx != nil && ok {
|
||||
return ethAnteHandler(ctx, ethTx, ak)
|
||||
isGenesis := ctx.BlockHeight() == 0
|
||||
signBytesList := auth.GetSignBytesList(newCtx.ChainID(), stdTx, signerAccs, isGenesis)
|
||||
stdSigs := stdTx.GetSignatures()
|
||||
|
||||
for i := 0; i < len(stdSigs); i++ {
|
||||
// check signature, return account with incremented nonce
|
||||
signerAccs[i], res = processSig(newCtx, signerAccs[i], stdSigs[i], signBytesList[i], sim)
|
||||
if !res.IsOK() {
|
||||
return newCtx, res, true
|
||||
}
|
||||
|
||||
return auth.NewAnteHandler(ak, fck)(ctx, stdTx, sim)
|
||||
ak.SetAccount(newCtx, signerAccs[i])
|
||||
}
|
||||
|
||||
return newCtx, sdk.Result{GasWanted: stdTx.Fee.Gas}, false
|
||||
}
|
||||
|
||||
// processSig verifies the signature and increments the nonce. If the account
|
||||
// doesn't have a pubkey, set it.
|
||||
func processSig(
|
||||
ctx sdk.Context, acc auth.Account, sig auth.StdSignature, signBytes []byte, sim bool,
|
||||
) (updatedAcc auth.Account, res sdk.Result) {
|
||||
|
||||
pubKey, res := auth.ProcessPubKey(acc, sig, sim)
|
||||
if !res.IsOK() {
|
||||
return nil, res
|
||||
}
|
||||
|
||||
err := acc.SetPubKey(pubKey)
|
||||
if err != nil {
|
||||
return nil, sdk.ErrInternal("failed to set PubKey on signer account").Result()
|
||||
}
|
||||
|
||||
consumeSigGas(ctx.GasMeter(), pubKey)
|
||||
if !sim && !pubKey.VerifyBytes(signBytes, sig.Signature) {
|
||||
return nil, sdk.ErrUnauthorized("signature verification failed").Result()
|
||||
}
|
||||
|
||||
err = acc.SetSequence(acc.GetSequence() + 1)
|
||||
if err != nil {
|
||||
return nil, sdk.ErrInternal("failed to set account nonce").Result()
|
||||
}
|
||||
|
||||
return acc, res
|
||||
}
|
||||
|
||||
func consumeSigGas(meter sdk.GasMeter, pubkey tmcrypto.PubKey) {
|
||||
switch pubkey.(type) {
|
||||
case crypto.PubKeySecp256k1:
|
||||
meter.ConsumeGas(secp256k1VerifyCost, "ante verify: secp256k1")
|
||||
default:
|
||||
panic("Unrecognized signature type")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,34 +165,133 @@ func NewAnteHandler(ak auth.AccountKeeper, fck auth.FeeCollectionKeeper) sdk.Ant
|
||||
// Ethereum Ante Handler
|
||||
|
||||
// ethAnteHandler defines an internal ante handler for an Ethereum transaction
|
||||
// ethTx that implements the sdk.Msg interface. The Ethereum transaction is a
|
||||
// single message inside a auth.StdTx.
|
||||
//
|
||||
// For now we simply pass the transaction on as the EVM shares common business
|
||||
// logic of an ante handler. Anything not handled by the EVM that should be
|
||||
// prior to transaction processing, should be done here.
|
||||
// ethTxMsg. During CheckTx, the transaction is passed through a series of
|
||||
// pre-message execution validation checks such as signature and account
|
||||
// verification in addition to minimum fees being checked. Otherwise, during
|
||||
// DeliverTx, the transaction is simply passed to the EVM which will also
|
||||
// perform the same series of checks. The distinction is made in CheckTx to
|
||||
// prevent spam and DoS attacks.
|
||||
func ethAnteHandler(
|
||||
ctx sdk.Context, ethTx *evmtypes.MsgEthereumTx, ak auth.AccountKeeper,
|
||||
ctx sdk.Context, ethTxMsg *evmtypes.EthereumTxMsg, ak auth.AccountKeeper,
|
||||
) (newCtx sdk.Context, res sdk.Result, abort bool) {
|
||||
|
||||
if ctx.IsCheckTx() {
|
||||
// Only perform pre-message (Ethereum transaction) execution validation
|
||||
// during CheckTx. Otherwise, during DeliverTx the EVM will handle them.
|
||||
if res := validateEthTxCheckTx(ctx, ak, ethTxMsg); !res.IsOK() {
|
||||
return newCtx, res, true
|
||||
}
|
||||
}
|
||||
|
||||
return ctx, sdk.Result{}, false
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Auxiliary
|
||||
func validateEthTxCheckTx(
|
||||
ctx sdk.Context, ak auth.AccountKeeper, ethTxMsg *evmtypes.EthereumTxMsg,
|
||||
) sdk.Result {
|
||||
|
||||
// isEthereumTx returns a boolean if a given standard SDK transaction contains
|
||||
// an Ethereum transaction. If so, the transaction is also returned. A standard
|
||||
// SDK transaction contains an Ethereum transaction if it only has a single
|
||||
// message and that embedded message if of type MsgEthereumTx.
|
||||
func isEthereumTx(tx auth.StdTx) (*evmtypes.MsgEthereumTx, bool) {
|
||||
msgs := tx.GetMsgs()
|
||||
if len(msgs) == 1 {
|
||||
ethTx, ok := msgs[0].(*evmtypes.MsgEthereumTx)
|
||||
if ok {
|
||||
return ethTx, true
|
||||
}
|
||||
// parse the chainID from a string to a base-10 integer
|
||||
chainID, ok := new(big.Int).SetString(ctx.ChainID(), 10)
|
||||
if !ok {
|
||||
return types.ErrInvalidChainID(fmt.Sprintf("invalid chainID: %s", ctx.ChainID())).Result()
|
||||
}
|
||||
|
||||
return nil, false
|
||||
// Validate sufficient fees have been provided that meet a minimum threshold
|
||||
// defined by the proposer (for mempool purposes during CheckTx).
|
||||
if res := ensureSufficientMempoolFees(ctx, ethTxMsg); !res.IsOK() {
|
||||
return res
|
||||
}
|
||||
|
||||
// validate enough intrinsic gas
|
||||
if res := validateIntrinsicGas(ethTxMsg); !res.IsOK() {
|
||||
return res
|
||||
}
|
||||
|
||||
// validate sender/signature
|
||||
signer, err := ethTxMsg.VerifySig(chainID)
|
||||
if err != nil {
|
||||
return sdk.ErrUnauthorized("signature verification failed").Result()
|
||||
}
|
||||
|
||||
// validate account (nonce and balance checks)
|
||||
if res := validateAccount(ctx, ak, ethTxMsg, signer); !res.IsOK() {
|
||||
return res
|
||||
}
|
||||
|
||||
return sdk.Result{}
|
||||
}
|
||||
|
||||
// validateIntrinsicGas validates that the Ethereum tx message has enough to
|
||||
// cover intrinsic gas. Intrinsic gas for a transaction is the amount of gas
|
||||
// that the transaction uses before the transaction is executed. The gas is a
|
||||
// constant value of 21000 plus any cost inccured by additional bytes of data
|
||||
// supplied with the transaction.
|
||||
func validateIntrinsicGas(ethTxMsg *evmtypes.EthereumTxMsg) sdk.Result {
|
||||
gas, err := ethcore.IntrinsicGas(ethTxMsg.Data.Payload, ethTxMsg.To() == nil, false)
|
||||
if err != nil {
|
||||
return sdk.ErrInternal(fmt.Sprintf("failed to compute intrinsic gas cost: %s", err)).Result()
|
||||
}
|
||||
|
||||
if ethTxMsg.Data.GasLimit < gas {
|
||||
return sdk.ErrInternal(
|
||||
fmt.Sprintf("intrinsic gas too low; %d < %d", ethTxMsg.Data.GasLimit, gas),
|
||||
).Result()
|
||||
}
|
||||
|
||||
return sdk.Result{}
|
||||
}
|
||||
|
||||
// validateAccount validates the account nonce and that the account has enough
|
||||
// funds to cover the tx cost.
|
||||
func validateAccount(
|
||||
ctx sdk.Context, ak auth.AccountKeeper, ethTxMsg *evmtypes.EthereumTxMsg, signer ethcmn.Address,
|
||||
) sdk.Result {
|
||||
|
||||
acc := ak.GetAccount(ctx, sdk.AccAddress(signer.Bytes()))
|
||||
|
||||
// on InitChain make sure account number == 0
|
||||
if ctx.BlockHeight() == 0 && acc.GetAccountNumber() != 0 {
|
||||
return sdk.ErrInternal(
|
||||
fmt.Sprintf(
|
||||
"invalid account number for height zero; got %d, expected 0", acc.GetAccountNumber(),
|
||||
)).Result()
|
||||
}
|
||||
|
||||
// Validate the transaction nonce is valid (equivalent to the sender account’s
|
||||
// current nonce).
|
||||
seq := acc.GetSequence()
|
||||
if ethTxMsg.Data.AccountNonce != seq {
|
||||
return sdk.ErrInvalidSequence(
|
||||
fmt.Sprintf("nonce too low; got %d, expected %d", ethTxMsg.Data.AccountNonce, seq)).Result()
|
||||
}
|
||||
|
||||
// validate sender has enough funds
|
||||
balance := acc.GetCoins().AmountOf(types.DenomDefault)
|
||||
if balance.BigInt().Cmp(ethTxMsg.Cost()) < 0 {
|
||||
return sdk.ErrInsufficientFunds(
|
||||
fmt.Sprintf("insufficient funds: %s < %s", balance, ethTxMsg.Cost()),
|
||||
).Result()
|
||||
}
|
||||
|
||||
return sdk.Result{}
|
||||
}
|
||||
|
||||
// ensureSufficientMempoolFees verifies that enough fees have been provided by the
|
||||
// Ethereum transaction that meet the minimum threshold set by the block
|
||||
// proposer.
|
||||
//
|
||||
// NOTE: This should only be ran during a CheckTx mode.
|
||||
func ensureSufficientMempoolFees(ctx sdk.Context, ethTxMsg *evmtypes.EthereumTxMsg) sdk.Result {
|
||||
// fee = GP * GL
|
||||
fee := sdk.Coins{sdk.NewInt64Coin(types.DenomDefault, ethTxMsg.Fee().Int64())}
|
||||
|
||||
// it is assumed that the minimum fees will only include the single valid denom
|
||||
if !ctx.MinimumFees().IsZero() && !fee.IsAllGTE(ctx.MinimumFees()) {
|
||||
// reject the transaction that does not meet the minimum fee
|
||||
return sdk.ErrInsufficientFee(
|
||||
fmt.Sprintf("insufficient fee, got: %q required: %q", fee, ctx.MinimumFees()),
|
||||
).Result()
|
||||
}
|
||||
|
||||
return sdk.Result{}
|
||||
}
|
||||
|
||||
+200
-63
@@ -5,12 +5,14 @@ import (
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/x/auth"
|
||||
evmtypes "github.com/cosmos/ethermint/x/evm/types"
|
||||
ethcmn "github.com/ethereum/go-ethereum/common"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tendermint/tendermint/crypto"
|
||||
tmcrypto "github.com/tendermint/tendermint/crypto"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/x/auth"
|
||||
"github.com/cosmos/ethermint/types"
|
||||
evmtypes "github.com/cosmos/ethermint/x/evm/types"
|
||||
)
|
||||
|
||||
func requireValidTx(
|
||||
@@ -45,64 +47,80 @@ func requireInvalidTx(
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidEthTx(t *testing.T) {
|
||||
input := newTestSetup()
|
||||
input.ctx = input.ctx.WithBlockHeight(1)
|
||||
|
||||
addr1, priv1 := newTestAddrKey()
|
||||
addr2, _ := newTestAddrKey()
|
||||
|
||||
acc1 := input.accKeeper.NewAccountWithAddress(input.ctx, addr1)
|
||||
acc1.SetCoins(newTestCoins())
|
||||
input.accKeeper.SetAccount(input.ctx, acc1)
|
||||
|
||||
acc2 := input.accKeeper.NewAccountWithAddress(input.ctx, addr2)
|
||||
acc2.SetCoins(newTestCoins())
|
||||
input.accKeeper.SetAccount(input.ctx, acc2)
|
||||
|
||||
// require a valid Ethereum tx to pass
|
||||
to := ethcmn.BytesToAddress(addr2.Bytes())
|
||||
amt := big.NewInt(32)
|
||||
gas := big.NewInt(20)
|
||||
ethMsg := evmtypes.NewEthereumTxMsg(0, to, amt, 22000, gas, []byte("test"))
|
||||
|
||||
tx := newTestEthTx(input.ctx, ethMsg, priv1)
|
||||
requireValidTx(t, input.anteHandler, input.ctx, tx, false)
|
||||
}
|
||||
|
||||
func TestValidTx(t *testing.T) {
|
||||
setup := newTestSetup()
|
||||
setup.ctx = setup.ctx.WithBlockHeight(1)
|
||||
input := newTestSetup()
|
||||
input.ctx = input.ctx.WithBlockHeight(1)
|
||||
|
||||
addr1, priv1 := newTestAddrKey()
|
||||
addr2, priv2 := newTestAddrKey()
|
||||
|
||||
acc1 := setup.accKeeper.NewAccountWithAddress(setup.ctx, addr1)
|
||||
acc1 := input.accKeeper.NewAccountWithAddress(input.ctx, addr1)
|
||||
acc1.SetCoins(newTestCoins())
|
||||
setup.accKeeper.SetAccount(setup.ctx, acc1)
|
||||
input.accKeeper.SetAccount(input.ctx, acc1)
|
||||
|
||||
acc2 := setup.accKeeper.NewAccountWithAddress(setup.ctx, addr2)
|
||||
acc2 := input.accKeeper.NewAccountWithAddress(input.ctx, addr2)
|
||||
acc2.SetCoins(newTestCoins())
|
||||
setup.accKeeper.SetAccount(setup.ctx, acc2)
|
||||
input.accKeeper.SetAccount(input.ctx, acc2)
|
||||
|
||||
// require a valid SDK tx to pass
|
||||
fee := newTestStdFee()
|
||||
msg1 := newTestMsg(addr1, addr2)
|
||||
msgs := []sdk.Msg{msg1}
|
||||
|
||||
privKeys := []crypto.PrivKey{priv1, priv2}
|
||||
accNums := []int64{acc1.GetAccountNumber(), acc2.GetAccountNumber()}
|
||||
accSeqs := []int64{acc1.GetSequence(), acc2.GetSequence()}
|
||||
privKeys := []tmcrypto.PrivKey{priv1, priv2}
|
||||
accNums := []uint64{acc1.GetAccountNumber(), acc2.GetAccountNumber()}
|
||||
accSeqs := []uint64{acc1.GetSequence(), acc2.GetSequence()}
|
||||
|
||||
tx := newTestSDKTx(setup.ctx, msgs, privKeys, accNums, accSeqs, fee)
|
||||
requireValidTx(t, setup.anteHandler, setup.ctx, tx, false)
|
||||
tx := newTestSDKTx(input.ctx, msgs, privKeys, accNums, accSeqs, fee)
|
||||
requireValidTx(t, input.anteHandler, input.ctx, tx, false)
|
||||
|
||||
// require accounts to update
|
||||
acc1 = setup.accKeeper.GetAccount(setup.ctx, addr1)
|
||||
acc2 = setup.accKeeper.GetAccount(setup.ctx, addr2)
|
||||
acc1 = input.accKeeper.GetAccount(input.ctx, addr1)
|
||||
acc2 = input.accKeeper.GetAccount(input.ctx, addr2)
|
||||
require.Equal(t, accSeqs[0]+1, acc1.GetSequence())
|
||||
require.Equal(t, accSeqs[1]+1, acc2.GetSequence())
|
||||
|
||||
// require a valid Ethereum tx to pass
|
||||
to := ethcmn.BytesToAddress(addr2.Bytes())
|
||||
amt := big.NewInt(32)
|
||||
gas := big.NewInt(20)
|
||||
ethMsg := evmtypes.NewMsgEthereumTx(0, to, amt, 20000, gas, []byte("test"))
|
||||
|
||||
tx = newTestEthTx(setup.ctx, ethMsg, priv1)
|
||||
requireValidTx(t, setup.anteHandler, setup.ctx, tx, false)
|
||||
}
|
||||
|
||||
func TestSDKInvalidSigs(t *testing.T) {
|
||||
setup := newTestSetup()
|
||||
setup.ctx = setup.ctx.WithBlockHeight(1)
|
||||
input := newTestSetup()
|
||||
input.ctx = input.ctx.WithBlockHeight(1)
|
||||
|
||||
addr1, priv1 := newTestAddrKey()
|
||||
addr2, priv2 := newTestAddrKey()
|
||||
addr3, priv3 := newTestAddrKey()
|
||||
|
||||
acc1 := setup.accKeeper.NewAccountWithAddress(setup.ctx, addr1)
|
||||
acc1 := input.accKeeper.NewAccountWithAddress(input.ctx, addr1)
|
||||
acc1.SetCoins(newTestCoins())
|
||||
setup.accKeeper.SetAccount(setup.ctx, acc1)
|
||||
input.accKeeper.SetAccount(input.ctx, acc1)
|
||||
|
||||
acc2 := setup.accKeeper.NewAccountWithAddress(setup.ctx, addr2)
|
||||
acc2 := input.accKeeper.NewAccountWithAddress(input.ctx, addr2)
|
||||
acc2.SetCoins(newTestCoins())
|
||||
setup.accKeeper.SetAccount(setup.ctx, acc2)
|
||||
input.accKeeper.SetAccount(input.ctx, acc2)
|
||||
|
||||
fee := newTestStdFee()
|
||||
msg1 := newTestMsg(addr1, addr2)
|
||||
@@ -110,66 +128,185 @@ func TestSDKInvalidSigs(t *testing.T) {
|
||||
// require validation failure with no signers
|
||||
msgs := []sdk.Msg{msg1}
|
||||
|
||||
privKeys := []crypto.PrivKey{}
|
||||
accNums := []int64{acc1.GetAccountNumber(), acc2.GetAccountNumber()}
|
||||
accSeqs := []int64{acc1.GetSequence(), acc2.GetSequence()}
|
||||
privKeys := []tmcrypto.PrivKey{}
|
||||
accNums := []uint64{acc1.GetAccountNumber(), acc2.GetAccountNumber()}
|
||||
accSeqs := []uint64{acc1.GetSequence(), acc2.GetSequence()}
|
||||
|
||||
tx := newTestSDKTx(setup.ctx, msgs, privKeys, accNums, accSeqs, fee)
|
||||
requireInvalidTx(t, setup.anteHandler, setup.ctx, tx, false, sdk.CodeUnauthorized)
|
||||
tx := newTestSDKTx(input.ctx, msgs, privKeys, accNums, accSeqs, fee)
|
||||
requireInvalidTx(t, input.anteHandler, input.ctx, tx, false, sdk.CodeUnauthorized)
|
||||
|
||||
// require validation failure with invalid number of signers
|
||||
msgs = []sdk.Msg{msg1}
|
||||
|
||||
privKeys = []crypto.PrivKey{priv1}
|
||||
accNums = []int64{acc1.GetAccountNumber(), acc2.GetAccountNumber()}
|
||||
accSeqs = []int64{acc1.GetSequence(), acc2.GetSequence()}
|
||||
privKeys = []tmcrypto.PrivKey{priv1}
|
||||
accNums = []uint64{acc1.GetAccountNumber(), acc2.GetAccountNumber()}
|
||||
accSeqs = []uint64{acc1.GetSequence(), acc2.GetSequence()}
|
||||
|
||||
tx = newTestSDKTx(setup.ctx, msgs, privKeys, accNums, accSeqs, fee)
|
||||
requireInvalidTx(t, setup.anteHandler, setup.ctx, tx, false, sdk.CodeUnauthorized)
|
||||
tx = newTestSDKTx(input.ctx, msgs, privKeys, accNums, accSeqs, fee)
|
||||
requireInvalidTx(t, input.anteHandler, input.ctx, tx, false, sdk.CodeUnauthorized)
|
||||
|
||||
// require validation failure with an invalid signer
|
||||
msg2 := newTestMsg(addr1, addr3)
|
||||
msgs = []sdk.Msg{msg1, msg2}
|
||||
|
||||
privKeys = []crypto.PrivKey{priv1, priv2, priv3}
|
||||
accNums = []int64{acc1.GetAccountNumber(), acc2.GetAccountNumber(), 0}
|
||||
accSeqs = []int64{acc1.GetSequence(), acc2.GetSequence(), 0}
|
||||
privKeys = []tmcrypto.PrivKey{priv1, priv2, priv3}
|
||||
accNums = []uint64{acc1.GetAccountNumber(), acc2.GetAccountNumber(), 0}
|
||||
accSeqs = []uint64{acc1.GetSequence(), acc2.GetSequence(), 0}
|
||||
|
||||
tx = newTestSDKTx(setup.ctx, msgs, privKeys, accNums, accSeqs, fee)
|
||||
requireInvalidTx(t, setup.anteHandler, setup.ctx, tx, false, sdk.CodeUnknownAddress)
|
||||
tx = newTestSDKTx(input.ctx, msgs, privKeys, accNums, accSeqs, fee)
|
||||
requireInvalidTx(t, input.anteHandler, input.ctx, tx, false, sdk.CodeUnknownAddress)
|
||||
}
|
||||
|
||||
func TestSDKInvalidAcc(t *testing.T) {
|
||||
setup := newTestSetup()
|
||||
setup.ctx = setup.ctx.WithBlockHeight(1)
|
||||
input := newTestSetup()
|
||||
input.ctx = input.ctx.WithBlockHeight(1)
|
||||
|
||||
addr1, priv1 := newTestAddrKey()
|
||||
|
||||
acc1 := setup.accKeeper.NewAccountWithAddress(setup.ctx, addr1)
|
||||
acc1 := input.accKeeper.NewAccountWithAddress(input.ctx, addr1)
|
||||
acc1.SetCoins(newTestCoins())
|
||||
setup.accKeeper.SetAccount(setup.ctx, acc1)
|
||||
input.accKeeper.SetAccount(input.ctx, acc1)
|
||||
|
||||
fee := newTestStdFee()
|
||||
msg1 := newTestMsg(addr1)
|
||||
msgs := []sdk.Msg{msg1}
|
||||
privKeys := []crypto.PrivKey{priv1}
|
||||
privKeys := []tmcrypto.PrivKey{priv1}
|
||||
|
||||
// require validation failure with invalid account number
|
||||
accNums := []int64{1}
|
||||
accSeqs := []int64{acc1.GetSequence()}
|
||||
accNums := []uint64{1}
|
||||
accSeqs := []uint64{acc1.GetSequence()}
|
||||
|
||||
tx := newTestSDKTx(setup.ctx, msgs, privKeys, accNums, accSeqs, fee)
|
||||
requireInvalidTx(t, setup.anteHandler, setup.ctx, tx, false, sdk.CodeInvalidSequence)
|
||||
tx := newTestSDKTx(input.ctx, msgs, privKeys, accNums, accSeqs, fee)
|
||||
requireInvalidTx(t, input.anteHandler, input.ctx, tx, false, sdk.CodeUnauthorized)
|
||||
|
||||
// require validation failure with invalid sequence (nonce)
|
||||
accNums = []int64{acc1.GetAccountNumber()}
|
||||
accSeqs = []int64{1}
|
||||
accNums = []uint64{acc1.GetAccountNumber()}
|
||||
accSeqs = []uint64{1}
|
||||
|
||||
tx = newTestSDKTx(setup.ctx, msgs, privKeys, accNums, accSeqs, fee)
|
||||
requireInvalidTx(t, setup.anteHandler, setup.ctx, tx, false, sdk.CodeInvalidSequence)
|
||||
tx = newTestSDKTx(input.ctx, msgs, privKeys, accNums, accSeqs, fee)
|
||||
requireInvalidTx(t, input.anteHandler, input.ctx, tx, false, sdk.CodeUnauthorized)
|
||||
}
|
||||
|
||||
func TestSDKGasConsumption(t *testing.T) {
|
||||
// TODO: Test gas consumption and OOG once ante handler implementation stabilizes
|
||||
t.SkipNow()
|
||||
func TestEthInvalidSig(t *testing.T) {
|
||||
input := newTestSetup()
|
||||
input.ctx = input.ctx.WithBlockHeight(1)
|
||||
|
||||
_, priv1 := newTestAddrKey()
|
||||
addr2, _ := newTestAddrKey()
|
||||
to := ethcmn.BytesToAddress(addr2.Bytes())
|
||||
amt := big.NewInt(32)
|
||||
gas := big.NewInt(20)
|
||||
ethMsg := evmtypes.NewEthereumTxMsg(0, to, amt, 22000, gas, []byte("test"))
|
||||
|
||||
tx := newTestEthTx(input.ctx, ethMsg, priv1)
|
||||
ctx := input.ctx.WithChainID("4")
|
||||
requireInvalidTx(t, input.anteHandler, ctx, tx, false, sdk.CodeUnauthorized)
|
||||
}
|
||||
|
||||
func TestEthInvalidNonce(t *testing.T) {
|
||||
input := newTestSetup()
|
||||
input.ctx = input.ctx.WithBlockHeight(1)
|
||||
|
||||
addr1, priv1 := newTestAddrKey()
|
||||
addr2, _ := newTestAddrKey()
|
||||
|
||||
acc := input.accKeeper.NewAccountWithAddress(input.ctx, addr1)
|
||||
acc.SetCoins(newTestCoins())
|
||||
acc.SetSequence(10)
|
||||
input.accKeeper.SetAccount(input.ctx, acc)
|
||||
|
||||
// require a valid Ethereum tx to pass
|
||||
to := ethcmn.BytesToAddress(addr2.Bytes())
|
||||
amt := big.NewInt(32)
|
||||
gas := big.NewInt(20)
|
||||
ethMsg := evmtypes.NewEthereumTxMsg(0, to, amt, 22000, gas, []byte("test"))
|
||||
|
||||
tx := newTestEthTx(input.ctx, ethMsg, priv1)
|
||||
requireInvalidTx(t, input.anteHandler, input.ctx, tx, false, sdk.CodeInvalidSequence)
|
||||
}
|
||||
|
||||
func TestEthInsufficientBalance(t *testing.T) {
|
||||
input := newTestSetup()
|
||||
input.ctx = input.ctx.WithBlockHeight(1)
|
||||
|
||||
addr1, priv1 := newTestAddrKey()
|
||||
addr2, _ := newTestAddrKey()
|
||||
|
||||
acc := input.accKeeper.NewAccountWithAddress(input.ctx, addr1)
|
||||
input.accKeeper.SetAccount(input.ctx, acc)
|
||||
|
||||
// require a valid Ethereum tx to pass
|
||||
to := ethcmn.BytesToAddress(addr2.Bytes())
|
||||
amt := big.NewInt(32)
|
||||
gas := big.NewInt(20)
|
||||
ethMsg := evmtypes.NewEthereumTxMsg(0, to, amt, 22000, gas, []byte("test"))
|
||||
|
||||
tx := newTestEthTx(input.ctx, ethMsg, priv1)
|
||||
requireInvalidTx(t, input.anteHandler, input.ctx, tx, false, sdk.CodeInsufficientFunds)
|
||||
}
|
||||
|
||||
func TestEthInvalidIntrinsicGas(t *testing.T) {
|
||||
input := newTestSetup()
|
||||
input.ctx = input.ctx.WithBlockHeight(1)
|
||||
|
||||
addr1, priv1 := newTestAddrKey()
|
||||
addr2, _ := newTestAddrKey()
|
||||
|
||||
acc := input.accKeeper.NewAccountWithAddress(input.ctx, addr1)
|
||||
acc.SetCoins(newTestCoins())
|
||||
input.accKeeper.SetAccount(input.ctx, acc)
|
||||
|
||||
// require a valid Ethereum tx to pass
|
||||
to := ethcmn.BytesToAddress(addr2.Bytes())
|
||||
amt := big.NewInt(32)
|
||||
gas := big.NewInt(20)
|
||||
gasLimit := uint64(1000)
|
||||
ethMsg := evmtypes.NewEthereumTxMsg(0, to, amt, gasLimit, gas, []byte("test"))
|
||||
|
||||
tx := newTestEthTx(input.ctx, ethMsg, priv1)
|
||||
requireInvalidTx(t, input.anteHandler, input.ctx, tx, false, sdk.CodeInternal)
|
||||
}
|
||||
|
||||
func TestEthInvalidMempoolFees(t *testing.T) {
|
||||
input := newTestSetup()
|
||||
input.ctx = input.ctx.WithBlockHeight(1)
|
||||
input.ctx = input.ctx.WithMinimumFees(sdk.Coins{sdk.NewInt64Coin(types.DenomDefault, 500000)})
|
||||
|
||||
addr1, priv1 := newTestAddrKey()
|
||||
addr2, _ := newTestAddrKey()
|
||||
|
||||
acc := input.accKeeper.NewAccountWithAddress(input.ctx, addr1)
|
||||
acc.SetCoins(newTestCoins())
|
||||
input.accKeeper.SetAccount(input.ctx, acc)
|
||||
|
||||
// require a valid Ethereum tx to pass
|
||||
to := ethcmn.BytesToAddress(addr2.Bytes())
|
||||
amt := big.NewInt(32)
|
||||
gas := big.NewInt(20)
|
||||
ethMsg := evmtypes.NewEthereumTxMsg(0, to, amt, 22000, gas, []byte("test"))
|
||||
|
||||
tx := newTestEthTx(input.ctx, ethMsg, priv1)
|
||||
requireInvalidTx(t, input.anteHandler, input.ctx, tx, false, sdk.CodeInsufficientFee)
|
||||
}
|
||||
|
||||
func TestEthInvalidChainID(t *testing.T) {
|
||||
input := newTestSetup()
|
||||
input.ctx = input.ctx.WithBlockHeight(1)
|
||||
|
||||
addr1, priv1 := newTestAddrKey()
|
||||
addr2, _ := newTestAddrKey()
|
||||
|
||||
acc := input.accKeeper.NewAccountWithAddress(input.ctx, addr1)
|
||||
acc.SetCoins(newTestCoins())
|
||||
input.accKeeper.SetAccount(input.ctx, acc)
|
||||
|
||||
// require a valid Ethereum tx to pass
|
||||
to := ethcmn.BytesToAddress(addr2.Bytes())
|
||||
amt := big.NewInt(32)
|
||||
gas := big.NewInt(20)
|
||||
ethMsg := evmtypes.NewEthereumTxMsg(0, to, amt, 22000, gas, []byte("test"))
|
||||
|
||||
tx := newTestEthTx(input.ctx, ethMsg, priv1)
|
||||
ctx := input.ctx.WithChainID("bad-chain-id")
|
||||
requireInvalidTx(t, input.anteHandler, ctx, tx, false, types.CodeInvalidChainID)
|
||||
}
|
||||
|
||||
+5
-3
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/cosmos/cosmos-sdk/x/slashing"
|
||||
"github.com/cosmos/cosmos-sdk/x/stake"
|
||||
|
||||
"github.com/cosmos/ethermint/crypto"
|
||||
evmtypes "github.com/cosmos/ethermint/x/evm/types"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
@@ -74,7 +75,7 @@ type (
|
||||
func NewEthermintApp(logger tmlog.Logger, db dbm.DB, baseAppOpts ...func(*bam.BaseApp)) *EthermintApp {
|
||||
cdc := CreateCodec()
|
||||
|
||||
baseApp := bam.NewBaseApp(appName, logger, db, auth.DefaultTxDecoder(cdc), baseAppOpts...)
|
||||
baseApp := bam.NewBaseApp(appName, logger, db, evmtypes.TxDecoder(cdc), baseAppOpts...)
|
||||
app := &EthermintApp{
|
||||
BaseApp: baseApp,
|
||||
cdc: cdc,
|
||||
@@ -89,8 +90,8 @@ func NewEthermintApp(logger tmlog.Logger, db dbm.DB, baseAppOpts ...func(*bam.Ba
|
||||
tParamsKey: storeKeyTransParams,
|
||||
}
|
||||
|
||||
app.accountKeeper = auth.NewAccountKeeper(app.cdc, app.accountKey, auth.ProtoBaseAccount)
|
||||
app.paramsKeeper = params.NewKeeper(app.cdc, app.paramsKey, app.tParamsKey)
|
||||
app.accountKeeper = auth.NewAccountKeeper(app.cdc, app.accountKey, auth.ProtoBaseAccount)
|
||||
app.feeCollKeeper = auth.NewFeeCollectionKeeper(app.cdc, app.feeCollKey)
|
||||
|
||||
// register message handlers
|
||||
@@ -106,7 +107,7 @@ func NewEthermintApp(logger tmlog.Logger, db dbm.DB, baseAppOpts ...func(*bam.Ba
|
||||
app.SetEndBlocker(app.EndBlocker)
|
||||
app.SetAnteHandler(NewAnteHandler(app.accountKeeper, app.feeCollKeeper))
|
||||
|
||||
app.MountStoresIAVL(
|
||||
app.MountStores(
|
||||
app.mainKey, app.accountKey, app.stakeKey, app.slashingKey,
|
||||
app.govKey, app.feeCollKey, app.paramsKey, app.storageKey,
|
||||
)
|
||||
@@ -165,6 +166,7 @@ func CreateCodec() *codec.Codec {
|
||||
// TODO: Add remaining codec registrations:
|
||||
// bank, staking, distribution, slashing, and gov
|
||||
|
||||
crypto.RegisterCodec(cdc)
|
||||
evmtypes.RegisterCodec(cdc)
|
||||
auth.RegisterCodec(cdc)
|
||||
sdk.RegisterCodec(cdc)
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"github.com/cosmos/cosmos-sdk/store"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
dbm "github.com/tendermint/tendermint/libs/db"
|
||||
)
|
||||
|
||||
func setupMultiStore() (sdk.MultiStore, *sdk.KVStoreKey, *sdk.KVStoreKey) {
|
||||
db := dbm.NewMemDB()
|
||||
capKey := sdk.NewKVStoreKey("capkey")
|
||||
capKey2 := sdk.NewKVStoreKey("capkey2")
|
||||
ms := store.NewCommitMultiStore(db)
|
||||
|
||||
ms.MountStoreWithDB(capKey, sdk.StoreTypeIAVL, db)
|
||||
ms.MountStoreWithDB(capKey2, sdk.StoreTypeIAVL, db)
|
||||
ms.LoadLatestVersion()
|
||||
|
||||
return ms, capKey, capKey2
|
||||
}
|
||||
+40
-25
@@ -6,46 +6,61 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
"github.com/cosmos/cosmos-sdk/store"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/x/auth"
|
||||
|
||||
"github.com/cosmos/ethermint/crypto"
|
||||
"github.com/cosmos/ethermint/types"
|
||||
evmtypes "github.com/cosmos/ethermint/x/evm/types"
|
||||
|
||||
ethcrypto "github.com/ethereum/go-ethereum/crypto"
|
||||
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
tmcrypto "github.com/tendermint/tendermint/crypto"
|
||||
tmsecp256k1 "github.com/tendermint/tendermint/crypto/secp256k1"
|
||||
dbm "github.com/tendermint/tendermint/libs/db"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
)
|
||||
|
||||
var testDenom = "testcoin"
|
||||
|
||||
type testSetup struct {
|
||||
ctx sdk.Context
|
||||
cdc *codec.Codec
|
||||
accKeeper auth.AccountKeeper
|
||||
feeKeeper auth.FeeCollectionKeeper
|
||||
anteHandler sdk.AnteHandler
|
||||
}
|
||||
|
||||
func newTestSetup() testSetup {
|
||||
cdc := codec.New()
|
||||
ms, capKey, capKey2 := setupMultiStore()
|
||||
db := dbm.NewMemDB()
|
||||
authCapKey := sdk.NewKVStoreKey("authCapKey")
|
||||
feeCapKey := sdk.NewKVStoreKey("feeCapKey")
|
||||
keyParams := sdk.NewKVStoreKey("params")
|
||||
tkeyParams := sdk.NewTransientStoreKey("transient_params")
|
||||
|
||||
auth.RegisterBaseAccount(cdc)
|
||||
ms := store.NewCommitMultiStore(db)
|
||||
ms.MountStoreWithDB(authCapKey, sdk.StoreTypeIAVL, db)
|
||||
ms.MountStoreWithDB(feeCapKey, sdk.StoreTypeIAVL, db)
|
||||
ms.MountStoreWithDB(keyParams, sdk.StoreTypeIAVL, db)
|
||||
ms.MountStoreWithDB(tkeyParams, sdk.StoreTypeIAVL, db)
|
||||
ms.LoadLatestVersion()
|
||||
|
||||
accKeeper := auth.NewAccountKeeper(cdc, capKey, auth.ProtoBaseAccount)
|
||||
feeKeeper := auth.NewFeeCollectionKeeper(cdc, capKey2)
|
||||
cdc := CreateCodec()
|
||||
cdc.RegisterConcrete(&sdk.TestMsg{}, "test/TestMsg", nil)
|
||||
|
||||
accKeeper := auth.NewAccountKeeper(cdc, authCapKey, auth.ProtoBaseAccount)
|
||||
feeKeeper := auth.NewFeeCollectionKeeper(cdc, feeCapKey)
|
||||
anteHandler := NewAnteHandler(accKeeper, feeKeeper)
|
||||
|
||||
ctx := sdk.NewContext(
|
||||
ms,
|
||||
abci.Header{ChainID: "3", Time: time.Now().UTC()},
|
||||
false,
|
||||
true,
|
||||
log.NewNopLogger(),
|
||||
)
|
||||
|
||||
return testSetup{
|
||||
ctx: ctx,
|
||||
cdc: cdc,
|
||||
accKeeper: accKeeper,
|
||||
feeKeeper: feeKeeper,
|
||||
anteHandler: anteHandler,
|
||||
@@ -57,55 +72,55 @@ func newTestMsg(addrs ...sdk.AccAddress) *sdk.TestMsg {
|
||||
}
|
||||
|
||||
func newTestCoins() sdk.Coins {
|
||||
return sdk.Coins{sdk.NewInt64Coin(testDenom, 10000000)}
|
||||
return sdk.Coins{sdk.NewInt64Coin(types.DenomDefault, 500000000)}
|
||||
}
|
||||
|
||||
func newTestStdFee() auth.StdFee {
|
||||
return auth.NewStdFee(5000, sdk.NewInt64Coin(testDenom, 150))
|
||||
return auth.NewStdFee(220000, sdk.NewInt64Coin(types.DenomDefault, 150))
|
||||
}
|
||||
|
||||
// GenerateAddress generates an Ethereum address.
|
||||
func newTestAddrKey() (sdk.AccAddress, tmcrypto.PrivKey) {
|
||||
priv := tmsecp256k1.GenPrivKey()
|
||||
addr := sdk.AccAddress(priv.PubKey().Address())
|
||||
return addr, priv
|
||||
privkey, _ := crypto.GenerateKey()
|
||||
addr := ethcrypto.PubkeyToAddress(privkey.PublicKey)
|
||||
|
||||
return sdk.AccAddress(addr.Bytes()), privkey
|
||||
}
|
||||
|
||||
func newTestSDKTx(
|
||||
ctx sdk.Context, msgs []sdk.Msg, privs []tmcrypto.PrivKey,
|
||||
accNums []int64, seqs []int64, fee auth.StdFee,
|
||||
accNums []uint64, seqs []uint64, fee auth.StdFee,
|
||||
) sdk.Tx {
|
||||
|
||||
sigs := make([]auth.StdSignature, len(privs))
|
||||
for i, priv := range privs {
|
||||
signBytes := auth.StdSignBytes(ctx.ChainID(), accNums[i], seqs[i], fee, msgs, "")
|
||||
|
||||
sig, err := priv.Sign(signBytes)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
sigs[i] = auth.StdSignature{
|
||||
PubKey: priv.PubKey(),
|
||||
Signature: sig,
|
||||
AccountNumber: accNums[i],
|
||||
Sequence: seqs[i],
|
||||
PubKey: priv.PubKey(),
|
||||
Signature: sig,
|
||||
}
|
||||
}
|
||||
|
||||
return auth.NewStdTx(msgs, fee, sigs, "")
|
||||
}
|
||||
|
||||
func newTestEthTx(ctx sdk.Context, msg *evmtypes.MsgEthereumTx, priv tmcrypto.PrivKey) sdk.Tx {
|
||||
func newTestEthTx(ctx sdk.Context, msg *evmtypes.EthereumTxMsg, priv tmcrypto.PrivKey) sdk.Tx {
|
||||
chainID, ok := new(big.Int).SetString(ctx.ChainID(), 10)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("invalid chainID: %s", ctx.ChainID()))
|
||||
}
|
||||
|
||||
privKey, err := crypto.PrivKeyToSecp256k1(priv)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("failed to convert private key: %s", err))
|
||||
privkey, ok := priv.(crypto.PrivKeySecp256k1)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("invalid private key type: %T", priv))
|
||||
}
|
||||
|
||||
msg.Sign(chainID, privKey)
|
||||
return auth.NewStdTx([]sdk.Msg{msg}, auth.StdFee{}, nil, "")
|
||||
msg.Sign(chainID, privkey.ToECDSA())
|
||||
return msg
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user