-
Notifications
You must be signed in to change notification settings - Fork 887
feat(precompiles): expose module Msg rpcs as precompile methods #3825
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
base: main
Are you sure you want to change the base?
Changes from all commits
a41813c
77f6fa5
99f081e
a849a0a
8a81e7c
350b1da
68d3043
da9e73c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ package slashing | |
|
|
||
| import ( | ||
| "embed" | ||
| "errors" | ||
| "fmt" | ||
| "math/big" | ||
|
|
||
|
|
@@ -14,9 +15,11 @@ import ( | |
| sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" | ||
| "github.com/sei-protocol/sei-chain/sei-cosmos/types/query" | ||
| slashingtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/slashing/types" | ||
| evmtypes "github.com/sei-protocol/sei-chain/x/evm/types" | ||
| ) | ||
|
|
||
| const ( | ||
| UnjailMethod = "unjail" | ||
| ParamsMethod = "params" | ||
| SigningInfoMethod = "signingInfo" | ||
| SigningInfosMethod = "signingInfos" | ||
|
|
@@ -32,9 +35,11 @@ const ( | |
| var f embed.FS | ||
|
|
||
| type PrecompileExecutor struct { | ||
| evmKeeper utils.EVMKeeper | ||
| slashingQuerier utils.SlashingQuerier | ||
| evmKeeper utils.EVMKeeper | ||
| slashingMsgServer utils.SlashingMsgServer | ||
| slashingQuerier utils.SlashingQuerier | ||
|
|
||
| UnjailID []byte | ||
| ParamsID []byte | ||
| SigningInfoID []byte | ||
| SigningInfosID []byte | ||
|
|
@@ -44,12 +49,15 @@ func NewPrecompile(keepers utils.Keepers) (*pcommon.DynamicGasPrecompile, error) | |
| newAbi := pcommon.MustGetABI(f, "abi.json") | ||
|
|
||
| p := &PrecompileExecutor{ | ||
| evmKeeper: keepers.EVMK(), | ||
| slashingQuerier: keepers.SlashingQ(), | ||
| evmKeeper: keepers.EVMK(), | ||
| slashingMsgServer: keepers.SlashingMS(), | ||
| slashingQuerier: keepers.SlashingQ(), | ||
| } | ||
|
|
||
| for name, m := range newAbi.Methods { | ||
| switch name { | ||
| case UnjailMethod: | ||
| p.UnjailID = m.ID | ||
| case ParamsMethod: | ||
| p.ParamsID = m.ID | ||
| case SigningInfoMethod: | ||
|
|
@@ -89,9 +97,55 @@ func (p PrecompileExecutor) Execute(ctx sdk.Context, method *abi.Method, caller | |
| case SigningInfosMethod: | ||
| return p.signingInfos(ctx, method, args, value) | ||
| } | ||
|
|
||
| // Transaction methods act on behalf of the caller, so they must not be | ||
| // reachable through delegatecall (which would let a contract act on | ||
| // behalf of its own caller) or staticcall. | ||
| if ctx.EVMPrecompileCalledFromDelegateCall() { | ||
| return nil, 0, errors.New("cannot delegatecall slashing") | ||
| } | ||
| if readOnly { | ||
| return nil, 0, errors.New("cannot call slashing precompile from staticcall") | ||
| } | ||
| switch method.Name { | ||
| case UnjailMethod: | ||
| return p.unjail(ctx, method, caller, args, value) | ||
| } | ||
| return | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [nit] This naked |
||
| } | ||
|
|
||
| // unjail unjails the validator whose operator address is the caller's | ||
| // associated Sei address. | ||
| func (p PrecompileExecutor) unjail(ctx sdk.Context, method *abi.Method, caller common.Address, args []interface{}, value *big.Int) ([]byte, uint64, error) { | ||
| if err := pcommon.ValidateNonPayable(value); err != nil { | ||
| return nil, 0, err | ||
| } | ||
|
|
||
| if err := pcommon.ValidateArgsLength(args, 0); err != nil { | ||
| return nil, 0, err | ||
| } | ||
|
|
||
| seiAddr, found := p.evmKeeper.GetSeiAddress(ctx, caller) | ||
| if !found { | ||
| return nil, 0, evmtypes.NewAssociationMissingErr(caller.Hex()) | ||
| } | ||
|
|
||
| msg := slashingtypes.NewMsgUnjail(sdk.ValAddress(seiAddr)) | ||
| if err := msg.ValidateBasic(); err != nil { | ||
| return nil, 0, err | ||
| } | ||
|
|
||
| if _, err := p.slashingMsgServer.Unjail(sdk.WrapSDKContext(ctx), msg); err != nil { | ||
| return nil, 0, err | ||
| } | ||
|
|
||
| bz, err := method.Outputs.Pack(true) | ||
| if err != nil { | ||
| return nil, 0, err | ||
| } | ||
| return bz, pcommon.GetRemainingGas(ctx, p.evmKeeper), nil | ||
| } | ||
|
|
||
| type SlashingParams struct { | ||
| SignedBlocksWindow int64 | ||
| MinSignedPerWindow string | ||
|
|
@@ -223,6 +277,13 @@ func (p PrecompileExecutor) EVMKeeper() utils.EVMKeeper { | |
| return p.evmKeeper | ||
| } | ||
|
|
||
| func (PrecompileExecutor) IsTransaction(string) bool { | ||
| return false | ||
| // IsTransaction returns true for methods that mutate state; all other slashing | ||
| // methods are views. | ||
| func (PrecompileExecutor) IsTransaction(method string) bool { | ||
| switch method { | ||
| case UnjailMethod: | ||
| return true | ||
| default: | ||
| return false | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,13 +7,18 @@ import ( | |
|
|
||
| "github.com/ethereum/go-ethereum/common" | ||
| "github.com/ethereum/go-ethereum/core/vm" | ||
| "github.com/sei-protocol/sei-chain/app" | ||
| "github.com/sei-protocol/sei-chain/precompiles/slashing" | ||
| "github.com/sei-protocol/sei-chain/sei-cosmos/crypto/keys/ed25519" | ||
| crptotypes "github.com/sei-protocol/sei-chain/sei-cosmos/crypto/types" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [nit] Typo in the import alias: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [nit] Typo in the import alias: |
||
| sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" | ||
| slashingtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/slashing/types" | ||
| "github.com/sei-protocol/sei-chain/sei-cosmos/x/staking/teststaking" | ||
| stakingtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/staking/types" | ||
| tmtypes "github.com/sei-protocol/sei-chain/sei-tendermint/proto/tendermint/types" | ||
| testkeeper "github.com/sei-protocol/sei-chain/testutil/keeper" | ||
| "github.com/sei-protocol/sei-chain/x/evm/state" | ||
| minttypes "github.com/sei-protocol/sei-chain/x/mint/types" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
|
|
@@ -191,3 +196,89 @@ func TestPrecompile_Run_SigningInfos(t *testing.T) { | |
| require.Error(t, err) | ||
| require.Equal(t, vm.ErrExecutionReverted, err) | ||
| } | ||
|
|
||
| func setupValidator(t *testing.T, ctx sdk.Context, a *app.App, bondStatus stakingtypes.BondStatus, valPub crptotypes.PubKey) sdk.ValAddress { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [nit] |
||
| valAddr := sdk.ValAddress(valPub.Address()) | ||
| bondDenom := a.StakingKeeper.GetParams(ctx).BondDenom | ||
| selfBond := sdk.NewCoins(sdk.Coin{Amount: sdk.NewInt(100), Denom: bondDenom}) | ||
|
|
||
| require.NoError(t, a.BankKeeper.MintCoins(ctx, minttypes.ModuleName, selfBond)) | ||
| require.NoError(t, a.BankKeeper.SendCoinsFromModuleToAccount(ctx, minttypes.ModuleName, sdk.AccAddress(valAddr), selfBond)) | ||
|
|
||
| sh := teststaking.NewHelper(t, ctx, a.StakingKeeper) | ||
| msg := sh.CreateValidatorMsg(valAddr, valPub, selfBond[0].Amount) | ||
| sh.Handle(msg, true) | ||
|
|
||
| val, found := a.StakingKeeper.GetValidator(ctx, valAddr) | ||
| require.True(t, found) | ||
|
|
||
| val = val.UpdateStatus(bondStatus) | ||
| a.StakingKeeper.SetValidator(ctx, val) | ||
|
|
||
| consAddr, err := val.GetConsAddr() | ||
| require.NoError(t, err) | ||
|
|
||
| signingInfo := slashingtypes.NewValidatorSigningInfo( | ||
| consAddr, | ||
| ctx.BlockHeight(), | ||
| 0, | ||
| time.Unix(0, 0), | ||
| false, | ||
| 0, | ||
| ) | ||
| a.SlashingKeeper.SetValidatorSigningInfo(ctx, consAddr, signingInfo) | ||
|
|
||
| return valAddr | ||
| } | ||
|
|
||
| func TestUnjail(t *testing.T) { | ||
| testApp := testkeeper.EVMTestApp | ||
| ctx := testApp.NewContext(false, tmtypes.Header{}).WithBlockHeight(2).WithBlockTime(time.Now()) | ||
| k := &testApp.EvmKeeper | ||
|
|
||
| valPub := ed25519.GenPrivKey().PubKey() | ||
| valAddr := setupValidator(t, ctx, testApp, stakingtypes.Unbonded, valPub) | ||
| consAddr := sdk.ConsAddress(valPub.Address()) | ||
| testApp.StakingKeeper.Jail(ctx, consAddr) | ||
| require.True(t, testApp.StakingKeeper.Validator(ctx, valAddr).IsJailed()) | ||
|
|
||
| operatorSeiAddr := sdk.AccAddress(valAddr) | ||
| _, operatorEvmAddr := testkeeper.MockAddressPair() | ||
| k.SetAddressMapping(ctx, operatorSeiAddr, operatorEvmAddr) | ||
| // an associated address that is not a validator operator | ||
| nonValidatorSeiAddr, nonValidatorEvmAddr := testkeeper.MockAddressPair() | ||
| k.SetAddressMapping(ctx, nonValidatorSeiAddr, nonValidatorEvmAddr) | ||
|
|
||
| p, err := slashing.NewPrecompile(testApp.GetPrecompileKeepers()) | ||
| require.NoError(t, err) | ||
| statedb := state.NewDBImpl(ctx, k, true) | ||
| evm := vm.EVM{StateDB: statedb, TxContext: vm.TxContext{Origin: operatorEvmAddr}} | ||
| executor := p.GetExecutor().(*slashing.PrecompileExecutor) | ||
| method, err := p.ABI.MethodById(executor.UnjailID) | ||
| require.NoError(t, err) | ||
|
|
||
| // should error because of read only call | ||
| _, _, err = p.RunAndCalculateGas(&evm, operatorEvmAddr, operatorEvmAddr, executor.UnjailID, 1000000, nil, nil, true, false) | ||
| require.Error(t, err) | ||
| // should error because of delegatecall | ||
| _, _, err = p.RunAndCalculateGas(&evm, operatorEvmAddr, operatorEvmAddr, executor.UnjailID, 1000000, nil, nil, false, true) | ||
| require.Error(t, err) | ||
| // should error because it's not payable | ||
| _, _, err = p.RunAndCalculateGas(&evm, operatorEvmAddr, operatorEvmAddr, executor.UnjailID, 1000000, big.NewInt(1), nil, false, false) | ||
| require.Error(t, err) | ||
| // should error because caller is not associated | ||
| _, unassociatedEvmAddr := testkeeper.MockAddressPair() | ||
| _, _, err = p.RunAndCalculateGas(&evm, unassociatedEvmAddr, unassociatedEvmAddr, executor.UnjailID, 1000000, nil, nil, false, false) | ||
| require.Error(t, err) | ||
| // should error because caller is not a validator operator | ||
| _, _, err = p.RunAndCalculateGas(&evm, nonValidatorEvmAddr, nonValidatorEvmAddr, executor.UnjailID, 1000000, nil, nil, false, false) | ||
| require.Error(t, err) | ||
|
|
||
| // happy path | ||
| ret, _, err := p.RunAndCalculateGas(&evm, operatorEvmAddr, operatorEvmAddr, executor.UnjailID, 1000000, nil, nil, false, false) | ||
| require.NoError(t, err) | ||
| outputs, err := method.Outputs.Unpack(ret) | ||
| require.NoError(t, err) | ||
| require.True(t, outputs[0].(bool)) | ||
| require.False(t, testApp.StakingKeeper.Validator(statedb.Ctx(), valAddr).IsJailed()) | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nit] Worth an explicit note that placing the delegatecall guard after the query dispatch is deliberate.
gov,distribution, andstakingall reject delegatecall at the top ofExecutefor every method including views; here the existingparams/signingInfo/signingInfosviews stay delegatecall-reachable. That's the right call (tightening them would be a consensus-visible behavior change for existing contracts), but the current comment only explains why tx methods are guarded, not why the guard sits below the view switch — a future reader could easily "fix" the ordering to match gov and silently break callers.