diff --git a/golang/cosmos/ante/inbound_test.go b/golang/cosmos/ante/inbound_test.go index 34a2020d2cf..653c11fa8e6 100644 --- a/golang/cosmos/ante/inbound_test.go +++ b/golang/cosmos/ante/inbound_test.go @@ -215,7 +215,7 @@ func (msk mockSwingsetKeeper) GetBeansPerUnit(ctx sdk.Context) map[string]sdkmat return nil } -func (msk mockSwingsetKeeper) ChargeBeans(ctx sdk.Context, addr sdk.AccAddress, beans sdkmath.Uint) error { +func (msk mockSwingsetKeeper) ChargeBeans(ctx sdk.Context, beansPerUnit map[string]sdkmath.Uint, addr sdk.AccAddress, beans sdkmath.Uint) error { return fmt.Errorf("not implemented") } @@ -223,6 +223,6 @@ func (msk mockSwingsetKeeper) GetSmartWalletState(ctx sdk.Context, addr sdk.AccA panic(fmt.Errorf("not implemented")) } -func (msk mockSwingsetKeeper) ChargeForSmartWallet(ctx sdk.Context, addr sdk.AccAddress) error { +func (msk mockSwingsetKeeper) ChargeForSmartWallet(ctx sdk.Context, beansPerUnit map[string]sdkmath.Uint, addr sdk.AccAddress) error { return fmt.Errorf("not implemented") } diff --git a/golang/cosmos/proto/agoric/swingset/swingset.proto b/golang/cosmos/proto/agoric/swingset/swingset.proto index 7a7c7a16d14..fdfd5f722f4 100644 --- a/golang/cosmos/proto/agoric/swingset/swingset.proto +++ b/golang/cosmos/proto/agoric/swingset/swingset.proto @@ -82,6 +82,18 @@ message Params { repeated QueueSize queue_max = 5 [ (gogoproto.nullable) = false ]; + + // Vat cleanup budget values. + // These values are used by SwingSet to control the pace of removing data + // associated with a terminated vat as described at + // https://github.com/Agoric/agoric-sdk/blob/master/packages/SwingSet/docs/run-policy.md#terminated-vat-cleanup + // + // There is no required order to this list of entries, but all the chain + // nodes must all serialize and deserialize the existing order without + // permuting it. + repeated UintMapEntry vat_cleanup_budget = 6 [ + (gogoproto.nullable) = false + ]; } // The current state of the module. @@ -119,6 +131,7 @@ message PowerFlagFee { } // Map element of a string key to a size. +// TODO: Replace with UintMapEntry? message QueueSize { option (gogoproto.equal) = true; @@ -129,6 +142,18 @@ message QueueSize { int32 size = 2; } +// Map element of a string key to an unsigned integer. +// The value uses cosmos-sdk Uint rather than a native Go type to ensure that +// zeroes survive "omitempty" JSON serialization. +message UintMapEntry { + option (gogoproto.equal) = true; + string key = 1; + string value = 2 [ + (gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Uint", + (gogoproto.nullable) = false + ]; +} + // Egress is the format for a swingset egress. message Egress { option (gogoproto.equal) = false; diff --git a/golang/cosmos/x/swingset/keeper/keeper.go b/golang/cosmos/x/swingset/keeper/keeper.go index dcd4790c445..28c7723b752 100644 --- a/golang/cosmos/x/swingset/keeper/keeper.go +++ b/golang/cosmos/x/swingset/keeper/keeper.go @@ -246,7 +246,10 @@ func (k Keeper) BlockingSend(ctx sdk.Context, action vm.Action) (string, error) } func (k Keeper) GetParams(ctx sdk.Context) (params types.Params) { - k.paramSpace.GetParamSet(ctx, ¶ms) + // Note the use of "IfExists"... + // migration fills in missing data with defaults, + // so it is the only consumer that should ever see a nil pair. + k.paramSpace.GetParamSetIfExists(ctx, ¶ms) return params } @@ -304,9 +307,12 @@ func (k Keeper) SetBeansOwing(ctx sdk.Context, addr sdk.AccAddress, beans sdkmat // ChargeBeans charges the given address the given number of beans. It divides // the beans into the number to debit immediately vs. the number to store in the // beansOwing. -func (k Keeper) ChargeBeans(ctx sdk.Context, addr sdk.AccAddress, beans sdkmath.Uint) error { - beansPerUnit := k.GetBeansPerUnit(ctx) - +func (k Keeper) ChargeBeans( + ctx sdk.Context, + beansPerUnit map[string]sdkmath.Uint, + addr sdk.AccAddress, + beans sdkmath.Uint, +) error { wasOwing := k.GetBeansOwing(ctx, addr) nowOwing := wasOwing.Add(beans) @@ -339,10 +345,13 @@ func (k Keeper) ChargeBeans(ctx sdk.Context, addr sdk.AccAddress, beans sdkmath. } // ChargeForSmartWallet charges the fee for provisioning a smart wallet. -func (k Keeper) ChargeForSmartWallet(ctx sdk.Context, addr sdk.AccAddress) error { - beansPerUnit := k.GetBeansPerUnit(ctx) +func (k Keeper) ChargeForSmartWallet( + ctx sdk.Context, + beansPerUnit map[string]sdkmath.Uint, + addr sdk.AccAddress, +) error { beans := beansPerUnit[types.BeansPerSmartWalletProvision] - err := k.ChargeBeans(ctx, addr, beans) + err := k.ChargeBeans(ctx, beansPerUnit, addr, beans) if err != nil { return err } diff --git a/golang/cosmos/x/swingset/types/default-params.go b/golang/cosmos/x/swingset/types/default-params.go index 46c7fe286d5..d267b48d659 100644 --- a/golang/cosmos/x/swingset/types/default-params.go +++ b/golang/cosmos/x/swingset/types/default-params.go @@ -22,13 +22,23 @@ const ( BeansPerXsnapComputron = "xsnapComputron" BeansPerSmartWalletProvision = "smartWalletProvision" + // PowerFlags. + PowerFlagSmartWallet = "SMART_WALLET" + // QueueSize keys. - // Keep up-to-date with updateQueueAllowed() in packanges/cosmic-swingset/src/launch-chain.js + // Keep up-to-date with updateQueueAllowed() in packages/cosmic-swingset/src/launch-chain.js QueueInbound = "inbound" QueueInboundMempool = "inbound_mempool" - // PowerFlags. - PowerFlagSmartWallet = "SMART_WALLET" + // Vat cleanup budget keys. + // Keep up-to-date with CleanupBudget in packages/cosmic-swingset/src/launch-chain.js + VatCleanupDefault = "default" + VatCleanupExports = "exports" + VatCleanupImports = "imports" + VatCleanupPromises = "promises" + VatCleanupKv = "kv" + VatCleanupSnapshots = "snapshots" + VatCleanupTranscripts = "transcripts" ) var ( @@ -62,10 +72,26 @@ var ( } DefaultInboundQueueMax = int32(1_000) - - DefaultQueueMax = []QueueSize{ + DefaultQueueMax = []QueueSize{ NewQueueSize(QueueInbound, DefaultInboundQueueMax), } + + DefaultVatCleanupDefault = sdk.NewUint(5) + // DefaultVatCleanupExports = DefaultVatCleanupDefault + // DefaultVatCleanupImports = DefaultVatCleanupDefault + // DefaultVatCleanupPromises = DefaultVatCleanupDefault + DefaultVatCleanupKv = sdk.NewUint(50) + // DefaultVatCleanupSnapshots = DefaultVatCleanupDefault + // DefaultVatCleanupTranscripts = DefaultVatCleanupDefault + DefaultVatCleanupBudget = []UintMapEntry{ + UintMapEntry{VatCleanupDefault, DefaultVatCleanupDefault}, + // UintMapEntry{VatCleanupExports, DefaultVatCleanupExports}, + // UintMapEntry{VatCleanupImports, DefaultVatCleanupImports}, + // UintMapEntry{VatCleanupPromises, DefaultVatCleanupPromises}, + UintMapEntry{VatCleanupKv, DefaultVatCleanupKv}, + // UintMapEntry{VatCleanupSnapshots, DefaultVatCleanupSnapshots}, + // UintMapEntry{VatCleanupTranscripts, DefaultVatCleanupTranscripts}, + } ) // move DefaultBeansPerUnit to a function to allow for boot overriding of the Default params diff --git a/golang/cosmos/x/swingset/types/expected_keepers.go b/golang/cosmos/x/swingset/types/expected_keepers.go index 3c20f7c3cf2..e8fdbf093b9 100644 --- a/golang/cosmos/x/swingset/types/expected_keepers.go +++ b/golang/cosmos/x/swingset/types/expected_keepers.go @@ -23,8 +23,8 @@ type AccountKeeper interface { type SwingSetKeeper interface { GetBeansPerUnit(ctx sdk.Context) map[string]sdkmath.Uint - ChargeBeans(ctx sdk.Context, addr sdk.AccAddress, beans sdkmath.Uint) error + ChargeBeans(ctx sdk.Context, beansPerUnit map[string]sdkmath.Uint, addr sdk.AccAddress, beans sdkmath.Uint) error IsHighPriorityAddress(ctx sdk.Context, addr sdk.AccAddress) (bool, error) GetSmartWalletState(ctx sdk.Context, addr sdk.AccAddress) SmartWalletState - ChargeForSmartWallet(ctx sdk.Context, addr sdk.AccAddress) error + ChargeForSmartWallet(ctx sdk.Context, beansPerUnit map[string]sdkmath.Uint, addr sdk.AccAddress) error } diff --git a/golang/cosmos/x/swingset/types/msgs.go b/golang/cosmos/x/swingset/types/msgs.go index 2a2ebeca465..1d0109b2048 100644 --- a/golang/cosmos/x/swingset/types/msgs.go +++ b/golang/cosmos/x/swingset/types/msgs.go @@ -8,9 +8,12 @@ import ( "strings" sdkioerrors "cosmossdk.io/errors" - "github.com/Agoric/agoric-sdk/golang/cosmos/vm" + sdkmath "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + + "github.com/Agoric/agoric-sdk/golang/cosmos/vm" ) const RouterKey = ModuleName // this was defined in your key.go file @@ -56,8 +59,14 @@ const ( // Charge an account address for the beans associated with given messages and storage. // See list of bean charges in default-params.go -func chargeAdmission(ctx sdk.Context, keeper SwingSetKeeper, addr sdk.AccAddress, msgs []string, storageLen uint64) error { - beansPerUnit := keeper.GetBeansPerUnit(ctx) +func chargeAdmission( + ctx sdk.Context, + keeper SwingSetKeeper, + beansPerUnit map[string]sdkmath.Uint, + addr sdk.AccAddress, + msgs []string, + storageLen uint64, +) error { beans := beansPerUnit[BeansPerInboundTx] beans = beans.Add(beansPerUnit[BeansPerMessage].MulUint64((uint64(len(msgs))))) for _, msg := range msgs { @@ -65,7 +74,7 @@ func chargeAdmission(ctx sdk.Context, keeper SwingSetKeeper, addr sdk.AccAddress } beans = beans.Add(beansPerUnit[BeansPerStorageByte].MulUint64(storageLen)) - return keeper.ChargeBeans(ctx, addr, beans) + return keeper.ChargeBeans(ctx, beansPerUnit, addr, beans) } // checkSmartWalletProvisioned verifies if a smart wallet message (MsgWalletAction @@ -74,7 +83,12 @@ func chargeAdmission(ctx sdk.Context, keeper SwingSetKeeper, addr sdk.AccAddress // 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 { +func checkSmartWalletProvisioned( + ctx sdk.Context, + keeper SwingSetKeeper, + beansPerUnit map[string]sdkmath.Uint, + addr sdk.AccAddress, +) error { walletState := keeper.GetSmartWalletState(ctx, addr) switch walletState { @@ -91,7 +105,7 @@ func checkSmartWalletProvisioned(ctx sdk.Context, keeper SwingSetKeeper, addr sd // 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) + return keeper.ChargeForSmartWallet(ctx, beansPerUnit, addr) } } @@ -118,7 +132,8 @@ func (msg MsgDeliverInbound) CheckAdmissibility(ctx sdk.Context, data interface{ } */ - return chargeAdmission(ctx, keeper, msg.Submitter, msg.Messages, 0) + beansPerUnit := keeper.GetBeansPerUnit(ctx) + return chargeAdmission(ctx, keeper, beansPerUnit, msg.Submitter, msg.Messages, 0) } // GetInboundMsgCount implements InboundMsgCarrier. @@ -184,12 +199,13 @@ func (msg MsgWalletAction) CheckAdmissibility(ctx sdk.Context, data interface{}) return sdkioerrors.Wrapf(sdkerrors.ErrInvalidRequest, "data must be a SwingSetKeeper, not a %T", data) } - err := checkSmartWalletProvisioned(ctx, keeper, msg.Owner) + beansPerUnit := keeper.GetBeansPerUnit(ctx) + err := checkSmartWalletProvisioned(ctx, keeper, beansPerUnit, msg.Owner) if err != nil { return err } - return chargeAdmission(ctx, keeper, msg.Owner, []string{msg.Action}, 0) + return chargeAdmission(ctx, keeper, beansPerUnit, msg.Owner, []string{msg.Action}, 0) } // GetInboundMsgCount implements InboundMsgCarrier. @@ -256,12 +272,13 @@ func (msg MsgWalletSpendAction) CheckAdmissibility(ctx sdk.Context, data interfa return sdkioerrors.Wrapf(sdkerrors.ErrInvalidRequest, "data must be a SwingSetKeeper, not a %T", data) } - err := checkSmartWalletProvisioned(ctx, keeper, msg.Owner) + beansPerUnit := keeper.GetBeansPerUnit(ctx) + err := checkSmartWalletProvisioned(ctx, keeper, beansPerUnit, msg.Owner) if err != nil { return err } - return chargeAdmission(ctx, keeper, msg.Owner, []string{msg.SpendAction}, 0) + return chargeAdmission(ctx, keeper, beansPerUnit, msg.Owner, []string{msg.SpendAction}, 0) } // GetInboundMsgCount implements InboundMsgCarrier. @@ -373,7 +390,8 @@ func (msg MsgInstallBundle) CheckAdmissibility(ctx sdk.Context, data interface{} if !ok { return sdkioerrors.Wrapf(sdkerrors.ErrInvalidRequest, "data must be a SwingSetKeeper, not a %T", data) } - return chargeAdmission(ctx, keeper, msg.Submitter, []string{msg.Bundle}, msg.ExpectedUncompressedSize()) + beansPerUnit := keeper.GetBeansPerUnit(ctx) + return chargeAdmission(ctx, keeper, beansPerUnit, msg.Submitter, []string{msg.Bundle}, msg.ExpectedUncompressedSize()) } // GetInboundMsgCount implements InboundMsgCarrier. diff --git a/golang/cosmos/x/swingset/types/params.go b/golang/cosmos/x/swingset/types/params.go index 9a18d03de8b..9b53ae58fbf 100644 --- a/golang/cosmos/x/swingset/types/params.go +++ b/golang/cosmos/x/swingset/types/params.go @@ -17,6 +17,7 @@ var ( ParamStoreKeyFeeUnitPrice = []byte("fee_unit_price") ParamStoreKeyPowerFlagFees = []byte("power_flag_fees") ParamStoreKeyQueueMax = []byte("queue_max") + ParamStoreKeyVatCleanupBudget = []byte("vat_cleanup_budget") ) func NewStringBeans(key string, beans sdkmath.Uint) StringBeans { @@ -53,6 +54,7 @@ func DefaultParams() Params { FeeUnitPrice: DefaultFeeUnitPrice, PowerFlagFees: DefaultPowerFlagFees, QueueMax: DefaultQueueMax, + VatCleanupBudget: DefaultVatCleanupBudget, } } @@ -69,6 +71,7 @@ func (p *Params) ParamSetPairs() paramtypes.ParamSetPairs { paramtypes.NewParamSetPair(ParamStoreKeyBootstrapVatConfig, &p.BootstrapVatConfig, validateBootstrapVatConfig), paramtypes.NewParamSetPair(ParamStoreKeyPowerFlagFees, &p.PowerFlagFees, validatePowerFlagFees), paramtypes.NewParamSetPair(ParamStoreKeyQueueMax, &p.QueueMax, validateQueueMax), + paramtypes.NewParamSetPair(ParamStoreKeyVatCleanupBudget, &p.VatCleanupBudget, validateVatCleanupBudget), } } @@ -89,6 +92,9 @@ func (p Params) ValidateBasic() error { if err := validateQueueMax(p.QueueMax); err != nil { return err } + if err := validateVatCleanupBudget(p.VatCleanupBudget); err != nil { + return err + } return nil } @@ -165,20 +171,42 @@ func validateQueueMax(i interface{}) error { return nil } +func validateVatCleanupBudget(i interface{}) error { + entries, ok := i.([]UintMapEntry) + if !ok { + return fmt.Errorf("invalid parameter type: %T", i) + } + hasDefault := false + for _, entry := range entries { + if entry.Key == VatCleanupDefault { + hasDefault = true + break + } + } + if len(entries) > 0 && !hasDefault { + return fmt.Errorf("`default` must be present in a non-empty vat cleanup budget") + } + return nil +} + // UpdateParams appends any missing params, configuring them to their defaults, // then returning the updated params or an error. Existing params are not // modified, regardless of their value, and they are not removed if they no // longer appear in the defaults. func UpdateParams(params Params) (Params, error) { - newBpu, err := appendMissingDefaultBeansPerUnit(params.BeansPerUnit, DefaultBeansPerUnit()) + newBpu, err := appendMissingDefaults(params.BeansPerUnit, DefaultBeansPerUnit()) if err != nil { return params, err } - newPff, err := appendMissingDefaultPowerFlagFees(params.PowerFlagFees, DefaultPowerFlagFees) + newPff, err := appendMissingDefaults(params.PowerFlagFees, DefaultPowerFlagFees) if err != nil { return params, err } - newQm, err := appendMissingDefaultQueueSize(params.QueueMax, DefaultQueueMax) + newQm, err := appendMissingDefaults(params.QueueMax, DefaultQueueMax) + if err != nil { + return params, err + } + newVcb, err := appendMissingDefaults(params.VatCleanupBudget, DefaultVatCleanupBudget) if err != nil { return params, err } @@ -186,55 +214,37 @@ func UpdateParams(params Params) (Params, error) { params.BeansPerUnit = newBpu params.PowerFlagFees = newPff params.QueueMax = newQm + params.VatCleanupBudget = newVcb return params, nil } -// appendMissingDefaultBeansPerUnit appends the default beans per unit entries -// not in the list of bean costs already, returning the possibly-updated list, -// or an error. -func appendMissingDefaultBeansPerUnit(bpu []StringBeans, defaultBpu []StringBeans) ([]StringBeans, error) { - existingBpu := make(map[string]struct{}, len(bpu)) - for _, ob := range bpu { - existingBpu[ob.Key] = struct{}{} - } - - for _, b := range defaultBpu { - if _, exists := existingBpu[b.Key]; !exists { - bpu = append(bpu, b) +// appendMissingDefaults appends to an input list any missing entries with their +// respective default values and returns the result. +func appendMissingDefaults[Entry StringBeans | PowerFlagFee | QueueSize | UintMapEntry](entries []Entry, defaults []Entry) ([]Entry, error) { + getKey := func(entry any) string { + switch e := entry.(type) { + case StringBeans: + return e.Key + case PowerFlagFee: + return e.PowerFlag + case QueueSize: + return e.Key + case UintMapEntry: + return e.Key } + panic("unreachable") } - return bpu, nil -} -// appendMissingDefaultPowerFlagFees appends the default power flag fee entries -// not in the list of power flags already, returning the possibly-updated list, -// or an error. -func appendMissingDefaultPowerFlagFees(pff []PowerFlagFee, defaultPff []PowerFlagFee) ([]PowerFlagFee, error) { - existingPff := make(map[string]struct{}, len(pff)) - for _, of := range pff { - existingPff[of.PowerFlag] = struct{}{} + existingKeys := make(map[string]bool, len(entries)) + for _, entry := range entries { + existingKeys[getKey(entry)] = true } - for _, f := range defaultPff { - if _, exists := existingPff[f.PowerFlag]; !exists { - pff = append(pff, f) + for _, defaultEntry := range defaults { + if exists := existingKeys[getKey(defaultEntry)]; !exists { + entries = append(entries, defaultEntry) } } - return pff, nil -} - -// appendMissingDefaultQueueSize appends the default queue size entries not in -// the list of sizes already, returning the possibly-updated list, or an error. -func appendMissingDefaultQueueSize(qs []QueueSize, defaultQs []QueueSize) ([]QueueSize, error) { - existingQs := make(map[string]struct{}, len(qs)) - for _, os := range qs { - existingQs[os.Key] = struct{}{} - } - for _, s := range defaultQs { - if _, exists := existingQs[s.Key]; !exists { - qs = append(qs, s) - } - } - return qs, nil + return entries, nil } diff --git a/golang/cosmos/x/swingset/types/params_test.go b/golang/cosmos/x/swingset/types/params_test.go index e75986736ad..ff3a7cbe149 100644 --- a/golang/cosmos/x/swingset/types/params_test.go +++ b/golang/cosmos/x/swingset/types/params_test.go @@ -80,7 +80,7 @@ func TestAddStorageBeanCost(t *testing.T) { }, } { t.Run(tt.name, func(t *testing.T) { - got, err := appendMissingDefaultBeansPerUnit(tt.in, []StringBeans{defaultStorageCost}) + got, err := appendMissingDefaults(tt.in, []StringBeans{defaultStorageCost}) if err != nil { t.Errorf("got error %v", err) } else if !reflect.DeepEqual(got, tt.want) { @@ -90,27 +90,93 @@ func TestAddStorageBeanCost(t *testing.T) { } } -func TestUpdateParams(t *testing.T) { - +func TestUpdateParamsFromEmpty(t *testing.T) { in := Params{ - BeansPerUnit: []beans{}, - BootstrapVatConfig: "baz", + BeansPerUnit: nil, + BootstrapVatConfig: "", FeeUnitPrice: sdk.NewCoins(sdk.NewInt64Coin("denom", 789)), - PowerFlagFees: []PowerFlagFee{}, - QueueMax: []QueueSize{}, + PowerFlagFees: nil, + QueueMax: nil, + VatCleanupBudget: nil, } want := Params{ BeansPerUnit: DefaultBeansPerUnit(), - BootstrapVatConfig: "baz", + BootstrapVatConfig: "", FeeUnitPrice: sdk.NewCoins(sdk.NewInt64Coin("denom", 789)), PowerFlagFees: DefaultPowerFlagFees, QueueMax: DefaultQueueMax, + VatCleanupBudget: DefaultVatCleanupBudget, + } + got, err := UpdateParams(in) + if err != nil { + t.Fatalf("UpdateParam error %v", err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("GOT\n%v\nWANTED\n%v", got, want) + } +} + +func TestUpdateParamsFromExisting(t *testing.T) { + defaultBeansPerUnit := DefaultBeansPerUnit() + customBeansPerUnit := NewStringBeans("foo", sdk.NewUint(1)) + customPowerFlagFee := NewPowerFlagFee("bar", sdk.NewCoins(sdk.NewInt64Coin("baz", 2))) + customQueueSize := NewQueueSize("qux", int32(3)) + customVatCleanup := UintMapEntry{"corge", sdk.NewUint(4)} + in := Params{ + BeansPerUnit: append([]StringBeans{customBeansPerUnit}, defaultBeansPerUnit[2:4]...), + BootstrapVatConfig: "", + FeeUnitPrice: sdk.NewCoins(sdk.NewInt64Coin("denom", 789)), + PowerFlagFees: []PowerFlagFee{customPowerFlagFee}, + QueueMax: []QueueSize{NewQueueSize(QueueInbound, int32(10)), customQueueSize}, + VatCleanupBudget: []UintMapEntry{customVatCleanup, UintMapEntry{VatCleanupDefault, sdk.NewUint(10)}}, + } + want := Params{ + BeansPerUnit: append(append(in.BeansPerUnit, defaultBeansPerUnit[0:2]...), defaultBeansPerUnit[4:]...), + BootstrapVatConfig: in.BootstrapVatConfig, + FeeUnitPrice: in.FeeUnitPrice, + PowerFlagFees: append(in.PowerFlagFees, DefaultPowerFlagFees...), + QueueMax: in.QueueMax, + VatCleanupBudget: append(in.VatCleanupBudget, DefaultVatCleanupBudget[1:]...), } got, err := UpdateParams(in) if err != nil { t.Fatalf("UpdateParam error %v", err) } if !reflect.DeepEqual(got, want) { - t.Errorf("got %v, want %v", got, want) + t.Errorf("GOT\n%v\nWANTED\n%v", got, want) + } +} + +func TestValidateParams(t *testing.T) { + params := Params{ + BeansPerUnit: DefaultBeansPerUnit(), + BootstrapVatConfig: "foo", + FeeUnitPrice: sdk.NewCoins(sdk.NewInt64Coin("denom", 789)), + PowerFlagFees: DefaultPowerFlagFees, + QueueMax: DefaultQueueMax, + VatCleanupBudget: DefaultVatCleanupBudget, + } + err := params.ValidateBasic() + if err != nil { + t.Errorf("unexpected ValidateBasic() error with default params: %v", err) + } + + customVatCleanup := UintMapEntry{"corge", sdk.NewUint(4)} + params.VatCleanupBudget = append(params.VatCleanupBudget, customVatCleanup) + err = params.ValidateBasic() + if err != nil { + t.Errorf("unexpected ValidateBasic() error with extended params: %v", err) + } + + params.VatCleanupBudget = params.VatCleanupBudget[1:] + err = params.ValidateBasic() + if err == nil { + t.Errorf("ValidateBasic() failed to reject VatCleanupBudget with missing `default` %v", params.VatCleanupBudget) + } + + params.VatCleanupBudget = []UintMapEntry{} + err = params.ValidateBasic() + if err != nil { + t.Errorf("unexpected ValidateBasic() error with empty VatCleanupBudget: %v", params.VatCleanupBudget) } } diff --git a/golang/cosmos/x/swingset/types/swingset.pb.go b/golang/cosmos/x/swingset/types/swingset.pb.go index 959c030f8d5..3fbbcccf06e 100644 --- a/golang/cosmos/x/swingset/types/swingset.pb.go +++ b/golang/cosmos/x/swingset/types/swingset.pb.go @@ -161,6 +161,15 @@ type Params struct { // nodes must all serialize and deserialize the existing order without // permuting it. QueueMax []QueueSize `protobuf:"bytes,5,rep,name=queue_max,json=queueMax,proto3" json:"queue_max"` + // Vat cleanup budget values. + // These values are used by SwingSet to control the pace of removing data + // associated with a terminated vat as described at + // https://github.com/Agoric/agoric-sdk/blob/master/packages/SwingSet/docs/run-policy.md#terminated-vat-cleanup + // + // There is no required order to this list of entries, but all the chain + // nodes must all serialize and deserialize the existing order without + // permuting it. + VatCleanupBudget []UintMapEntry `protobuf:"bytes,6,rep,name=vat_cleanup_budget,json=vatCleanupBudget,proto3" json:"vat_cleanup_budget"` } func (m *Params) Reset() { *m = Params{} } @@ -230,6 +239,13 @@ func (m *Params) GetQueueMax() []QueueSize { return nil } +func (m *Params) GetVatCleanupBudget() []UintMapEntry { + if m != nil { + return m.VatCleanupBudget + } + return nil +} + // The current state of the module. type State struct { // The allowed number of items to add to queues, as determined by SwingSet. @@ -379,6 +395,7 @@ func (m *PowerFlagFee) GetFee() github_com_cosmos_cosmos_sdk_types.Coins { } // Map element of a string key to a size. +// TODO: Replace with UintMapEntry? type QueueSize struct { // What the size is for. Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` @@ -433,6 +450,54 @@ func (m *QueueSize) GetSize_() int32 { return 0 } +// Map element of a string key to an unsigned integer. +// The value uses cosmos-sdk Uint rather than a native Go type to ensure that +// zeroes survive "omitempty" JSON serialization. +type UintMapEntry struct { + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Value github_com_cosmos_cosmos_sdk_types.Uint `protobuf:"bytes,2,opt,name=value,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Uint" json:"value"` +} + +func (m *UintMapEntry) Reset() { *m = UintMapEntry{} } +func (m *UintMapEntry) String() string { return proto.CompactTextString(m) } +func (*UintMapEntry) ProtoMessage() {} +func (*UintMapEntry) Descriptor() ([]byte, []int) { + return fileDescriptor_ff9c341e0de15f8b, []int{7} +} +func (m *UintMapEntry) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *UintMapEntry) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_UintMapEntry.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *UintMapEntry) XXX_Merge(src proto.Message) { + xxx_messageInfo_UintMapEntry.Merge(m, src) +} +func (m *UintMapEntry) XXX_Size() int { + return m.Size() +} +func (m *UintMapEntry) XXX_DiscardUnknown() { + xxx_messageInfo_UintMapEntry.DiscardUnknown(m) +} + +var xxx_messageInfo_UintMapEntry proto.InternalMessageInfo + +func (m *UintMapEntry) GetKey() string { + if m != nil { + return m.Key + } + return "" +} + // Egress is the format for a swingset egress. type Egress struct { Nickname string `protobuf:"bytes,1,opt,name=nickname,proto3" json:"nickname" yaml:"nickname"` @@ -445,7 +510,7 @@ func (m *Egress) Reset() { *m = Egress{} } func (m *Egress) String() string { return proto.CompactTextString(m) } func (*Egress) ProtoMessage() {} func (*Egress) Descriptor() ([]byte, []int) { - return fileDescriptor_ff9c341e0de15f8b, []int{7} + return fileDescriptor_ff9c341e0de15f8b, []int{8} } func (m *Egress) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -508,7 +573,7 @@ func (m *SwingStoreArtifact) Reset() { *m = SwingStoreArtifact{} } func (m *SwingStoreArtifact) String() string { return proto.CompactTextString(m) } func (*SwingStoreArtifact) ProtoMessage() {} func (*SwingStoreArtifact) Descriptor() ([]byte, []int) { - return fileDescriptor_ff9c341e0de15f8b, []int{8} + return fileDescriptor_ff9c341e0de15f8b, []int{9} } func (m *SwingStoreArtifact) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -559,6 +624,7 @@ func init() { proto.RegisterType((*StringBeans)(nil), "agoric.swingset.StringBeans") proto.RegisterType((*PowerFlagFee)(nil), "agoric.swingset.PowerFlagFee") proto.RegisterType((*QueueSize)(nil), "agoric.swingset.QueueSize") + proto.RegisterType((*UintMapEntry)(nil), "agoric.swingset.UintMapEntry") proto.RegisterType((*Egress)(nil), "agoric.swingset.Egress") proto.RegisterType((*SwingStoreArtifact)(nil), "agoric.swingset.SwingStoreArtifact") } @@ -566,60 +632,64 @@ func init() { func init() { proto.RegisterFile("agoric/swingset/swingset.proto", fileDescriptor_ff9c341e0de15f8b) } var fileDescriptor_ff9c341e0de15f8b = []byte{ - // 842 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x55, 0xcf, 0x6f, 0xe3, 0x44, - 0x14, 0x8e, 0x49, 0x52, 0x9a, 0x97, 0x6c, 0xbb, 0x0c, 0x95, 0x36, 0x54, 0x6c, 0xa6, 0xf2, 0x85, - 0x4a, 0xab, 0x8d, 0xb7, 0x20, 0x84, 0x94, 0x15, 0x87, 0xb8, 0xea, 0x6a, 0x25, 0x04, 0x0a, 0x8e, - 0xca, 0x01, 0x81, 0xac, 0x89, 0x33, 0x31, 0xd3, 0x3a, 0x1e, 0xef, 0xcc, 0xf4, 0xd7, 0xfe, 0x03, - 0x70, 0x41, 0x42, 0x9c, 0x38, 0xf6, 0xcc, 0x5f, 0xb2, 0xc7, 0x3d, 0x22, 0x0e, 0x06, 0xb5, 0x17, - 0xd4, 0x63, 0x8e, 0x48, 0x48, 0x68, 0x66, 0x1c, 0xc7, 0xa2, 0x48, 0xf4, 0xc2, 0x29, 0xf3, 0x7e, - 0x7d, 0xef, 0x7d, 0xdf, 0x1b, 0x4f, 0xa0, 0x47, 0x62, 0x2e, 0x58, 0xe4, 0xc9, 0x33, 0x96, 0xc6, - 0x92, 0xaa, 0xf2, 0xd0, 0xcf, 0x04, 0x57, 0x1c, 0x6d, 0xda, 0x78, 0x7f, 0xe9, 0xde, 0xde, 0x8a, - 0x79, 0xcc, 0x4d, 0xcc, 0xd3, 0x27, 0x9b, 0xb6, 0xdd, 0x8b, 0xb8, 0x9c, 0x73, 0xe9, 0x4d, 0x88, - 0xa4, 0xde, 0xe9, 0xde, 0x84, 0x2a, 0xb2, 0xe7, 0x45, 0x9c, 0xa5, 0x36, 0xee, 0x7e, 0xeb, 0xc0, - 0xfd, 0x7d, 0x2e, 0xe8, 0xc1, 0x29, 0x49, 0x46, 0x82, 0x67, 0x5c, 0x92, 0x04, 0x6d, 0x41, 0x53, - 0x31, 0x95, 0xd0, 0xae, 0xb3, 0xe3, 0xec, 0xb6, 0x02, 0x6b, 0xa0, 0x1d, 0x68, 0x4f, 0xa9, 0x8c, - 0x04, 0xcb, 0x14, 0xe3, 0x69, 0xf7, 0x0d, 0x13, 0xab, 0xba, 0xd0, 0x87, 0xd0, 0xa4, 0xa7, 0x24, - 0x91, 0xdd, 0xfa, 0x4e, 0x7d, 0xb7, 0xfd, 0xfe, 0x3b, 0xfd, 0x7f, 0xcc, 0xd8, 0x5f, 0x76, 0xf2, - 0x1b, 0xaf, 0x72, 0x5c, 0x0b, 0x6c, 0xf6, 0xa0, 0xf1, 0xdd, 0x25, 0xae, 0xb9, 0x12, 0xd6, 0x97, - 0x61, 0x34, 0x80, 0xce, 0x91, 0xe4, 0x69, 0x98, 0x51, 0x31, 0x67, 0x4a, 0xda, 0x39, 0xfc, 0x07, - 0x8b, 0x1c, 0xbf, 0x7d, 0x41, 0xe6, 0xc9, 0xc0, 0xad, 0x46, 0xdd, 0xa0, 0xad, 0xcd, 0x91, 0xb5, - 0xd0, 0x23, 0x78, 0xf3, 0x48, 0x86, 0x11, 0x9f, 0x52, 0x3b, 0xa2, 0x8f, 0x16, 0x39, 0xde, 0x58, - 0x96, 0x99, 0x80, 0x1b, 0xac, 0x1d, 0xc9, 0x7d, 0x7d, 0xf8, 0xbe, 0x0e, 0x6b, 0x23, 0x22, 0xc8, - 0x5c, 0xa2, 0xe7, 0xb0, 0x31, 0xa1, 0x24, 0x95, 0x1a, 0x36, 0x3c, 0x49, 0x99, 0xea, 0x3a, 0x86, - 0xc5, 0xbb, 0xb7, 0x58, 0x8c, 0x95, 0x60, 0x69, 0xec, 0xeb, 0xe4, 0x82, 0x48, 0xc7, 0x54, 0x8e, - 0xa8, 0x38, 0x4c, 0x99, 0x42, 0x2f, 0x60, 0x63, 0x46, 0xa9, 0xc1, 0x08, 0x33, 0xc1, 0x22, 0x3d, - 0x88, 0xd5, 0xc3, 0x2e, 0xa3, 0xaf, 0x97, 0xd1, 0x2f, 0x96, 0xd1, 0xdf, 0xe7, 0x2c, 0xf5, 0x9f, - 0x68, 0x98, 0x9f, 0x7f, 0xc3, 0xbb, 0x31, 0x53, 0xdf, 0x9c, 0x4c, 0xfa, 0x11, 0x9f, 0x7b, 0xc5, - 0xe6, 0xec, 0xcf, 0x63, 0x39, 0x3d, 0xf6, 0xd4, 0x45, 0x46, 0xa5, 0x29, 0x90, 0x41, 0x67, 0x46, - 0xa9, 0xee, 0x36, 0xd2, 0x0d, 0xd0, 0x13, 0xd8, 0x9a, 0x70, 0xae, 0xa4, 0x12, 0x24, 0x0b, 0x4f, - 0x89, 0x0a, 0x23, 0x9e, 0xce, 0x58, 0xdc, 0xad, 0x9b, 0x25, 0xa1, 0x32, 0xf6, 0x05, 0x51, 0xfb, - 0x26, 0x82, 0x3e, 0x81, 0xcd, 0x8c, 0x9f, 0x51, 0x11, 0xce, 0x12, 0x12, 0x87, 0x33, 0x4a, 0x65, - 0xb7, 0x61, 0xa6, 0x7c, 0x78, 0x8b, 0xef, 0x48, 0xe7, 0x3d, 0x4b, 0x48, 0xfc, 0x8c, 0xd2, 0x82, - 0xf0, 0xbd, 0xac, 0xe2, 0x93, 0xe8, 0x63, 0x68, 0xbd, 0x38, 0xa1, 0x27, 0x34, 0x9c, 0x93, 0xf3, - 0x6e, 0xd3, 0xc0, 0x6c, 0xdf, 0x82, 0xf9, 0x5c, 0x67, 0x8c, 0xd9, 0xcb, 0x25, 0xc6, 0xba, 0x29, - 0xf9, 0x94, 0x9c, 0x0f, 0xd6, 0x7f, 0xba, 0xc4, 0xb5, 0x3f, 0x2e, 0xb1, 0xe3, 0x7e, 0x06, 0xcd, - 0xb1, 0x22, 0x8a, 0xa2, 0x03, 0xb8, 0x67, 0x11, 0x49, 0x92, 0xf0, 0x33, 0x3a, 0x2d, 0x96, 0xf1, - 0xdf, 0xa8, 0x1d, 0x53, 0x36, 0xb4, 0x55, 0x6e, 0x02, 0xed, 0xca, 0xb6, 0xd0, 0x7d, 0xa8, 0x1f, - 0xd3, 0x8b, 0xe2, 0x5a, 0xeb, 0x23, 0x3a, 0x80, 0xa6, 0xd9, 0x5d, 0x71, 0x57, 0x3c, 0x8d, 0xf1, - 0x6b, 0x8e, 0xdf, 0xbb, 0xc3, 0x1e, 0x0e, 0x59, 0xaa, 0x02, 0x5b, 0x3d, 0x68, 0x98, 0xe9, 0x7f, - 0x74, 0xa0, 0x53, 0x15, 0x0b, 0x3d, 0x04, 0x58, 0x89, 0x5c, 0xb4, 0x6d, 0x95, 0xd2, 0xa1, 0xaf, - 0xa1, 0x3e, 0xa3, 0xff, 0xcb, 0xed, 0xd0, 0xb8, 0xc5, 0x50, 0x1f, 0x41, 0xab, 0xd4, 0xe8, 0x5f, - 0x04, 0x40, 0xd0, 0x90, 0xec, 0xa5, 0xfd, 0x56, 0x9a, 0x81, 0x39, 0x17, 0x85, 0x7f, 0x39, 0xb0, - 0x76, 0x10, 0x0b, 0x2a, 0x25, 0x7a, 0x0a, 0xeb, 0x29, 0x8b, 0x8e, 0x53, 0x32, 0x2f, 0xde, 0x04, - 0x1f, 0xdf, 0xe4, 0xb8, 0xf4, 0x2d, 0x72, 0xbc, 0x69, 0x3f, 0xb0, 0xa5, 0xc7, 0x0d, 0xca, 0x20, - 0xfa, 0x0a, 0x1a, 0x19, 0xa5, 0xc2, 0x74, 0xe8, 0xf8, 0xcf, 0x6f, 0x72, 0x6c, 0xec, 0x45, 0x8e, - 0xdb, 0xb6, 0x48, 0x5b, 0xee, 0x9f, 0x39, 0x7e, 0x7c, 0x07, 0x7a, 0xc3, 0x28, 0x1a, 0x4e, 0xa7, - 0x7a, 0xa8, 0xc0, 0xa0, 0xa0, 0x00, 0xda, 0x2b, 0x89, 0xed, 0xcb, 0xd3, 0xf2, 0xf7, 0xae, 0x72, - 0x0c, 0xe5, 0x26, 0xe4, 0x4d, 0x8e, 0xa1, 0x54, 0x5d, 0x2e, 0x72, 0xfc, 0x56, 0xd1, 0xb8, 0xf4, - 0xb9, 0x41, 0x25, 0xc1, 0xf0, 0xaf, 0xb9, 0x0a, 0xd0, 0x58, 0xdf, 0xb2, 0xb1, 0xe2, 0x82, 0x0e, - 0x85, 0x62, 0x33, 0x12, 0x29, 0xf4, 0x08, 0x1a, 0x15, 0x19, 0x1e, 0x68, 0x36, 0x85, 0x04, 0x05, - 0x1b, 0x4b, 0xdf, 0x38, 0x75, 0xf2, 0x94, 0x28, 0x52, 0x50, 0x37, 0xc9, 0xda, 0x5e, 0x25, 0x6b, - 0xcb, 0x0d, 0x8c, 0xd3, 0x76, 0xf5, 0x0f, 0x5f, 0x5d, 0xf5, 0x9c, 0xd7, 0x57, 0x3d, 0xe7, 0xf7, - 0xab, 0x9e, 0xf3, 0xc3, 0x75, 0xaf, 0xf6, 0xfa, 0xba, 0x57, 0xfb, 0xe5, 0xba, 0x57, 0xfb, 0xf2, - 0x69, 0x45, 0x9e, 0xa1, 0xfd, 0x73, 0xb0, 0x1f, 0x83, 0x91, 0x27, 0xe6, 0x09, 0x49, 0xe3, 0xa5, - 0x6e, 0xe7, 0xab, 0xff, 0x0d, 0xa3, 0xdb, 0x64, 0xcd, 0x3c, 0xf7, 0x1f, 0xfc, 0x1d, 0x00, 0x00, - 0xff, 0xff, 0xee, 0x34, 0x5f, 0xf6, 0x57, 0x06, 0x00, 0x00, + // 897 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x55, 0x4d, 0x6f, 0xe3, 0x44, + 0x18, 0x8e, 0xc9, 0x07, 0xcd, 0x9b, 0x6c, 0x5b, 0x86, 0x4a, 0x1b, 0x2a, 0x36, 0xae, 0x7c, 0xa1, + 0xd2, 0x6a, 0x93, 0x2d, 0x08, 0x21, 0x65, 0xc5, 0x21, 0x8e, 0xb2, 0x5a, 0x09, 0x2d, 0xca, 0x3a, + 0x2a, 0x07, 0x04, 0xb2, 0x26, 0xce, 0xc4, 0x4c, 0xeb, 0x78, 0xbc, 0x9e, 0x49, 0xda, 0xee, 0x1f, + 0x80, 0x23, 0xe2, 0xc4, 0xb1, 0x67, 0x7e, 0xc9, 0x1e, 0xf7, 0x88, 0x38, 0x98, 0x55, 0x7b, 0x41, + 0x3d, 0xe6, 0x88, 0x84, 0x84, 0xe6, 0x23, 0xae, 0x45, 0x17, 0x51, 0x21, 0xed, 0xc9, 0xf3, 0x7e, + 0x3f, 0xef, 0xf3, 0x8c, 0x6d, 0x68, 0xe3, 0x90, 0xa5, 0x34, 0xe8, 0xf2, 0x13, 0x1a, 0x87, 0x9c, + 0x88, 0xfc, 0xd0, 0x49, 0x52, 0x26, 0x18, 0xda, 0xd2, 0xf1, 0xce, 0xda, 0xbd, 0xbb, 0x13, 0xb2, + 0x90, 0xa9, 0x58, 0x57, 0x9e, 0x74, 0xda, 0x6e, 0x3b, 0x60, 0x7c, 0xce, 0x78, 0x77, 0x82, 0x39, + 0xe9, 0x2e, 0x0f, 0x26, 0x44, 0xe0, 0x83, 0x6e, 0xc0, 0x68, 0xac, 0xe3, 0xce, 0xf7, 0x16, 0x6c, + 0x0f, 0x58, 0x4a, 0x86, 0x4b, 0x1c, 0x8d, 0x52, 0x96, 0x30, 0x8e, 0x23, 0xb4, 0x03, 0x55, 0x41, + 0x45, 0x44, 0x5a, 0xd6, 0x9e, 0xb5, 0x5f, 0xf7, 0xb4, 0x81, 0xf6, 0xa0, 0x31, 0x25, 0x3c, 0x48, + 0x69, 0x22, 0x28, 0x8b, 0x5b, 0xef, 0xa8, 0x58, 0xd1, 0x85, 0x3e, 0x85, 0x2a, 0x59, 0xe2, 0x88, + 0xb7, 0xca, 0x7b, 0xe5, 0xfd, 0xc6, 0xc7, 0x1f, 0x74, 0xfe, 0x81, 0xb1, 0xb3, 0x9e, 0xe4, 0x56, + 0x5e, 0x66, 0x76, 0xc9, 0xd3, 0xd9, 0xbd, 0xca, 0x0f, 0xe7, 0x76, 0xc9, 0xe1, 0xb0, 0xb1, 0x0e, + 0xa3, 0x1e, 0x34, 0x8f, 0x38, 0x8b, 0xfd, 0x84, 0xa4, 0x73, 0x2a, 0xb8, 0xc6, 0xe1, 0xde, 0x5d, + 0x65, 0xf6, 0xfb, 0x67, 0x78, 0x1e, 0xf5, 0x9c, 0x62, 0xd4, 0xf1, 0x1a, 0xd2, 0x1c, 0x69, 0x0b, + 0xdd, 0x87, 0x77, 0x8f, 0xb8, 0x1f, 0xb0, 0x29, 0xd1, 0x10, 0x5d, 0xb4, 0xca, 0xec, 0xcd, 0x75, + 0x99, 0x0a, 0x38, 0x5e, 0xed, 0x88, 0x0f, 0xe4, 0xe1, 0x75, 0x19, 0x6a, 0x23, 0x9c, 0xe2, 0x39, + 0x47, 0x4f, 0x60, 0x73, 0x42, 0x70, 0xcc, 0x65, 0x5b, 0x7f, 0x11, 0x53, 0xd1, 0xb2, 0xd4, 0x16, + 0x1f, 0xde, 0xd8, 0x62, 0x2c, 0x52, 0x1a, 0x87, 0xae, 0x4c, 0x36, 0x8b, 0x34, 0x55, 0xe5, 0x88, + 0xa4, 0x87, 0x31, 0x15, 0xe8, 0x39, 0x6c, 0xce, 0x08, 0x51, 0x3d, 0xfc, 0x24, 0xa5, 0x81, 0x04, + 0xa2, 0xf9, 0xd0, 0x62, 0x74, 0xa4, 0x18, 0x1d, 0x23, 0x46, 0x67, 0xc0, 0x68, 0xec, 0x3e, 0x94, + 0x6d, 0x7e, 0xf9, 0xdd, 0xde, 0x0f, 0xa9, 0xf8, 0x6e, 0x31, 0xe9, 0x04, 0x6c, 0xde, 0x35, 0xca, + 0xe9, 0xc7, 0x03, 0x3e, 0x3d, 0xee, 0x8a, 0xb3, 0x84, 0x70, 0x55, 0xc0, 0xbd, 0xe6, 0x8c, 0x10, + 0x39, 0x6d, 0x24, 0x07, 0xa0, 0x87, 0xb0, 0x33, 0x61, 0x4c, 0x70, 0x91, 0xe2, 0xc4, 0x5f, 0x62, + 0xe1, 0x07, 0x2c, 0x9e, 0xd1, 0xb0, 0x55, 0x56, 0x22, 0xa1, 0x3c, 0xf6, 0x15, 0x16, 0x03, 0x15, + 0x41, 0x5f, 0xc0, 0x56, 0xc2, 0x4e, 0x48, 0xea, 0xcf, 0x22, 0x1c, 0xfa, 0x33, 0x42, 0x78, 0xab, + 0xa2, 0x50, 0xde, 0xbb, 0xb1, 0xef, 0x48, 0xe6, 0x3d, 0x8e, 0x70, 0xf8, 0x98, 0x10, 0xb3, 0xf0, + 0x9d, 0xa4, 0xe0, 0xe3, 0xe8, 0x73, 0xa8, 0x3f, 0x5f, 0x90, 0x05, 0xf1, 0xe7, 0xf8, 0xb4, 0x55, + 0x55, 0x6d, 0x76, 0x6f, 0xb4, 0x79, 0x26, 0x33, 0xc6, 0xf4, 0xc5, 0xba, 0xc7, 0x86, 0x2a, 0x79, + 0x8a, 0x4f, 0xd1, 0x33, 0x40, 0x0a, 0x73, 0x44, 0x70, 0xbc, 0x48, 0xfc, 0xc9, 0x62, 0x1a, 0x12, + 0xd1, 0xaa, 0xfd, 0x0b, 0x9c, 0x43, 0x1a, 0x8b, 0xa7, 0x38, 0x19, 0xc6, 0x22, 0x3d, 0x33, 0xad, + 0xb6, 0x97, 0x58, 0x0c, 0x74, 0xb5, 0xab, 0x8a, 0x7b, 0x1b, 0x3f, 0x9f, 0xdb, 0xa5, 0x3f, 0xce, + 0x6d, 0xcb, 0xf9, 0x12, 0xaa, 0x63, 0x81, 0x05, 0x41, 0x43, 0xb8, 0xa3, 0x41, 0xe2, 0x28, 0x62, + 0x27, 0x64, 0x6a, 0xf4, 0xfd, 0x6f, 0xa0, 0x4d, 0x55, 0xd6, 0xd7, 0x55, 0x4e, 0x04, 0x8d, 0xc2, + 0x05, 0x40, 0xdb, 0x50, 0x3e, 0x26, 0x67, 0xe6, 0x4d, 0x91, 0x47, 0x34, 0x84, 0xaa, 0xba, 0x0e, + 0xe6, 0xfa, 0x75, 0x65, 0x8f, 0xdf, 0x32, 0xfb, 0xa3, 0x5b, 0x48, 0x2b, 0x57, 0xf3, 0x74, 0x75, + 0xaf, 0xa2, 0xd0, 0xff, 0x64, 0x41, 0xb3, 0xc8, 0x3f, 0xba, 0x07, 0x70, 0xad, 0x9b, 0x19, 0x5b, + 0xcf, 0xd5, 0x40, 0xdf, 0x42, 0x79, 0x46, 0xde, 0xca, 0x85, 0x93, 0x7d, 0x0d, 0xa8, 0xcf, 0xa0, + 0x9e, 0x73, 0xf4, 0x06, 0x02, 0x10, 0x54, 0x38, 0x7d, 0xa1, 0x5f, 0xbf, 0xaa, 0xa7, 0xce, 0xa6, + 0x70, 0x0e, 0xcd, 0xa2, 0x7a, 0x6f, 0x26, 0x6f, 0x89, 0xa3, 0x05, 0xf9, 0xdf, 0xe4, 0xa9, 0x6a, + 0x33, 0xee, 0x2f, 0x0b, 0x6a, 0xc3, 0x30, 0x25, 0x9c, 0xa3, 0x47, 0xb0, 0x11, 0xd3, 0xe0, 0x38, + 0xc6, 0x73, 0xf3, 0x55, 0x73, 0xed, 0xab, 0xcc, 0xce, 0x7d, 0xab, 0xcc, 0xde, 0xd2, 0x9f, 0x88, + 0xb5, 0xc7, 0xf1, 0xf2, 0x20, 0xfa, 0x06, 0x2a, 0x09, 0x21, 0xa9, 0xc2, 0xd4, 0x74, 0x9f, 0x5c, + 0x65, 0xb6, 0xb2, 0x57, 0x99, 0xdd, 0xd0, 0x45, 0xd2, 0x72, 0xfe, 0xcc, 0xec, 0x07, 0xb7, 0x80, + 0xd9, 0x0f, 0x82, 0xfe, 0x74, 0x2a, 0x41, 0x79, 0xaa, 0x0b, 0xf2, 0xa0, 0x71, 0xad, 0xa8, 0xfe, + 0x76, 0xd6, 0xdd, 0x83, 0x8b, 0xcc, 0x86, 0x5c, 0x78, 0x7e, 0x95, 0xd9, 0x90, 0x8b, 0xcc, 0x57, + 0x99, 0xfd, 0x9e, 0x19, 0x9c, 0xfb, 0x1c, 0xaf, 0x90, 0xa0, 0xf6, 0x2f, 0x39, 0x02, 0xd0, 0x58, + 0x5e, 0xea, 0xb1, 0x60, 0x29, 0xe9, 0xa7, 0x82, 0xce, 0x70, 0x20, 0xd0, 0x7d, 0xa8, 0x14, 0x68, + 0xb8, 0x2b, 0xb7, 0x31, 0x14, 0x98, 0x6d, 0xf4, 0xfa, 0xca, 0x29, 0x93, 0xa7, 0x58, 0x60, 0xb3, + 0xba, 0x4a, 0x96, 0xf6, 0x75, 0xb2, 0xb4, 0x1c, 0x4f, 0x39, 0xf5, 0x54, 0xf7, 0xf0, 0xe5, 0x45, + 0xdb, 0x7a, 0x75, 0xd1, 0xb6, 0x5e, 0x5f, 0xb4, 0xad, 0x1f, 0x2f, 0xdb, 0xa5, 0x57, 0x97, 0xed, + 0xd2, 0xaf, 0x97, 0xed, 0xd2, 0xd7, 0x8f, 0x0a, 0xf4, 0xf4, 0xf5, 0xef, 0x4d, 0xbf, 0x7b, 0x8a, + 0x9e, 0x90, 0x45, 0x38, 0x0e, 0xd7, 0xbc, 0x9d, 0x5e, 0xff, 0xf9, 0x14, 0x6f, 0x93, 0x9a, 0xfa, + 0x61, 0x7d, 0xf2, 0x77, 0x00, 0x00, 0x00, 0xff, 0xff, 0xec, 0x66, 0x1a, 0x0f, 0x19, 0x07, 0x00, + 0x00, } func (this *Params) Equal(that interface{}) bool { @@ -676,6 +746,14 @@ func (this *Params) Equal(that interface{}) bool { return false } } + if len(this.VatCleanupBudget) != len(that1.VatCleanupBudget) { + return false + } + for i := range this.VatCleanupBudget { + if !this.VatCleanupBudget[i].Equal(&that1.VatCleanupBudget[i]) { + return false + } + } return true } func (this *StringBeans) Equal(that interface{}) bool { @@ -764,6 +842,33 @@ func (this *QueueSize) Equal(that interface{}) bool { } return true } +func (this *UintMapEntry) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + that1, ok := that.(*UintMapEntry) + if !ok { + that2, ok := that.(UintMapEntry) + if ok { + that1 = &that2 + } else { + return false + } + } + if that1 == nil { + return this == nil + } else if this == nil { + return false + } + if this.Key != that1.Key { + return false + } + if !this.Value.Equal(that1.Value) { + return false + } + return true +} func (m *CoreEvalProposal) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) @@ -872,6 +977,20 @@ func (m *Params) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if len(m.VatCleanupBudget) > 0 { + for iNdEx := len(m.VatCleanupBudget) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.VatCleanupBudget[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintSwingset(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x32 + } + } if len(m.QueueMax) > 0 { for iNdEx := len(m.QueueMax) - 1; iNdEx >= 0; iNdEx-- { { @@ -1094,6 +1213,46 @@ func (m *QueueSize) MarshalToSizedBuffer(dAtA []byte) (int, error) { return len(dAtA) - i, nil } +func (m *UintMapEntry) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *UintMapEntry) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *UintMapEntry) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + { + size := m.Value.Size() + i -= size + if _, err := m.Value.MarshalTo(dAtA[i:]); err != nil { + return 0, err + } + i = encodeVarintSwingset(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 + if len(m.Key) > 0 { + i -= len(m.Key) + copy(dAtA[i:], m.Key) + i = encodeVarintSwingset(dAtA, i, uint64(len(m.Key))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + func (m *Egress) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) @@ -1262,6 +1421,12 @@ func (m *Params) Size() (n int) { n += 1 + l + sovSwingset(uint64(l)) } } + if len(m.VatCleanupBudget) > 0 { + for _, e := range m.VatCleanupBudget { + l = e.Size() + n += 1 + l + sovSwingset(uint64(l)) + } + } return n } @@ -1330,6 +1495,21 @@ func (m *QueueSize) Size() (n int) { return n } +func (m *UintMapEntry) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Key) + if l > 0 { + n += 1 + l + sovSwingset(uint64(l)) + } + l = m.Value.Size() + n += 1 + l + sovSwingset(uint64(l)) + return n +} + func (m *Egress) Size() (n int) { if m == nil { return 0 @@ -1835,6 +2015,40 @@ func (m *Params) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 6: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field VatCleanupBudget", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowSwingset + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthSwingset + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthSwingset + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.VatCleanupBudget = append(m.VatCleanupBudget, UintMapEntry{}) + if err := m.VatCleanupBudget[len(m.VatCleanupBudget)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipSwingset(dAtA[iNdEx:]) @@ -2273,6 +2487,122 @@ func (m *QueueSize) Unmarshal(dAtA []byte) error { } return nil } +func (m *UintMapEntry) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowSwingset + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: UintMapEntry: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: UintMapEntry: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Key", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowSwingset + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthSwingset + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthSwingset + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Key = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Value", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowSwingset + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthSwingset + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthSwingset + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.Value.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipSwingset(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthSwingset + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func (m *Egress) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 diff --git a/golang/cosmos/x/vstorage/testing/queue.go b/golang/cosmos/x/vstorage/testing/queue.go index 14f52bf0f1c..c0cd8350ae8 100644 --- a/golang/cosmos/x/vstorage/testing/queue.go +++ b/golang/cosmos/x/vstorage/testing/queue.go @@ -8,11 +8,12 @@ import ( ) func GetQueueItems(ctx sdk.Context, vstorageKeeper keeper.Keeper, queuePath string) ([]string, error) { - head, err := vstorageKeeper.GetIntValue(ctx, queuePath+".head") + unlimitedCtx := ctx.WithGasMeter(sdk.NewInfiniteGasMeter()) + head, err := vstorageKeeper.GetIntValue(unlimitedCtx, queuePath+".head") if err != nil { return nil, err } - tail, err := vstorageKeeper.GetIntValue(ctx, queuePath+".tail") + tail, err := vstorageKeeper.GetIntValue(unlimitedCtx, queuePath+".tail") if err != nil { return nil, err } @@ -21,7 +22,7 @@ func GetQueueItems(ctx sdk.Context, vstorageKeeper keeper.Keeper, queuePath stri var i int64 for i = 0; i < length; i++ { path := fmt.Sprintf("%s.%s", queuePath, head.Add(sdk.NewInt(i)).String()) - values[i] = vstorageKeeper.GetEntry(ctx, path).StringValue() + values[i] = vstorageKeeper.GetEntry(unlimitedCtx, path).StringValue() } return values, nil } diff --git a/golang/cosmos/x/vtransfer/types/genesis.pb.go b/golang/cosmos/x/vtransfer/types/genesis.pb.go index 69f088dbb80..5d0fdbd3249 100644 --- a/golang/cosmos/x/vtransfer/types/genesis.pb.go +++ b/golang/cosmos/x/vtransfer/types/genesis.pb.go @@ -26,6 +26,7 @@ const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package // The initial and exported module state. type GenesisState struct { + // The list of account addresses that are being watched by the VM. WatchedAddresses []github_com_cosmos_cosmos_sdk_types.AccAddress `protobuf:"bytes,1,rep,name=watched_addresses,json=watchedAddresses,proto3,casttype=github.com/cosmos/cosmos-sdk/types.AccAddress" json:"watched_addresses" yaml:"watched_addresses"` } diff --git a/packages/SwingSet/src/controller/initializeSwingset.js b/packages/SwingSet/src/controller/initializeSwingset.js index 5b66bc53538..3fb6f0a0768 100644 --- a/packages/SwingSet/src/controller/initializeSwingset.js +++ b/packages/SwingSet/src/controller/initializeSwingset.js @@ -2,7 +2,7 @@ import fs from 'fs'; import path from 'path'; -import { assert, Fail } from '@endo/errors'; +import { assert, b, Fail } from '@endo/errors'; import { makeTracer } from '@agoric/internal'; import { mustMatch } from '@agoric/store'; import bundleSource from '@endo/bundle-source'; @@ -86,16 +86,6 @@ export async function buildKernelBundles() { return harden({ kernel: kernelBundle, ...vdBundles }); } -function byName(a, b) { - if (a.name < b.name) { - return -1; - } - if (a.name > b.name) { - return 1; - } - return 0; -} - /** * Scan a directory for files defining the vats to bootstrap for a swingset, and * produce a swingset config object for what was found there. Looks for files @@ -126,18 +116,18 @@ export function loadBasedir(basedir, options = {}) { const { includeDevDependencies = false, bundleFormat = undefined } = options; /** @type { SwingSetConfigDescriptor } */ const vats = {}; - const subs = fs.readdirSync(basedir, { withFileTypes: true }); - subs.sort(byName); - for (const dirent of subs) { - if ( - dirent.name.startsWith('vat-') && - dirent.name.endsWith('.js') && - dirent.isFile() - ) { - const name = dirent.name.slice('vat-'.length, -'.js'.length); - const vatSourcePath = path.resolve(basedir, dirent.name); - vats[name] = { sourceSpec: vatSourcePath, parameters: {} }; - } + const rVatName = /^vat-(.*)\.js$/s; + const files = fs.readdirSync(basedir, { withFileTypes: true }); + const vatFiles = files.flatMap(dirent => { + const file = dirent.name; + const m = rVatName.exec(file); + return m && dirent.isFile() ? [{ file, label: m[1] }] : []; + }); + // eslint-disable-next-line no-shadow,no-nested-ternary + vatFiles.sort((a, b) => (a.label < b.label ? -1 : a.label > b.label ? 1 : 0)); + for (const { file, label } of vatFiles) { + const vatSourcePath = path.resolve(basedir, file); + vats[label] = { sourceSpec: vatSourcePath, parameters: {} }; } /** @type {string | void} */ let bootstrapPath = path.resolve(basedir, 'bootstrap.js'); @@ -185,37 +175,42 @@ async function resolveSpecFromConfig(referrer, specPath) { } /** - * For each entry in a config descriptor (i.e, `vats`, `bundles`, etc), convert - * it to normal form: resolve each pathname to a context-insensitive absolute - * path and make sure it has a `parameters` property if it's supposed to. + * Convert each entry in a config descriptor group (`vats`/`bundles`/etc.) to + * normal form: resolve each pathname to a context-insensitive absolute path and + * run any other appropriate fixup. * - * @param {SwingSetConfigDescriptor | void} desc The config descriptor to be normalized. - * @param {string} referrer The pathname of the file or directory in which the - * config file was found - * @param {boolean} expectParameters `true` if the entries should have parameters (for - * example, `true` for `vats` but `false` for bundles). + * @param {SwingSetConfig} config + * @param {'vats' | 'bundles' | 'devices'} groupName + * @param {string | undefined} configPath of the containing config file + * @param {string} referrer URL + * @param {(entry: SwingSetConfigProperties, name?: string) => void} [fixupEntry] + * A function to call on each entry to e.g. add defaults for missing fields + * such as vat `parameters`. */ -async function normalizeConfigDescriptor(desc, referrer, expectParameters) { - const normalizeSpec = async (entry, key) => { - return resolveSpecFromConfig(referrer, entry[key]).then(spec => { - fs.existsSync(spec) || - Fail`spec for ${entry[key]} does not exist: ${spec}`; - entry[key] = spec; - }); +async function normalizeConfigDescriptor( + config, + groupName, + configPath, + referrer, + fixupEntry, +) { + const normalizeSpec = async (entry, specKey, name) => { + const sourcePath = await resolveSpecFromConfig(referrer, entry[specKey]); + fs.existsSync(sourcePath) || + Fail`${sourcePath} for ${b(groupName)}[${name}].${b(specKey)} in ${configPath} config file does not exist`; + entry[specKey] = sourcePath; }; const jobs = []; + const desc = config[groupName]; if (desc) { - for (const name of Object.keys(desc)) { - const entry = desc[name]; + for (const [name, entry] of Object.entries(desc)) { + fixupEntry?.(entry, name); if ('sourceSpec' in entry) { - jobs.push(normalizeSpec(entry, 'sourceSpec')); + jobs.push(normalizeSpec(entry, 'sourceSpec', name)); } if ('bundleSpec' in entry) { - jobs.push(normalizeSpec(entry, 'bundleSpec')); - } - if (expectParameters && !entry.parameters) { - entry.parameters = {}; + jobs.push(normalizeSpec(entry, 'bundleSpec', name)); } } } @@ -223,27 +218,41 @@ async function normalizeConfigDescriptor(desc, referrer, expectParameters) { } /** - * Read and parse a swingset config file and return it in normalized form. - * - * @param {string} configPath Path to the config file to be processed - * - * @returns {Promise} the contained config object, in normalized form, or null if the - * requested config file did not exist. + * @param {SwingSetConfig} config + * @param {string} [configPath] + * @returns {Promise} + * @throws {Error} if the config is invalid + */ +export async function normalizeConfig(config, configPath) { + const base = `file://${process.cwd()}/`; + const referrer = configPath + ? new URL(configPath, base).href + : new URL(base).href; + const fixupVat = vat => (vat.parameters ||= {}); + await Promise.all([ + normalizeConfigDescriptor(config, 'vats', configPath, referrer, fixupVat), + normalizeConfigDescriptor(config, 'bundles', configPath, referrer), + // TODO: represent devices + // normalizeConfigDescriptor(config, 'devices', configPath, referrer), + ]); + config.bootstrap || + Fail`no designated bootstrap vat in ${configPath} config file`; + (config.vats && config.vats[/** @type {string} */ (config.bootstrap)]) || + Fail`bootstrap vat ${config.bootstrap} not found in ${configPath} config file`; +} + +/** + * Read and normalize a swingset config file. * - * @throws {Error} if the file existed but was inaccessible, malformed, or otherwise - * invalid. + * @param {string} configPath + * @returns {Promise} the normalized config, + * or null if the file did not exist */ export async function loadSwingsetConfigFile(configPath) { await null; try { const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); - const referrer = new URL(configPath, `file://${process.cwd()}/`).toString(); - await normalizeConfigDescriptor(config.vats, referrer, true); - await normalizeConfigDescriptor(config.bundles, referrer, false); - // await normalizeConfigDescriptor(config.devices, referrer, true); // TODO: represent devices - config.bootstrap || Fail`no designated bootstrap vat in ${configPath}`; - (config.vats && config.vats[config.bootstrap]) || - Fail`bootstrap vat ${config.bootstrap} not found in ${configPath}`; + await normalizeConfig(config, configPath); return config; } catch (e) { console.error(`failed to load ${configPath}`); diff --git a/packages/SwingSet/src/index.js b/packages/SwingSet/src/index.js index c8e72886361..47ee711aeee 100644 --- a/packages/SwingSet/src/index.js +++ b/packages/SwingSet/src/index.js @@ -8,6 +8,7 @@ export { buildKernelBundles, loadBasedir, loadSwingsetConfigFile, + normalizeConfig, } from './controller/initializeSwingset.js'; export { upgradeSwingset } from './controller/upgradeSwingset.js'; export { diff --git a/packages/SwingSet/src/kernel/state/kernelKeeper.js b/packages/SwingSet/src/kernel/state/kernelKeeper.js index 1d64b225f1c..976e8d919f1 100644 --- a/packages/SwingSet/src/kernel/state/kernelKeeper.js +++ b/packages/SwingSet/src/kernel/state/kernelKeeper.js @@ -43,6 +43,7 @@ const enableKernelGC = true; * @typedef { import('../../types-external.js').SnapStore } SnapStore * @typedef { import('../../types-external.js').TranscriptStore } TranscriptStore * @typedef { import('../../types-external.js').VatKeeper } VatKeeper + * @typedef { Pick } VatUndertaker * @typedef { import('../../types-internal.js').InternalKernelOptions } InternalKernelOptions * @typedef { import('../../types-internal.js').ReapDirtThreshold } ReapDirtThreshold * @import {PromiseRecord} from '../../types-internal.js'; @@ -422,6 +423,8 @@ export default function makeKernelKeeper( const ephemeral = harden({ /** @type { Map } */ vatKeepers: new Map(), + /** @type { Map } */ + vatUndertakers: new Map(), deviceKeepers: new Map(), // deviceID -> deviceKeeper }); @@ -1044,7 +1047,7 @@ export default function makeKernelKeeper( // first or vref first), and delete the other one in the same // call, so we don't wind up with half an entry. - const vatKeeper = provideVatKeeper(vatID); + const undertaker = provideVatUndertaker(vatID); const clistPrefix = `${vatID}.c.`; const exportPrefix = `${clistPrefix}o+`; const importPrefix = `${clistPrefix}o-`; @@ -1092,7 +1095,7 @@ export default function makeKernelKeeper( // drop+retire const kref = kvStore.get(k) || Fail`getNextKey ensures get`; const vref = stripPrefix(clistPrefix, k); - vatKeeper.deleteCListEntry(kref, vref); + undertaker.deleteCListEntry(kref, vref); // that will also delete both db keys work.imports += 1; remaining -= 1; @@ -1109,7 +1112,7 @@ export default function makeKernelKeeper( for (const k of enumeratePrefixedKeys(kvStore, promisePrefix)) { const kref = kvStore.get(k) || Fail`getNextKey ensures get`; const vref = stripPrefix(clistPrefix, k); - vatKeeper.deleteCListEntry(kref, vref); + undertaker.deleteCListEntry(kref, vref); // that will also delete both db keys work.promises += 1; remaining -= 1; @@ -1131,7 +1134,7 @@ export default function makeKernelKeeper( // this will internally loop through 'budget' deletions remaining = budget.snapshots ?? budget.default; - const dsc = vatKeeper.deleteSnapshots(remaining); + const dsc = undertaker.deleteSnapshots(remaining); work.snapshots += dsc.cleanups; remaining -= dsc.cleanups; if (remaining <= 0) { @@ -1140,7 +1143,7 @@ export default function makeKernelKeeper( // same remaining = budget.transcripts ?? budget.default; - const dts = vatKeeper.deleteTranscripts(remaining); + const dts = undertaker.deleteTranscripts(remaining); work.transcripts += dts.cleanups; remaining -= dts.cleanups; // last task, so increment cleanups, but dc.done is authoritative @@ -1697,6 +1700,26 @@ export default function makeKernelKeeper( initializeVatState(kvStore, transcriptStore, vatID, source, options); } + /** @type {import('./vatKeeper.js').VatKeeperPowers} */ + const vatKeeperPowers = { + transcriptStore, + kernelSlog, + addKernelObject, + addKernelPromiseForVat, + kernelObjectExists, + incrementRefCount, + decrementRefCount, + getObjectRefCount, + setObjectRefCount, + getReachableAndVatSlot, + addMaybeFreeKref, + incStat, + decStat, + getCrankNumber, + scheduleReap, + snapStore, + }; + function provideVatKeeper(vatID) { insistVatID(vatID); const found = ephemeral.vatKeepers.get(vatID); @@ -1704,30 +1727,36 @@ export default function makeKernelKeeper( return found; } assert(kvStore.has(`${vatID}.o.nextID`), `${vatID} was not initialized`); - const vk = makeVatKeeper( - kvStore, - transcriptStore, - kernelSlog, - vatID, - addKernelObject, - addKernelPromiseForVat, - kernelObjectExists, - incrementRefCount, - decrementRefCount, - getObjectRefCount, - setObjectRefCount, - getReachableAndVatSlot, - addMaybeFreeKref, - incStat, - decStat, - getCrankNumber, - scheduleReap, - snapStore, - ); + const vk = makeVatKeeper(vatID, kvStore, vatKeeperPowers); ephemeral.vatKeepers.set(vatID, vk); return vk; } + /** + * Produce an attenuated vatKeeper for slow vat termination (and that + * therefore does not insist on liveness, unlike provideVatKeeper). + * + * @param {string} vatID + */ + function provideVatUndertaker(vatID) { + insistVatID(vatID); + const found = ephemeral.vatUndertakers.get(vatID); + if (found !== undefined) { + return found; + } + const { deleteCListEntry, deleteSnapshots, deleteTranscripts } = + ephemeral.vatKeepers.get(vatID) || + makeVatKeeper(vatID, kvStore, vatKeeperPowers); + /** @type {VatUndertaker} */ + const undertaker = harden({ + deleteCListEntry, + deleteSnapshots, + deleteTranscripts, + }); + ephemeral.vatUndertakers.set(vatID, undertaker); + return undertaker; + } + function vatIsAlive(vatID) { insistVatID(vatID); return kvStore.has(`${vatID}.o.nextID`) && !terminatedVats.includes(vatID); diff --git a/packages/SwingSet/src/kernel/state/vatKeeper.js b/packages/SwingSet/src/kernel/state/vatKeeper.js index 1d4d6b1f12f..d307bc77e7b 100644 --- a/packages/SwingSet/src/kernel/state/vatKeeper.js +++ b/packages/SwingSet/src/kernel/state/vatKeeper.js @@ -84,49 +84,53 @@ export function initializeVatState( } /** - * Produce a vat keeper for a vat. + * @typedef {object} VatKeeperPowers + * @property {TranscriptStore} transcriptStore Accompanying transcript store, for the transcripts + * @property {*} kernelSlog + * @property {*} addKernelObject Kernel function to add a new object to the kernel's mapping tables. + * @property {*} addKernelPromiseForVat Kernel function to add a new promise to the kernel's mapping tables. + * @property {(kernelSlot: string) => boolean} kernelObjectExists + * @property {*} incrementRefCount + * @property {*} decrementRefCount + * @property {(kernelSlot: string) => {reachable: number, recognizable: number}} getObjectRefCount + * @property {(kernelSlot: string, o: { reachable: number, recognizable: number }) => void} setObjectRefCount + * @property {(vatID: string, kernelSlot: string) => {isReachable: boolean, vatSlot: string}} getReachableAndVatSlot + * @property {(kernelSlot: string) => void} addMaybeFreeKref + * @property {*} incStat + * @property {*} decStat + * @property {*} getCrankNumber + * @property {*} scheduleReap + * @property {SnapStore} snapStore + */ + +/** + * Produce a "vat keeper" for the kernel state of a vat. * - * @param {KVStore} kvStore The keyValue store in which the persistent state will be kept - * @param {TranscriptStore} transcriptStore Accompanying transcript store, for the transcripts - * @param {*} kernelSlog * @param {string} vatID The vat ID string of the vat in question - * @param {*} addKernelObject Kernel function to add a new object to the kernel's - * mapping tables. - * @param {*} addKernelPromiseForVat Kernel function to add a new promise to the - * kernel's mapping tables. - * @param {(kernelSlot: string) => boolean} kernelObjectExists - * @param {*} incrementRefCount - * @param {*} decrementRefCount - * @param {(kernelSlot: string) => {reachable: number, recognizable: number}} getObjectRefCount - * @param {(kernelSlot: string, o: { reachable: number, recognizable: number }) => void} setObjectRefCount - * @param {(vatID: string, kernelSlot: string) => {isReachable: boolean, vatSlot: string}} getReachableAndVatSlot - * @param {(kernelSlot: string) => void} addMaybeFreeKref - * @param {*} incStat - * @param {*} decStat - * @param {*} getCrankNumber - * @param {*} scheduleReap - * @param {SnapStore} [snapStore] - * returns an object to hold and access the kernel's state for the given vat + * @param {KVStore} kvStore The keyValue store in which the persistent state will be kept + * @param {VatKeeperPowers} powers */ export function makeVatKeeper( - kvStore, - transcriptStore, - kernelSlog, vatID, - addKernelObject, - addKernelPromiseForVat, - kernelObjectExists, - incrementRefCount, - decrementRefCount, - getObjectRefCount, - setObjectRefCount, - getReachableAndVatSlot, - addMaybeFreeKref, - incStat, - decStat, - getCrankNumber, - scheduleReap, - snapStore = undefined, + kvStore, + { + transcriptStore, + kernelSlog, + addKernelObject, + addKernelPromiseForVat, + kernelObjectExists, + incrementRefCount, + decrementRefCount, + getObjectRefCount, + setObjectRefCount, + getReachableAndVatSlot, + addMaybeFreeKref, + incStat, + decStat, + getCrankNumber, + scheduleReap, + snapStore, + }, ) { insistVatID(vatID); diff --git a/packages/SwingSet/tools/bootstrap-relay.js b/packages/SwingSet/tools/bootstrap-relay.js index 730a7aa6593..8b30c77e5b6 100644 --- a/packages/SwingSet/tools/bootstrap-relay.js +++ b/packages/SwingSet/tools/bootstrap-relay.js @@ -1,3 +1,12 @@ +/** + * @file Source code for a bootstrap vat that runs blockchain behaviors (such as + * bridge vat integration) and exposes reflective methods for use in testing. + * + * TODO: Build from ./vat-puppet.js makeReflectionMethods + * and share code with packages/vats/tools/vat-reflective-chain-bootstrap.js + * (which basically extends this for better [mock] blockchain integration). + */ + import { Fail, q } from '@endo/errors'; import { objectMap } from '@agoric/internal'; import { Far, E } from '@endo/far'; diff --git a/packages/SwingSet/tools/vat-puppet.js b/packages/SwingSet/tools/vat-puppet.js new file mode 100644 index 00000000000..d3a9ed3d78e --- /dev/null +++ b/packages/SwingSet/tools/vat-puppet.js @@ -0,0 +1,111 @@ +/** + * @file Source code for a vat that exposes reflective methods for use in + * testing. + */ + +import { Fail, q } from '@endo/errors'; +import { Far, E } from '@endo/far'; +import { makePromiseKit } from '@endo/promise-kit'; +import { objectMap } from '@agoric/internal'; + +/** + * @callback Die + * @param {unknown} completion + * @param {[target: unknown, method: string, ...args: unknown[]]} [finalSend] + */ + +/** + * @typedef {Array<[name: string, ...args: unknown[]]>} CallLog + */ + +/** + * @param {import('@agoric/swingset-vat').VatPowers} vatPowers + * @param {import('@agoric/vat-data').Baggage} baggage + */ +export const makeReflectionMethods = (vatPowers, baggage) => { + let baggageHoldCount = 0; + /** @type {Map} */ + const callLogsByRemotable = new Map(); + const heldInHeap = []; + const send = (target, method, ...args) => E(target)[method](...args); + const makeSpy = (value, name, callLog) => { + const spyName = `get ${name}`; + const spy = { + [spyName](...args) { + callLog.push([name, ...args]); + return value; + }, + }[spyName]; + return spy; + }; + + return { + /** @type {Die} */ + dieHappy: (completion, finalSend) => { + vatPowers.exitVat(completion); + if (finalSend) send(...finalSend); + }, + + /** @type {Die} */ + dieSad: (reason, finalSend) => { + vatPowers.exitVatWithFailure(/** @type {Error} */ (reason)); + if (finalSend) send(...finalSend); + }, + + holdInBaggage: (...values) => { + for (const value of values) { + baggage.init(`held-${baggageHoldCount}`, value); + baggageHoldCount += 1; + } + return baggageHoldCount; + }, + + holdInHeap: (...values) => heldInHeap.push(...values), + + makePromiseKit: () => { + const { promise, ...resolverMethods } = makePromiseKit(); + void promise.catch(() => {}); + const resolver = Far('resolver', resolverMethods); + return harden({ promise, resolver }); + }, + + makeUnsettledPromise() { + const { promise } = makePromiseKit(); + void promise.catch(() => {}); + return promise; + }, + + /** + * Returns a remotable with methods that return provided values. Invocations + * of those methods and their arguments are captured for later retrieval by + * `getCallLogForRemotable`. + * + * @param {string} [label] + * @param {Record} [fields] + */ + makeRemotable: (label = 'Remotable', fields = {}) => { + /** @type {CallLog} */ + const callLog = []; + const methods = objectMap(fields, (value, name) => + makeSpy(value, name, callLog), + ); + const remotable = Far(label, { ...methods }); + callLogsByRemotable.set(remotable, callLog); + return remotable; + }, + + /** + * @param {object} remotable + * @returns {CallLog} + */ + getCallLogForRemotable: remotable => + callLogsByRemotable.get(remotable) || + Fail`unknown remotable ${q(remotable)}`, + }; +}; +harden(makeReflectionMethods); + +export function buildRootObject(vatPowers, _vatParameters, baggage) { + const methods = makeReflectionMethods(vatPowers, baggage); + return Far('root', methods); +} diff --git a/packages/cosmic-proto/proto/agoric/swingset/swingset.proto b/packages/cosmic-proto/proto/agoric/swingset/swingset.proto index 7a7c7a16d14..fdfd5f722f4 100644 --- a/packages/cosmic-proto/proto/agoric/swingset/swingset.proto +++ b/packages/cosmic-proto/proto/agoric/swingset/swingset.proto @@ -82,6 +82,18 @@ message Params { repeated QueueSize queue_max = 5 [ (gogoproto.nullable) = false ]; + + // Vat cleanup budget values. + // These values are used by SwingSet to control the pace of removing data + // associated with a terminated vat as described at + // https://github.com/Agoric/agoric-sdk/blob/master/packages/SwingSet/docs/run-policy.md#terminated-vat-cleanup + // + // There is no required order to this list of entries, but all the chain + // nodes must all serialize and deserialize the existing order without + // permuting it. + repeated UintMapEntry vat_cleanup_budget = 6 [ + (gogoproto.nullable) = false + ]; } // The current state of the module. @@ -119,6 +131,7 @@ message PowerFlagFee { } // Map element of a string key to a size. +// TODO: Replace with UintMapEntry? message QueueSize { option (gogoproto.equal) = true; @@ -129,6 +142,18 @@ message QueueSize { int32 size = 2; } +// Map element of a string key to an unsigned integer. +// The value uses cosmos-sdk Uint rather than a native Go type to ensure that +// zeroes survive "omitempty" JSON serialization. +message UintMapEntry { + option (gogoproto.equal) = true; + string key = 1; + string value = 2 [ + (gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Uint", + (gogoproto.nullable) = false + ]; +} + // Egress is the format for a swingset egress. message Egress { option (gogoproto.equal) = false; diff --git a/packages/cosmic-proto/src/codegen/agoric/swingset/swingset.ts b/packages/cosmic-proto/src/codegen/agoric/swingset/swingset.ts index 29464d50d08..3f978f341fa 100644 --- a/packages/cosmic-proto/src/codegen/agoric/swingset/swingset.ts +++ b/packages/cosmic-proto/src/codegen/agoric/swingset/swingset.ts @@ -103,6 +103,17 @@ export interface Params { * permuting it. */ queueMax: QueueSize[]; + /** + * Vat cleanup budget values. + * These values are used by SwingSet to control the pace of removing data + * associated with a terminated vat as described at + * https://github.com/Agoric/agoric-sdk/blob/master/packages/SwingSet/docs/run-policy.md#terminated-vat-cleanup + * + * There is no required order to this list of entries, but all the chain + * nodes must all serialize and deserialize the existing order without + * permuting it. + */ + vatCleanupBudget: UintMapEntry[]; } export interface ParamsProtoMsg { typeUrl: '/agoric.swingset.Params'; @@ -115,6 +126,7 @@ export interface ParamsSDKType { bootstrap_vat_config: string; power_flag_fees: PowerFlagFeeSDKType[]; queue_max: QueueSizeSDKType[]; + vat_cleanup_budget: UintMapEntrySDKType[]; } /** The current state of the module. */ export interface State { @@ -162,7 +174,10 @@ export interface PowerFlagFeeSDKType { power_flag: string; fee: CoinSDKType[]; } -/** Map element of a string key to a size. */ +/** + * Map element of a string key to a size. + * TODO: Replace with UintMapEntry? + */ export interface QueueSize { /** What the size is for. */ key: string; @@ -173,11 +188,36 @@ export interface QueueSizeProtoMsg { typeUrl: '/agoric.swingset.QueueSize'; value: Uint8Array; } -/** Map element of a string key to a size. */ +/** + * Map element of a string key to a size. + * TODO: Replace with UintMapEntry? + */ export interface QueueSizeSDKType { key: string; size: number; } +/** + * Map element of a string key to an unsigned integer. + * The value uses cosmos-sdk Uint rather than a native Go type to ensure that + * zeroes survive "omitempty" JSON serialization. + */ +export interface UintMapEntry { + key: string; + value: string; +} +export interface UintMapEntryProtoMsg { + typeUrl: '/agoric.swingset.UintMapEntry'; + value: Uint8Array; +} +/** + * Map element of a string key to an unsigned integer. + * The value uses cosmos-sdk Uint rather than a native Go type to ensure that + * zeroes survive "omitempty" JSON serialization. + */ +export interface UintMapEntrySDKType { + key: string; + value: string; +} /** Egress is the format for a swingset egress. */ export interface Egress { nickname: string; @@ -388,6 +428,7 @@ function createBaseParams(): Params { bootstrapVatConfig: '', powerFlagFees: [], queueMax: [], + vatCleanupBudget: [], }; } export const Params = { @@ -411,6 +452,9 @@ export const Params = { for (const v of message.queueMax) { QueueSize.encode(v!, writer.uint32(42).fork()).ldelim(); } + for (const v of message.vatCleanupBudget) { + UintMapEntry.encode(v!, writer.uint32(50).fork()).ldelim(); + } return writer; }, decode(input: BinaryReader | Uint8Array, length?: number): Params { @@ -440,6 +484,11 @@ export const Params = { case 5: message.queueMax.push(QueueSize.decode(reader, reader.uint32())); break; + case 6: + message.vatCleanupBudget.push( + UintMapEntry.decode(reader, reader.uint32()), + ); + break; default: reader.skipType(tag & 7); break; @@ -464,6 +513,9 @@ export const Params = { queueMax: Array.isArray(object?.queueMax) ? object.queueMax.map((e: any) => QueueSize.fromJSON(e)) : [], + vatCleanupBudget: Array.isArray(object?.vatCleanupBudget) + ? object.vatCleanupBudget.map((e: any) => UintMapEntry.fromJSON(e)) + : [], }; }, toJSON(message: Params): JsonSafe { @@ -498,6 +550,13 @@ export const Params = { } else { obj.queueMax = []; } + if (message.vatCleanupBudget) { + obj.vatCleanupBudget = message.vatCleanupBudget.map(e => + e ? UintMapEntry.toJSON(e) : undefined, + ); + } else { + obj.vatCleanupBudget = []; + } return obj; }, fromPartial(object: Partial): Params { @@ -511,6 +570,8 @@ export const Params = { object.powerFlagFees?.map(e => PowerFlagFee.fromPartial(e)) || []; message.queueMax = object.queueMax?.map(e => QueueSize.fromPartial(e)) || []; + message.vatCleanupBudget = + object.vatCleanupBudget?.map(e => UintMapEntry.fromPartial(e)) || []; return message; }, fromProtoMsg(message: ParamsProtoMsg): Params { @@ -819,6 +880,78 @@ export const QueueSize = { }; }, }; +function createBaseUintMapEntry(): UintMapEntry { + return { + key: '', + value: '', + }; +} +export const UintMapEntry = { + typeUrl: '/agoric.swingset.UintMapEntry', + encode( + message: UintMapEntry, + writer: BinaryWriter = BinaryWriter.create(), + ): BinaryWriter { + if (message.key !== '') { + writer.uint32(10).string(message.key); + } + if (message.value !== '') { + writer.uint32(18).string(message.value); + } + return writer; + }, + decode(input: BinaryReader | Uint8Array, length?: number): UintMapEntry { + const reader = + input instanceof BinaryReader ? input : new BinaryReader(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseUintMapEntry(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + message.key = reader.string(); + break; + case 2: + message.value = reader.string(); + break; + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }, + fromJSON(object: any): UintMapEntry { + return { + key: isSet(object.key) ? String(object.key) : '', + value: isSet(object.value) ? String(object.value) : '', + }; + }, + toJSON(message: UintMapEntry): JsonSafe { + const obj: any = {}; + message.key !== undefined && (obj.key = message.key); + message.value !== undefined && (obj.value = message.value); + return obj; + }, + fromPartial(object: Partial): UintMapEntry { + const message = createBaseUintMapEntry(); + message.key = object.key ?? ''; + message.value = object.value ?? ''; + return message; + }, + fromProtoMsg(message: UintMapEntryProtoMsg): UintMapEntry { + return UintMapEntry.decode(message.value); + }, + toProto(message: UintMapEntry): Uint8Array { + return UintMapEntry.encode(message).finish(); + }, + toProtoMsg(message: UintMapEntry): UintMapEntryProtoMsg { + return { + typeUrl: '/agoric.swingset.UintMapEntry', + value: UintMapEntry.encode(message).finish(), + }; + }, +}; function createBaseEgress(): Egress { return { nickname: '', diff --git a/packages/cosmic-swingset/src/chain-main.js b/packages/cosmic-swingset/src/chain-main.js index 10b669da82a..0221529fbf5 100644 --- a/packages/cosmic-swingset/src/chain-main.js +++ b/packages/cosmic-swingset/src/chain-main.js @@ -30,6 +30,7 @@ import { } from '@agoric/internal/src/lib-chainStorage.js'; import { makeShutdown } from '@agoric/internal/src/node/shutdown.js'; +import { makeInitMsg } from '@agoric/internal/src/chain-utils.js'; import * as STORAGE_PATH from '@agoric/internal/src/chain-storage-paths.js'; import * as ActionType from '@agoric/internal/src/action-types.js'; import { BridgeId, CosmosInitKeyToBridgeId } from '@agoric/internal'; @@ -104,32 +105,6 @@ const validateSwingsetConfig = swingsetConfig => { Fail`maxVatsOnline must be a positive integer`; }; -/** - * A boot message consists of cosmosInitAction fields that are subject to - * consensus. See cosmosInitAction in {@link ../../../golang/cosmos/app/app.go}. - * - * @param {any} initAction - */ -const makeBootMsg = initAction => { - const { - type, - blockTime, - blockHeight, - chainID, - params, - // NB: resolvedConfig is independent of consensus and MUST NOT be included - supplyCoins, - } = initAction; - return { - type, - blockTime, - blockHeight, - chainID, - params, - supplyCoins, - }; -}; - /** * @template {unknown} [T=unknown] * @param {(req: string) => string} call @@ -454,16 +429,16 @@ export default async function main(progname, args, { env, homedir, agcc }) { }; const argv = { - bootMsg: makeBootMsg(initAction), + bootMsg: makeInitMsg(initAction), }; const getVatConfig = async () => { - const vatHref = await importMetaResolve( + const href = await importMetaResolve( env.CHAIN_BOOTSTRAP_VAT_CONFIG || argv.bootMsg.params.bootstrap_vat_config, import.meta.url, ); - const vatconfig = new URL(vatHref).pathname; - return vatconfig; + const { pathname } = new URL(href); + return pathname; }; // Delay makeShutdown to override the golang interrupts diff --git a/packages/cosmic-swingset/src/helpers/bufferedStorage.js b/packages/cosmic-swingset/src/helpers/bufferedStorage.js index 1b58c69b1b4..8d860e06f58 100644 --- a/packages/cosmic-swingset/src/helpers/bufferedStorage.js +++ b/packages/cosmic-swingset/src/helpers/bufferedStorage.js @@ -15,6 +15,11 @@ import { assert, Fail } from '@endo/errors'; * }} KVStore */ +/** + * @template {unknown} [T=unknown] + * @typedef {KVStore & {commit: () => void, abort: () => void}} BufferedKVStore + */ + /** * Assert function to ensure that an object implements the StorageAPI * interface: methods { has, getNextKey, get, set, delete } @@ -33,21 +38,188 @@ export function insistStorageAPI(kvStore) { } } +// TODO: Replace compareByCodePoints and makeKVStoreFromMap and +// provideEnhancedKVStore with imports when +// available. +// https://github.com/Agoric/agoric-sdk/pull/10299 + +const compareByCodePoints = (left, right) => { + const leftIter = left[Symbol.iterator](); + const rightIter = right[Symbol.iterator](); + for (;;) { + const { value: leftChar } = leftIter.next(); + const { value: rightChar } = rightIter.next(); + if (leftChar === undefined && rightChar === undefined) { + return 0; + } else if (leftChar === undefined) { + // left is a prefix of right. + return -1; + } else if (rightChar === undefined) { + // right is a prefix of left. + return 1; + } + const leftCodepoint = /** @type {number} */ (leftChar.codePointAt(0)); + const rightCodepoint = /** @type {number} */ (rightChar.codePointAt(0)); + if (leftCodepoint < rightCodepoint) return -1; + if (leftCodepoint > rightCodepoint) return 1; + } +}; + +/** + * @template [T=unknown] + * @param {Map} map + * @returns {KVStore} + */ +export const makeKVStoreFromMap = map => { + let sortedKeys; + let priorKeyReturned; + let priorKeyIndex; + + const ensureSorted = () => { + if (!sortedKeys) { + sortedKeys = [...map.keys()]; + sortedKeys.sort(compareByCodePoints); + } + }; + + const clearGetNextKeyCache = () => { + priorKeyReturned = undefined; + priorKeyIndex = -1; + }; + clearGetNextKeyCache(); + + const clearSorted = () => { + sortedKeys = undefined; + clearGetNextKeyCache(); + }; + + /** @type {KVStore} */ + const fakeStore = harden({ + has: key => map.has(key), + get: key => map.get(key), + getNextKey: priorKey => { + assert.typeof(priorKey, 'string'); + ensureSorted(); + const start = + compareByCodePoints(priorKeyReturned, priorKey) <= 0 + ? priorKeyIndex + 1 + : 0; + for (let i = start; i < sortedKeys.length; i += 1) { + const key = sortedKeys[i]; + if (compareByCodePoints(key, priorKey) <= 0) continue; + priorKeyReturned = key; + priorKeyIndex = i; + return key; + } + // reached end without finding the key, so clear our cache + clearGetNextKeyCache(); + return undefined; + }, + set: (key, value) => { + if (!map.has(key)) clearSorted(); + map.set(key, value); + }, + delete: key => { + if (map.has(key)) clearSorted(); + map.delete(key); + }, + }); + return fakeStore; +}; + +/** + * Return an object representing KVStore contents as both a KVStore and a Map. + * + * Iterating over the map while mutating it is "unsupported" (entries inserted + * that sort before the current iteration point will be skipped). + * + * The `size` property is not supported. + * + * @template [T=unknown] + * @param {Map | KVStore} [mapOrKvStore] + */ +export function provideEnhancedKVStore(mapOrKvStore = new Map()) { + if (!('getNextKey' in mapOrKvStore)) { + mapOrKvStore = makeKVStoreFromMap(mapOrKvStore); + } + + if (!('keys' in mapOrKvStore)) { + const kvStore = mapOrKvStore; + const map = harden({ + ...mapOrKvStore, + set(key, value) { + kvStore.set(key, value); + return map; + }, + delete(key) { + const had = kvStore.has(key); + kvStore.delete(key); + return had; + }, + clear() { + for (const key of map.keys()) { + kvStore.delete(key); + } + }, + /** @returns {number} */ + get size() { + throw new Error('size not implemented.'); + }, + *entries() { + for (const key of map.keys()) { + yield [key, /** @type {string} */ (kvStore.get(key))]; + } + }, + *keys() { + /** @type {string | undefined} */ + let key = ''; + if (kvStore.has(key)) { + yield key; + } + // eslint-disable-next-line no-cond-assign + while ((key = kvStore.getNextKey(key))) { + yield key; + } + }, + *values() { + for (const key of map.keys()) { + yield /** @type {string} */ (kvStore.get(key)); + } + }, + forEach(callbackfn, thisArg) { + for (const key of map.keys()) { + Reflect.apply(callbackfn, thisArg, [ + /** @type {string} */ (kvStore.get(key)), + key, + map, + ]); + } + }, + [Symbol.iterator]() { + return map.entries(); + }, + }); + mapOrKvStore = map; + } + + return /** @type {Map & KVStore} */ (mapOrKvStore); +} + /** * Create a StorageAPI object that buffers writes to a wrapped StorageAPI object * until told to commit (or abort) them. * - * @template {unknown} [T=unknown] + * @template [T=unknown] * @param {KVStore} kvStore The StorageAPI object to wrap * @param {{ - * onGet?: (key: string, value: T) => void, // a callback invoked after getting a value from kvStore + * onGet?: (key: string, value: T | undefined) => void, // a callback invoked after getting a value from kvStore * onPendingSet?: (key: string, value: T) => void, // a callback invoked after a new uncommitted value is set * onPendingDelete?: (key: string) => void, // a callback invoked after a new uncommitted delete * onCommit?: () => void, // a callback invoked after pending operations have been committed * onAbort?: () => void, // a callback invoked after pending operations have been aborted * }} listeners Optional callbacks to be invoked when respective events occur * - * @returns {{kvStore: KVStore, commit: () => void, abort: () => void}} + * @returns {{kvStore: KVStore} & Pick, 'commit' | 'abort'>} */ export function makeBufferedStorage(kvStore, listeners = {}) { insistStorageAPI(kvStore); @@ -59,6 +231,7 @@ export function makeBufferedStorage(kvStore, listeners = {}) { const additions = new Map(); const deletions = new Set(); + /** @type {KVStore} */ const buffered = { has(key) { assert.typeof(key, 'string'); @@ -71,7 +244,6 @@ export function makeBufferedStorage(kvStore, listeners = {}) { if (additions.has(key)) return additions.get(key); if (deletions.has(key)) return undefined; const value = kvStore.get(key); - // @ts-expect-error value may be undefined if (onGet !== undefined) onGet(key, value); return value; }, @@ -118,19 +290,21 @@ export function makeBufferedStorage(kvStore, listeners = {}) { /** * @template {unknown} [T=unknown] * @param {KVStore} kvStore + * @returns {BufferedKVStore} */ export const makeReadCachingStorage = kvStore => { // In addition to the wrapping write buffer, keep a simple cache of // read values for has and get. + const deleted = Symbol('deleted'); + const undef = Symbol('undefined'); + /** @typedef {(typeof deleted) | (typeof undef)} ReadCacheSentinel */ + /** @type {Map} */ let cache; function resetCache() { cache = new Map(); } resetCache(); - const deleted = Symbol('deleted'); - const undef = Symbol('undefined'); - /** @type {KVStore} */ const storage = harden({ has(key) { @@ -190,5 +364,5 @@ export const makeReadCachingStorage = kvStore => { onCommit: resetCache, onAbort: resetCache, }); - return harden({ ...buffered, commit, abort }); + return harden({ .../** @type {KVStore} */ (buffered), commit, abort }); }; diff --git a/packages/cosmic-swingset/src/helpers/json-stable-stringify.js b/packages/cosmic-swingset/src/helpers/json-stable-stringify.js index a2585b49c12..607630950df 100644 --- a/packages/cosmic-swingset/src/helpers/json-stable-stringify.js +++ b/packages/cosmic-swingset/src/helpers/json-stable-stringify.js @@ -47,12 +47,18 @@ export default function stableStringify(obj, opts) { })(opts.cmp); const seen = []; - return (function stringify(parent, key, node, level) { + /** + * @param {object | unknown[]} parent + * @param {PropertyKey} key + * @param {unknown} node + * @param {number} level + */ + const stringify = (parent, key, node, level) => { const indent = space ? `\n${new Array(level + 1).join(space)}` : ''; const colonSeparator = space ? ': ' : ':'; - if (node && node.toJSON && typeof node.toJSON === 'function') { - node = node.toJSON(); + if (node && typeof node === 'object' && 'toJSON' in node) { + if (typeof node.toJSON === 'function') node = node.toJSON(); } node = replacer.call(parent, key, node); @@ -90,5 +96,6 @@ export default function stableStringify(obj, opts) { } seen.splice(seen.indexOf(node), 1); return `{${out.join(',')}${indent}}`; - })({ '': obj }, '', obj, 0); + }; + return stringify({ '': obj }, '', obj, 0); } diff --git a/packages/cosmic-swingset/src/launch-chain.js b/packages/cosmic-swingset/src/launch-chain.js index c2211d66a4c..9500a380a12 100644 --- a/packages/cosmic-swingset/src/launch-chain.js +++ b/packages/cosmic-swingset/src/launch-chain.js @@ -21,6 +21,7 @@ import { makeSwingsetController, loadBasedir, loadSwingsetConfigFile, + normalizeConfig, upgradeSwingset, } from '@agoric/swingset-vat'; import { waitUntilQuiescent } from '@agoric/internal/src/lib-nodejs/waitUntilQuiescent.js'; @@ -52,8 +53,13 @@ import { makeQueue, makeQueueStorageMock } from './helpers/make-queue.js'; import { exportStorage } from './export-storage.js'; import { parseLocatedJson } from './helpers/json.js'; +const { hasOwn } = Object; + +/** @import { BlockInfo } from '@agoric/internal/src/chain-utils.js' */ /** @import { Mailbox, RunPolicy, SwingSetConfig } from '@agoric/swingset-vat' */ -/** @import { KVStore } from './helpers/bufferedStorage.js' */ +/** @import { KVStore, BufferedKVStore } from './helpers/bufferedStorage.js' */ + +/** @typedef {ReturnType>} InboundQueue */ const console = anylogger('launch-chain'); const blockManagerConsole = anylogger('block-manager'); @@ -87,6 +93,24 @@ const parseUpgradePlanInfo = (upgradePlan, prefix = '') => { * `chainStorageEntries: [ ['c.o', '"top"'], ['c.o.i'], ['c.o.i.n', '42'], ['c.o.w', '"moo"'] ]`). */ +/** + * @typedef {'leftover' | 'forced' | 'high-priority' | 'timer' | 'queued' | 'cleanup'} CrankerPhase + * - leftover: work from a previous block + * - forced: work that claims the entirety of the current block + * - high-priority: queued work the precedes timer advancement + * - intermission: needed to note state exports and update consistency hashes + * - queued: queued work the follows timer advancement + * - cleanup: for dealing with data from terminated vats + */ + +/** @type {CrankerPhase} */ +const CLEANUP = 'cleanup'; + +/** + * @typedef {(phase: CrankerPhase) => Promise} Cranker runs the kernel + * and reports if it is time to stop + */ + /** * Return the key in the reserved "host.*" section of the swing-store * @@ -96,9 +120,9 @@ const getHostKey = path => `host.${path}`; /** * @param {KVStore} mailboxStorage - * @param {((dstID: string, obj: any) => any)} bridgeOutbound + * @param {((destPort: string, msg: unknown) => unknown)} bridgeOutbound * @param {SwingStoreKernelStorage} kernelStorage - * @param {string | (() => string | Promise)} vatconfig absolute path or thunk + * @param {import('@endo/far').ERef | (() => ERef)} getVatConfig * @param {unknown} bootstrapArgs JSON-serializable data * @param {{}} env * @param {*} options @@ -107,7 +131,7 @@ export async function buildSwingset( mailboxStorage, bridgeOutbound, kernelStorage, - vatconfig, + getVatConfig, bootstrapArgs, env, { @@ -139,13 +163,19 @@ export async function buildSwingset( return; } - const configLocation = await (typeof vatconfig === 'function' - ? vatconfig() - : vatconfig); - let config = await loadSwingsetConfigFile(configLocation); - if (config === null) { - config = loadBasedir(configLocation); - } + const { config, configLocation } = await (async () => { + const objOrPath = await (typeof getVatConfig === 'function' + ? getVatConfig() + : getVatConfig); + if (typeof objOrPath === 'string') { + const path = objOrPath; + const configFromFile = await loadSwingsetConfigFile(path); + const obj = configFromFile || loadBasedir(path); + return { config: obj, configLocation: path }; + } + await normalizeConfig(objOrPath); + return { config: objOrPath, configLocation: undefined }; + })(); config.devices = { mailbox: { @@ -248,6 +278,7 @@ export async function buildSwingset( * shouldRun(): boolean; * remainingBeans(): bigint | undefined; * totalBeans(): bigint; + * startCleanup(): boolean; * }} ChainRunPolicy */ @@ -259,25 +290,66 @@ export async function buildSwingset( */ /** - * @param {BeansPerUnit} beansPerUnit + * Return a stateful run policy that supports two phases: first allow + * non-cleanup work (presumably deliveries) until an overrideable computron + * budget is exhausted, then (iff no work was done and at least one vat cleanup + * budget field is positive) a cleanup phase that allows cleanup work (and + * presumably nothing else) until one of those fields is exhausted. + * https://github.com/Agoric/agoric-sdk/issues/8928#issuecomment-2053357870 + * + * @param {object} params + * @param {BeansPerUnit} params.beansPerUnit + * @param {import('@agoric/swingset-vat').CleanupBudget} [params.vatCleanupBudget] * @param {boolean} [ignoreBlockLimit] * @returns {ChainRunPolicy} */ function computronCounter( - { + { beansPerUnit, vatCleanupBudget }, + ignoreBlockLimit = false, +) { + const { [BeansPerBlockComputeLimit]: blockComputeLimit, [BeansPerVatCreation]: vatCreation, [BeansPerXsnapComputron]: xsnapComputron, - }, - ignoreBlockLimit = false, -) { + } = beansPerUnit; assert.typeof(blockComputeLimit, 'bigint'); assert.typeof(vatCreation, 'bigint'); assert.typeof(xsnapComputron, 'bigint'); + let totalBeans = 0n; const shouldRun = () => ignoreBlockLimit || totalBeans < blockComputeLimit; - const remainingBeans = () => - ignoreBlockLimit ? undefined : blockComputeLimit - totalBeans; + + const remainingCleanups = { default: Infinity, ...vatCleanupBudget }; + const defaultCleanupBudget = remainingCleanups.default; + let cleanupStarted = false; + let cleanupDone = false; + const cleanupPossible = + Object.values(remainingCleanups).length > 0 + ? Object.values(remainingCleanups).some(n => n > 0) + : defaultCleanupBudget > 0; + if (!cleanupPossible) cleanupDone = true; + /** @type {() => (false | import('@agoric/swingset-vat').CleanupBudget)} */ + const allowCleanup = () => + cleanupStarted && !cleanupDone && { ...remainingCleanups }; + const startCleanup = () => { + assert(!cleanupStarted); + cleanupStarted = true; + return totalBeans === 0n && !cleanupDone; + }; + const didCleanup = details => { + for (const [phase, count] of Object.entries(details.cleanups)) { + if (phase === 'total') continue; + if (!hasOwn(remainingCleanups, phase)) { + // TODO: log unknown phases? + remainingCleanups[phase] = defaultCleanupBudget; + } + remainingCleanups[phase] -= count; + if (remainingCleanups[phase] <= 0) cleanupDone = true; + } + // We return true to allow processing of any BOYD/GC prompted by cleanup, + // even if cleanup as such is now done. + return true; + }; const policy = harden({ vatCreated() { @@ -303,19 +375,63 @@ function computronCounter( emptyCrank() { return shouldRun(); }, + allowCleanup, + didCleanup, + shouldRun, - remainingBeans, - totalBeans() { - return totalBeans; - }, + remainingBeans: () => + ignoreBlockLimit ? undefined : blockComputeLimit - totalBeans, + totalBeans: () => totalBeans, + startCleanup, }); return policy; } +/** + * @template [T=unknown] + * @typedef {object} LaunchOptions + * @property {import('./helpers/make-queue.js').QueueStorage} actionQueueStorage + * @property {import('./helpers/make-queue.js').QueueStorage} highPriorityQueueStorage + * @property {string} [kernelStateDBDir] + * @property {import('@agoric/swing-store').SwingStore} [swingStore] + * @property {BufferedKVStore} mailboxStorage + * TODO: Merge together BufferedKVStore and QueueStorage (get/set/delete/commit/abort) + * @property {() => Promise} clearChainSends + * @property {() => void} replayChainSends + * @property {((destPort: string, msg: unknown) => unknown)} bridgeOutbound + * @property {() => ({publish: (value: unknown) => Promise})} [makeInstallationPublisher] + * @property {import('@endo/far').ERef | (() => ERef)} vatconfig + * either an object or a path to a file which JSON-decodes into an object, + * provided directly or through a thunk and/or promise. If the result is an + * object, it may be mutated to normalize and/or extend it. + * @property {unknown} argv for the bootstrap vat (and despite the name, usually + * an object rather than an array) + * @property {typeof process['env']} [env] + * @property {string} [debugName] + * @property {boolean} [verboseBlocks] + * @property {ReturnType} [metricsProvider] + * @property {import('@agoric/telemetry').SlogSender} [slogSender] + * @property {string} [swingStoreTraceFile] + * @property {(...args: unknown[]) => void} [swingStoreExportCallback] + * @property {boolean} [keepSnapshots] + * @property {boolean} [keepTranscripts] + * @property {ReturnType} [archiveSnapshot] + * @property {ReturnType} [archiveTranscript] + * @property {() => object | Promise} [afterCommitCallback] + * @property {import('./chain-main.js').CosmosSwingsetConfig} swingsetConfig + * TODO refactor to clarify relationship vs. import('@agoric/swingset-vat').SwingSetConfig + * --- maybe partition into in-consensus "config" vs. consensus-independent "options"? + * (which would mostly just require `bundleCachePath` to become a `buildSwingset` input) + */ + +/** + * @param {LaunchOptions} options + */ export async function launch({ actionQueueStorage, highPriorityQueueStorage, kernelStateDBDir, + swingStore, mailboxStorage, clearChainSends, replayChainSends, @@ -363,26 +479,36 @@ export async function launch({ // invoked sequentially like if they were awaited, and the block manager // synchronizes before finishing END_BLOCK let pendingSwingStoreExport = Promise.resolve(); - const swingStoreExportCallbackWithQueue = - swingStoreExportCallback && makeWithQueue()(swingStoreExportCallback); - const swingStoreExportSyncCallback = - swingStoreExportCallback && - (updates => { + const swingStoreExportSyncCallback = (() => { + if (!swingStoreExportCallback) return undefined; + const enqueueSwingStoreExportCallback = makeWithQueue()( + swingStoreExportCallback, + ); + return updates => { assert(allowExportCallback, 'export-data callback called at bad time'); - pendingSwingStoreExport = swingStoreExportCallbackWithQueue(updates); + pendingSwingStoreExport = enqueueSwingStoreExportCallback(updates); + }; + })(); + + if (swingStore) { + !swingStoreExportCallback || + Fail`swingStoreExportCallback is not compatible with a provided swingStore; either drop the former or allow launch to open the latter`; + kernelStateDBDir === undefined || + kernelStateDBDir === swingStore.internal.dirPath || + Fail`provided kernelStateDBDir does not match provided swingStore`; + } + const { kernelStorage, hostStorage } = + swingStore || + openSwingStore(/** @type {string} */ (kernelStateDBDir), { + traceFile: swingStoreTraceFile, + exportCallback: swingStoreExportSyncCallback, + keepSnapshots, + keepTranscripts, + archiveSnapshot, + archiveTranscript, }); - - const { kernelStorage, hostStorage } = openSwingStore(kernelStateDBDir, { - traceFile: swingStoreTraceFile, - exportCallback: swingStoreExportSyncCallback, - keepSnapshots, - keepTranscripts, - archiveSnapshot, - archiveTranscript, - }); const { kvStore, commit } = hostStorage; - /** @typedef {ReturnType>} InboundQueue */ /** @type {InboundQueue} */ const actionQueue = makeQueue(actionQueueStorage); /** @type {InboundQueue} */ @@ -450,10 +576,15 @@ export async function launch({ /** * @param {number} blockHeight * @param {ChainRunPolicy} runPolicy + * @returns {Cranker} */ function makeRunSwingset(blockHeight, runPolicy) { let runNum = 0; - async function runSwingset() { + async function runSwingset(phase) { + if (phase === CLEANUP) { + const allowCleanup = runPolicy.startCleanup(); + if (!allowCleanup) return false; + } const startBeans = runPolicy.totalBeans(); controller.writeSlogObject({ type: 'cosmic-swingset-run-start', @@ -492,10 +623,10 @@ export async function launch({ timer.poll(blockTime); // This is before the initial block, we need to finish processing the // entire bootstrap before opening for business. - const runPolicy = computronCounter(params.beansPerUnit, true); + const runPolicy = computronCounter(params, true); const runSwingset = makeRunSwingset(blockHeight, runPolicy); - await runSwingset(); + await runSwingset('forced'); } async function saveChainState() { @@ -687,15 +818,16 @@ export async function launch({ * newly added, running the kernel to completion after each. * * @param {InboundQueue} inboundQueue - * @param {ReturnType} runSwingset + * @param {Cranker} runSwingset + * @param {CrankerPhase} phase */ - async function processActions(inboundQueue, runSwingset) { + async function processActions(inboundQueue, runSwingset, phase) { let keepGoing = true; for await (const { action, context } of inboundQueue.consumeAll()) { const inboundNum = `${context.blockHeight}-${context.txHash}-${context.msgIdx}`; inboundQueueMetrics.decStat(); await performAction(action, inboundNum); - keepGoing = await runSwingset(); + keepGoing = await runSwingset(phase); if (!keepGoing) { // any leftover actions will remain on the inbound queue for possible // processing in the next block @@ -705,20 +837,32 @@ export async function launch({ return keepGoing; } - async function runKernel(runSwingset, blockHeight, blockTime) { + /** + * Trigger the Swingset runs for this block, stopping when out of relevant + * work or when instructed to (whichever comes first). + * + * @param {Cranker} runSwingset + * @param {BlockInfo['blockHeight']} blockHeight + * @param {BlockInfo['blockTime']} blockTime + */ + async function processBlockActions(runSwingset, blockHeight, blockTime) { // First, complete leftover work, if any - let keepGoing = await runSwingset(); + let keepGoing = await runSwingset('leftover'); if (!keepGoing) return; // Then, if we have anything in the special runThisBlock queue, process // it and do no further work. if (runThisBlock.size()) { - await processActions(runThisBlock, runSwingset); + await processActions(runThisBlock, runSwingset, 'forced'); return; } // Then, process as much as we can from the priorityQueue. - keepGoing = await processActions(highPriorityQueue, runSwingset); + keepGoing = await processActions( + highPriorityQueue, + runSwingset, + 'high-priority', + ); if (!keepGoing) return; // Then, update the timer device with the new external time, which might @@ -737,11 +881,14 @@ export async function launch({ // We must run the kernel even if nothing was added since the kernel // only notes state exports and updates consistency hashes when attempting // to perform a crank. - keepGoing = await runSwingset(); + keepGoing = await runSwingset('timer'); if (!keepGoing) return; // Finally, process as much as we can from the actionQueue. - await processActions(actionQueue, runSwingset); + await processActions(actionQueue, runSwingset, 'queued'); + + // Cleanup after terminated vats as allowed. + await runSwingset('cleanup'); } async function endBlock(blockHeight, blockTime, params) { @@ -759,11 +906,11 @@ export async function launch({ // It will also run to completion any work that swingset still had pending. const neverStop = runThisBlock.size() > 0; - // make a runPolicy that will be shared across all cycles - const runPolicy = computronCounter(params.beansPerUnit, neverStop); + // Process the work for this block using a dedicated Cranker with a stateful + // run policy. + const runPolicy = computronCounter(params, neverStop); const runSwingset = makeRunSwingset(blockHeight, runPolicy); - - await runKernel(runSwingset, blockHeight, blockTime); + await processBlockActions(runSwingset, blockHeight, blockTime); if (END_BLOCK_SPIN_MS) { // Introduce a busy-wait to artificially put load on the chain. @@ -950,6 +1097,7 @@ export async function launch({ case ActionType.AG_COSMOS_INIT: { allowExportCallback = true; // cleared by saveOutsideState in COMMIT_BLOCK const { blockHeight, isBootstrap, upgradeDetails } = action; + // TODO: parseParams(action.params), for validation? if (!blockNeedsExecution(blockHeight)) { return true; diff --git a/packages/cosmic-swingset/src/params.js b/packages/cosmic-swingset/src/params.js index f3a57b03749..31f9c6e92ed 100644 --- a/packages/cosmic-swingset/src/params.js +++ b/packages/cosmic-swingset/src/params.js @@ -1,9 +1,20 @@ // @ts-check // @jessie-check -import { Fail } from '@endo/errors'; +import { X, Fail, makeError } from '@endo/errors'; import { Nat, isNat } from '@endo/nat'; +/** + * @template {number | bigint} T + * @param {T} n + * @returns {T} + */ +const requireNat = n => (isNat(n) ? n : Fail`${n} must be a positive integer`); + +/** + * @param {string} s + * @returns {bigint} + */ export const stringToNat = s => { typeof s === 'string' || Fail`${s} must be a string`; const bint = BigInt(s); @@ -12,16 +23,33 @@ export const stringToNat = s => { return nat; }; -/** @param {{key: string, size: number}[]} queueSizeEntries */ -export const parseQueueSizes = queueSizeEntries => +/** + * @template T + * @template U + * @param {Array<[key: string, value: T]>} entries + * @param {(value: T) => U} [mapper] + */ +const recordFromEntries = ( + entries, + mapper = x => /** @type {U} */ (/** @type {unknown} */ (x)), +) => Object.fromEntries( - queueSizeEntries.map(({ key, size }) => { + entries.map(([key, value]) => { typeof key === 'string' || Fail`Key ${key} must be a string`; - isNat(size) || Fail`Size ${size} is not a positive integer`; - return [key, size]; + try { + return [key, mapper(value)]; + } catch (err) { + throw makeError(X`${key} value was invalid`, undefined, { cause: err }); + } }), ); +export const parseQueueSizes = entryRecords => + recordFromEntries( + entryRecords.map(({ key, size }) => [key, size]), + requireNat, + ); + /** @param {Record} queueSizes */ export const encodeQueueSizes = queueSizes => Object.entries(queueSizes).map(([key, size]) => { @@ -38,15 +66,16 @@ export const parseParams = params => { beans_per_unit: rawBeansPerUnit, fee_unit_price: rawFeeUnitPrice, queue_max: rawQueueMax, + vat_cleanup_budget: rawVatCleanupBudget, } = params; + Array.isArray(rawBeansPerUnit) || Fail`beansPerUnit must be an array, not ${rawBeansPerUnit}`; - const beansPerUnit = Object.fromEntries( - rawBeansPerUnit.map(({ key, beans }) => { - typeof key === 'string' || Fail`Key ${key} must be a string`; - return [key, stringToNat(beans)]; - }), + const beansPerUnit = recordFromEntries( + rawBeansPerUnit.map(({ key, beans }) => [key, beans]), + stringToNat, ); + Array.isArray(rawFeeUnitPrice) || Fail`feeUnitPrice ${rawFeeUnitPrice} must be an array`; const feeUnitPrice = rawFeeUnitPrice.map(({ denom, amount }) => { @@ -54,9 +83,20 @@ export const parseParams = params => { denom || Fail`denom ${denom} must be non-empty`; return { denom, amount: stringToNat(amount) }; }); + Array.isArray(rawQueueMax) || Fail`queueMax must be an array, not ${rawQueueMax}`; const queueMax = parseQueueSizes(rawQueueMax); - return { beansPerUnit, feeUnitPrice, queueMax }; + Array.isArray(rawVatCleanupBudget) || + Fail`vatCleanupBudget must be an array, not ${rawVatCleanupBudget}`; + const vatCleanupBudget = recordFromEntries( + rawVatCleanupBudget.map(({ key, value }) => [key, value]), + s => Number(stringToNat(s)), + ); + rawVatCleanupBudget.length === 0 || + vatCleanupBudget.default !== undefined || + Fail`vatCleanupBudget.default must be provided when vatCleanupBudget is not empty`; + + return { beansPerUnit, feeUnitPrice, queueMax, vatCleanupBudget }; }; diff --git a/packages/cosmic-swingset/src/sim-chain.js b/packages/cosmic-swingset/src/sim-chain.js index c665ebdc6c9..f5192ce1a20 100644 --- a/packages/cosmic-swingset/src/sim-chain.js +++ b/packages/cosmic-swingset/src/sim-chain.js @@ -56,6 +56,7 @@ async function makeMapStorage(file) { } export async function connectToFakeChain(basedir, GCI, delay, inbound) { + const env = process.env; const initialHeight = 0; const mailboxFile = path.join(basedir, `fake-chain-${GCI}-mailbox.json`); const bootAddress = `${GCI}-client`; @@ -75,13 +76,13 @@ export async function connectToFakeChain(basedir, GCI, delay, inbound) { }; const getVatConfig = async () => { - const url = await importMetaResolve( - process.env.CHAIN_BOOTSTRAP_VAT_CONFIG || + const href = await importMetaResolve( + env.CHAIN_BOOTSTRAP_VAT_CONFIG || argv.bootMsg.params.bootstrap_vat_config, import.meta.url, ); - const vatconfig = new URL(url).pathname; - return vatconfig; + const { pathname } = new URL(href); + return pathname; }; const stateDBdir = path.join(basedir, `fake-chain-${GCI}-state`); function replayChainSends() { @@ -91,7 +92,6 @@ export async function connectToFakeChain(basedir, GCI, delay, inbound) { return []; } - const env = process.env; const { metricsProvider } = getTelemetryProviders({ console, env, diff --git a/packages/cosmic-swingset/src/sim-params.js b/packages/cosmic-swingset/src/sim-params.js index 441c223b950..4971fe6ed20 100644 --- a/packages/cosmic-swingset/src/sim-params.js +++ b/packages/cosmic-swingset/src/sim-params.js @@ -1,6 +1,7 @@ // @jessie-check // @ts-check +import { Fail } from '@endo/errors'; import { Nat } from '@endo/nat'; const makeStringBeans = (key, beans) => ({ key, beans: `${Nat(beans)}` }); @@ -69,13 +70,50 @@ export const defaultPowerFlagFees = [ ]; export const QueueInbound = 'inbound'; - export const defaultInboundQueueMax = 1_000; - export const defaultQueueMax = [ makeQueueSize(QueueInbound, defaultInboundQueueMax), ]; +/** + * @enum {(typeof VatCleanupPhase)[keyof typeof VatCleanupPhase]} + */ +export const VatCleanupPhase = /** @type {const} */ ({ + Default: 'default', + Exports: 'exports', + Imports: 'imports', + Promises: 'promises', + Kv: 'kv', + Snapshots: 'snapshots', + Transcripts: 'transcripts', +}); + +/** @typedef {Partial>} VatCleanupKeywordsRecord */ + +/** @type {VatCleanupKeywordsRecord} */ +export const VatCleanupDefaults = { + Default: 5, + Kv: 50, +}; + +/** + * @param {VatCleanupKeywordsRecord} keywordsRecord + * @returns {import('@agoric/cosmic-proto/swingset/swingset.js').ParamsSDKType['vat_cleanup_budget']} + */ +export const makeVatCleanupBudgetFromKeywords = keywordsRecord => { + return Object.entries(keywordsRecord).map(([keyName, value]) => { + Object.hasOwn(VatCleanupPhase, keyName) || + Fail`unknown vat cleanup phase keyword ${keyName}`; + return { + key: Reflect.get(VatCleanupPhase, keyName), + value: `${Nat(value)}`, + }; + }); +}; + +export const defaultVatCleanupBudget = + makeVatCleanupBudgetFromKeywords(VatCleanupDefaults); + /** * @type {import('@agoric/cosmic-proto/swingset/swingset.js').ParamsSDKType} */ @@ -85,4 +123,5 @@ export const DEFAULT_SIM_SWINGSET_PARAMS = { bootstrap_vat_config: defaultBootstrapVatConfig, power_flag_fees: defaultPowerFlagFees, queue_max: defaultQueueMax, + vat_cleanup_budget: defaultVatCleanupBudget, }; diff --git a/packages/cosmic-swingset/test/run-policy.test.js b/packages/cosmic-swingset/test/run-policy.test.js new file mode 100644 index 00000000000..7a368621b76 --- /dev/null +++ b/packages/cosmic-swingset/test/run-policy.test.js @@ -0,0 +1,247 @@ +/* eslint-env node */ +import test from 'ava'; +import { assert, q, Fail } from '@endo/errors'; +import { E } from '@endo/far'; +import { BridgeId, objectMap } from '@agoric/internal'; +import { makeFakeStorageKit } from '@agoric/internal/src/storage-test-utils.js'; +import { + defaultBootstrapMessage, + defaultInitMessage, + makeCosmicSwingsetTestKit, +} from '../tools/test-kit.js'; +import { provideEnhancedKVStore } from '../src/helpers/bufferedStorage.js'; +import { + DEFAULT_SIM_SWINGSET_PARAMS, + makeVatCleanupBudgetFromKeywords, +} from '../src/sim-params.js'; + +/** @import { KVStore } from '../src/helpers/bufferedStorage.js' */ + +/** + * Converts a Record into Record + * for defining e.g. a `bundles` group. + * + * @param {Record} src + * @returns {import('@agoric/swingset-vat').SwingSetConfigDescriptor} + */ +const makeSourceDescriptors = src => { + const hardened = objectMap(src, sourceSpec => ({ sourceSpec })); + return JSON.parse(JSON.stringify(hardened)); +}; + +/** + * @param {import('../src/sim-params.js').VatCleanupKeywordsRecord} budget + * @returns {import('@agoric/cosmic-proto/swingset/swingset.js').ParamsSDKType} + */ +const makeCleanupBudgetParams = budget => { + return { + ...DEFAULT_SIM_SWINGSET_PARAMS, + vat_cleanup_budget: makeVatCleanupBudgetFromKeywords(budget), + }; +}; + +test('cleanup work must be limited by vat_cleanup_budget', async t => { + let finish; + t.teardown(() => finish?.()); + // Make a test kit. + const fakeStorageKit = makeFakeStorageKit(''); + const { toStorage: handleVstorage } = fakeStorageKit; + const receiveBridgeSend = (destPort, msg) => { + switch (destPort) { + case BridgeId.STORAGE: { + return handleVstorage(msg); + } + default: + Fail`port ${q(destPort)} not implemented for message ${msg}`; + } + }; + const options = { + bundles: makeSourceDescriptors({ + puppet: '@agoric/swingset-vat/tools/vat-puppet.js', + }), + configOverrides: { + // Aggressive GC. + defaultReapInterval: 1, + // Ensure multiple spans and snapshots. + defaultManagerType: 'xsnap', + snapshotInitial: 2, + snapshotInterval: 4, + }, + fixupInitMessage: () => ({ + ...defaultBootstrapMessage, + params: makeCleanupBudgetParams({ Default: 0 }), + }), + }; + const testKit = await makeCosmicSwingsetTestKit(receiveBridgeSend, options); + const { pushCoreEval, runNextBlock, shutdown, swingStore } = testKit; + finish = shutdown; + await runNextBlock(); + + // Define helper functions for interacting with its swing store. + const mapStore = provideEnhancedKVStore( + /** @type {KVStore} */ (swingStore.kernelStorage.kvStore), + ); + /** @type {(key: string) => string} */ + const mustGet = key => { + const value = mapStore.get(key); + assert(value !== undefined, `kvStore entry for ${key} must exist`); + return value; + }; + + // Launch the new vat and capture its ID. + pushCoreEval( + `${async powers => { + const { bootstrap } = powers.vats; + await E(bootstrap).createVat('doomed', 'puppet'); + }}`, + ); + await runNextBlock(); + const vatIDs = JSON.parse(mustGet('vat.dynamicIDs')); + const vatID = vatIDs.at(-1); + t.is( + vatID, + 'v8', + `time to update expected vatID to ${JSON.stringify(vatIDs)}.at(-1)?`, + ); + t.false( + JSON.parse(mustGet('vats.terminated')).includes(vatID), + 'must not be terminated', + ); + // This key is/was used as a predicate for vat liveness. + // https://github.com/Agoric/agoric-sdk/blob/7ae1f278fa8cbeb0cfc777b7cebf507b1f07c958/packages/SwingSet/src/kernel/state/kernelKeeper.js#L1706 + const sentinelKey = `${vatID}.o.nextID`; + t.true(mapStore.has(sentinelKey)); + + // Define helper functions for interacting the vat's kvStore. + const getKV = () => + [...mapStore].filter(([key]) => key.startsWith(`${vatID}.`)); + const initialEntries = new Map(getKV()); + t.not(initialEntries.size, 0, 'initial kvStore entries must exist'); + + // Give the vat a big footprint. + pushCoreEval( + `${async powers => { + const { bootstrap } = powers.vats; + const doomed = await E(bootstrap).getVatRoot('doomed'); + + const makeArray = (length, makeElem) => Array.from({ length }, makeElem); + + // import 20 remotables and 10 promises + const doomedRemotableImports = await Promise.all( + makeArray(20, (_, i) => + E(bootstrap).makeRemotable(`doomed import ${i}`), + ), + ); + const doomedPromiseImports = ( + await Promise.all(makeArray(10, () => E(bootstrap).makePromiseKit())) + ).map(kit => kit.promise); + const doomedImports = [ + ...doomedRemotableImports, + ...doomedPromiseImports, + ]; + await E(doomed).holdInHeap(doomedImports); + + // export 20 remotables and 10 promises to bootstrap + const doomedRemotableExports = await Promise.all( + makeArray(20, (_, i) => E(doomed).makeRemotable(`doomed export ${i}`)), + ); + const doomedPromiseExports = ( + await Promise.all(makeArray(10, () => E(doomed).makePromiseKit())) + ).map(kit => { + const { promise } = kit; + void promise.catch(() => {}); + return promise; + }); + const doomedExports = [ + ...doomedRemotableExports, + ...doomedPromiseExports, + ]; + await E(bootstrap).holdInHeap(doomedExports); + + // make 20 extra vatstore entries + await E(doomed).holdInBaggage(...makeArray(20, (_, i) => i)); + }}`, + ); + await runNextBlock(); + t.false( + JSON.parse(mustGet('vats.terminated')).includes(vatID), + 'must not be terminated', + ); + const peakEntries = new Map(getKV()); + t.deepEqual( + [...peakEntries.keys()].filter(key => initialEntries.has(key)), + [...initialEntries.keys()], + 'initial kvStore keys must still exist', + ); + t.true( + peakEntries.size > initialEntries.size + 20, + `kvStore entry count must grow by more than 20: ${initialEntries.size} -> ${peakEntries.size}`, + ); + + // Terminate the vat and verify lack of cleanup. + pushCoreEval( + `${async powers => { + const { bootstrap } = powers.vats; + const adminNode = await E(bootstrap).getVatAdminNode('doomed'); + await E(adminNode).terminateWithFailure(); + }}`, + ); + await runNextBlock(); + t.true( + JSON.parse(mustGet('vats.terminated')).includes(vatID), + 'must be terminated', + ); + t.deepEqual( + [...getKV().map(([key]) => key)], + [...peakEntries.keys()], + 'kvStore keys must remain', + ); + + // Allow some cleanup. + // TODO: Verify snapshots and transcripts with `Default: 2` + // cf. packages/SwingSet/test/vat-admin/slow-termination/bootstrap-slow-terminate.js + await runNextBlock({ + params: makeCleanupBudgetParams({ Default: 2 ** 32, Kv: 0 }), + }); + const onlyKV = getKV(); + t.true( + onlyKV.length < peakEntries.size, + `kvStore entry count should have dropped from export/import cleanup: ${peakEntries.size} -> ${onlyKV.length}`, + ); + await runNextBlock({ + params: makeCleanupBudgetParams({ Default: 2 ** 32, Kv: 3 }), + }); + t.is(getKV().length, onlyKV.length - 3, 'initial kvStore deletion'); + let lastBlockInfo = await runNextBlock(); + t.is(getKV().length, onlyKV.length - 6, 'further kvStore deletion'); + + // Wait for the sentinel key to be removed, then re-instantiate the swingset + // and allow remaining cleanup. + while (mapStore.has(sentinelKey)) { + lastBlockInfo = await runNextBlock(); + } + await shutdown({ kernelOnly: true }); + finish = null; + { + // Pick up where we left off with the same data and block, + // but with a new budget. + const newOptions = { + ...options, + swingStore, + fixupInitMessage: () => ({ + ...defaultInitMessage, + ...lastBlockInfo, + params: makeCleanupBudgetParams({ Default: 2 ** 32 }), + }), + }; + // eslint-disable-next-line no-shadow + const { runNextBlock, shutdown } = await makeCosmicSwingsetTestKit( + receiveBridgeSend, + newOptions, + ); + finish = shutdown; + + await runNextBlock(); + t.is(getKV().length, 0, 'cleanup complete'); + } +}); diff --git a/packages/cosmic-swingset/tools/test-kit.js b/packages/cosmic-swingset/tools/test-kit.js new file mode 100644 index 00000000000..88018ef591d --- /dev/null +++ b/packages/cosmic-swingset/tools/test-kit.js @@ -0,0 +1,392 @@ +/* eslint-env node */ + +import * as fsPromises from 'node:fs/promises'; +import * as pathNamespace from 'node:path'; +import { Fail } from '@endo/errors'; +import { + SwingsetMessageType, + QueuedActionType, +} from '@agoric/internal/src/action-types.js'; +import { makeInitMsg } from '@agoric/internal/src/chain-utils.js'; +import { initSwingStore } from '@agoric/swing-store'; +import { makeSlogSender } from '@agoric/telemetry'; +import { launch } from '../src/launch-chain.js'; +import { DEFAULT_SIM_SWINGSET_PARAMS } from '../src/sim-params.js'; +import { + makeBufferedStorage, + makeKVStoreFromMap, +} from '../src/helpers/bufferedStorage.js'; +import { makeQueue, makeQueueStorageMock } from '../src/helpers/make-queue.js'; + +/** @import { BlockInfo, InitMsg } from '@agoric/internal/src/chain-utils.js' */ +/** @import { Mailbox, ManagerType, SwingSetConfig } from '@agoric/swingset-vat' */ +/** @import { KVStore } from '../src/helpers/bufferedStorage.js' */ +/** @import { InboundQueue } from '../src/launch-chain.js'; */ + +/** + * @template T + * @typedef {(input: T) => T} Replacer + */ + +/** @type {Replacer} */ +const deepCopyData = obj => JSON.parse(JSON.stringify(obj)); + +/** @type {Replacer} */ +const stripUndefined = obj => + Object.fromEntries( + Object.entries(obj).filter(([_key, value]) => value !== undefined), + ); + +export const defaultInitMessage = harden( + makeInitMsg({ + type: SwingsetMessageType.AG_COSMOS_INIT, + blockHeight: 100, + blockTime: Math.floor(Date.parse('2020-01-01T00:00Z') / 1000), + chainID: 'localtest', + params: DEFAULT_SIM_SWINGSET_PARAMS, + supplyCoins: [], + + // cosmos-sdk module port mappings are generally ignored in testing, but + // relevant in live blockchains. + // Include them with unpredictable values. + ...Object.fromEntries( + Object.entries({ + storagePort: 0, + swingsetPort: 0, + vbankPort: 0, + vibcPort: 0, + }) + .sort(() => Math.random() - 0.5) + .map(([name, _zero], i) => [name, i + 1]), + ), + }), +); +export const defaultBootstrapMessage = harden({ + ...deepCopyData(defaultInitMessage), + blockHeight: 1, + blockTime: Math.floor(Date.parse('2010-01-01T00:00Z') / 1000), + isBootstrap: true, + supplyCoins: [ + { denom: 'ubld', amount: `${50_000n * 10n ** 6n}` }, + { denom: 'uist', amount: `${1_000_000n * 10n ** 6n}` }, + ], +}); + +/** + * This is intended as the minimum practical definition needed for testing that + * runs with a mock chain on the other side of a bridge. The bootstrap vat is a + * generic object that exposes reflective methods for inspecting and + * interacting with devices and other vats, and is also capable of handling + * 'CORE_EVAL' requests containing a list of { json_permits, js_code } 'evals' + * by evaluating the code in an environment constrained by the permits (and it + * registers itself with the bridge vat as the recipient of such requests). + * + * @type {SwingSetConfig} + */ +const baseConfig = harden({ + defaultReapInterval: 'never', + defaultReapGCKrefs: 'never', + defaultManagerType: undefined, + bootstrap: 'bootstrap', + vats: { + bootstrap: { + sourceSpec: '@agoric/vats/tools/bootstrap-chain-reflective.js', + creationOptions: { + critical: true, + }, + parameters: { + baseManifest: 'MINIMAL', + }, + }, + }, + bundles: { + agoricNames: { + sourceSpec: '@agoric/vats/src/vat-agoricNames.js', + }, + bridge: { + sourceSpec: '@agoric/vats/src/vat-bridge.js', + }, + }, +}); + +/** + * Start a SwingSet kernel to be used by tests and benchmarks, returning objects + * and functions for representing a (mock) blockchain to which it is connected. + * + * Not all `launch`/`buildSwingset` inputs are exposed as inputs here, but that + * should be fixed if/when the need arises (while continuing to construct + * defaults as appropriate). + * + * The shutdown() function _must_ be called after the test or benchmarks are + * complete, else V8 will see the xsnap workers still running, and will never + * exit (leading to a timeout error). Ava tests should use + * t.after.always(shutdown), because the normal t.after() hooks are not run if a + * test fails. + * + * @param {((destPort: string, msg: unknown) => unknown)} receiveBridgeSend + * @param {object} [options] + * @param {string | null} [options.bundleDir] relative to working directory + * @param {SwingSetConfig['bundles']} [options.bundles] extra bundles configuration + * @param {Partial} [options.configOverrides] extensions to the + * default SwingSet configuration (may be overridden by more specific options + * such as `defaultManagerType`) + * @param {string} [options.debugName] + * @param {ManagerType} [options.defaultManagerType] As documented at + * {@link ../../../docs/env.md#swingset_worker_type}, the implicit default of + * 'local' can be overridden by a SWINGSET_WORKER_TYPE environment variable. + * @param {typeof process['env']} [options.env] + * @param {Replacer} [options.fixupInitMessage] a final opportunity to make + * any changes + * @param {Replacer} [options.fixupConfig] a final opportunity + * to make any changes + * @param {import('@agoric/telemetry').SlogSender} [options.slogSender] + * @param {import('../src/chain-main.js').CosmosSwingsetConfig} [options.swingsetConfig] + * @param {import('@agoric/swing-store').SwingStore} [options.swingStore] + * defaults to a new in-memory store + * @param {SwingSetConfig['vats']} [options.vats] extra static vat configuration + * @param {string} [options.baseBootstrapManifest] identifies the colletion of + * "behaviors" to run at bootstrap for creating and configuring the initial + * population of vats (see + * {@link ../../vats/tools/bootstrap-chain-reflective.js}) + * @param {string[]} [options.addBootstrapBehaviors] additional specific + * behavior functions to augment the selected manifest (see + * {@link ../../vats/src/core}) + * @param {string[]} [options.bootstrapCoreEvals] code defining functions to be + * called with a set of powers, each in their own isolated compartment + * @param {object} [powers] + * @param {Pick} [powers.fsp] + * @param {typeof import('node:path').resolve} [powers.resolvePath] + */ +export const makeCosmicSwingsetTestKit = async ( + receiveBridgeSend, + { + // Options for the SwingSet controller/kernel. + bundleDir = 'bundles', + bundles, + configOverrides, + defaultManagerType, + debugName, + env = process.env, + fixupInitMessage, + fixupConfig, + slogSender, + swingsetConfig = {}, + swingStore, + vats, + + // Options for vats (particularly the reflective bootstrap vat). + baseBootstrapManifest, + addBootstrapBehaviors, + bootstrapCoreEvals, + } = {}, + { fsp = fsPromises, resolvePath = pathNamespace.resolve } = {}, +) => { + await null; + /** @type {SwingSetConfig} */ + let config = { + ...deepCopyData(baseConfig), + ...configOverrides, + ...stripUndefined({ defaultManagerType }), + }; + if (bundleDir) { + bundleDir = resolvePath(bundleDir); + config.bundleCachePath = bundleDir; + await fsp.mkdir(bundleDir, { recursive: true }); + } + config.bundles = { ...config.bundles, ...bundles }; + config.vats = { ...config.vats, ...vats }; + + // @ts-expect-error we assume that config.bootstrap is not undefined + const bootstrapVatDesc = config.vats[config.bootstrap]; + Object.assign( + bootstrapVatDesc.parameters, + stripUndefined({ + baseManifest: baseBootstrapManifest, + addBehaviors: addBootstrapBehaviors, + coreProposalCodeSteps: bootstrapCoreEvals, + }), + ); + + if (fixupConfig) config = fixupConfig(config); + + let initMessage = deepCopyData(defaultInitMessage); + if (fixupInitMessage) initMessage = fixupInitMessage(initMessage); + initMessage?.type === SwingsetMessageType.AG_COSMOS_INIT || + Fail`initMessage must be AG_COSMOS_INIT`; + if (initMessage.isBootstrap === undefined && !swingStore) { + initMessage.isBootstrap = true; + } + + if (!swingStore) swingStore = initSwingStore(); // in-memory + const { hostStorage } = swingStore; + + const actionQueueStorage = makeQueueStorageMock().storage; + const highPriorityQueueStorage = makeQueueStorageMock().storage; + const { kvStore: mailboxKVStore, ...mailboxBufferMethods } = + makeBufferedStorage( + /** @type {KVStore} */ (makeKVStoreFromMap(new Map())), + ); + const mailboxStorage = { ...mailboxKVStore, ...mailboxBufferMethods }; + + const savedChainSends = []; + const clearChainSends = async () => savedChainSends.splice(0); + const replayChainSends = (..._args) => { + throw Error('not implemented'); + }; + + if (!slogSender && (env.SLOGFILE || env.SLOGSENDER)) { + slogSender = await makeSlogSender({ env }); + } + + const launchResult = await launch({ + swingStore, + actionQueueStorage, + highPriorityQueueStorage, + mailboxStorage, + clearChainSends, + replayChainSends, + bridgeOutbound: receiveBridgeSend, + vatconfig: config, + argv: { bootMsg: initMessage }, + env, + debugName, + slogSender, + swingsetConfig, + }); + const { blockingSend, shutdown: shutdownKernel } = launchResult; + /** @type {(options?: { kernelOnly?: boolean }) => Promise} */ + const shutdown = async ({ kernelOnly = false } = {}) => { + await shutdownKernel(); + if (kernelOnly) return; + await hostStorage.close(); + }; + + // Remember information about the current block, starting with the init + // message. + let { + isBootstrap: needsBootstrap, + blockHeight: lastBlockHeight, + blockTime: lastBlockTime, + params: lastBlockParams, + } = initMessage; + let lastBlockWalltime = Date.now(); + await blockingSend(initMessage); + + /** + * @returns {BlockInfo} + */ + const getLastBlockInfo = () => ({ + blockHeight: lastBlockHeight, + blockTime: lastBlockTime, + params: lastBlockParams, + }); + + // Advance block time at a nominal rate of one second per real millisecond, + // but introduce discontinuities as necessary to maintain monotonicity. + const nextBlockTime = () => { + const delta = Math.floor(Date.now() - lastBlockWalltime); + return lastBlockTime + (delta > 0 ? delta : 1); + }; + + let blockTxCount = 0; + + /** + * @param {Partial} [blockInfo] + */ + const runNextBlock = async ({ + blockHeight = needsBootstrap ? lastBlockHeight : lastBlockHeight + 1, + blockTime = needsBootstrap ? lastBlockTime : nextBlockTime(), + params = lastBlockParams, + } = {}) => { + needsBootstrap || + blockHeight > lastBlockHeight || + Fail`blockHeight ${blockHeight} must be greater than ${lastBlockHeight}`; + needsBootstrap || + blockTime > lastBlockTime || + Fail`blockTime ${blockTime} must be greater than ${lastBlockTime}`; + needsBootstrap = false; + lastBlockWalltime = Date.now(); + lastBlockHeight = blockHeight; + lastBlockTime = blockTime; + lastBlockParams = params; + blockTxCount = 0; + const context = { blockHeight, blockTime }; + await blockingSend({ + type: SwingsetMessageType.BEGIN_BLOCK, + ...context, + params, + }); + await blockingSend({ type: SwingsetMessageType.END_BLOCK, ...context }); + await blockingSend({ type: SwingsetMessageType.COMMIT_BLOCK, ...context }); + await blockingSend({ + type: SwingsetMessageType.AFTER_COMMIT_BLOCK, + ...context, + }); + return getLastBlockInfo(); + }; + + /** @type {InboundQueue} */ + const actionQueue = makeQueue(actionQueueStorage); + /** @type {InboundQueue} */ + const highPriorityQueue = makeQueue(highPriorityQueueStorage); + /** + * @param {{ type: QueuedActionType } & Record} action + * @param {InboundQueue} [queue] + */ + const pushQueueRecord = (action, queue = actionQueue) => { + blockTxCount += 1; + queue.push({ + action, + context: { + blockHeight: lastBlockHeight + 1, + txHash: blockTxCount, + msgIdx: '', + }, + }); + }; + /** + * @param {string} fnText must evaluate to a function that will be invoked in + * a core eval compartment with a "powers" argument as attenuated by + * `jsonPermits` (with no attenuation by default). + * @param {string} [jsonPermits] must deserialize into a BootstrapManifestPermit + * @param {InboundQueue} [queue] + */ + const pushCoreEval = ( + fnText, + jsonPermits = 'true', + queue = highPriorityQueue, + ) => { + // Fail noisily if fnText does not evaluate to a function. + // This must be refactored if there is ever a need for such input. + const fn = new Compartment().evaluate(fnText); + typeof fn === 'function' || Fail`text must evaluate to a function`; + /** @type {import('@agoric/vats/src/core/lib-boot.js').BootstrapManifestPermit} */ + // eslint-disable-next-line no-unused-vars + const permit = JSON.parse(jsonPermits); + /** @type {import('@agoric/cosmic-proto/swingset/swingset.js').CoreEvalSDKType} */ + const coreEvalDesc = { + json_permits: jsonPermits, + js_code: fnText, + }; + const action = { + type: QueuedActionType.CORE_EVAL, + evals: [coreEvalDesc], + }; + pushQueueRecord(action, queue); + }; + + return { + // SwingSet-oriented references. + actionQueue, + highPriorityQueue, + mailboxStorage, + shutdown, + swingStore, + + // Functions specific to this kit. + getLastBlockInfo, + pushQueueRecord, + pushCoreEval, + runNextBlock, + }; +}; diff --git a/packages/internal/README.md b/packages/internal/README.md index 032078bb0fd..cebfad5ae9a 100644 --- a/packages/internal/README.md +++ b/packages/internal/README.md @@ -10,11 +10,11 @@ Like all `@agoric` packages it follows Semantic Versioning. Unlike the others, i # Design -It is meant to be a home for modules that have no Agoric-specific dependencies themselves. It does depend on a these other @agoric packages but they are all destined to migrate out of the repo, +It is meant to be a home for modules that have no dependencies on other packages in this repository, except for the following packages that do not theirselves depend upon any other @agoric packages and may be destined for migration elsewhere: -- base-zone -- store -- assert +- [base-zone](../base-zone) +- [store](../store) +- [cosmic-proto](../cosmic-proto) This package may not take dependencies on any others in this repository. diff --git a/packages/internal/package.json b/packages/internal/package.json index bb40c46d42f..77ab29fce95 100755 --- a/packages/internal/package.json +++ b/packages/internal/package.json @@ -34,6 +34,7 @@ "jessie.js": "^0.3.4" }, "devDependencies": { + "@agoric/cosmic-proto": "^0.4.0", "@endo/exo": "^1.5.6", "@endo/init": "^1.1.6", "ava": "^5.3.0", diff --git a/packages/internal/src/action-types.js b/packages/internal/src/action-types.js index 30e363f5117..837059d9b52 100644 --- a/packages/internal/src/action-types.js +++ b/packages/internal/src/action-types.js @@ -1,19 +1,76 @@ // @jessie-check -export const AG_COSMOS_INIT = 'AG_COSMOS_INIT'; -export const SWING_STORE_EXPORT = 'SWING_STORE_EXPORT'; -export const BEGIN_BLOCK = 'BEGIN_BLOCK'; +/** + * Types of messages used for communication between a cosmos-sdk blockchain node + * and its paired swingset VM, especially for the ABCI lifecycle. See: + * + * - https://github.com/tendermint/tendermint/blob/v0.34.x/spec/abci/abci.md#block-execution + * - ../../../golang/cosmos/vm/action.go + * - ../../../golang/cosmos/app/app.go + * - ../../../golang/cosmos/x/swingset/abci.go + * - ../../../golang/cosmos/x/swingset/keeper/swing_store_exports_handler.go + * - ../../cosmic-swingset/src/chain-main.js + * - ../../cosmic-swingset/src/launch-chain.js + * + * @enum {(typeof SwingsetMessageType)[keyof typeof SwingsetMessageType]} + */ +export const SwingsetMessageType = /** @type {const} */ ({ + AG_COSMOS_INIT: 'AG_COSMOS_INIT', // used to synchronize at process launch + BEGIN_BLOCK: 'BEGIN_BLOCK', + END_BLOCK: 'END_BLOCK', + COMMIT_BLOCK: 'COMMIT_BLOCK', + AFTER_COMMIT_BLOCK: 'AFTER_COMMIT_BLOCK', + SWING_STORE_EXPORT: 'SWING_STORE_EXPORT', // used to synchronize data export +}); +harden(SwingsetMessageType); + +// TODO: Update all imports to use SwingsetMessageType. But until then... +export const { + AG_COSMOS_INIT, + BEGIN_BLOCK, + END_BLOCK, + COMMIT_BLOCK, + AFTER_COMMIT_BLOCK, + SWING_STORE_EXPORT, +} = SwingsetMessageType; + +/** + * Types of "action" messages consumed by the swingset VM from actionQueue or + * highPriorityQueue during END_BLOCK. See: + * + * - ../../../golang/cosmos/x/swingset/keeper/msg_server.go + * - ../../../golang/cosmos/x/swingset/keeper/proposal.go + * - ../../../golang/cosmos/x/vbank/vbank.go + * - ../../../golang/cosmos/x/vibc/handler.go + * - ../../../golang/cosmos/x/vibc/keeper/triggers.go + * - ../../../golang/cosmos/x/vibc/types/ibc_module.go + * + * @enum {(typeof QueuedActionType)[keyof typeof QueuedActionType]} + */ +export const QueuedActionType = /** @type {const} */ ({ + CORE_EVAL: 'CORE_EVAL', + DELIVER_INBOUND: 'DELIVER_INBOUND', + IBC_EVENT: 'IBC_EVENT', + INSTALL_BUNDLE: 'INSTALL_BUNDLE', + PLEASE_PROVISION: 'PLEASE_PROVISION', + VBANK_BALANCE_UPDATE: 'VBANK_BALANCE_UPDATE', + WALLET_ACTION: 'WALLET_ACTION', + WALLET_SPEND_ACTION: 'WALLET_SPEND_ACTION', +}); +harden(QueuedActionType); + +// TODO: Update all imports to use QueuedActionType. But until then... +export const { + CORE_EVAL, + DELIVER_INBOUND, + IBC_EVENT, + INSTALL_BUNDLE, + PLEASE_PROVISION, + VBANK_BALANCE_UPDATE, + WALLET_ACTION, + WALLET_SPEND_ACTION, +} = QueuedActionType; + export const CALCULATE_FEES_IN_BEANS = 'CALCULATE_FEES_IN_BEANS'; -export const CORE_EVAL = 'CORE_EVAL'; -export const DELIVER_INBOUND = 'DELIVER_INBOUND'; -export const END_BLOCK = 'END_BLOCK'; -export const COMMIT_BLOCK = 'COMMIT_BLOCK'; -export const AFTER_COMMIT_BLOCK = 'AFTER_COMMIT_BLOCK'; -export const IBC_EVENT = 'IBC_EVENT'; -export const PLEASE_PROVISION = 'PLEASE_PROVISION'; -export const VBANK_BALANCE_UPDATE = 'VBANK_BALANCE_UPDATE'; -export const WALLET_ACTION = 'WALLET_ACTION'; -export const WALLET_SPEND_ACTION = 'WALLET_SPEND_ACTION'; -export const INSTALL_BUNDLE = 'INSTALL_BUNDLE'; export const VTRANSFER_IBC_EVENT = 'VTRANSFER_IBC_EVENT'; export const KERNEL_UPGRADE_EVENTS = 'KERNEL_UPGRADE_EVENTS'; diff --git a/packages/internal/src/chain-utils.js b/packages/internal/src/chain-utils.js new file mode 100644 index 00000000000..fe1716a6016 --- /dev/null +++ b/packages/internal/src/chain-utils.js @@ -0,0 +1,56 @@ +/** + * @file Types and utilities for supporting blockchain functionality without + * risking import cycles. + * + * TODO: Integrate (or integrate with) any/all of: + * + * - ./action-types.js + * - ./chain-storage-paths.js + * - ./config.js + * - ../../cosmic-proto (if comfortable co-residing with generated code) + */ + +import * as _ActionType from './action-types.js'; + +/** @typedef {`${bigint}`} NatString */ + +/** + * @typedef {object} BlockInfo + * @property {number} blockHeight + * @property {number} blockTime POSIX Seconds Since the Epoch + * @property {import('@agoric/cosmic-proto/swingset/swingset.js').ParamsSDKType} params + */ + +/** + * @typedef {BlockInfo & { + * type: typeof _ActionType.AG_COSMOS_INIT; + * chainID: string; + * supplyCoins: { denom: string; amount: NatString }[]; + * }} InitMsg + * cosmosInitAction fields that are subject to consensus. See cosmosInitAction + * in {@link ../../../golang/cosmos/app/app.go}. + */ + +/** + * @param {any} initAction + * @returns {InitMsg} + */ +export const makeInitMsg = initAction => { + const { + type, + blockHeight, + blockTime, + chainID, + params, + // NB: resolvedConfig is independent of consensus and MUST NOT be included + supplyCoins, + } = initAction; + return { + type, + blockHeight, + blockTime, + chainID, + params, + supplyCoins, + }; +}; diff --git a/packages/swing-store/src/exporter.js b/packages/swing-store/src/exporter.js index 0e355c50b9b..4b39a8c69a3 100644 --- a/packages/swing-store/src/exporter.js +++ b/packages/swing-store/src/exporter.js @@ -176,7 +176,7 @@ export function makeSwingStoreExporter(dirPath, options = {}) { function getArtifactNames() { if (artifactMode !== 'debug') { // synchronously throw if this DB will not be able to yield all the desired artifacts - const internal = { snapStore, bundleStore, transcriptStore }; + const internal = { dirPath, snapStore, bundleStore, transcriptStore }; assertComplete(internal, artifactMode); } return generateArtifactNames(); diff --git a/packages/swing-store/src/internal.js b/packages/swing-store/src/internal.js index 6abc2485e6d..6b3ff234ce1 100644 --- a/packages/swing-store/src/internal.js +++ b/packages/swing-store/src/internal.js @@ -6,6 +6,7 @@ import { Fail, q } from '@endo/errors'; * @typedef { import('./bundleStore.js').BundleStoreInternal } BundleStoreInternal * * @typedef {{ + * dirPath: string | null, * transcriptStore: TranscriptStoreInternal, * snapStore: SnapStoreInternal, * bundleStore: BundleStoreInternal, diff --git a/packages/swing-store/src/swingStore.js b/packages/swing-store/src/swingStore.js index 64d2d01de67..3bcfe08b5a2 100644 --- a/packages/swing-store/src/swingStore.js +++ b/packages/swing-store/src/swingStore.js @@ -505,6 +505,7 @@ export function makeSwingStore(dirPath, forceReset, options = {}) { /** @type {import('./internal.js').SwingStoreInternal} */ const internal = harden({ + dirPath, snapStore, transcriptStore, bundleStore, diff --git a/packages/vats/src/core/basic-behaviors.js b/packages/vats/src/core/basic-behaviors.js index 4bbbdf69ad9..5783e87d0fc 100644 --- a/packages/vats/src/core/basic-behaviors.js +++ b/packages/vats/src/core/basic-behaviors.js @@ -21,29 +21,6 @@ import { makeScopedBridge } from '../bridge.js'; /** @import {GovernableStartFn, GovernanceFacetKit} from '@agoric/governance/src/types.js'; */ -/** - * In golang/cosmos/app/app.go, we define cosmosInitAction with type - * AG_COSMOS_INIT, with the following shape. - * - * The uist supplyCoins value is taken from genesis, thereby authorizing the - * minting an initial supply of RUN. - */ -// eslint-disable-next-line no-unused-vars -const bootMsgEx = { - type: 'AG_COSMOS_INIT', - chainID: 'agoric', - storagePort: 1, - supplyCoins: [ - { denom: 'provisionpass', amount: '100' }, - { denom: 'sendpacketpass', amount: '100' }, - { denom: 'ubld', amount: '1000000000000000' }, - { denom: 'uist', amount: '50000000000' }, - ], - swingsetPort: 4, - vbankPort: 3, - vibcPort: 2, -}; - /** * TODO: review behaviors carefully for powers that go out of scope, since we * may want/need them later. @@ -560,7 +537,11 @@ export const installBootContracts = async ({ * Mint IST genesis supply. * * @param {BootstrapPowers & { - * vatParameters: { argv: { bootMsg?: typeof bootMsgEx } }; + * vatParameters: { + * argv: { + * bootMsg?: import('@agoric/internal/src/chain-utils.js').InitMsg; + * }; + * }; * }} powers */ export const mintInitialSupply = async ({ diff --git a/packages/vats/src/core/lib-boot.js b/packages/vats/src/core/lib-boot.js index 486a7f12603..ada8fedc1f4 100644 --- a/packages/vats/src/core/lib-boot.js +++ b/packages/vats/src/core/lib-boot.js @@ -47,7 +47,7 @@ const setDiff = (a, b) => a.filter(x => !b.includes(x)); /** * @param {import('@agoric/swingset-vat').VatPowers & { * D: DProxy; - * logger: (msg) => void; + * logger?: typeof console.log; * }} vatPowers * @param {Record} vatParameters * @param {BootstrapManifest} bootManifest diff --git a/packages/vats/test/vat-bank-integration.test.js b/packages/vats/test/vat-bank-integration.test.js index 33cfe964744..e06fb358a21 100644 --- a/packages/vats/test/vat-bank-integration.test.js +++ b/packages/vats/test/vat-bank-integration.test.js @@ -3,6 +3,7 @@ import { test } from '@agoric/swingset-vat/tools/prepare-test-env-ava.js'; import { makeScalarMapStore } from '@agoric/vat-data'; import { E } from '@endo/far'; +import { AG_COSMOS_INIT } from '@agoric/internal/src/action-types.js'; import { makePromiseKit } from '@endo/promise-kit'; import { makeZoeKitForTest } from '@agoric/zoe/tools/setup-zoe.js'; import { observeIteration } from '@agoric/notifier'; @@ -45,14 +46,12 @@ test('mintInitialSupply, addBankAssets bootstrap actions', async t => { }); // Genesis RUN supply: 50 + /** @type {import('@agoric/internal/src/chain-utils.js').InitMsg} */ + // @ts-expect-error missing properties const bootMsg = { - type: 'INIT@@', + type: AG_COSMOS_INIT, chainID: 'ag', - storagePort: 1, supplyCoins: [{ amount: '50000000', denom: 'uist' }], - swingsetPort: 4, - vbankPort: 2, - vibcPort: 3, }; // Now run the function under test. diff --git a/packages/vats/tools/bootstrap-chain-reflective.js b/packages/vats/tools/bootstrap-chain-reflective.js new file mode 100644 index 00000000000..74bdc8259b0 --- /dev/null +++ b/packages/vats/tools/bootstrap-chain-reflective.js @@ -0,0 +1,194 @@ +/** + * @file Source code for a bootstrap vat that runs blockchain behaviors (such as + * bridge vat integration) and exposes reflective methods for use in testing. + * + * TODO: Share code with packages/SwingSet/tools/bootstrap-relay.js + */ + +import { Fail, q } from '@endo/errors'; +import { Far, E } from '@endo/far'; +import { makePromiseKit } from '@endo/promise-kit'; +import { buildManualTimer } from '@agoric/swingset-vat/tools/manual-timer.js'; +import { makeReflectionMethods } from '@agoric/swingset-vat/tools/vat-puppet.js'; +import { makeDurableZone } from '@agoric/zone/durable.js'; +import { makeBootstrap } from '../src/core/lib-boot.js'; +import * as basicBehaviorsNamespace from '../src/core/basic-behaviors.js'; +import * as chainBehaviorsNamespace from '../src/core/chain-behaviors.js'; +import * as utils from '../src/core/utils.js'; + +// Gather up all defined bootstrap behaviors. +const { BASIC_BOOTSTRAP_PERMITS: BASIC_BOOTSTRAP, ...basicBehaviors } = + basicBehaviorsNamespace; +const { + CHAIN_BOOTSTRAP_MANIFEST: CHAIN_BOOTSTRAP, + SHARED_CHAIN_BOOTSTRAP_MANIFEST: SHARED_CHAIN_BOOTSTRAP, + ...chainBehaviors +} = chainBehaviorsNamespace; +const manifests = { BASIC_BOOTSTRAP, CHAIN_BOOTSTRAP, SHARED_CHAIN_BOOTSTRAP }; +const allBehaviors = { ...basicBehaviors, ...chainBehaviors }; +export const modules = { + behaviors: { ...allBehaviors }, + utils: { ...utils }, +}; + +// Support constructing a new manifest as a subset from the union of all +// standard manifests. +const allPermits = Object.fromEntries( + Object.values(manifests) + .map(manifest => Object.entries(manifest)) + .flat(), +); +const makeManifestForBehaviors = behaviors => { + const manifest = {}; + for (const behavior of behaviors) { + const { name } = behavior; + Object.hasOwn(allPermits, name) || Fail`missing permit for ${name}`; + manifest[name] = allPermits[name]; + } + return manifest; +}; + +// Define a minimal manifest of entries plucked from the above union. +manifests.MINIMAL = makeManifestForBehaviors([ + allBehaviors.bridgeCoreEval, + allBehaviors.makeBridgeManager, + allBehaviors.makeVatsFromBundles, + allBehaviors.startTimerService, + allBehaviors.setupClientManager, +]); + +/** + * @param {VatPowers & { D: DProxy; testLog: typeof console.log }} vatPowers + * @param {{ + * baseManifest?: string; + * addBehaviors?: string[]; + * coreProposalCodeSteps?: string[]; + * }} bootstrapParameters + * @param {import('@agoric/vat-data').Baggage} baggage + */ +export const buildRootObject = (vatPowers, bootstrapParameters, baggage) => { + const manualTimer = buildManualTimer(); + let vatAdmin; + const { promise: vatAdminP, resolve: captureVatAdmin } = makePromiseKit(); + void vatAdminP.then(value => (vatAdmin = value)); // for better debugging + /** @typedef {{ root: object; incarnationNumber?: number }} VatRecord */ + /** + * @typedef {VatRecord & + * import('@agoric/swingset-vat').CreateVatResults & { + * bundleCap: unknown; + * }} DynamicVatRecord + */ + /** @type {Map} */ + const vatRecords = new Map(); + const devicesByName = new Map(); + + const { baseManifest: manifestName = 'MINIMAL', addBehaviors = [] } = + bootstrapParameters; + Object.hasOwn(manifests, manifestName) || + Fail`missing manifest ${manifestName}`; + const manifest = { + ...manifests[manifestName], + ...makeManifestForBehaviors(addBehaviors), + }; + + /** + * bootstrapBase provides CORE_EVAL support, and also exposes: + * + * - promise-space functions consumeItem(name), produceItem(name, resolution), + * resetItem(name) + * - awaitVatObject(presence: object, path?: PropertyKey[]) + * - snapshotStore(store: { entries: () => Iterable<[K, V]> }): Array<[K, + * V]> + */ + const bootstrapBase = makeBootstrap( + { ...vatPowers, logger: vatPowers.testLog }, + bootstrapParameters, + manifest, + allBehaviors, + modules, + makeDurableZone(baggage), + ); + + const reflectionMethods = makeReflectionMethods(vatPowers, baggage); + + return Far('root', { + ...reflectionMethods, + + ...bootstrapBase, + bootstrap: async (vats, devices) => { + await bootstrapBase.bootstrap(vats, devices); + + // createVatAdminService is idempotent (despite the name). + captureVatAdmin(E(vats.vatAdmin).createVatAdminService(devices.vatAdmin)); + + // Capture references to static vats and devices. + for (const [name, root] of Object.entries(vats)) { + if (name !== 'vatAdmin') { + vatRecords.set(name, { root }); + } + } + for (const [name, device] of Object.entries(devices)) { + devicesByName.set(name, device); + } + }, + + getDevice: deviceName => devicesByName.get(deviceName), + + getManualTimer: () => manualTimer, + + getVatAdmin: () => vatAdmin || vatAdminP, + + getVatAdminNode: vatName => { + const vat = + vatRecords.get(vatName) || Fail`unknown vat name: ${q(vatName)}`; + const { adminNode } = /** @type {DynamicVatRecord} */ (vat); + return adminNode; + }, + + getVatRoot: vatName => { + const vat = + vatRecords.get(vatName) || Fail`unknown vat name: ${q(vatName)}`; + const { root } = vat; + return root; + }, + + /** + * @param {string} vatName + * @param {string} [bundleCapName] + * @param {{ vatParameters?: object } & Record} [vatOptions] + * @returns {Promise} root object of the new vat + */ + createVat: async (vatName, bundleCapName = vatName, vatOptions = {}) => { + const bundleCap = await E(vatAdminP).getNamedBundleCap(bundleCapName); + const { root, adminNode } = await E(vatAdminP).createVat(bundleCap, { + vatParameters: {}, + ...vatOptions, + }); + vatRecords.set(vatName, { root, adminNode, bundleCap }); + return root; + }, + + /** + * @param {string} vatName + * @param {string} [bundleCapName] + * @param {{ vatParameters?: object } & Record} [vatOptions] + * @returns {Promise} the resulting incarnation number + */ + upgradeVat: async (vatName, bundleCapName, vatOptions = {}) => { + const vatRecord = /** @type {DynamicVatRecord} */ ( + vatRecords.get(vatName) || Fail`unknown vat name: ${q(vatName)}` + ); + const bundleCap = await (bundleCapName + ? E(vatAdminP).getNamedBundleCap(bundleCapName) + : vatRecord.bundleCap); + const upgradeOptions = { vatParameters: {}, ...vatOptions }; + const { incarnationNumber } = await E(vatRecord.adminNode).upgrade( + bundleCap, + upgradeOptions, + ); + vatRecord.incarnationNumber = incarnationNumber; + return incarnationNumber; + }, + }); +}; +harden(buildRootObject); diff --git a/packages/vats/tsconfig.build.json b/packages/vats/tsconfig.build.json index fe5ea57d603..1487c804a7b 100644 --- a/packages/vats/tsconfig.build.json +++ b/packages/vats/tsconfig.build.json @@ -2,5 +2,7 @@ "extends": [ "./tsconfig.json", "../../tsconfig-build-options.json" - ] + ], + // FIXME: https://github.com/Agoric/agoric-sdk/issues/10351 + "exclude": ["tools/bootstrap-chain-reflective.js"] }