Skip to content

Commit

Permalink
feat(x/swingset): auto-provision smart wallet
Browse files Browse the repository at this point in the history
  • Loading branch information
mhofman committed Dec 1, 2023
1 parent 35ac2da commit 6b5dcae
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 27 deletions.
4 changes: 4 additions & 0 deletions golang/cosmos/ante/inbound_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
19 changes: 19 additions & 0 deletions golang/cosmos/x/swingset/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
59 changes: 54 additions & 5 deletions golang/cosmos/x/swingset/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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
Expand All @@ -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(),
Expand All @@ -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
}
Expand All @@ -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) {
Expand Down
38 changes: 22 additions & 16 deletions golang/cosmos/x/swingset/types/default-params.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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)
Expand All @@ -75,5 +80,6 @@ func DefaultBeansPerUnit() []StringBeans {
NewStringBeans(BeansPerStorageByte, DefaultBeansPerStorageByte),
NewStringBeans(BeansPerVatCreation, DefaultBeansPerVatCreation),
NewStringBeans(BeansPerXsnapComputron, DefaultBeansPerXsnapComputron),
NewStringBeans(BeansPerSmartWalletProvision, DefaultBeansPerSmartWalletProvision),
}
}
1 change: 1 addition & 0 deletions golang/cosmos/x/swingset/types/expected_keepers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
26 changes: 20 additions & 6 deletions golang/cosmos/x/swingset/types/msgs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
}
}

Expand Down Expand Up @@ -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
}

Expand Down

0 comments on commit 6b5dcae

Please sign in to comment.