Skip to content

Commit

Permalink
feat(dogfood): implement ExportGenesis (#95)
Browse files Browse the repository at this point in the history
* feat(dogfood): re-scaffold genesis

In preparation for the `ExportGenesis` implementation, re-scaffold the
genesis state of the dogfood module.

* fix(dogfood): remove deprecated keys

* refactor: use new dogfood genesis

* refactor(dogfood): - validatorupdate from gen

* refactor(dogfood): update ImportGenesis

...for new genesis structure

* refactor(dogfood): simplify gen state names

* feat(dogfood): implement ExportGenesis

Partially resolves #54

* chore: lint

* proto(dogfood): remove unused import

* fix(local): compat with new dogfood genesis

* chore: lint proto file and gen

* fix(test): use correct struct member name

* chore: update go version to 1.21.11

* fix(ci): disable buggy workflows

till they can be fixed

* chore: lint

* fix(dogfood): don't stop iterating in export

The return value should be `false` to continue

* fix(doc): add comment about exported validators

* doc(dogfood): log iteration failure

If such an iteration failure happens during export, a pipe to `jq` will
fail and inform the user of the failure.

* docs(delegation): validation of record key params

* fix(app): register operator hooks for dogfood

* feat(operator): return `OptingOut` status

Whenever a query for consensus keys or addresses is answered, it should
specify whether the operator is currently in the process of opting out.
This is because the data is retained until the opt out is complete, even
though the operator may not be actively validating. The purpose of the
data retention, as always, is to slash the operator (if required), based
on the key.

* refactor(operator): rationalize opt-in/out

When an operator opts into an AVS, they should be required to supply the
consensus (or other) key to opt-in. Similarly, when they opt-out, the
key should automatically be removed.

This commit implements that functionality, however, it is restricted to
`ctx.ChainID()`. For an overbroad implementation, an AVS registry from
AVS address -> key type should be implemented.

* refactor(delegation): verbose panic

* fix(dogfood): query validator by cons addr not hex

* refactor(dogfood): move opt func to correct loc

* chore(x/operator): lint go

* fix(operator): wrap and return error

---------

Co-authored-by: cloud8little <[email protected]>
  • Loading branch information
MaxMustermann2 and cloud8little authored Jun 17, 2024
1 parent a4eb17b commit 002d277
Show file tree
Hide file tree
Showing 33 changed files with 2,447 additions and 837 deletions.
4 changes: 4 additions & 0 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,10 @@ func NewExocoreApp(
app.AssetsKeeper, // assets for vote power
)

(&app.OperatorKeeper).SetHooks(
app.StakingKeeper.OperatorHooks(),
)

(&app.EpochsKeeper).SetHooks(
app.StakingKeeper.EpochsHooks(),
)
Expand Down
3 changes: 3 additions & 0 deletions app/ethtest_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"time"

"cosmossdk.io/math"
"cosmossdk.io/simapp"
"github.com/cosmos/cosmos-sdk/baseapp"
"github.com/cosmos/cosmos-sdk/codec"
Expand Down Expand Up @@ -252,6 +253,8 @@ func genesisStateWithValSet(codec codec.Codec, genesisState simapp.GenesisState,
Power: 1,
},
},
[]dogfoodtypes.EpochToOperatorAddrs{}, []dogfoodtypes.EpochToConsensusAddrs{},
[]dogfoodtypes.EpochToUndelegationRecordKeys{}, math.NewInt(1),
)
genesisState[dogfoodtypes.ModuleName] = codec.MustMarshalJSON(dogfoodGenesis)

Expand Down
9 changes: 4 additions & 5 deletions app/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,13 @@ func (app *ExocoreApp) ExportAppStateAndValidators(
return servertypes.ExportedApp{}, err
}

validators, err := app.StakingKeeper.WriteValidators(ctx)
if err != nil {
return servertypes.ExportedApp{}, err
}
// the x/dogfood validator set is exported in its `val_set` key, and hence,
// does not need to be part of the app export. in other words, we do not
// duplicate the exported validator set. besides, as far as i can tell, the
// SDK does not use the Validators member of the ExportedApp struct.

