evm: implement vm.GetHashFn (#620)
* evm: implement vm.GetHashFn * check nil case * test * handle 3 cases * use switch statement * stateDB tests * abci changes * fix LGTM issue * final tests * changelog * remove epoch * update test * clean test * rm epoch
This commit is contained in:
parent
7efd10eb21
commit
c4a3c0a96e
@ -40,6 +40,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
|
||||
### Bug Fixes
|
||||
|
||||
* (evm) [\#621](https://github.com/cosmos/ethermint/issues/621) EVM `GenesisAccount` fields now share the same format as the auth module `Account`.
|
||||
* (evm) [\#618](https://github.com/cosmos/ethermint/issues/618) Add missing EVM `Context` `GetHash` field that retrieves a the header hash from a given block height.
|
||||
* (app) [\#617](https://github.com/cosmos/ethermint/issues/617) Fix genesis export functionality.
|
||||
|
||||
## [v0.3.1] - 2020-11-24
|
||||
|
@ -28,7 +28,6 @@ func NewDefaultGenesisState() simapp.GenesisState {
|
||||
func (app *EthermintApp) ExportAppStateAndValidators(
|
||||
forZeroHeight bool, jailWhiteList []string,
|
||||
) (appState json.RawMessage, validators []tmtypes.GenesisValidator, err error) {
|
||||
|
||||
// Creates context with current height and checks txs for ctx to be usable by start of next block
|
||||
ctx := app.NewContext(true, abci.Header{Height: app.LastBlockHeight()})
|
||||
|
||||
|
@ -8,6 +8,8 @@ import (
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
|
||||
ethtypes "github.com/ethereum/go-ethereum/core/types"
|
||||
|
||||
"github.com/cosmos/ethermint/x/evm/types"
|
||||
)
|
||||
|
||||
// BeginBlock sets the block hash -> block height map for the previous block height
|
||||
@ -29,18 +31,25 @@ func (k *Keeper) BeginBlock(ctx sdk.Context, req abci.RequestBeginBlock) {
|
||||
|
||||
// EndBlock updates the accounts and commits state objects to the KV Store, while
|
||||
// deleting the empty ones. It also sets the bloom filers for the request block to
|
||||
// the store. The EVM end block loginc doesn't update the validator set, thus it returns
|
||||
// the store. The EVM end block logic doesn't update the validator set, thus it returns
|
||||
// an empty slice.
|
||||
func (k Keeper) EndBlock(ctx sdk.Context, req abci.RequestEndBlock) []abci.ValidatorUpdate {
|
||||
// Gas costs are handled within msg handler so costs should be ignored
|
||||
ctx = ctx.WithGasMeter(sdk.NewInfiniteGasMeter())
|
||||
|
||||
// Set the hash for the current height.
|
||||
// NOTE: we set the hash here instead of on BeginBlock in order to set the final block prior to
|
||||
// an upgrade. If we set it on BeginBlock the last block from prior to the upgrade wouldn't be
|
||||
// included on the store.
|
||||
hash := types.HashFromContext(ctx)
|
||||
k.SetHeightHash(ctx, uint64(ctx.BlockHeight()), hash)
|
||||
|
||||
// Update account balances before committing other parts of state
|
||||
k.UpdateAccounts(ctx)
|
||||
|
||||
// Commit state objects to KV store
|
||||
_, err := k.Commit(ctx, true)
|
||||
if err != nil {
|
||||
if _, err := k.Commit(ctx, true); err != nil {
|
||||
k.Logger(ctx).Error("failed to commit state objects", "error", err, "height", ctx.BlockHeight())
|
||||
panic(err)
|
||||
}
|
||||
|
||||
|
@ -88,6 +88,21 @@ func (k Keeper) SetBlockHash(ctx sdk.Context, hash []byte, height int64) {
|
||||
store.Set(hash, bz)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Epoch Height -> hash mapping functions
|
||||
// Required by EVM context's GetHashFunc
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// GetHeightHash returns the block header hash associated with a given block height and chain epoch number.
|
||||
func (k Keeper) GetHeightHash(ctx sdk.Context, height uint64) common.Hash {
|
||||
return k.CommitStateDB.WithContext(ctx).GetHeightHash(height)
|
||||
}
|
||||
|
||||
// SetHeightHash sets the block header hash associated with a given height.
|
||||
func (k Keeper) SetHeightHash(ctx sdk.Context, height uint64, hash common.Hash) {
|
||||
k.CommitStateDB.WithContext(ctx).SetHeightHash(height, hash)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Block bloom bits mapping functions
|
||||
// Required by Web3 API.
|
||||
|
@ -42,7 +42,7 @@ func (suite *KeeperTestSuite) SetupTest() {
|
||||
checkTx := false
|
||||
|
||||
suite.app = app.Setup(checkTx)
|
||||
suite.ctx = suite.app.BaseApp.NewContext(checkTx, abci.Header{Height: 1, ChainID: "3", Time: time.Now().UTC()})
|
||||
suite.ctx = suite.app.BaseApp.NewContext(checkTx, abci.Header{Height: 1, ChainID: "ethermint-3", Time: time.Now().UTC()})
|
||||
suite.querier = keeper.NewQuerier(suite.app.EvmKeeper)
|
||||
suite.address = ethcmn.HexToAddress(addrHex)
|
||||
|
||||
|
@ -124,7 +124,7 @@ func (suite *JournalTestSuite) setup() {
|
||||
evmSubspace := paramsKeeper.Subspace(types.DefaultParamspace).WithKeyTable(ParamKeyTable())
|
||||
|
||||
ak := auth.NewAccountKeeper(cdc, authKey, authSubspace, ethermint.ProtoAccount)
|
||||
suite.ctx = sdk.NewContext(cms, abci.Header{ChainID: "8"}, false, tmlog.NewNopLogger())
|
||||
suite.ctx = sdk.NewContext(cms, abci.Header{ChainID: "ethermint-8"}, false, tmlog.NewNopLogger())
|
||||
suite.stateDB = NewCommitStateDB(suite.ctx, storeKey, evmSubspace, ak).WithContext(suite.ctx)
|
||||
suite.stateDB.SetParams(DefaultParams())
|
||||
}
|
||||
|
@ -27,8 +27,18 @@ var (
|
||||
KeyPrefixCode = []byte{0x04}
|
||||
KeyPrefixStorage = []byte{0x05}
|
||||
KeyPrefixChainConfig = []byte{0x06}
|
||||
KeyPrefixHeightHash = []byte{0x07}
|
||||
)
|
||||
|
||||
// HeightHashKey returns the key for the given chain epoch and height.
|
||||
// The key will be composed in the following order:
|
||||
// key = prefix + bytes(height)
|
||||
// This ordering facilitates the iteration by height for the EVM GetHashFn
|
||||
// queries.
|
||||
func HeightHashKey(height uint64) []byte {
|
||||
return sdk.Uint64ToBigEndian(height)
|
||||
}
|
||||
|
||||
// BloomKey defines the store key for a block Bloom
|
||||
func BloomKey(height int64) []byte {
|
||||
return sdk.Uint64ToBigEndian(uint64(height))
|
||||
|
@ -47,11 +47,42 @@ type ExecutionResult struct {
|
||||
GasInfo GasInfo
|
||||
}
|
||||
|
||||
func (st StateTransition) newEVM(ctx sdk.Context, csdb *CommitStateDB, gasLimit uint64, gasPrice *big.Int, config ChainConfig) *vm.EVM {
|
||||
// GetHashFn implements vm.GetHashFunc for Ethermint. It handles 3 cases:
|
||||
// 1. The requested height matches the current height from context (and thus same epoch number)
|
||||
// 2. The requested height is from an previous height from the same chain epoch
|
||||
// 3. The requested height is from a height greater than the latest one
|
||||
func GetHashFn(ctx sdk.Context, csdb *CommitStateDB) vm.GetHashFunc {
|
||||
return func(height uint64) common.Hash {
|
||||
switch {
|
||||
case ctx.BlockHeight() == int64(height):
|
||||
// Case 1: The requested height matches the one from the context so we can retrieve the header
|
||||
// hash directly from the context.
|
||||
return HashFromContext(ctx)
|
||||
|
||||
case ctx.BlockHeight() > int64(height):
|
||||
// Case 2: if the chain is not the current height we need to retrieve the hash from the store for the
|
||||
// current chain epoch. This only applies if the current height is greater than the requested height.
|
||||
return csdb.WithContext(ctx).GetHeightHash(height)
|
||||
|
||||
default:
|
||||
// Case 3: heights greater than the current one returns an empty hash.
|
||||
return common.Hash{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (st StateTransition) newEVM(
|
||||
ctx sdk.Context,
|
||||
csdb *CommitStateDB,
|
||||
gasLimit uint64,
|
||||
gasPrice *big.Int,
|
||||
config ChainConfig,
|
||||
) *vm.EVM {
|
||||
// Create context for evm
|
||||
context := vm.Context{
|
||||
CanTransfer: core.CanTransfer,
|
||||
Transfer: core.Transfer,
|
||||
GetHash: GetHashFn(ctx, csdb),
|
||||
Origin: st.Sender,
|
||||
Coinbase: common.Address{}, // there's no benefitiary since we're not mining
|
||||
BlockNumber: big.NewInt(ctx.BlockHeight()),
|
||||
@ -134,7 +165,7 @@ func (st StateTransition) TransitionDb(ctx sdk.Context, config ChainConfig) (*Ex
|
||||
recipientLog = fmt.Sprintf("contract address %s", contractAddress.String())
|
||||
default:
|
||||
if !params.EnableCall {
|
||||
return nil, ErrCreateDisabled
|
||||
return nil, ErrCallDisabled
|
||||
}
|
||||
|
||||
// Increment the nonce for the next transaction (just for evm state transition)
|
||||
@ -223,3 +254,21 @@ func (st StateTransition) TransitionDb(ctx sdk.Context, config ChainConfig) (*Ex
|
||||
|
||||
return executionResult, nil
|
||||
}
|
||||
|
||||
// HashFromContext returns the Ethereum Header hash from the context's Tendermint
|
||||
// block header.
|
||||
func HashFromContext(ctx sdk.Context) common.Hash {
|
||||
// cast the ABCI header to tendermint Header type
|
||||
tmHeader := AbciHeaderToTendermint(ctx.BlockHeader())
|
||||
|
||||
// get the Tendermint block hash from the current header
|
||||
tmBlockHash := tmHeader.Hash()
|
||||
|
||||
// NOTE: if the validator set hash is missing the hash will be returned as nil,
|
||||
// so we need to check for this case to prevent a panic when calling Bytes()
|
||||
if tmBlockHash == nil {
|
||||
return common.Hash{}
|
||||
}
|
||||
|
||||
return common.BytesToHash(tmBlockHash.Bytes())
|
||||
}
|
||||
|
@ -3,16 +3,108 @@ package types_test
|
||||
import (
|
||||
"math/big"
|
||||
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
|
||||
"github.com/cosmos/ethermint/crypto/ethsecp256k1"
|
||||
ethermint "github.com/cosmos/ethermint/types"
|
||||
"github.com/cosmos/ethermint/x/evm/types"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
ethcmn "github.com/ethereum/go-ethereum/common"
|
||||
ethcrypto "github.com/ethereum/go-ethereum/crypto"
|
||||
)
|
||||
|
||||
func (suite *StateDBTestSuite) TestGetHashFn() {
|
||||
testCase := []struct {
|
||||
name string
|
||||
height uint64
|
||||
malleate func()
|
||||
expEmptyHash bool
|
||||
}{
|
||||
{
|
||||
"valid hash, case 1",
|
||||
1,
|
||||
func() {
|
||||
suite.ctx = suite.ctx.WithBlockHeader(
|
||||
abci.Header{
|
||||
ChainID: "ethermint-1",
|
||||
Height: 1,
|
||||
ValidatorsHash: []byte("val_hash"),
|
||||
},
|
||||
)
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"case 1, nil tendermint hash",
|
||||
1,
|
||||
func() {},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"valid hash, case 2",
|
||||
1,
|
||||
func() {
|
||||
suite.ctx = suite.ctx.WithBlockHeader(
|
||||
abci.Header{
|
||||
ChainID: "ethermint-1",
|
||||
Height: 100,
|
||||
ValidatorsHash: []byte("val_hash"),
|
||||
},
|
||||
)
|
||||
hash := types.HashFromContext(suite.ctx)
|
||||
suite.stateDB.WithContext(suite.ctx).SetHeightHash(1, hash)
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"height not found, case 2",
|
||||
1,
|
||||
func() {
|
||||
suite.ctx = suite.ctx.WithBlockHeader(
|
||||
abci.Header{
|
||||
ChainID: "ethermint-1",
|
||||
Height: 100,
|
||||
ValidatorsHash: []byte("val_hash"),
|
||||
},
|
||||
)
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"empty hash, case 3",
|
||||
1000,
|
||||
func() {
|
||||
suite.ctx = suite.ctx.WithBlockHeader(
|
||||
abci.Header{
|
||||
ChainID: "ethermint-1",
|
||||
Height: 100,
|
||||
ValidatorsHash: []byte("val_hash"),
|
||||
},
|
||||
)
|
||||
},
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCase {
|
||||
suite.Run(tc.name, func() {
|
||||
suite.SetupTest() // reset
|
||||
|
||||
tc.malleate()
|
||||
|
||||
hash := types.GetHashFn(suite.ctx, suite.stateDB)(tc.height)
|
||||
if tc.expEmptyHash {
|
||||
suite.Require().Equal(common.Hash{}.String(), hash.String())
|
||||
} else {
|
||||
suite.Require().NotEqual(common.Hash{}.String(), hash.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *StateDBTestSuite) TestTransitionDb() {
|
||||
suite.stateDB.SetNonce(suite.address, 123)
|
||||
|
||||
@ -104,9 +196,52 @@ func (suite *StateDBTestSuite) TestTransitionDb() {
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"call disabled",
|
||||
func() {
|
||||
params := types.NewParams(ethermint.AttoPhoton, true, false)
|
||||
suite.stateDB.SetParams(params)
|
||||
},
|
||||
types.StateTransition{
|
||||
AccountNonce: 123,
|
||||
Price: big.NewInt(10),
|
||||
GasLimit: 11,
|
||||
Recipient: &recipient,
|
||||
Amount: big.NewInt(50),
|
||||
Payload: []byte("data"),
|
||||
ChainID: big.NewInt(1),
|
||||
Csdb: suite.stateDB,
|
||||
TxHash: ðcmn.Hash{},
|
||||
Sender: suite.address,
|
||||
Simulate: suite.ctx.IsCheckTx(),
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"create disabled",
|
||||
func() {
|
||||
params := types.NewParams(ethermint.AttoPhoton, false, true)
|
||||
suite.stateDB.SetParams(params)
|
||||
},
|
||||
types.StateTransition{
|
||||
AccountNonce: 123,
|
||||
Price: big.NewInt(10),
|
||||
GasLimit: 11,
|
||||
Recipient: nil,
|
||||
Amount: big.NewInt(50),
|
||||
Payload: []byte("data"),
|
||||
ChainID: big.NewInt(1),
|
||||
Csdb: suite.stateDB,
|
||||
TxHash: ðcmn.Hash{},
|
||||
Sender: suite.address,
|
||||
Simulate: suite.ctx.IsCheckTx(),
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"nil gas price",
|
||||
func() {
|
||||
suite.stateDB.SetParams(types.DefaultParams())
|
||||
invalidGas := sdk.DecCoins{
|
||||
{Denom: ethermint.AttoPhoton},
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ import (
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/x/params"
|
||||
|
||||
emint "github.com/cosmos/ethermint/types"
|
||||
ethermint "github.com/cosmos/ethermint/types"
|
||||
|
||||
ethcmn "github.com/ethereum/go-ethereum/common"
|
||||
ethstate "github.com/ethereum/go-ethereum/core/state"
|
||||
@ -107,7 +107,7 @@ func NewCommitStateDB(
|
||||
}
|
||||
}
|
||||
|
||||
// WithContext returns a Database with an updated sdk context
|
||||
// WithContext returns a Database with an updated SDK context
|
||||
func (csdb *CommitStateDB) WithContext(ctx sdk.Context) *CommitStateDB {
|
||||
csdb.ctx = ctx
|
||||
return csdb
|
||||
@ -117,6 +117,13 @@ func (csdb *CommitStateDB) WithContext(ctx sdk.Context) *CommitStateDB {
|
||||
// Setters
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// SetHeightHash sets the block header hash associated with a given height.
|
||||
func (csdb *CommitStateDB) SetHeightHash(height uint64, hash ethcmn.Hash) {
|
||||
store := prefix.NewStore(csdb.ctx.KVStore(csdb.storeKey), KeyPrefixHeightHash)
|
||||
key := HeightHashKey(height)
|
||||
store.Set(key, hash.Bytes())
|
||||
}
|
||||
|
||||
// SetParams sets the evm parameters to the param space.
|
||||
func (csdb *CommitStateDB) SetParams(params Params) {
|
||||
csdb.paramSpace.SetParamSet(csdb.ctx, ¶ms)
|
||||
@ -286,6 +293,18 @@ func (csdb *CommitStateDB) SlotInAccessList(addr ethcmn.Address, slot ethcmn.Has
|
||||
// Getters
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// GetHeightHash returns the block header hash associated with a given block height and chain epoch number.
|
||||
func (csdb *CommitStateDB) GetHeightHash(height uint64) ethcmn.Hash {
|
||||
store := prefix.NewStore(csdb.ctx.KVStore(csdb.storeKey), KeyPrefixHeightHash)
|
||||
key := HeightHashKey(height)
|
||||
bz := store.Get(key)
|
||||
if len(bz) == 0 {
|
||||
return ethcmn.Hash{}
|
||||
}
|
||||
|
||||
return ethcmn.BytesToHash(bz)
|
||||
}
|
||||
|
||||
// GetParams returns the total set of evm parameters.
|
||||
func (csdb *CommitStateDB) GetParams() (params Params) {
|
||||
csdb.paramSpace.GetParamSet(csdb.ctx, ¶ms)
|
||||
@ -680,7 +699,7 @@ func (csdb *CommitStateDB) Reset(_ ethcmn.Hash) error {
|
||||
func (csdb *CommitStateDB) UpdateAccounts() {
|
||||
for _, stateEntry := range csdb.stateObjects {
|
||||
currAcc := csdb.accountKeeper.GetAccount(csdb.ctx, sdk.AccAddress(stateEntry.address.Bytes()))
|
||||
emintAcc, ok := currAcc.(*emint.EthAccount)
|
||||
ethermintAcc, ok := currAcc.(*ethermint.EthAccount)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
@ -688,12 +707,12 @@ func (csdb *CommitStateDB) UpdateAccounts() {
|
||||
evmDenom := csdb.GetParams().EvmDenom
|
||||
balance := sdk.Coin{
|
||||
Denom: evmDenom,
|
||||
Amount: emintAcc.GetCoins().AmountOf(evmDenom),
|
||||
Amount: ethermintAcc.GetCoins().AmountOf(evmDenom),
|
||||
}
|
||||
|
||||
if stateEntry.stateObject.Balance() != balance.Amount.BigInt() && balance.IsValid() ||
|
||||
stateEntry.stateObject.Nonce() != emintAcc.GetSequence() {
|
||||
stateEntry.stateObject.account = emintAcc
|
||||
stateEntry.stateObject.Nonce() != ethermintAcc.GetSequence() {
|
||||
stateEntry.stateObject.account = ethermintAcc
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ func (suite *StateDBTestSuite) SetupTest() {
|
||||
checkTx := false
|
||||
|
||||
suite.app = app.Setup(checkTx)
|
||||
suite.ctx = suite.app.BaseApp.NewContext(checkTx, abci.Header{Height: 1})
|
||||
suite.ctx = suite.app.BaseApp.NewContext(checkTx, abci.Header{Height: 1, ChainID: "ethermint-1"})
|
||||
suite.stateDB = suite.app.EvmKeeper.CommitStateDB.WithContext(suite.ctx)
|
||||
|
||||
privkey, err := ethsecp256k1.GenerateKey()
|
||||
@ -67,6 +67,17 @@ func (suite *StateDBTestSuite) TestParams() {
|
||||
suite.Require().Equal(newParams, params)
|
||||
}
|
||||
|
||||
func (suite *StateDBTestSuite) TestGetHeightHash() {
|
||||
hash := suite.stateDB.GetHeightHash(0)
|
||||
suite.Require().Equal(ethcmn.Hash{}.String(), hash.String())
|
||||
|
||||
expHash := ethcmn.BytesToHash([]byte("hash"))
|
||||
suite.stateDB.SetHeightHash(10, expHash)
|
||||
|
||||
hash = suite.stateDB.GetHeightHash(10)
|
||||
suite.Require().Equal(expHash.String(), hash.String())
|
||||
}
|
||||
|
||||
func (suite *StateDBTestSuite) TestBloomFilter() {
|
||||
// Prepare db for logs
|
||||
tHash := ethcmn.BytesToHash([]byte{0x1})
|
||||
|
@ -8,6 +8,10 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/crypto/sha3"
|
||||
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
tmtypes "github.com/tendermint/tendermint/types"
|
||||
"github.com/tendermint/tendermint/version"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||
@ -152,3 +156,36 @@ func recoverEthSig(R, S, Vb *big.Int, sigHash ethcmn.Hash) (ethcmn.Address, erro
|
||||
|
||||
return addr, nil
|
||||
}
|
||||
|
||||
// AbciHeaderToTendermint is a util function to parse a tendermint ABCI Header to
|
||||
// tendermint types Header.
|
||||
func AbciHeaderToTendermint(header abci.Header) tmtypes.Header {
|
||||
return tmtypes.Header{
|
||||
Version: version.Consensus{
|
||||
Block: version.Protocol(header.Version.Block),
|
||||
App: version.Protocol(header.Version.App),
|
||||
},
|
||||
ChainID: header.ChainID,
|
||||
Height: header.Height,
|
||||
Time: header.Time,
|
||||
|
||||
LastBlockID: tmtypes.BlockID{
|
||||
Hash: header.LastBlockId.Hash,
|
||||
PartsHeader: tmtypes.PartSetHeader{
|
||||
Total: int(header.LastBlockId.PartsHeader.Total),
|
||||
Hash: header.LastBlockId.PartsHeader.Hash,
|
||||
},
|
||||
},
|
||||
LastCommitHash: header.LastCommitHash,
|
||||
DataHash: header.DataHash,
|
||||
|
||||
ValidatorsHash: header.ValidatorsHash,
|
||||
NextValidatorsHash: header.NextValidatorsHash,
|
||||
ConsensusHash: header.ConsensusHash,
|
||||
AppHash: header.AppHash,
|
||||
LastResultsHash: header.LastResultsHash,
|
||||
|
||||
EvidenceHash: header.EvidenceHash,
|
||||
ProposerAddress: header.ProposerAddress,
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user