From cdbaa774b64fdec04f7f3553d8a18157758625ae Mon Sep 17 00:00:00 2001 From: Mathieu Hofman Date: Wed, 29 Nov 2023 16:33:49 +0000 Subject: [PATCH] feat(x/swingset): auto-provision smart wallet --- golang/cosmos/ante/inbound_test.go | 4 ++ golang/cosmos/x/swingset/keeper/keeper.go | 19 ++++++ golang/cosmos/x/swingset/keeper/msg_server.go | 59 +++++++++++++++++-- .../cosmos/x/swingset/types/default-params.go | 38 +++++++----- .../x/swingset/types/expected_keepers.go | 1 + golang/cosmos/x/swingset/types/msgs.go | 26 ++++++-- 6 files changed, 120 insertions(+), 27 deletions(-) diff --git a/golang/cosmos/ante/inbound_test.go b/golang/cosmos/ante/inbound_test.go index 1fd52a0ffb28..2f3e42e38a90 100644 --- a/golang/cosmos/ante/inbound_test.go +++ b/golang/cosmos/ante/inbound_test.go @@ -221,3 +221,7 @@ func (msk mockSwingsetKeeper) ChargeBeans(ctx sdk.Context, addr sdk.AccAddress, func (msk mockSwingsetKeeper) GetSmartWalletState(ctx sdk.Context, addr sdk.AccAddress) (swingtypes.SmartWalletState, error) { return swingtypes.SmartWalletStateUnspecified, fmt.Errorf("not implemented") } + +func (msk mockSwingsetKeeper) ChargeForSmartWallet(ctx sdk.Context, addr sdk.AccAddress) error { + return fmt.Errorf("not implemented") +} diff --git a/golang/cosmos/x/swingset/keeper/keeper.go b/golang/cosmos/x/swingset/keeper/keeper.go index a4352d191f82..038f8f5a6077 100644 --- a/golang/cosmos/x/swingset/keeper/keeper.go +++ b/golang/cosmos/x/swingset/keeper/keeper.go @@ -335,6 +335,25 @@ func (k Keeper) ChargeBeans(ctx sdk.Context, addr sdk.AccAddress, beans sdk.Uint return nil } +// ChargeForSmartWallet charges the fee for provisioning a smart wallet. +func (k Keeper) ChargeForSmartWallet(ctx sdk.Context, addr sdk.AccAddress) error { + beansPerUnit := k.GetBeansPerUnit(ctx) + beans := beansPerUnit[types.BeansPerSmartWalletProvision] + err := k.ChargeBeans(ctx, addr, beans) + if err != nil { + return err + } + + // TODO: mark that a smart wallet provision is pending. However in that case, + // auto-provisioning should still be performed (but without fees being charged), + // until the controller actually provisions the smart wallet (the operation may + // transiently fail, requiring retries until success). + // However the provisioning code is not currently idempotent, and has side + // effects when the smart wallet is already provisioned. + + return nil +} + // makeFeeMenu returns a map from power flag to its fee. In the case of duplicates, the // first one wins. func makeFeeMenu(powerFlagFees []types.PowerFlagFee) map[string]sdk.Coins { diff --git a/golang/cosmos/x/swingset/keeper/msg_server.go b/golang/cosmos/x/swingset/keeper/msg_server.go index 0d959ce23ccc..d89700fa1fc6 100644 --- a/golang/cosmos/x/swingset/keeper/msg_server.go +++ b/golang/cosmos/x/swingset/keeper/msg_server.go @@ -80,6 +80,11 @@ type walletAction struct { func (keeper msgServer) WalletAction(goCtx context.Context, msg *types.MsgWalletAction) (*types.MsgWalletActionResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) + err := keeper.provisionIfNeeded(ctx, msg.Owner) + if err != nil { + return nil, err + } + action := &walletAction{ Type: "WALLET_ACTION", Owner: msg.Owner.String(), @@ -89,7 +94,7 @@ func (keeper msgServer) WalletAction(goCtx context.Context, msg *types.MsgWallet } // fmt.Fprintf(os.Stderr, "Context is %+v\n", ctx) - err := keeper.routeAction(ctx, msg, action) + err = keeper.routeAction(ctx, msg, action) // fmt.Fprintln(os.Stderr, "Returned from SwingSet", out, err) if err != nil { return nil, err @@ -108,6 +113,11 @@ type walletSpendAction struct { func (keeper msgServer) WalletSpendAction(goCtx context.Context, msg *types.MsgWalletSpendAction) (*types.MsgWalletSpendActionResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) + err := keeper.provisionIfNeeded(ctx, msg.Owner) + if err != nil { + return nil, err + } + action := &walletSpendAction{ Type: "WALLET_SPEND_ACTION", Owner: msg.Owner.String(), @@ -116,7 +126,7 @@ func (keeper msgServer) WalletSpendAction(goCtx context.Context, msg *types.MsgW BlockTime: ctx.BlockTime().Unix(), } // fmt.Fprintf(os.Stderr, "Context is %+v\n", ctx) - err := keeper.routeAction(ctx, msg, action) + err = keeper.routeAction(ctx, msg, action) if err != nil { return nil, err } @@ -125,9 +135,48 @@ func (keeper msgServer) WalletSpendAction(goCtx context.Context, msg *types.MsgW type provisionAction struct { *types.MsgProvision - Type string `json:"type"` // PLEASE_PROVISION - BlockHeight int64 `json:"blockHeight"` - BlockTime int64 `json:"blockTime"` + Type string `json:"type"` // PLEASE_PROVISION + BlockHeight int64 `json:"blockHeight"` + BlockTime int64 `json:"blockTime"` + AutoProvision bool `json:"autoProvision"` +} + +// provisionIfNeeded generates a provision action if no smart wallet is already +// provisioned for the account. This assumes that all messages for +// non-provisioned smart wallets allowed by the admission AnteHandler should +// auto-provision the smart wallet. +func (keeper msgServer) provisionIfNeeded(ctx sdk.Context, owner sdk.AccAddress) error { + // We need to generate a provision action until the smart wallet has + // been fully provisioned by the controller. This is because a provision is + // not guaranteed to succeed (e.g. lack of provision pool funds) + walletState, err := keeper.GetSmartWalletState(ctx, owner) + if err != nil { + return err + } else if walletState == types.SmartWalletStateProvisioned { + return nil + } + + msg := &types.MsgProvision{ + Address: owner, + Submitter: owner, + PowerFlags: []string{types.PowerFlagSmartWallet}, + } + + action := &provisionAction{ + MsgProvision: msg, + Type: "PLEASE_PROVISION", + BlockHeight: ctx.BlockHeight(), + BlockTime: ctx.BlockTime().Unix(), + AutoProvision: true, + } + + err = keeper.routeAction(ctx, msg, action) + // fmt.Fprintln(os.Stderr, "Returned from SwingSet", out, err) + if err != nil { + return err + } + + return nil } func (keeper msgServer) Provision(goCtx context.Context, msg *types.MsgProvision) (*types.MsgProvisionResponse, error) { diff --git a/golang/cosmos/x/swingset/types/default-params.go b/golang/cosmos/x/swingset/types/default-params.go index 83e75ef109ae..073c5a3ac22a 100644 --- a/golang/cosmos/x/swingset/types/default-params.go +++ b/golang/cosmos/x/swingset/types/default-params.go @@ -11,20 +11,24 @@ import ( // experience if they don't. const ( - BeansPerFeeUnit = "feeUnit" - BeansPerInboundTx = "inboundTx" - BeansPerBlockComputeLimit = "blockComputeLimit" - BeansPerMessage = "message" - BeansPerMessageByte = "messageByte" - BeansPerMinFeeDebit = "minFeeDebit" - BeansPerStorageByte = "storageByte" - BeansPerVatCreation = "vatCreation" - BeansPerXsnapComputron = "xsnapComputron" + BeansPerFeeUnit = "feeUnit" + BeansPerInboundTx = "inboundTx" + BeansPerBlockComputeLimit = "blockComputeLimit" + BeansPerMessage = "message" + BeansPerMessageByte = "messageByte" + BeansPerMinFeeDebit = "minFeeDebit" + BeansPerStorageByte = "storageByte" + BeansPerVatCreation = "vatCreation" + BeansPerXsnapComputron = "xsnapComputron" + BeansPerSmartWalletProvision = "smartWalletProvision" // QueueSize keys. // Keep up-to-date with updateQueueAllowed() in packanges/cosmic-swingset/src/launch-chain.js QueueInbound = "inbound" QueueInboundMempool = "inbound_mempool" + + // PowerFlags. + PowerFlagSmartWallet = "SMART_WALLET" ) var ( @@ -43,17 +47,18 @@ var ( // TODO: create the cost model we want, and update these to be more principled. // These defaults currently make deploying an ag-solo cost less than $1.00. - DefaultBeansPerFeeUnit = sdk.NewUint(1_000_000_000_000) // $1 - DefaultBeansPerInboundTx = DefaultBeansPerFeeUnit.Quo(sdk.NewUint(100)) // $0.01 - DefaultBeansPerMessage = DefaultBeansPerFeeUnit.Quo(sdk.NewUint(1_000)) // $0.001 - DefaultBeansPerMessageByte = DefaultBeansPerFeeUnit.Quo(sdk.NewUint(50_000)) // $0.00002 - DefaultBeansPerMinFeeDebit = DefaultBeansPerFeeUnit.Quo(sdk.NewUint(5)) // $0.2 - DefaultBeansPerStorageByte = DefaultBeansPerFeeUnit.Quo(sdk.NewUint(500)) // $0.002 + DefaultBeansPerFeeUnit = sdk.NewUint(1_000_000_000_000) // $1 + DefaultBeansPerInboundTx = DefaultBeansPerFeeUnit.Quo(sdk.NewUint(100)) // $0.01 + DefaultBeansPerMessage = DefaultBeansPerFeeUnit.Quo(sdk.NewUint(1_000)) // $0.001 + DefaultBeansPerMessageByte = DefaultBeansPerFeeUnit.Quo(sdk.NewUint(50_000)) // $0.00002 + DefaultBeansPerMinFeeDebit = DefaultBeansPerFeeUnit.Quo(sdk.NewUint(5)) // $0.2 + DefaultBeansPerStorageByte = DefaultBeansPerFeeUnit.Quo(sdk.NewUint(500)) // $0.002 + DefaultBeansPerSmartWalletProvision = DefaultBeansPerFeeUnit // $1 DefaultBootstrapVatConfig = "@agoric/vats/decentral-core-config.json" DefaultPowerFlagFees = []PowerFlagFee{ - NewPowerFlagFee("SMART_WALLET", sdk.NewCoins(sdk.NewInt64Coin("ubld", 10_000_000))), + NewPowerFlagFee(PowerFlagSmartWallet, sdk.NewCoins(sdk.NewInt64Coin("ubld", 10_000_000))), } DefaultInboundQueueMax = int32(1_000) @@ -75,5 +80,6 @@ func DefaultBeansPerUnit() []StringBeans { NewStringBeans(BeansPerStorageByte, DefaultBeansPerStorageByte), NewStringBeans(BeansPerVatCreation, DefaultBeansPerVatCreation), NewStringBeans(BeansPerXsnapComputron, DefaultBeansPerXsnapComputron), + NewStringBeans(BeansPerSmartWalletProvision, DefaultBeansPerSmartWalletProvision), } } diff --git a/golang/cosmos/x/swingset/types/expected_keepers.go b/golang/cosmos/x/swingset/types/expected_keepers.go index 14abb2354311..1331e97cf9e0 100644 --- a/golang/cosmos/x/swingset/types/expected_keepers.go +++ b/golang/cosmos/x/swingset/types/expected_keepers.go @@ -25,4 +25,5 @@ type SwingSetKeeper interface { ChargeBeans(ctx sdk.Context, addr sdk.AccAddress, beans sdk.Uint) error IsHighPriorityAddress(ctx sdk.Context, addr sdk.AccAddress) (bool, error) GetSmartWalletState(ctx sdk.Context, addr sdk.AccAddress) (SmartWalletState, error) + ChargeForSmartWallet(ctx sdk.Context, addr sdk.AccAddress) error } diff --git a/golang/cosmos/x/swingset/types/msgs.go b/golang/cosmos/x/swingset/types/msgs.go index 8a7496fdb179..90f673284267 100644 --- a/golang/cosmos/x/swingset/types/msgs.go +++ b/golang/cosmos/x/swingset/types/msgs.go @@ -49,7 +49,11 @@ func chargeAdmission(ctx sdk.Context, keeper SwingSetKeeper, addr sdk.AccAddress } // checkSmartWalletProvisioned verifies if a smart wallet message can be -// delivered for the owner's address. +// delivered for the owner's address. A message is allowed if a smart wallet +// is already provisioned for the address, or if the provisioning fee is +// charged successfully. +// All messages for non-provisioned smart wallets allowed here will result in +// an auto-provision action generated by the msg server. func checkSmartWalletProvisioned(ctx sdk.Context, keeper SwingSetKeeper, addr sdk.AccAddress) error { walletState, err := keeper.GetSmartWalletState(ctx, addr) if err != nil { @@ -58,13 +62,19 @@ func checkSmartWalletProvisioned(ctx sdk.Context, keeper SwingSetKeeper, addr sd switch walletState { case SmartWalletStateProvisioned: - // The address has a smart wallet + // The address already has a smart wallet return nil case SmartWalletStatePending: - // A provision is pending execution + // A provision (either explicit or automatic) may be pending execution in + // the controller, or if we ever allow multiple swingset messages per + // transaction, a previous message may have provisioned the wallet. return nil default: - return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "Owner address does not have a smart wallet") + // Charge for the smart wallet. + // This is a separate charge from the smart wallet action which triggered the check + // TODO: Currently this call does not mark the smart wallet provisioning as + // pending, resulting in multiple provisioning charges for the owner. + return keeper.ChargeForSmartWallet(ctx, addr) } } @@ -301,8 +311,12 @@ func (msg MsgProvision) ValidateBasic() error { // CheckAdmissibility implements the vm.ControllerAdmissionMsg interface. func (msg MsgProvision) CheckAdmissibility(ctx sdk.Context, data interface{}) (sdk.Context, error) { - // We have our own fee charging mechanism within Swingset itself, - // so there are no admission restriction here. + // TODO: consider disallowing a provision message for a smart wallet if the + // smart wallet is already provisioned or pending provisioning. However we + // currently do not track whether a smart wallet is pending provisioning. + + // For explicitly provisioning, swingset will take care of charging, + // so we skip admission fees. return ctx, nil }