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

feat(delegation): load genesis state #26

Merged
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
3 changes: 1 addition & 2 deletions precompiles/delegation/delegation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"github.com/ExocoreNetwork/exocore/precompiles/delegation"
"github.com/ExocoreNetwork/exocore/x/assets/types"
assetstype "github.com/ExocoreNetwork/exocore/x/assets/types"
keeper2 "github.com/ExocoreNetwork/exocore/x/delegation/keeper"
delegationtype "github.com/ExocoreNetwork/exocore/x/delegation/types"
"github.com/ExocoreNetwork/exocore/x/deposit/keeper"
sdk "github.com/cosmos/cosmos-sdk/types"
Expand Down Expand Up @@ -308,7 +307,7 @@ func (s *DelegationPrecompileSuite) TestRunUnDelegateFromThroughClientChain() {

delegateAsset := func(staker []byte, delegateAmount sdkmath.Int) {
// deposit asset for delegation test
delegateToParams := &keeper2.DelegationOrUndelegationParams{
delegateToParams := &delegationtype.DelegationOrUndelegationParams{
ClientChainLzID: 101,
Action: types.DelegateTo,
StakerAddress: staker,
Expand Down
6 changes: 3 additions & 3 deletions precompiles/delegation/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@ import (

exocmn "github.com/ExocoreNetwork/exocore/precompiles/common"
"github.com/ExocoreNetwork/exocore/x/assets/types"
keeper2 "github.com/ExocoreNetwork/exocore/x/delegation/keeper"
delegationtypes "github.com/ExocoreNetwork/exocore/x/delegation/types"
sdk "github.com/cosmos/cosmos-sdk/types"
cmn "github.com/evmos/evmos/v14/precompiles/common"
)

func (p Precompile) GetDelegationParamsFromInputs(ctx sdk.Context, args []interface{}) (*keeper2.DelegationOrUndelegationParams, error) {
func (p Precompile) GetDelegationParamsFromInputs(ctx sdk.Context, args []interface{}) (*delegationtypes.DelegationOrUndelegationParams, error) {
if len(args) != 6 {
return nil, fmt.Errorf(cmn.ErrInvalidNumberOfArgs, 6, len(args))
}
delegationParams := &keeper2.DelegationOrUndelegationParams{}
delegationParams := &delegationtypes.DelegationOrUndelegationParams{}
clientChainLzID, ok := args[0].(uint16)
if !ok {
return nil, fmt.Errorf(exocmn.ErrContractInputParaOrType, 0, reflect.TypeOf(args[0]), clientChainLzID)
Expand Down
27 changes: 27 additions & 0 deletions proto/exocore/delegation/v1/genesis.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
syntax = "proto3";

package exocore.delegation.v1;

import "gogoproto/gogo.proto";

import "exocore/delegation/v1/tx.proto";

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

// GenesisState defines the delegation module's state. It needs to encompass
// all of the state that is required to start the chain from the genesis
// or in the event of a restart. At this point, it is only built with
// the former in mind. There are no params in this module.
message GenesisState {
// delegations is a list of all delegations in the system.
repeated DelegationsByStaker delegations = 1 [(gogoproto.nullable) = false];
}

// DelegationsByStaker is a list of delegations for a single staker.
message DelegationsByStaker {
// staker_id is the staker's account address + _ + l0 chain id (hex).``
string staker_id = 1 [(gogoproto.customname) = "StakerID"];
// delegations is the list of delegations for the staker, indexed by the
// asset_id.
repeated DelegatedSingleAssetInfo delegations = 2 [(gogoproto.nullable) = false];
}
22 changes: 13 additions & 9 deletions proto/exocore/delegation/v1/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,22 @@
}

// DelegatedSingleAssetInfo is a single asset info that is delegated by a staker.
message DelegatedSingleAssetInfo {

Check failure on line 24 in proto/exocore/delegation/v1/tx.proto

View workflow job for this annotation

GitHub Actions / break-check

Previously present field "2" with name "total_delegated_amount" on message "DelegatedSingleAssetInfo" was deleted.

Check failure on line 24 in proto/exocore/delegation/v1/tx.proto

View workflow job for this annotation

GitHub Actions / break-check

Previously present message "DelegatedSingleAssetInfo.PerOperatorAmountsEntry" was deleted from file.
// asset_id is the asset id.
string asset_id = 1 [(gogoproto.customname) = "AssetID"];
// total_delegated_amount is the total amount of the asset delegated.
string total_delegated_amount = 2
[
(cosmos_proto.scalar) = "cosmos.Int",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int",
(gogoproto.nullable) = false
];
// per_operator_amounts is the amount of the asset delegated to each operator.
map<string, ValueField> per_operator_amounts = 3;
// since Cosmos uses an IAVL+ tree where the order of insertion affects the state root (even
// if the items are unrelated), and deserializing a protobuf map into Golang does not
// guarantee order, we cannot use a map here. Instead, we use a repeated field of key-value
// pairs.
repeated KeyValue per_operator_amounts = 3 [(gogoproto.nullable) = false];

Check failure on line 31 in proto/exocore/delegation/v1/tx.proto

View workflow job for this annotation

GitHub Actions / break-check

Field "3" on message "DelegatedSingleAssetInfo" changed type from "exocore.delegation.v1.DelegatedSingleAssetInfo.PerOperatorAmountsEntry" to "exocore.delegation.v1.KeyValue".
}

// KeyValue is a key-value pair. It is a helper struct to represent a map in Protobuf.
message KeyValue {
// key is the key of the key-value pair.
string key = 1;
// value is the value of the key-value pair.
ValueField value = 2;
}

// DelegationApproveInfo is the delegation approve info.
Expand Down
50 changes: 26 additions & 24 deletions x/delegation/keeper/cross_chain_tx_process.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,8 @@ import (
assetstype "github.com/ExocoreNetwork/exocore/x/assets/types"
delegationtype "github.com/ExocoreNetwork/exocore/x/delegation/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/ethereum/go-ethereum/common"
)

type DelegationOrUndelegationParams struct {
ClientChainLzID uint64
Action assetstype.CrossChainOpType
AssetsAddress []byte
OperatorAddress sdk.AccAddress
StakerAddress []byte
OpAmount sdkmath.Int
LzNonce uint64
TxHash common.Hash
// todo: The operator approved signature might be needed here in future
}

// The event hook process has been deprecated, now we use precompile contract to trigger the calls.
// solidity encode: bytes memory actionArgs = abi.encodePacked(token, operator, msg.sender, amount);
// _sendInterchainMsg(Action.DEPOSIT, actionArgs);
Expand Down Expand Up @@ -108,15 +95,28 @@ type DelegationOrUndelegationParams struct {
}, nil
}*/

// DelegateTo : It doesn't need to check the active status of the operator in middlewares when delegating assets to the operator. This is because it adds assets to the operator's amount. But it needs to check if operator has been slashed or frozen.
func (k *Keeper) DelegateTo(ctx sdk.Context, params *DelegationOrUndelegationParams) error {
// DelegateTo : It doesn't need to check the active status of the operator in middlewares when
// delegating assets to the operator. This is because it adds assets to the operator's amount.
// But it needs to check if operator has been slashed or frozen.
func (k Keeper) DelegateTo(ctx sdk.Context, params *delegationtype.DelegationOrUndelegationParams) error {
return k.delegateTo(ctx, params, true)
}

// delegateTo is the internal private version of DelegateTo. if the notGenesis parameter is
// false, the operator keeper and the delegation hooks are not called.
func (k *Keeper) delegateTo(
ctx sdk.Context,
params *delegationtype.DelegationOrUndelegationParams,
notGenesis bool,
) error {
// check if the delegatedTo address is an operator
if !k.operatorKeeper.IsOperator(ctx, params.OperatorAddress) {
return errorsmod.Wrap(delegationtype.ErrOperatorNotExist, fmt.Sprintf("input operatorAddr is:%s", params.OperatorAddress))
}

// check if the operator has been slashed or frozen
if k.slashKeeper.IsOperatorFrozen(ctx, params.OperatorAddress) {
// skip the check if not genesis (or chain restart)
if notGenesis && k.slashKeeper.IsOperatorFrozen(ctx, params.OperatorAddress) {
return delegationtype.ErrOperatorIsFrozen
}

Expand Down Expand Up @@ -166,21 +166,23 @@ func (k *Keeper) DelegateTo(ctx sdk.Context, params *DelegationOrUndelegationPar
if err != nil {
return err
}
// call operator module to bond the increased assets to the opted-in AVS
err = k.operatorKeeper.UpdateOptedInAssetsState(ctx, stakerID, assetID, params.OperatorAddress.String(), params.OpAmount)
if err != nil {
return err
}

// call the hooks registered by the other modules
k.Hooks().AfterDelegation(ctx, params.OperatorAddress)
if notGenesis {
// call operator module to bond the increased assets to the opted-in AVS
err = k.operatorKeeper.UpdateOptedInAssetsState(ctx, stakerID, assetID, params.OperatorAddress.String(), params.OpAmount)
if err != nil {
return err
}
// call the hooks registered by the other modules
k.Hooks().AfterDelegation(ctx, params.OperatorAddress)
}
return nil
}

// UndelegateFrom The undelegation needs to consider whether the operator's opted-in assets can exit from the AVS.
// Because only after the operator has served the AVS can the staking asset be undelegated.
// So we use two steps to handle the undelegation. Fist,record the undelegation request and the corresponding exit time which needs to be obtained from the operator opt-in module. Then,we handle the record when the exit time has expired.
func (k *Keeper) UndelegateFrom(ctx sdk.Context, params *DelegationOrUndelegationParams) error {
func (k *Keeper) UndelegateFrom(ctx sdk.Context, params *delegationtype.DelegationOrUndelegationParams) error {
// check if the UndelegatedFrom address is an operator
if !k.operatorKeeper.IsOperator(ctx, params.OperatorAddress) {
return delegationtype.ErrOperatorNotExist
Expand Down
6 changes: 3 additions & 3 deletions x/delegation/keeper/delegation_op_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func (suite *DelegationTestSuite) TestDelegateTo() {

opAccAddr, err := sdk.AccAddressFromBech32("exo13h6xg79g82e2g2vhjwg7j4r2z2hlncelwutkjr")
suite.NoError(err)
delegationParams := &keeper2.DelegationOrUndelegationParams{
delegationParams := &delegationtype.DelegationOrUndelegationParams{
ClientChainLzID: clientChainLzID,
Action: types.DelegateTo,
AssetsAddress: usdtAddress[:],
Expand Down Expand Up @@ -106,7 +106,7 @@ func (suite *DelegationTestSuite) TestUndelegateFrom() {

opAccAddr, err := sdk.AccAddressFromBech32("exo13h6xg79g82e2g2vhjwg7j4r2z2hlncelwutkjr")
suite.NoError(err)
delegationEvent := &keeper2.DelegationOrUndelegationParams{
delegationEvent := &delegationtype.DelegationOrUndelegationParams{
ClientChainLzID: clientChainLzID,
Action: types.DelegateTo,
AssetsAddress: usdtAddress[:],
Expand Down Expand Up @@ -205,7 +205,7 @@ func (suite *DelegationTestSuite) TestCompleteUndelegation() {

opAccAddr, err := sdk.AccAddressFromBech32("exo13h6xg79g82e2g2vhjwg7j4r2z2hlncelwutkjr")
suite.NoError(err)
delegationEvent := &keeper2.DelegationOrUndelegationParams{
delegationEvent := &delegationtype.DelegationOrUndelegationParams{
ClientChainLzID: clientChainLzID,
Action: types.DelegateTo,
AssetsAddress: usdtAddress[:],
Expand Down
60 changes: 60 additions & 0 deletions x/delegation/keeper/genesis.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package keeper

import (
assetstype "github.com/ExocoreNetwork/exocore/x/assets/types"
delegationtype "github.com/ExocoreNetwork/exocore/x/delegation/types"
abci "github.com/cometbft/cometbft/abci/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/ethereum/go-ethereum/common"
)

// InitGenesis initializes the module's state from a provided genesis state.
// Since this action typically occurs on chain starts, this function is allowed to panic.
func (k Keeper) InitGenesis(
ctx sdk.Context,
gs delegationtype.GenesisState,
) []abci.ValidatorUpdate {
// TODO(mm): is it possible to parallelize these without using goroutines?
for _, level1 := range gs.Delegations {
stakerID := level1.StakerID
// #nosec G703 // already validated
stakerAddress, lzID, _ := assetstype.ParseID(stakerID)
// we have checked IsHexAddress already
stakerAddressBytes := common.HexToAddress(stakerAddress)
Copy link
Contributor

Choose a reason for hiding this comment

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

If the genesis state contains a lot of delegate information, the current nested loop may affect the performance of the initialization. Consider whether it is possible to improve efficiency by optimizing data structures or parallel processing.

  1. Consider distributing processing operations on each delegate to different goroutines to execute in parallel. Ensure data consistency and thread safety when processing in parallel.Pay attention to the number of goroutines to avoid resource contention caused by creating too many goroutines
  2. If the delegateTo function supports batch processing, that is, processing multiple delegates at a time, we can collect a batch of delegates in each goroutine and call delegateTo to process them. This reduces the number of function calls and increases efficiency.

Copy link
Contributor Author

@MaxMustermann2 MaxMustermann2 Apr 9, 2024

Choose a reason for hiding this comment

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

1. It is not trivial to build something like this because the order of goroutine execution is not guaranteed. This can result in inconsistency across nodes in calculating the state root, since it depends on the order of insertion.
2. Within delegateTo, there are 6 different function calls, only 4 of which can be batched either on the staker + asset level or on the operator + asset level. Others cannot be batched, hence, batching is not necessarily possible.

I have added a TODO for this.

for _, level2 := range level1.Delegations {
assetID := level2.AssetID
// #nosec G703 // already validated
assetAddress, _, _ := assetstype.ParseID(assetID)
// we have checked IsHexAddress already
assetAddressBytes := common.HexToAddress(assetAddress)
for _, level3 := range level2.PerOperatorAmounts {
operator := level3.Key
wrappedAmount := level3.Value
amount := wrappedAmount.Amount
// #nosec G703 // already validated
accAddress, _ := sdk.AccAddressFromBech32(operator)
delegationParams := &delegationtype.DelegationOrUndelegationParams{
ClientChainLzID: lzID,
Action: assetstype.DelegateTo,
AssetsAddress: assetAddressBytes.Bytes(),
OperatorAddress: accAddress,
StakerAddress: stakerAddressBytes.Bytes(),
OpAmount: amount,
// the uninitialized members are not used in this context
// they are the LzNonce and TxHash
}
if err := k.delegateTo(ctx, delegationParams, false); err != nil {
panic(err)
}
}
}
}
return []abci.ValidatorUpdate{}
}

// ExportGenesis returns the module's exported genesis
func (Keeper) ExportGenesis(sdk.Context) *delegationtype.GenesisState {
genesis := delegationtype.DefaultGenesis()
// TODO
return genesis
}
50 changes: 49 additions & 1 deletion x/delegation/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package delegation

import (
"context"
"encoding/json"
"fmt"

"github.com/ExocoreNetwork/exocore/x/delegation/client/cli"
"github.com/ExocoreNetwork/exocore/x/delegation/keeper"
Expand Down Expand Up @@ -38,7 +40,10 @@ func (b AppModuleBasic) RegisterInterfaces(registry codectypes.InterfaceRegistry
delegationtype.RegisterInterfaces(registry)
}

func (b AppModuleBasic) RegisterGRPCGatewayRoutes(c client.Context, serveMux *runtime.ServeMux) {
func (b AppModuleBasic) RegisterGRPCGatewayRoutes(
c client.Context,
serveMux *runtime.ServeMux,
) {
if err := delegationtype.RegisterQueryHandlerClient(context.Background(), serveMux, delegationtype.NewQueryClient(c)); err != nil {
panic(err)
}
Expand Down Expand Up @@ -92,3 +97,46 @@ func (am AppModule) EndBlock(ctx sdk.Context, req abci.RequestEndBlock) []abci.V
am.keeper.EndBlock(ctx, req)
return []abci.ValidatorUpdate{}
}

// DefaultGenesis returns a default GenesisState for the module, marshaled to json.RawMessage.
// The default GenesisState need to be defined by the module developer and is primarily used for
// testing
func (AppModuleBasic) DefaultGenesis(cdc codec.JSONCodec) json.RawMessage {
return cdc.MustMarshalJSON(delegationtype.DefaultGenesis())
}

// ValidateGenesis used to validate the GenesisState, given in its json.RawMessage form
func (AppModuleBasic) ValidateGenesis(
cdc codec.JSONCodec,
_ client.TxEncodingConfig,
bz json.RawMessage,
) error {
var genState delegationtype.GenesisState
if err := cdc.UnmarshalJSON(bz, &genState); err != nil {
return fmt.Errorf(
"failed to unmarshal %s genesis state: %w",
delegationtype.ModuleName,
err,
)
}
return genState.Validate()
}

// InitGenesis performs the module's genesis initialization. It returns no validator updates.
func (am AppModule) InitGenesis(
ctx sdk.Context,
cdc codec.JSONCodec,
gs json.RawMessage,
) []abci.ValidatorUpdate {
var genState delegationtype.GenesisState
// Initialize global index to index in genesis state
cdc.MustUnmarshalJSON(gs, &genState)

return am.keeper.InitGenesis(ctx, genState)
}

// ExportGenesis returns the module's exported genesis state as raw JSON bytes.
func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.RawMessage {
genState := am.keeper.ExportGenesis(ctx)
return cdc.MustMarshalJSON(genState)
}
20 changes: 20 additions & 0 deletions x/delegation/types/cross_chain_params.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package types

import (
sdkmath "cosmossdk.io/math"
assetstype "github.com/ExocoreNetwork/exocore/x/assets/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/ethereum/go-ethereum/common"
)

type DelegationOrUndelegationParams struct {
ClientChainLzID uint64
Action assetstype.CrossChainOpType
AssetsAddress []byte
OperatorAddress sdk.AccAddress
StakerAddress []byte
OpAmount sdkmath.Int
LzNonce uint64
TxHash common.Hash
// todo: The operator approved signature might be needed here in future
}
Loading
Loading