From 1adbe8d04ab4b6b210dea92bd962c61eddac428d Mon Sep 17 00:00:00 2001 From: Pino' Surace Date: Fri, 1 Sep 2023 17:14:46 +0200 Subject: [PATCH] Add implementation + some e2e test --- tests/e2e/grants_test.go | 100 +++++++++++++++++++ x/wasm/types/authz.go | 40 ++++++-- x/wasm/types/authz_test.go | 190 ++++++++++++++++++++++++++++++++++++- x/wasm/types/codec.go | 2 + 4 files changed, 322 insertions(+), 10 deletions(-) diff --git a/tests/e2e/grants_test.go b/tests/e2e/grants_test.go index 45a4b32b46..418c989e5b 100644 --- a/tests/e2e/grants_test.go +++ b/tests/e2e/grants_test.go @@ -2,6 +2,7 @@ package e2e_test import ( "fmt" + "os" "testing" "time" @@ -10,6 +11,7 @@ import ( errorsmod "cosmossdk.io/errors" + wasmvm "github.com/CosmWasm/wasmvm" "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" sdk "github.com/cosmos/cosmos-sdk/types" @@ -119,3 +121,101 @@ func TestGrants(t *testing.T) { }) } } + +func TestStoreCodeGrant(t *testing.T) { + // Given a grant for address B by A created + // When B uploads code from A + // Then the grant is executed as defined + // And + // - balance A reduced (on success) + // - balance B not touched + + reflectWasmCode, err := os.ReadFile("../../x/wasm/keeper/testdata/reflect_1_1.wasm") + require.NoError(t, err) + + reflectCodeChecksum, err := wasmvm.CreateChecksum(reflectWasmCode) + require.NoError(t, err) + + coord := ibctesting.NewCoordinator(t, 1) + chain := coord.GetChain(ibctesting.GetChainID(1)) + + granterAddr := chain.SenderAccount.GetAddress() + granteePrivKey := secp256k1.GenPrivKey() + granteeAddr := sdk.AccAddress(granteePrivKey.PubKey().Address().Bytes()) + otherPrivKey := secp256k1.GenPrivKey() + otherAddr := sdk.AccAddress(otherPrivKey.PubKey().Address().Bytes()) + + chain.Fund(granteeAddr, sdk.NewInt(1_000_000)) + chain.Fund(otherAddr, sdk.NewInt(1_000_000)) + assert.Equal(t, sdk.NewInt(1_000_000), chain.Balance(granteeAddr, sdk.DefaultBondDenom).Amount) + + specs := map[string]struct { + codeHash []byte + instantiatePermission types.AccessConfig + senderKey cryptotypes.PrivKey + expErr *errorsmod.Error + }{ + "any code hash": { + codeHash: []byte("*"), + instantiatePermission: types.AllowEverybody, + senderKey: granteePrivKey, + }, + "match code hash and permission": { + codeHash: reflectCodeChecksum, + instantiatePermission: types.AllowEverybody, + senderKey: granteePrivKey, + }, + "not match code hash": { + codeHash: []byte("ABC"), + instantiatePermission: types.AllowEverybody, + senderKey: granteePrivKey, + expErr: sdkerrors.ErrUnauthorized, + }, + "not match permission": { + codeHash: []byte("*"), + instantiatePermission: types.AllowNobody, + senderKey: granteePrivKey, + expErr: sdkerrors.ErrUnauthorized, + }, + "non authorized sender address": { + codeHash: []byte("*"), + instantiatePermission: types.AllowEverybody, + senderKey: otherPrivKey, + expErr: authz.ErrNoAuthorizationFound, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + // setup grant + grant, err := types.NewCodeGrant(spec.codeHash, spec.instantiatePermission) + require.NoError(t, err) + authorization := types.NewStoreCodeAuthorization(*grant) + expiry := time.Now().Add(time.Hour) + grantMsg, err := authz.NewMsgGrant(granterAddr, granteeAddr, authorization, &expiry) + require.NoError(t, err) + _, err = chain.SendMsgs(grantMsg) + require.NoError(t, err) + + granterStartBalance := chain.Balance(granterAddr, sdk.DefaultBondDenom).Amount + + // when + execMsg := authz.NewMsgExec(spec.senderKey.PubKey().Address().Bytes(), []sdk.Msg{&types.MsgStoreCode{ + Sender: granterAddr.String(), + WASMByteCode: reflectWasmCode, + InstantiatePermission: &types.AllowEverybody, + }}) + _, gotErr := chain.SendNonDefaultSenderMsgs(spec.senderKey, &execMsg) + + // then + if spec.expErr != nil { + require.True(t, spec.expErr.Is(gotErr)) + assert.Equal(t, sdk.NewInt(1_000_000), chain.Balance(granteeAddr, sdk.DefaultBondDenom).Amount) + assert.Equal(t, granterStartBalance, chain.Balance(granterAddr, sdk.DefaultBondDenom).Amount) + return + } + require.NoError(t, gotErr) + assert.Equal(t, sdk.NewInt(1_000_000), chain.Balance(granteeAddr, sdk.DefaultBondDenom).Amount) + //assert.True(t, granterStartBalance.GT(chain.Balance(granterAddr, sdk.DefaultBondDenom).Amount)) + }) + } +} diff --git a/x/wasm/types/authz.go b/x/wasm/types/authz.go index 04d1f5afc8..e4d03a920c 100644 --- a/x/wasm/types/authz.go +++ b/x/wasm/types/authz.go @@ -1,12 +1,14 @@ package types import ( + "bytes" "strings" "github.com/cosmos/gogoproto/proto" errorsmod "cosmossdk.io/errors" + wasmvm "github.com/CosmWasm/wasmvm" cdctypes "github.com/cosmos/cosmos-sdk/codec/types" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" @@ -35,14 +37,29 @@ func (a StoreCodeAuthorization) MsgTypeURL() string { return sdk.MsgTypeURL(&MsgStoreCode{}) } -// NewAuthz factory method to create an Authorization with updated grants -func (a StoreCodeAuthorization) NewAuthz(g []CodeGrant) authztypes.Authorization { - return NewStoreCodeAuthorization(g...) -} - // Accept implements Authorization.Accept. func (a *StoreCodeAuthorization) Accept(ctx sdk.Context, msg sdk.Msg) (authztypes.AcceptResponse, error) { - panic("implement") + var code []byte + var permission AccessConfig + switch msg := msg.(type) { + case *MsgStoreCode: + code = msg.WASMByteCode + permission = *msg.InstantiatePermission + default: + return authztypes.AcceptResponse{}, sdkerrors.ErrInvalidRequest.Wrap("unknown msg type") + } + + checksum, err := wasmvm.CreateChecksum(code) + if err != nil { + return authztypes.AcceptResponse{}, sdkerrors.ErrInvalidRequest.Wrap("checksum") + } + + for _, grant := range a.Grants { + if grant.Accept(checksum, permission) { + return authztypes.AcceptResponse{Accept: true}, nil + } + } + return authztypes.AcceptResponse{Accept: false}, nil } // ValidateBasic implements Authorization.ValidateBasic. @@ -68,6 +85,9 @@ func NewCodeGrant(codeHash []byte, instantiatePermission AccessConfig) (*CodeGra // ValidateBasic validates the grant func (g CodeGrant) ValidateBasic() error { + if len(g.CodeHash) == 0 { + return ErrEmpty.Wrap("code hash") + } if g.InstantiatePermission != nil { if err := g.InstantiatePermission.ValidateBasic(); err != nil { return errorsmod.Wrap(err, "instantiate permission") @@ -76,6 +96,14 @@ func (g CodeGrant) ValidateBasic() error { return nil } +// Accept checks if checksum and permission match the grant +func (g CodeGrant) Accept(checksum []byte, permission AccessConfig) bool { + if !bytes.Equal(g.CodeHash, []byte("*")) && !bytes.Equal(g.CodeHash, checksum) { + return false + } + return permission.IsSubset(*g.InstantiatePermission) +} + // AuthzableWasmMsg is abstract wasm tx message that is supported in authz type AuthzableWasmMsg interface { GetFunds() sdk.Coins diff --git a/x/wasm/types/authz_test.go b/x/wasm/types/authz_test.go index b02abea936..daf66e89f5 100644 --- a/x/wasm/types/authz_test.go +++ b/x/wasm/types/authz_test.go @@ -9,6 +9,7 @@ import ( errorsmod "cosmossdk.io/errors" + wasmvm "github.com/CosmWasm/wasmvm" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" authztypes "github.com/cosmos/cosmos-sdk/x/authz" @@ -488,26 +489,26 @@ func TestValidateContractAuthorization(t *testing.T) { "contract execution": { setup: func(t *testing.T) validatable { t.Helper() - return NewContractMigrationAuthorization(*validGrant) + return NewContractExecutionAuthorization(*validGrant) }, }, "contract execution - duplicate grants": { setup: func(t *testing.T) validatable { t.Helper() - return NewContractMigrationAuthorization(*validGrant, *validGrant) + return NewContractExecutionAuthorization(*validGrant, *validGrant) }, }, "contract execution - invalid grant": { setup: func(t *testing.T) validatable { t.Helper() - return NewContractMigrationAuthorization(*validGrant, *invalidGrant) + return NewContractExecutionAuthorization(*validGrant, *invalidGrant) }, expErr: true, }, "contract execution - empty grants": { setup: func(t *testing.T) validatable { t.Helper() - return NewContractMigrationAuthorization() + return NewContractExecutionAuthorization() }, expErr: true, }, @@ -744,3 +745,184 @@ func mustGrant(contract sdk.AccAddress, limit ContractAuthzLimitX, filter Contra } return *g } + +func TestValidateCodeGrant(t *testing.T) { + specs := map[string]struct { + codeHash []byte + instantiatePermission AccessConfig + expErr bool + }{ + "all good": { + codeHash: []byte("ABC"), + instantiatePermission: AllowEverybody, + }, + "empty code hash": { + codeHash: []byte{}, + instantiatePermission: AllowEverybody, + expErr: true, + }, + "nil code hash": { + codeHash: nil, + instantiatePermission: AllowEverybody, + expErr: true, + }, + "invalid permission": { + codeHash: []byte("ABC"), + instantiatePermission: AccessConfig{Permission: AccessTypeUnspecified}, + expErr: true, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + grant, err := NewCodeGrant(spec.codeHash, spec.instantiatePermission) + require.NoError(t, err) + + gotErr := grant.ValidateBasic() + if spec.expErr { + require.Error(t, gotErr) + return + } + require.NoError(t, gotErr) + }) + } +} + +func TestValidateStoreCodeAuthorization(t *testing.T) { + validGrant, err := NewCodeGrant([]byte("ABC"), AllowEverybody) + require.NoError(t, err) + invalidGrant, err := NewCodeGrant(nil, AllowEverybody) + require.NoError(t, err) + + specs := map[string]struct { + setup func(t *testing.T) validatable + expErr bool + }{ + "all good": { + setup: func(t *testing.T) validatable { + t.Helper() + return NewStoreCodeAuthorization(*validGrant) + }, + }, + "duplicate grants": { + setup: func(t *testing.T) validatable { + t.Helper() + return NewStoreCodeAuthorization(*validGrant, *validGrant) + }, + }, + "invalid grant": { + setup: func(t *testing.T) validatable { + t.Helper() + return NewStoreCodeAuthorization(*validGrant, *invalidGrant) + }, + expErr: true, + }, + "empty grants": { + setup: func(t *testing.T) validatable { + t.Helper() + return NewStoreCodeAuthorization() + }, + expErr: true, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + gotErr := spec.setup(t).ValidateBasic() + if spec.expErr { + require.Error(t, gotErr) + return + } + require.NoError(t, gotErr) + }) + } +} + +func TestStoreCodeAuthorizationAccept(t *testing.T) { + reflectCodeHash, err := wasmvm.CreateChecksum(reflectWasmCode) + require.NoError(t, err) + + grantWildcard, err := NewCodeGrant([]byte("*"), AllowEverybody) + require.NoError(t, err) + + grantReflectCode, err := NewCodeGrant(reflectCodeHash, AllowNobody) + require.NoError(t, err) + + grantOtherCode, err := NewCodeGrant([]byte("ABC"), AllowEverybody) + require.NoError(t, err) + + specs := map[string]struct { + auth authztypes.Authorization + msg sdk.Msg + expResult authztypes.AcceptResponse + expErr *errorsmod.Error + }{ + "accepted wildcard": { + auth: NewStoreCodeAuthorization(*grantWildcard), + msg: &MsgStoreCode{ + Sender: sdk.AccAddress(randBytes(SDKAddrLen)).String(), + WASMByteCode: reflectWasmCode, + InstantiatePermission: &AllowEverybody, + }, + expResult: authztypes.AcceptResponse{ + Accept: true, + }, + }, + "accepted reflect code": { + auth: NewStoreCodeAuthorization(*grantReflectCode), + msg: &MsgStoreCode{ + Sender: sdk.AccAddress(randBytes(SDKAddrLen)).String(), + WASMByteCode: reflectWasmCode, + InstantiatePermission: &AllowNobody, + }, + expResult: authztypes.AcceptResponse{ + Accept: true, + }, + }, + "not accepted - no matching code": { + auth: NewStoreCodeAuthorization(*grantOtherCode), + msg: &MsgStoreCode{ + Sender: sdk.AccAddress(randBytes(SDKAddrLen)).String(), + WASMByteCode: reflectWasmCode, + InstantiatePermission: &AllowEverybody, + }, + expResult: authztypes.AcceptResponse{ + Accept: false, + }, + }, + "not accepted - no matching permission": { + auth: NewStoreCodeAuthorization(*grantReflectCode), + msg: &MsgStoreCode{ + Sender: sdk.AccAddress(randBytes(SDKAddrLen)).String(), + WASMByteCode: reflectWasmCode, + InstantiatePermission: &AllowEverybody, + }, + expResult: authztypes.AcceptResponse{ + Accept: false, + }, + }, + "invalid msg type": { + auth: NewStoreCodeAuthorization(*grantWildcard), + msg: &MsgMigrateContract{ + Sender: sdk.AccAddress(randBytes(SDKAddrLen)).String(), + Contract: sdk.AccAddress(randBytes(SDKAddrLen)).String(), + CodeID: 1, + Msg: []byte(`{"foo":"bar"}`), + }, + expResult: authztypes.AcceptResponse{ + Accept: false, + }, + expErr: sdkerrors.ErrInvalidRequest, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + ctx := sdk.Context{}.WithGasMeter(sdk.NewInfiniteGasMeter()) + gotResult, gotErr := spec.auth.Accept(ctx, spec.msg) + if spec.expErr != nil { + require.ErrorIs(t, gotErr, spec.expErr) + return + } + require.NoError(t, gotErr) + assert.Equal(t, spec.expResult, gotResult) + }) + } +} diff --git a/x/wasm/types/codec.go b/x/wasm/types/codec.go index aafa435fb9..4366a9a9c1 100644 --- a/x/wasm/types/codec.go +++ b/x/wasm/types/codec.go @@ -56,6 +56,7 @@ func RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) { cdc.RegisterConcrete(&MaxFundsLimit{}, "wasm/MaxFundsLimit", nil) cdc.RegisterConcrete(&CombinedLimit{}, "wasm/CombinedLimit", nil) + cdc.RegisterConcrete(&StoreCodeAuthorization{}, "wasm/StoreCodeAuthorization", nil) cdc.RegisterConcrete(&ContractExecutionAuthorization{}, "wasm/ContractExecutionAuthorization", nil) cdc.RegisterConcrete(&ContractMigrationAuthorization{}, "wasm/ContractMigrationAuthorization", nil) } @@ -117,6 +118,7 @@ func RegisterInterfaces(registry types.InterfaceRegistry) { registry.RegisterImplementations( (*authz.Authorization)(nil), + &StoreCodeAuthorization{}, &ContractExecutionAuthorization{}, &ContractMigrationAuthorization{}, )