return servertypes.ExportedApp{
AppState: appState,
Validators: validators,
Height: height,
ConsensusParams: app.BaseApp.GetConsensusParams(ctx),
}, nil
Expand Down
6 changes: 6 additions & 0 deletions app/test_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"

"cosmossdk.io/math"
"cosmossdk.io/simapp"
dbm "github.com/cometbft/cometbft-db"
abci "github.com/cometbft/cometbft/abci/types"
Expand Down Expand Up @@ -267,13 +268,18 @@ func GenesisStateWithValSet(app *ExocoreApp, genesisState simapp.GenesisState,
delegationGenesis := delegationtypes.NewGenesis(delegationsByStaker)
genesisState[delegationtypes.ModuleName] = app.AppCodec().MustMarshalJSON(delegationGenesis)

// create a dogfood genesis with just the validator set, that is, the bare
// minimum valid genesis required to start a chain.
dogfoodGenesis := dogfoodtypes.NewGenesis(
dogfoodtypes.DefaultParams(), []dogfoodtypes.GenesisValidator{
{
PublicKey: consensusKeyRecords[0].Chains[0].ConsensusKey,
Power: 1,
},
},
[]dogfoodtypes.EpochToOperatorAddrs{}, []dogfoodtypes.EpochToConsensusAddrs{},
[]dogfoodtypes.EpochToUndelegationRecordKeys{},
math.NewInt(1), // total vote power
)
genesisState[dogfoodtypes.ModuleName] = app.AppCodec().MustMarshalJSON(dogfoodGenesis)

Expand Down
6 changes: 6 additions & 0 deletions cmd/exocored/testnet.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"sort"
"strings"

"cosmossdk.io/math"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"

Expand Down Expand Up @@ -459,6 +460,7 @@ func getTestExocoreGenesis(
Power: power,
})
}
totalPower := math.NewInt(power * int64(len(operatorAddrs)))
return assetstypes.NewGenesis(
assetstypes.DefaultParams(),
clientChains, []assetstypes.StakingAssetInfo{
Expand All @@ -482,6 +484,10 @@ func getTestExocoreGenesis(
[]string{assetID},
),
validators,
[]dogfoodtypes.EpochToOperatorAddrs{},
[]dogfoodtypes.EpochToConsensusAddrs{},
[]dogfoodtypes.EpochToUndelegationRecordKeys{},
totalPower,
)
}

Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ require (
github.com/smartystreets/goconvey v1.6.4
github.com/spf13/cast v1.5.1
github.com/spf13/cobra v1.7.0
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.16.0
github.com/stretchr/testify v1.8.4
go.opencensus.io v0.24.0
Expand All @@ -40,6 +41,7 @@ require (
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2
google.golang.org/genproto/googleapis/api v0.0.0-20231212172506-995d672761c0
google.golang.org/grpc v1.60.1
google.golang.org/protobuf v1.33.0
gopkg.in/yaml.v2 v2.4.0
sigs.k8s.io/yaml v1.3.0
)
Expand Down Expand Up @@ -201,7 +203,6 @@ require (
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d // indirect
github.com/spf13/afero v1.10.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/status-im/keycard-go v0.2.0 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
github.com/supranational/blst v0.3.11 // indirect
Expand Down Expand Up @@ -232,7 +233,6 @@ require (
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto v0.0.0-20240102182953-50ed04b92917 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
Expand Down
5 changes: 3 additions & 2 deletions local_node.sh
Original file line number Diff line number Diff line change
Expand Up @@ -115,10 +115,11 @@ if [[ $overwrite == "y" || $overwrite == "Y" ]]; then
jq '.app_state["delegation"]["delegations"][0]["delegations"][0]["per_operator_amounts"][0]["value"]["amount"]="5000"' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS"

# x/dogfood
jq '.app_state["dogfood"]["initial_val_set"][0]["public_key"]="0xf0f6919e522c5b97db2c8255bff743f9dfddd7ad9fc37cb0c1670b480d0f9914"' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS"
jq '.app_state["dogfood"]["initial_val_set"][0]["power"]="5000"' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS"
jq '.app_state["dogfood"]["val_set"][0]["public_key"]="0xf0f6919e522c5b97db2c8255bff743f9dfddd7ad9fc37cb0c1670b480d0f9914"' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS"
jq '.app_state["dogfood"]["val_set"][0]["power"]="5000"' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS"
# change the epoch to an hour when starting a local node, which facilitates the testing.
jq '.app_state["dogfood"]["params"]["epoch_identifier"]="hour"' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS"
jq '.app_state["dogfood"]["last_total_power"]="5000"' "$GENESIS" >"$TMP_GENESIS" && mv "$TMP_GENESIS" "$GENESIS"

# x/epochs
HOUR_EPOCH='{
Expand Down
93 changes: 89 additions & 4 deletions proto/exocore/dogfood/v1/genesis.proto
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,113 @@ syntax = "proto3";

package exocore.dogfood.v1;

import "amino/amino.proto";
import "gogoproto/gogo.proto";

import "exocore/dogfood/v1/params.proto";

option go_package = "github.com/ExocoreNetwork/exocore/x/dogfood/types";

// GenesisState defines the dogfood module's genesis state.
// GenesisState defines the dogfood module's genesis state. Note that, as always,
// `genesis` is a misnomer. Ideally, this state can be exported at any point in
// time (or height), and reimported elsewhere where it will be the new genesis
// potentially at a non-zero height. In other words, it is the entire, current,
// state of the module.
message GenesisState {
// params refers to the parameters of the module.
Params params = 1 [(gogoproto.nullable) = false];

// initial_val_set is the initial validator set.
repeated GenesisValidator initial_val_set = 2
// val_set is the initial validator set. it onyl represents the active
// validators.
repeated GenesisValidator val_set = 2
[ (gogoproto.nullable) = false ];

// opt_out_expiries is a list of (future) epochs at the end of which the
// corresponding operators' opt-out will expire. we store this, as well as its reverse
// lookup.
repeated EpochToOperatorAddrs opt_out_expiries = 3
[ (gogoproto.nullable) = false ];

// epochs_consensus_addrs is a list of epochs at the end of which the corresponding
// consensus addresses should be pruned from the operator module.
repeated EpochToConsensusAddrs consensus_addrs_to_prune = 4
[ (gogoproto.nullable) = false ];

// undelegation_maturities is a list of epochs at the end of which the corresponding
// undelegations will mature. we store its reverse lookup as well.
repeated EpochToUndelegationRecordKeys undelegation_maturities = 5
[ (gogoproto.nullable) = false ];

// data against HistoricalInfoBytePrefix is not made available in the module
// state for import / export. this is in line with Cosmos SDK.

// the data indexed by the pending keys is created within the epochs hooks
// which happen in the BeginBlocker. it is applied during the EndBlocker and
// then immediately cleared.
// remember that data can be exported from a node that is stopped.
// a node can be stopped only if it has committed a block. if a full
// block is committed, data that is saved to state during BeginBlock
// and cleared at EndBlock will not be available. hence, we don't need
// to make data for any of the `pending` keys available here.

// last_total_power tracks the total voting power as of the last validator set
// update. such an update is most likely to be at the end of the last epoch (or the
// beginning of this one, to be more precise) and less likely to be at other blocks,
// since the validator set can otherwise only change as a result of slashing events.
bytes last_total_power = 6 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int",
(gogoproto.nullable) = false,
(amino.dont_omitempty) = true
];

// validator_updates is a list of validator updates applied at the end of the previous
// block. it is then cleared at the end of the next block, and hence, is available
// for other modules to access during that block. however, for a new chain, it does
// not make sense for it to exist, since all of the validators should be considered
// as an update. this behaviour is the same as the Cosmos SDK.
}

// GenesisValidator defines a genesis validator. It is a helper struct
// used for serializing the genesis state.
// used for serializing the genesis state. The only reason it is a different
// structure is to support importing hex public keys from Solidity.
// TODO: consider this set up when resolving issue 73 about storage
// optimization between dogfood and operator modules.
// https://github.com/ExocoreNetwork/exocore/issues/73
message GenesisValidator {
// public_key is the consensus public key of the validator. It should
// be exactly 32 bytes, but this is not enforced in protobuf.
string public_key = 1;
// power is the voting power of the validator.
int64 power = 2;
}

// EpochToOperatorAddress is used to store a mapping from epoch to a list of
// operator account addresses.
message EpochToOperatorAddrs {
// epoch is the epoch in question.
int64 epoch = 1;
// operator_acc_addrs is the list of account addresses to expire at this epoch.
// It is of type string for human readability of the genesis file.
repeated string operator_acc_addrs = 2;
}

// EpochToConsensusAddrs is used to store a mapping from the epoch to a list of
// consensus addresses.
message EpochToConsensusAddrs {
// epoch is the epoch in question.
int64 epoch = 1;
// cons_addrs is the list of consensus addresses to prune at this epoch.
// It is of type string for human readability of the genesis file.
repeated string cons_addrs = 2;
}

// EpochToUndelegationRecordKeys is used to store a mapping from an epoch to a list of
// undelegations which mature at that epoch.
message EpochToUndelegationRecordKeys {
// epoch is the epoch in question.
int64 epoch = 1;
// undelegation_record_keys is the list of undelegations (defined by the record key)
// to expire at this epoch.
// It is of type string for human readability of the genesis file.
repeated string undelegation_record_keys = 2;
}
8 changes: 8 additions & 0 deletions proto/exocore/operator/v1/query.proto
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ message QueryOperatorConsKeyRequest {
message QueryOperatorConsKeyResponse {
// public_key is the consensus public key of the operator.
tendermint.crypto.PublicKey public_key = 1 [ (gogoproto.nullable) = false ];
// opting_out is a flag to indicate if the operator is opting out of consensus.
bool opting_out = 2;
}

// QueryOperatorConsAddressRequest is the request to obtain the consensus address of the operator
Expand All @@ -63,6 +65,8 @@ message QueryOperatorConsAddressResponse {
// cons_addr is the consensus address corresponding to the consensus public key
// currently in use by the operator.
string cons_addr = 1;
// opting_out is a flag to indicate if the operator is opting out of consensus.
bool opting_out = 2;
}

// QueryAllOperatorConsKeysByChainIDRequest is the request to obtain all operator addresses
Expand Down Expand Up @@ -92,6 +96,8 @@ message OperatorConsKeyPair {
string operator_acc_addr = 1;
// public_key is the consensus public key of the operator.
tendermint.crypto.PublicKey public_key = 2;
// opting_out is a flag to indicate if the operator is opting out of consensus.
bool opting_out = 3;
}

// QueryAllOperatorConsAddrsByChainIDRequest is the request to obtain all operator addresses
Expand Down Expand Up @@ -121,6 +127,8 @@ message OperatorConsAddrPair {
// cons_addr is the consensus address corresponding to the consensus public key
// currently in use by the operator.
string cons_addr = 2;
// opting_out is a flag to indicate if the operator is opting out of consensus.
bool opting_out = 3;
}

// Query defines the gRPC querier service.
Expand Down
26 changes: 6 additions & 20 deletions proto/exocore/operator/v1/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,9 @@ message OptIntoAVSReq {
[(cosmos_proto.scalar) = "cosmos.AddressString"];
// avs_address is the address of the AVS - either an 0x address or a chainID.
string avs_address = 2;
// optional parameter to provide the consensus key or the BLS key, depending
// on the AVS. we still have to design this fully.
string public_key = 3;
}

// OptIntoAVSResponse is the response to a opt into an AVS request.
Expand Down Expand Up @@ -187,33 +190,16 @@ message SetConsKeyReq {
// SetConsKeyResponse is the response to SetConsKeyReq.
message SetConsKeyResponse {}

// InitConsKeyRemovalReq is the request for an operator to stop validating on a chain. It
// allows the operator to remove their consensus key from the specified chain. It must be
// followed by a separate call to OptOutOfAVS to remove the operator from the AVS.
message InitConsKeyRemovalReq {
option (cosmos.msg.v1.signer) = "address";
option (amino.name) = "cosmos-sdk/InitConsKeyRemovalReq";
option (gogoproto.equal) = false;
option (gogoproto.goproto_getters) = false;
// address is the operator address
string address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"];
// chain_id is the identifier for the chain that wants to opt out.
string chain_id = 2 [(gogoproto.customname) = "ChainID"];
}

// InitConsKeyRemovalResponse defines the InitConsKeyRemovalReq response.
message InitConsKeyRemovalResponse {}

// Msg defines the operator Msg service.
service Msg {
option (cosmos.msg.v1.service) = true;
// RegisterOperator registers a new operator.
rpc RegisterOperator(RegisterOperatorReq) returns (RegisterOperatorResponse);

// SetConsKey sets the operator's consensus key for a chain.
// SetConsKey sets the operator's consensus key for a chain. To do this, the operator
// must have previously opted into the chain.
// TODO; rationalize this with non-chain AVSs wherein other keys can be set.
rpc SetConsKey(SetConsKeyReq) returns (SetConsKeyResponse) {};
// InitConsKeyRemoval removes the operator's consensus key for a chain.
rpc InitConsKeyRemoval(InitConsKeyRemovalReq) returns (InitConsKeyRemovalResponse) {};

// OptIntoAVS opts an operator into an AVS.
rpc OptIntoAVS(OptIntoAVSReq) returns (OptIntoAVSResponse);
Expand Down
6 changes: 6 additions & 0 deletions testutil/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,8 @@ func (suite *BaseTestSuite) SetupWithGenesisValSet(genAccs []authtypes.GenesisAc
delegationGenesis := delegationtypes.NewGenesis(delegationsByStaker)
genesisState[delegationtypes.ModuleName] = app.AppCodec().MustMarshalJSON(delegationGenesis)

// create a dogfood genesis with just the validator set, that is, the bare
// minimum valid genesis required to start a chain.
dogfoodGenesis := dogfoodtypes.NewGenesis(
dogfoodtypes.DefaultParams(), []dogfoodtypes.GenesisValidator{
{
Expand All @@ -240,6 +242,10 @@ func (suite *BaseTestSuite) SetupWithGenesisValSet(genAccs []authtypes.GenesisAc
Power: 1,
},
},
[]dogfoodtypes.EpochToOperatorAddrs{},
[]dogfoodtypes.EpochToConsensusAddrs{},
[]dogfoodtypes.EpochToUndelegationRecordKeys{},
math.NewInt(2), // must match total vote power
)
genesisState[dogfoodtypes.ModuleName] = app.AppCodec().MustMarshalJSON(dogfoodGenesis)

Expand Down
3 changes: 2 additions & 1 deletion x/delegation/keeper/genesis.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package keeper

import (
errorsmod "cosmossdk.io/errors"
assetstype "github.com/ExocoreNetwork/exocore/x/assets/types"
delegationtype "github.com/ExocoreNetwork/exocore/x/delegation/types"
abci "github.com/cometbft/cometbft/abci/types"
Expand Down Expand Up @@ -44,7 +45,7 @@ func (k Keeper) InitGenesis(
// they are the LzNonce and TxHash
}
if err := k.delegateTo(ctx, delegationParams, false); err != nil {
panic(err)
panic(errorsmod.Wrap(err, "failed to delegate to operator"))
}
}
}
Expand Down
Loading

0 comments on commit 002d277

Please sign in to comment.