Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: [Core] account abstraction (EIP-2938 ) integration #4205

Closed
wants to merge 4 commits into from
Closed
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 api/service/explorer/structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ func GetTransaction(tx *types.Transaction, addressBlock *types.Block) (*Transact
utils.Logger().Error().Err(err).Msg("Error when parsing tx into message")
}
gasFee := big.NewInt(0)
gasFee = gasFee.Mul(tx.GasPrice(), new(big.Int).SetUint64(tx.GasLimit()))
gasFee = gasFee.Mul(tx.RawGasPrice(), new(big.Int).SetUint64(tx.GasLimit()))
to := ""
if msg.To() != nil {
if to, err = common2.AddressToBech32(*msg.To()); err != nil {
Expand Down
166 changes: 166 additions & 0 deletions core/aa_transaction_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
// Copyright 2015 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum 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 go-ethereum 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 go-ethereum library. If not, see <http://www.gnu.org/licenses/>.

package core

import (
"encoding/binary"
"math/big"
"testing"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/crypto"
blockfactory "github.com/harmony-one/harmony/block/factory"
"github.com/harmony-one/harmony/core/state"
"github.com/harmony-one/harmony/core/types"
"github.com/harmony-one/harmony/core/vm"
chain2 "github.com/harmony-one/harmony/internal/chain"
"github.com/harmony-one/harmony/internal/params"
)

const (
contractCode = "3373ffffffffffffffffffffffffffffffffffffffff1460245736601f57005b600080fd5b60016000355b81900380602a57602035603957005b604035aa00"
)

// aaTransaction creates a new AA transaction to a deployed instance of the test contract.
// Parameters are the contract address, the transaction gas limit,
// the number of loops to run inside the contract (minimum: 1, gas per loop: 26),
// whether to call PAYGAS at the end, the gas price to be returned from PAYGAS (if called).
func aaTransaction(to common.Address, gaslimit uint64, loops uint64, callPaygas bool, gasPrice *big.Int) *types.Transaction {
data := make([]byte, 0x60)
if loops == 0 {
loops = 1
}
binary.BigEndian.PutUint64(data[0x18:0x20], loops)
if callPaygas {
data[0x3f] = 1
}
gasPriceBytes := gasPrice.Bytes()
copy(data[0x60-len(gasPriceBytes):], gasPriceBytes)

return types.NewTransaction(0, to, 0, big.NewInt(0), gaslimit, big.NewInt(0), data).WithAASignature()
}

func setupBlockchain(blockGasLimit uint64) *BlockChain {
key, _ := crypto.GenerateKey()
gspec := Genesis{
Config: params.TestChainConfig,
Factory: blockfactory.ForTest,
Alloc: GenesisAlloc{
crypto.PubkeyToAddress(key.PublicKey): {
Balance: big.NewInt(8e18),
},
},
GasLimit: blockGasLimit,
ShardID: 0,
}
database := rawdb.NewMemoryDatabase()
genesis := gspec.MustCommit(database)
_ = genesis
engine := chain2.NewEngine()
blockchain, _ := NewBlockChain(database, nil, gspec.Config, engine, vm.Config{}, nil)
return blockchain
}

func testValidate(blockchain *BlockChain, statedb *state.DB, transaction *types.Transaction, validationGasLimit uint64, expectedErr error, t *testing.T) {
t.Helper()
var (
snapshotRevisionId = statedb.Snapshot()
context = NewEVMContext(types.AAEntryMessage, blockchain.CurrentHeader(), blockchain, &common.Address{})
vmenv = vm.NewEVM(context, statedb, blockchain.Config(), vm.Config{})
err = Validate(transaction, types.HomesteadSigner{}, vmenv, validationGasLimit)
)
if err != expectedErr {
t.Error("\n\texpected:", expectedErr, "\n\tgot:", err)
}
statedb.RevertToSnapshot(snapshotRevisionId)
}

func TestTransactionValidation(t *testing.T) {
var (
blockchain = setupBlockchain(10000000)
statedb, _ = blockchain.State()

key, _ = crypto.GenerateKey()
contractCreator = crypto.PubkeyToAddress(key.PublicKey)
contractAddress = crypto.CreateAddress(contractCreator, 0)
tx = &types.Transaction{}
)
statedb.SetBalance(contractAddress, big.NewInt(100000))
statedb.SetCode(contractAddress, common.FromHex(contractCode))
statedb.SetNonce(contractAddress, 1)

// test: invalid, no PAYGAS
tx = aaTransaction(contractAddress, 100000, 1, false, big.NewInt(1))
testValidate(blockchain, statedb, tx, 400000, ErrNoPaygas, t)

// test: invalid, insufficient funds
tx = aaTransaction(contractAddress, 100000, 1, true, big.NewInt(2))
testValidate(blockchain, statedb, tx, 400000, vm.ErrPaygasInsufficientFunds, t)

// test: invalid, gasLimit too low for intrinsic gas cost
tx = aaTransaction(contractAddress, 1, 1, true, big.NewInt(1))
testValidate(blockchain, statedb, tx, 400000, ErrIntrinsicGas, t)

// test: invalid, gasLimit too low for loops
tx = aaTransaction(contractAddress, 100000, 100000, true, big.NewInt(1))
testValidate(blockchain, statedb, tx, 400000, vm.ErrOutOfGas, t)

// test: valid, gasLimit < validationGasLimit
tx = aaTransaction(contractAddress, 100000, 1, true, big.NewInt(1))
testValidate(blockchain, statedb, tx, 400000, nil, t)

// test: valid, validationGasLimit < gasLimit
tx = aaTransaction(contractAddress, 100000, 1, true, big.NewInt(1))
testValidate(blockchain, statedb, tx, 50000, nil, t)

// test: invalid, validationGasLimit too low for intrinsic gas cost
tx = aaTransaction(contractAddress, 100000, 1, true, big.NewInt(1))
testValidate(blockchain, statedb, tx, 1, ErrIntrinsicGas, t)

// test: invalid, validationGasLimit too low for loops
tx = aaTransaction(contractAddress, 100000, 100000, true, big.NewInt(1))
testValidate(blockchain, statedb, tx, 50000, vm.ErrOutOfGas, t)

statedb.SetBalance(contractAddress, big.NewInt(0))

// test: invalid, insufficient funds
tx = aaTransaction(contractAddress, 100000, 1, true, big.NewInt(1))
testValidate(blockchain, statedb, tx, 400000, vm.ErrPaygasInsufficientFunds, t)

// test: valid, gasPrice 0
tx = aaTransaction(contractAddress, 100000, 1, true, big.NewInt(0))
testValidate(blockchain, statedb, tx, 400000, nil, t)
}

func TestMalformedTransaction(t *testing.T) {
var (
blockchain = setupBlockchain(10000000)
statedb, _ = blockchain.State()

context = NewEVMContext(types.AAEntryMessage, blockchain.CurrentHeader(), blockchain, &common.Address{})
vmenv = vm.NewEVM(context, statedb, blockchain.Config(), vm.Config{})

key, _ = crypto.GenerateKey()
tx = pricedTransaction(0, 0, 100000, big.NewInt(0), key)
err = Validate(tx.(*types.Transaction), types.HomesteadSigner{}, vmenv, 400000)
expectedErr = ErrMalformedAATransaction
)
if err != expectedErr {
t.Error("\n\texpected:", expectedErr, "\n\tgot:", err)
}
}
6 changes: 6 additions & 0 deletions core/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,10 @@ var (

// ErrShardStateNotMatch is returned if the calculated shardState hash not equal that in the block header
ErrShardStateNotMatch = errors.New("shard state root hash not match")

ErrInvalidAATransaction = errors.New("invalid account abstraction transaction")

ErrInvalidAAPrefix = errors.New("invalid account abstraction prefix")

ErrNoPaygas = errors.New("account abstraction transaction did not call PAYGAS")
)
14 changes: 13 additions & 1 deletion core/evm.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ import (
stakingTypes "github.com/harmony-one/harmony/staking/types"
)

type headerGetter interface {
// GetHeader returns the hash corresponding to their hash.
GetHeader(common.Hash, uint64) *block.Header
}

// ChainContext supports retrieving headers and consensus parameters from the
// current blockchain to be used during transaction processing.
type ChainContext interface {
Expand Down Expand Up @@ -77,6 +82,10 @@ func NewEVMContext(msg Message, header *block.Header, chain ChainContext, author
vrfAndProof := header.Vrf()
copy(vrf[:], vrfAndProof[:32])
}
var paygasMode = vm.PaygasNoOp
if msg.IsAA() {
paygasMode = vm.PaygasContinue
}
return vm.Context{
CanTransfer: CanTransfer,
Transfer: Transfer,
Expand All @@ -100,6 +109,9 @@ func NewEVMContext(msg Message, header *block.Header, chain ChainContext, author
CalculateMigrationGas: CalculateMigrationGasFn(chain),
ShardID: chain.ShardID(),
NumShards: shard.Schedule.InstanceForEpoch(header.Epoch()).NumShards(),

PaygasMode: paygasMode,
TxGasLimit: msg.Gas(),
}
}

Expand Down Expand Up @@ -418,7 +430,7 @@ func CalculateMigrationGasFn(chain ChainContext) vm.CalculateMigrationGasFunc {
}

// GetHashFn returns a GetHashFunc which retrieves header hashes by number
func GetHashFn(ref *block.Header, chain ChainContext) func(n uint64) common.Hash {
func GetHashFn(ref *block.Header, chain headerGetter) func(n uint64) common.Hash {
var cache map[uint64]common.Hash

return func(n uint64) common.Hash {
Expand Down
3 changes: 1 addition & 2 deletions core/rawdb/accessors_offchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package rawdb

import (
"encoding/binary"
"fmt"
"math/big"

"github.com/ethereum/go-ethereum/common"
Expand Down Expand Up @@ -393,7 +392,7 @@ func ReadBlockCommitSig(db DatabaseReader, blockNum uint64) ([]byte, error) {
// this is only needed for the compatibility in the migration moment.
data, err = db.Get(lastCommitsKey)
if err != nil {
return nil, errors.New(fmt.Sprintf("cannot read commit sig for block: %d ", blockNum))
return nil, errors.Errorf("cannot read commit sig for block %d", blockNum)
}
}
return data, nil
Expand Down
66 changes: 55 additions & 11 deletions core/state_transition.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ var (
errDupBlsKey = errors.New("BLS key exists")
)

var aaPrefix = [...]byte{
0x33, 0x73, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0x14, 0x60, 0x24, 0x57, 0x36, 0x60, 0x1f, 0x57, 0x00, 0x5b, 0x60, 0x00, 0x80, 0xfd, 0x5b,
}

/*
StateTransition is the State Transitioning Model which is described as follows:

Expand Down Expand Up @@ -88,6 +93,8 @@ type Message interface {
Nonce() uint64
CheckNonce() bool
Data() []byte
IsAA() bool
Sponsor() common.Address
Type() types.TransactionType
BlockNum() *big.Int
}
Expand Down Expand Up @@ -166,28 +173,34 @@ func (st *StateTransition) to() common.Address {

func (st *StateTransition) useGas(amount uint64) error {
if st.gas < amount {
return vm.ErrOutOfGas
return ErrIntrinsicGas
}
st.gas -= amount

return nil
}

func (st *StateTransition) buyGas() error {
mgval := new(big.Int).Mul(new(big.Int).SetUint64(st.msg.Gas()), st.gasPrice)
if have := st.state.GetBalance(st.msg.From()); have.Cmp(mgval) < 0 {
return errors.Wrapf(
errInsufficientBalanceForGas,
"had: %s but need: %s", have.String(), mgval.String(),
)
var mgval *big.Int
if !st.msg.IsAA() {
mgval = new(big.Int).Mul(new(big.Int).SetUint64(st.msg.Gas()), st.gasPrice)
if have := st.state.GetBalance(st.msg.From()); have.Cmp(mgval) < 0 {
return errors.Wrapf(
errInsufficientBalanceForGas,
"had: %s but need: %s", have.String(), mgval.String(),
)
}
}
if err := st.gp.SubGas(st.msg.Gas()); err != nil {
return err
}
st.gas += st.msg.Gas()

st.initialGas = st.msg.Gas()
st.state.SubBalance(st.msg.From(), mgval)
if !st.msg.IsAA() {
st.state.SubBalance(st.msg.Sponsor(), mgval)
}

return nil
}

Expand All @@ -209,6 +222,25 @@ func (st *StateTransition) preCheck() error {
// returning the result including the used gas. It returns an error if failed.
// An error indicates a consensus issue.
func (st *StateTransition) TransitionDb() (ExecutionResult, error) {
if st.msg.IsAA() {
if st.msg.Nonce() != 0 || st.msg.GasPrice().Sign() != 0 || st.msg.Value().Sign() != 0 {
return ExecutionResult{}, ErrInvalidAATransaction
}

code := st.evm.StateDB.GetCode(st.to())
if code == nil || len(code) < len(aaPrefix) {
return ExecutionResult{}, ErrInvalidAAPrefix
}
for i := range aaPrefix {
if code[i] != aaPrefix[i] {
return ExecutionResult{}, ErrInvalidAAPrefix
}
}
if st.evm.PaygasMode == vm.PaygasNoOp {
st.evm.PaygasMode = vm.PaygasContinue
}
}

if err := st.preCheck(); err != nil {
return ExecutionResult{}, err
}
Expand Down Expand Up @@ -236,9 +268,18 @@ func (st *StateTransition) TransitionDb() (ExecutionResult, error) {
if contractCreation {
ret, _, st.gas, vmErr = evm.Create(sender, st.data, st.gas, st.value)
} else {
// Increment the nonce for the next transaction
st.state.SetNonce(msg.From(), st.state.GetNonce(sender.Address())+1)
if !msg.IsAA() {
// Increment the nonce for the next transaction
st.state.SetNonce(msg.From(), st.state.GetNonce(sender.Address())+1)
}
ret, st.gas, vmErr = evm.Call(sender, st.to(), st.data, st.gas, st.value)
if msg.IsAA() && st.evm.PaygasMode != vm.PaygasNoOp {
if vmErr != nil {
return ExecutionResult{}, vmErr
} else {
return ExecutionResult{}, ErrNoPaygas
}
}
}
if vmErr != nil {
utils.Logger().Debug().Err(vmErr).Msg("VM returned with error")
Expand All @@ -250,6 +291,9 @@ func (st *StateTransition) TransitionDb() (ExecutionResult, error) {
return ExecutionResult{}, vmErr
}
}
if st.msg.IsAA() {
st.gasPrice = st.evm.GasPrice
}
st.refundGas()

// Burn Txn Fees after staking epoch
Expand All @@ -275,7 +319,7 @@ func (st *StateTransition) refundGas() {

// Return ETH for remaining gas, exchanged at the original rate.
remaining := new(big.Int).Mul(new(big.Int).SetUint64(st.gas), st.gasPrice)
st.state.AddBalance(st.msg.From(), remaining)
st.state.AddBalance(st.msg.Sponsor(), remaining)

// Also return remaining gas to the block gas counter so it is
// available for the next transaction.
Expand Down
Loading