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(accounts): Allow funds to be sent to accounts on init and execute #19360

Merged
merged 15 commits into from
Feb 8, 2024
261 changes: 211 additions & 50 deletions api/cosmos/accounts/testing/counter/v1/counter.pulsar.go

Large diffs are not rendered by default.

494 changes: 405 additions & 89 deletions api/cosmos/accounts/v1/tx.pulsar.go

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions proto/cosmos/accounts/testing/counter/v1/counter.proto
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ package cosmos.accounts.testing.counter.v1;

option go_package = "cosmossdk.io/x/accounts/testing/counter/v1";

import "cosmos/base/v1beta1/coin.proto";
import "gogoproto/gogo.proto";

// MsgInit defines a message which initializes the counter with a given amount.
message MsgInit {
// initial_value is the initial amount to set the counter to.
Expand Down Expand Up @@ -39,6 +42,9 @@ message MsgTestDependenciesResponse {
uint64 before_gas = 3;
// after_gas is used to test gas meter increasing.
uint64 after_gas = 4;
// funds reports the funds from the implementation.Funds method.
repeated cosmos.base.v1beta1.Coin funds = 5
[(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins", (gogoproto.nullable) = false];
}

// QueryCounterRequest is used to query the counter value.
Expand Down
10 changes: 10 additions & 0 deletions proto/cosmos/accounts/v1/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ option go_package = "cosmossdk.io/x/accounts/v1";
import "google/protobuf/any.proto";
import "cosmos/msg/v1/msg.proto";
import "cosmos/accounts/v1/account_abstraction.proto";
import "cosmos/base/v1beta1/coin.proto";
import "gogoproto/gogo.proto";

// Msg defines the Msg service for the x/accounts module.
service Msg {
Expand All @@ -33,6 +35,10 @@ message MsgInit {
string account_type = 2;
// message is the message to be sent to the account.
google.protobuf.Any message = 3;
// funds contains the coins that the account wants to
// send alongside the request.
repeated cosmos.base.v1beta1.Coin funds = 4
[(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins", (gogoproto.nullable) = false];
}

// MsgInitResponse defines the Create response type for the Msg/Create RPC method.
Expand All @@ -52,6 +58,10 @@ message MsgExecute {
string target = 2;
// message is the message to be sent to the account.
google.protobuf.Any message = 3;
// funds contains the coins that the account wants to
// send alongside the request.
repeated cosmos.base.v1beta1.Coin funds = 4
[(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins", (gogoproto.nullable) = false];
}

// MsgExecuteResponse defines the Execute response type for the Msg/Execute RPC method.
Expand Down
21 changes: 21 additions & 0 deletions simapp/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ import (
dbm "github.com/cosmos/cosmos-db"
"github.com/cosmos/gogoproto/proto"
"github.com/spf13/cast"
"google.golang.org/protobuf/runtime/protoiface"

autocliv1 "cosmossdk.io/api/cosmos/autocli/v1"
reflectionv1 "cosmossdk.io/api/cosmos/reflection/v1"
"cosmossdk.io/client/v2/autocli"
coreaddress "cosmossdk.io/core/address"
"cosmossdk.io/core/appmodule"
"cosmossdk.io/log"
storetypes "cosmossdk.io/store/types"
Expand Down Expand Up @@ -292,6 +294,7 @@ func NewSimApp(
runtime.HeaderService{},
runtime.BranchService{},
runtime.GasService{},
makeSendCoinsMsg(addressCodec),
addressCodec,
appCodec,
app.MsgServiceRouter(),
Expand Down Expand Up @@ -811,3 +814,21 @@ func BlockedAddresses() map[string]bool {

return modAccAddrs
}

func makeSendCoinsMsg(addressCodec coreaddress.Codec) func([]byte, []byte, sdk.Coins) (protoiface.MessageV1, error) {
Copy link
Member

Choose a reason for hiding this comment

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

imho, this makes the wiring of this module quite weird.
can we add docs here on why its needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes I do agree this makes things weird.

return func(from, to []byte, coins sdk.Coins) (protoiface.MessageV1, error) {
fromStr, err := addressCodec.BytesToString(from)
if err != nil {
return nil, err
}
toStr, err := addressCodec.BytesToString(to)
if err != nil {
return nil, err
}
return &banktypes.MsgSend{
FromAddress: fromStr,
ToAddress: toStr,
Amount: coins,
}, nil
}
}
4 changes: 2 additions & 2 deletions tests/e2e/accounts/account_abstraction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,12 @@ func TestAccountAbstraction(t *testing.T) {

_, aaAddr, err := ak.Init(ctx, "aa_minimal", accCreator, &rotationv1.MsgInit{
PubKeyBytes: privKey.PubKey().Bytes(),
})
}, nil)
Copy link
Contributor

Choose a reason for hiding this comment

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

The addition of nil as an argument in Init calls should correspond to the new funds parameter. Ensure tests are updated to cover scenarios where funds are actually transferred.

require.NoError(t, err)

_, aaFullAddr, err := ak.Init(ctx, "aa_full", accCreator, &rotationv1.MsgInit{
PubKeyBytes: privKey.PubKey().Bytes(),
})
}, nil)
Copy link
Contributor

Choose a reason for hiding this comment

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

Similar to the previous comment, the nil argument addition in Init calls needs to be accompanied by tests that validate funds transfer logic.

require.NoError(t, err)

aaAddrStr, err := app.AuthKeeper.AddressCodec().BytesToString(aaAddr)
Expand Down
26 changes: 23 additions & 3 deletions tests/e2e/accounts/wiring_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import (
"testing"

"cosmossdk.io/core/header"
storetypes "cosmossdk.io/store/types"
counterv1 "cosmossdk.io/x/accounts/testing/counter/v1"
"cosmossdk.io/x/bank/testutil"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/require"
)
Expand All @@ -21,20 +23,31 @@ func TestDependencies(t *testing.T) {
app := setupApp(t)
ak := app.AccountsKeeper
ctx := sdk.NewContext(app.CommitMultiStore(), false, app.Logger()).WithHeaderInfo(header.Info{ChainID: "chain-id"})
ctx = ctx.WithGasMeter(storetypes.NewGasMeter(500_000))

_, counterAddr, err := ak.Init(ctx, "counter", accCreator, &counterv1.MsgInit{
InitialValue: 0,
})
}, nil)
testinginprod marked this conversation as resolved.
Show resolved Hide resolved
require.NoError(t, err)
// test dependencies
r, err := ak.Execute(ctx, counterAddr, []byte("test"), &counterv1.MsgTestDependencies{})
creatorInitFunds := sdk.NewCoins(sdk.NewInt64Coin("stake", 100_000))
err = testutil.FundAccount(ctx, app.BankKeeper, accCreator, creatorInitFunds)
require.NoError(t, err)
sentFunds := sdk.NewCoins(sdk.NewInt64Coin("stake", 50_000))
r, err := ak.Execute(
ctx,
counterAddr,
accCreator,
&counterv1.MsgTestDependencies{},
sentFunds,
)
testinginprod marked this conversation as resolved.
Show resolved Hide resolved
require.NoError(t, err)
res := r.(*counterv1.MsgTestDependenciesResponse)

// test gas
require.NotZero(t, res.BeforeGas)
require.NotZero(t, res.AfterGas)
require.Equal(t, uint64(10), res.AfterGas-res.BeforeGas)
require.Equal(t, int(uint64(10)), int(res.AfterGas-res.BeforeGas))

// test header service
require.Equal(t, ctx.HeaderInfo().ChainID, res.ChainId)
Expand All @@ -43,4 +56,11 @@ func TestDependencies(t *testing.T) {
wantAddr, err := app.AuthKeeper.AddressCodec().BytesToString(counterAddr)
require.NoError(t, err)
require.Equal(t, wantAddr, res.Address)

// test funds
creatorFunds := app.BankKeeper.GetAllBalances(ctx, accCreator)
require.Equal(t, creatorInitFunds.Sub(sentFunds...), creatorFunds)

accFunds := app.BankKeeper.GetAllBalances(ctx, counterAddr)
require.Equal(t, sentFunds, accFunds)
}
5 changes: 5 additions & 0 deletions x/accounts/accountstd/exports.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"

"cosmossdk.io/x/accounts/internal/implementation"
sdk "github.com/cosmos/cosmos-sdk/types"

"github.com/cosmos/cosmos-sdk/types/address"
)
Expand Down Expand Up @@ -77,6 +78,10 @@ func SenderIsAccountsModule(ctx context.Context) bool {
return bytes.Equal(Sender(ctx), accountsModuleAddress)
}

// Funds returns if any funds were sent during the execute or init request. In queries this
// returns nil.
func Funds(ctx context.Context) sdk.Coins { return implementation.Funds(ctx) }

// ExecModule can be used to execute a message towards a module.
func ExecModule[Resp any, RespProto implementation.ProtoMsgG[Resp], Req any, ReqProto implementation.ProtoMsgG[Req]](ctx context.Context, msg ReqProto) (RespProto, error) {
return implementation.ExecModule[Resp, RespProto, Req, ReqProto](ctx, msg)
Expand Down
8 changes: 4 additions & 4 deletions x/accounts/genesis_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ func TestGenesis(t *testing.T) {
// we init two accounts of the same type

// we set counter to 10
_, addr1, err := k.Init(ctx, "test", []byte("sender"), &types.Empty{})
_, addr1, err := k.Init(ctx, "test", []byte("sender"), &types.Empty{}, nil)
testinginprod marked this conversation as resolved.
Show resolved Hide resolved
require.NoError(t, err)
_, err = k.Execute(ctx, addr1, []byte("sender"), &types.UInt64Value{Value: 10})
_, err = k.Execute(ctx, addr1, []byte("sender"), &types.UInt64Value{Value: 10}, nil)
testinginprod marked this conversation as resolved.
Show resolved Hide resolved
require.NoError(t, err)

// we set counter to 20
_, addr2, err := k.Init(ctx, "test", []byte("sender"), &types.Empty{})
_, addr2, err := k.Init(ctx, "test", []byte("sender"), &types.Empty{}, nil)
testinginprod marked this conversation as resolved.
Show resolved Hide resolved
require.NoError(t, err)
_, err = k.Execute(ctx, addr2, []byte("sender"), &types.UInt64Value{Value: 20})
_, err = k.Execute(ctx, addr2, []byte("sender"), &types.UInt64Value{Value: 20}, nil)
testinginprod marked this conversation as resolved.
Show resolved Hide resolved
require.NoError(t, err)

// export state
Expand Down
45 changes: 27 additions & 18 deletions x/accounts/internal/implementation/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"cosmossdk.io/core/header"
"cosmossdk.io/core/store"
"cosmossdk.io/x/accounts/internal/prefixstore"
sdk "github.com/cosmos/cosmos-sdk/types"
)

var AccountStatePrefix = collections.NewPrefix(255)
Expand All @@ -25,12 +26,21 @@ type contextValue struct {
store store.KVStore // store is the prefixed store for the account.
sender []byte // sender is the address of the entity invoking the account action.
whoami []byte // whoami is the address of the account being invoked.
funds sdk.Coins // funds reports the coins sent alongside the request.
parentContext context.Context // parentContext that was used to build the account context.
moduleExec ModuleExecFunc // moduleExec is a function that executes a module message, when the resp type is known.
moduleExecUntyped ModuleExecUntypedFunc // moduleExecUntyped is a function that executes a module message, when the resp type is unknown.
moduleQuery ModuleQueryFunc // moduleQuery is a function that queries a module.
}

func addCtx(ctx context.Context, value contextValue) context.Context {
return context.WithValue(ctx, contextKey{}, value)
}

func getCtx(ctx context.Context) contextValue {
return ctx.Value(contextKey{}).(contextValue)
}

// MakeAccountContext creates a new account execution context given:
// storeSvc: which fetches the x/accounts module store.
// accountAddr: the address of the account being invoked, which is used to give the
Expand All @@ -44,14 +54,16 @@ func MakeAccountContext(
accNumber uint64,
accountAddr []byte,
sender []byte,
funds sdk.Coins,
moduleExec ModuleExecFunc,
moduleExecUntyped ModuleExecUntypedFunc,
moduleQuery ModuleQueryFunc,
) context.Context {
return context.WithValue(ctx, contextKey{}, contextValue{
return addCtx(ctx, contextValue{
store: makeAccountStore(ctx, storeSvc, accNumber),
sender: sender,
whoami: accountAddr,
funds: funds,
parentContext: ctx,
moduleExec: moduleExec,
moduleExecUntyped: moduleExecUntyped,
Expand All @@ -71,7 +83,7 @@ func makeAccountStore(ctx context.Context, storeSvc store.KVStoreService, accNum
// ExecModuleUntyped can be used to execute a message towards a module, when the response type is unknown.
func ExecModuleUntyped(ctx context.Context, msg ProtoMsg) (ProtoMsg, error) {
// get sender
v := ctx.Value(contextKey{}).(contextValue)
v := getCtx(ctx)

resp, err := v.moduleExecUntyped(v.parentContext, v.whoami, msg)
if err != nil {
Expand All @@ -84,7 +96,7 @@ func ExecModuleUntyped(ctx context.Context, msg ProtoMsg) (ProtoMsg, error) {
// ExecModule can be used to execute a message towards a module.
func ExecModule[Resp any, RespProto ProtoMsgG[Resp], Req any, ReqProto ProtoMsgG[Req]](ctx context.Context, msg ReqProto) (RespProto, error) {
// get sender
v := ctx.Value(contextKey{}).(contextValue)
v := getCtx(ctx)

// execute module, unwrapping the original context.
resp := RespProto(new(Resp))
Expand All @@ -100,7 +112,7 @@ func ExecModule[Resp any, RespProto ProtoMsgG[Resp], Req any, ReqProto ProtoMsgG
func QueryModule[Resp any, RespProto ProtoMsgG[Resp], Req any, ReqProto ProtoMsgG[Req]](ctx context.Context, req ReqProto) (RespProto, error) {
// we do not need to check the sender in a query because it is not a state transition.
// we also unwrap the original context.
v := ctx.Value(contextKey{}).(contextValue)
v := getCtx(ctx)
resp := RespProto(new(Resp))
err := v.moduleQuery(v.parentContext, req, resp)
if err != nil {
Expand All @@ -110,20 +122,21 @@ func QueryModule[Resp any, RespProto ProtoMsgG[Resp], Req any, ReqProto ProtoMsg
}

// openKVStore returns the prefixed store for the account given the context.
func openKVStore(ctx context.Context) store.KVStore {
return ctx.Value(contextKey{}).(contextValue).store
}
func openKVStore(ctx context.Context) store.KVStore { return getCtx(ctx).store }

// Sender returns the address of the entity invoking the account action.
func Sender(ctx context.Context) []byte {
return ctx.Value(contextKey{}).(contextValue).sender
return getCtx(ctx).sender
}

// Whoami returns the address of the account being invoked.
func Whoami(ctx context.Context) []byte {
return ctx.Value(contextKey{}).(contextValue).whoami
return getCtx(ctx).whoami
}

// Funds returns the funds associated with the execution context.
func Funds(ctx context.Context) sdk.Coins { return getCtx(ctx).funds }

type headerService struct{ hs header.Service }

func (h headerService) GetHeaderInfo(ctx context.Context) header.Info {
Expand All @@ -132,9 +145,7 @@ func (h headerService) GetHeaderInfo(ctx context.Context) header.Info {

var _ gas.Service = (*gasService)(nil)

type gasService struct {
gs gas.Service
}
type gasService struct{ gs gas.Service }

func (g gasService) GetGasMeter(ctx context.Context) gas.Meter {
return g.gs.GetGasMeter(getParentContext(ctx))
Expand All @@ -145,17 +156,15 @@ func (g gasService) GetBlockGasMeter(ctx context.Context) gas.Meter {
}

func (g gasService) WithGasMeter(ctx context.Context, meter gas.Meter) context.Context {
v := ctx.Value(contextKey{}).(contextValue)
v := getCtx(ctx)
v.parentContext = g.gs.WithGasMeter(v.parentContext, meter)
return context.WithValue(v.parentContext, contextKey{}, v)
}

func (g gasService) WithBlockGasMeter(ctx context.Context, meter gas.Meter) context.Context {
v := ctx.Value(contextKey{}).(contextValue)
v := getCtx(ctx)
v.parentContext = g.gs.WithBlockGasMeter(v.parentContext, meter)
return context.WithValue(v.parentContext, contextKey{}, v)
return addCtx(v.parentContext, v)
}

func getParentContext(ctx context.Context) context.Context {
return ctx.Value(contextKey{}).(contextValue).parentContext
}
func getParentContext(ctx context.Context) context.Context { return getCtx(ctx).parentContext }
8 changes: 4 additions & 4 deletions x/accounts/internal/implementation/context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func TestMakeAccountContext(t *testing.T) {
sender := []byte("sender")
sb := collections.NewSchemaBuilderFromAccessor(openKVStore)

accountCtx := MakeAccountContext(originalContext, storeService, 1, accountAddr, sender, nil, nil, nil)
accountCtx := MakeAccountContext(originalContext, storeService, 1, accountAddr, sender, nil, nil, nil, nil)
testinginprod marked this conversation as resolved.
Show resolved Hide resolved

// ensure whoami
require.Equal(t, accountAddr, Whoami(accountCtx))
Expand All @@ -44,7 +44,7 @@ func TestMakeAccountContext(t *testing.T) {
require.Equal(t, []byte{0, 0, 0, 0, 0, 0, 3, 232}, value)

// ensure calling ExecModule works
accountCtx = MakeAccountContext(originalContext, storeService, 1, []byte("legit-exec-module"), []byte("invoker"), func(ctx context.Context, sender []byte, msg, msgResp ProtoMsg) error {
accountCtx = MakeAccountContext(originalContext, storeService, 1, []byte("legit-exec-module"), []byte("invoker"), nil, func(ctx context.Context, sender []byte, msg, msgResp ProtoMsg) error {
testinginprod marked this conversation as resolved.
Show resolved Hide resolved
// ensure we unwrapped the context when invoking a module call
require.Equal(t, originalContext, ctx)
Merge(msgResp, &types.StringValue{Value: "module exec was called"})
Expand All @@ -56,7 +56,7 @@ func TestMakeAccountContext(t *testing.T) {
require.True(t, Equal(&types.StringValue{Value: "module exec was called"}, resp))

// ensure calling ExecModuleUntyped works
accountCtx = MakeAccountContext(originalContext, storeService, 1, []byte("legit-exec-module-untyped"), []byte("invoker"), nil, func(ctx context.Context, sender []byte, msg ProtoMsg) (ProtoMsg, error) {
accountCtx = MakeAccountContext(originalContext, storeService, 1, []byte("legit-exec-module-untyped"), []byte("invoker"), nil, nil, func(ctx context.Context, sender []byte, msg ProtoMsg) (ProtoMsg, error) {
testinginprod marked this conversation as resolved.
Show resolved Hide resolved
require.Equal(t, originalContext, ctx)
return &types.StringValue{Value: "module exec untyped was called"}, nil
}, nil)
Expand All @@ -67,7 +67,7 @@ func TestMakeAccountContext(t *testing.T) {

// ensure calling QueryModule works, also by setting everything else communication related to nil
// we can guarantee that exec paths do not impact query paths.
accountCtx = MakeAccountContext(originalContext, storeService, 1, nil, nil, nil, nil, func(ctx context.Context, req, resp ProtoMsg) error {
accountCtx = MakeAccountContext(originalContext, storeService, 1, nil, nil, nil, nil, nil, func(ctx context.Context, req, resp ProtoMsg) error {
testinginprod marked this conversation as resolved.
Show resolved Hide resolved
require.Equal(t, originalContext, ctx)
Merge(resp, &types.StringValue{Value: "module query was called"})
return nil
Expand Down
Loading
Loading