From 02c8138bb2090bc995bfe517bc52952014458984 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Fri, 27 Sep 2024 01:30:31 -0400 Subject: [PATCH 01/34] feat(x/swingset): Add parameters for controlling vat cleanup budget Ref #8928 --- .../proto/agoric/swingset/swingset.proto | 25 ++++++++++ .../cosmos/x/swingset/types/default-params.go | 34 +++++++++++-- golang/cosmos/x/swingset/types/params.go | 50 +++++++++++++++++++ golang/cosmos/x/swingset/types/params_test.go | 48 +++++++++++++++--- 4 files changed, 144 insertions(+), 13 deletions(-) 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/types/default-params.go b/golang/cosmos/x/swingset/types/default-params.go index 46c7fe286d5..3cc11c881ea 100644 --- a/golang/cosmos/x/swingset/types/default-params.go +++ b/golang/cosmos/x/swingset/types/default-params.go @@ -22,13 +22,22 @@ 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" + VatCleanupKv = "kv" + VatCleanupSnapshots = "snapshots" + VatCleanupTranscripts = "transcripts" ) var ( @@ -62,10 +71,25 @@ var ( } DefaultInboundQueueMax = int32(1_000) - - DefaultQueueMax = []QueueSize{ + DefaultQueueMax = []QueueSize{ NewQueueSize(QueueInbound, DefaultInboundQueueMax), } + + // FIXME: Settle on default values + DefaultVatCleanupDefault = sdk.NewUint(0) // 5 + DefaultVatCleanupExports = sdk.NewUint(0) // 5 + DefaultVatCleanupImports = sdk.NewUint(0) // 5 + DefaultVatCleanupKv = sdk.NewUint(0) // 50 + DefaultVatCleanupSnapshots = sdk.NewUint(0) // 50 + DefaultVatCleanupTranscripts = sdk.NewUint(0) // 50 + DefaultVatCleanupBudget = []UintMapEntry{ + UintMapEntry{VatCleanupDefault, DefaultVatCleanupDefault}, + UintMapEntry{VatCleanupExports, DefaultVatCleanupExports}, + UintMapEntry{VatCleanupImports, DefaultVatCleanupImports}, + 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/params.go b/golang/cosmos/x/swingset/types/params.go index 9a18d03de8b..9e4a63e83f5 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,6 +171,14 @@ func validateQueueMax(i interface{}) error { return nil } +func validateVatCleanupBudget(i interface{}) error { + _, ok := i.([]UintMapEntry) + if !ok { + return fmt.Errorf("invalid parameter type: %T", i) + } + 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 @@ -182,10 +196,15 @@ func UpdateParams(params Params) (Params, error) { if err != nil { return params, err } + newVcb, err := appendMissingDefaults(params.VatCleanupBudget, DefaultVatCleanupBudget) + if err != nil { + return params, err + } params.BeansPerUnit = newBpu params.PowerFlagFees = newPff params.QueueMax = newQm + params.VatCleanupBudget = newVcb return params, nil } @@ -238,3 +257,34 @@ func appendMissingDefaultQueueSize(qs []QueueSize, defaultQs []QueueSize) ([]Que } return qs, nil } + +// 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") + } + + existingKeys := make(map[string]bool, len(entries)) + for _, entry := range entries { + existingKeys[getKey(entry)] = true + } + + for _, defaultEntry := range defaults { + if exists := existingKeys[getKey(defaultEntry)]; !exists { + entries = append(entries, defaultEntry) + } + } + + 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..ec0b44ea2ce 100644 --- a/golang/cosmos/x/swingset/types/params_test.go +++ b/golang/cosmos/x/swingset/types/params_test.go @@ -90,27 +90,59 @@ 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) } } From 6548bfa836bcdb9813fea606e524e6c59e35f1c5 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Fri, 27 Sep 2024 01:34:29 -0400 Subject: [PATCH 02/34] build(golang/cosmos): Generate types with `make proto-gen` --- golang/cosmos/x/swingset/types/swingset.pb.go | 442 +++++++++++++++--- golang/cosmos/x/vtransfer/types/genesis.pb.go | 1 + 2 files changed, 387 insertions(+), 56 deletions(-) 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/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"` } From 3ff320401035f465aa3efbe4bcfe07f5a6d23db4 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Fri, 27 Sep 2024 01:34:29 -0400 Subject: [PATCH 03/34] build(cosmic-proto): Generate types with `yarn build && yarn codegen` --- .../proto/agoric/swingset/swingset.proto | 25 ++++ .../src/codegen/agoric/swingset/swingset.ts | 137 +++++++++++++++++- 2 files changed, 160 insertions(+), 2 deletions(-) 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: '', From 0571652f3ae3c334923607c902102c785f5ff979 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Fri, 27 Sep 2024 01:39:14 -0400 Subject: [PATCH 04/34] refactor(x/swingset): DRY out UpdateParams --- golang/cosmos/x/swingset/types/params.go | 56 +------------------ golang/cosmos/x/swingset/types/params_test.go | 2 +- 2 files changed, 4 insertions(+), 54 deletions(-) diff --git a/golang/cosmos/x/swingset/types/params.go b/golang/cosmos/x/swingset/types/params.go index 9e4a63e83f5..30b12d19be3 100644 --- a/golang/cosmos/x/swingset/types/params.go +++ b/golang/cosmos/x/swingset/types/params.go @@ -184,15 +184,15 @@ func validateVatCleanupBudget(i interface{}) error { // 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 } @@ -208,56 +208,6 @@ func UpdateParams(params Params) (Params, error) { 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) - } - } - 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{}{} - } - - for _, f := range defaultPff { - if _, exists := existingPff[f.PowerFlag]; !exists { - pff = append(pff, f) - } - } - 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 -} - // 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) { diff --git a/golang/cosmos/x/swingset/types/params_test.go b/golang/cosmos/x/swingset/types/params_test.go index ec0b44ea2ce..b43e08351f3 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) { From e6ad881669bbf0840ce7aa25050200f135d7ebf1 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Fri, 27 Sep 2024 01:41:11 -0400 Subject: [PATCH 05/34] chore(cosmic-swingset): Add new parameters to sim-params.js --- packages/cosmic-swingset/src/sim-params.js | 24 ++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/cosmic-swingset/src/sim-params.js b/packages/cosmic-swingset/src/sim-params.js index 441c223b950..332febc1f6e 100644 --- a/packages/cosmic-swingset/src/sim-params.js +++ b/packages/cosmic-swingset/src/sim-params.js @@ -69,13 +69,32 @@ export const defaultPowerFlagFees = [ ]; export const QueueInbound = 'inbound'; - export const defaultInboundQueueMax = 1_000; - export const defaultQueueMax = [ makeQueueSize(QueueInbound, defaultInboundQueueMax), ]; +export const vatCleanupDefault = 'default'; +export const defaultVatCleanupDefault = 0; +export const vatCleanupExports = 'exports'; +export const defaultVatCleanupExports = 0; +export const vatCleanupImports = 'imports'; +export const defaultVatCleanupImports = 0; +export const vatCleanupKv = 'kv'; +export const defaultVatCleanupKv = 0; +export const vatCleanupSnapshots = 'snapshots'; +export const defaultVatCleanupSnapshots = 0; +export const vatCleanupTranscripts = 'transcripts'; +export const defaultVatCleanupTranscripts = 0; +export const defaultVatCleanupBudget = [ + { key: vatCleanupDefault, value: `${defaultVatCleanupDefault}` }, + { key: vatCleanupExports, value: `${defaultVatCleanupExports}` }, + { key: vatCleanupImports, value: `${defaultVatCleanupImports}` }, + { key: vatCleanupKv, value: `${defaultVatCleanupKv}` }, + { key: vatCleanupSnapshots, value: `${defaultVatCleanupSnapshots}` }, + { key: vatCleanupTranscripts, value: `${defaultVatCleanupTranscripts}` }, +]; + /** * @type {import('@agoric/cosmic-proto/swingset/swingset.js').ParamsSDKType} */ @@ -85,4 +104,5 @@ export const DEFAULT_SIM_SWINGSET_PARAMS = { bootstrap_vat_config: defaultBootstrapVatConfig, power_flag_fees: defaultPowerFlagFees, queue_max: defaultQueueMax, + vat_cleanup_budget: defaultVatCleanupBudget, }; From 80bcca0745ac037d31a45e017b171212efaf282a Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Fri, 27 Sep 2024 01:57:12 -0400 Subject: [PATCH 06/34] feat(cosmic-swingset): Update parseParams to read and validate vat cleanup budget data Ref #8928 --- packages/cosmic-swingset/src/params.js | 43 ++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/packages/cosmic-swingset/src/params.js b/packages/cosmic-swingset/src/params.js index f3a57b03749..655a6cd3d11 100644 --- a/packages/cosmic-swingset/src/params.js +++ b/packages/cosmic-swingset/src/params.js @@ -1,9 +1,13 @@ // @ts-check // @jessie-check -import { Fail } from '@endo/errors'; +import { X, Fail, makeError } from '@endo/errors'; import { Nat, isNat } from '@endo/nat'; +/** + * @param {string} s + * @returns {bigint} + */ export const stringToNat = s => { typeof s === 'string' || Fail`${s} must be a string`; const bint = BigInt(s); @@ -12,6 +16,27 @@ export const stringToNat = s => { return nat; }; +/** + * @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( + entries.map(([key, value]) => { + typeof key === 'string' || Fail`Key ${key} must be a string`; + try { + return [key, mapper(value)]; + } catch (err) { + throw makeError(X`${key} value was invalid`, undefined, { cause: err }); + } + }), + ); + /** @param {{key: string, size: number}[]} queueSizeEntries */ export const parseQueueSizes = queueSizeEntries => Object.fromEntries( @@ -38,7 +63,9 @@ 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( @@ -47,6 +74,7 @@ export const parseParams = params => { return [key, stringToNat(beans)]; }), ); + Array.isArray(rawFeeUnitPrice) || Fail`feeUnitPrice ${rawFeeUnitPrice} must be an array`; const feeUnitPrice = rawFeeUnitPrice.map(({ denom, amount }) => { @@ -54,9 +82,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 }; }; From 54b68426385f64a68631b11458abb8be2b2eb7fd Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Fri, 27 Sep 2024 01:58:21 -0400 Subject: [PATCH 07/34] refactor(cosmic-swingset): DRY out parseParams --- packages/cosmic-swingset/src/params.js | 27 +++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/packages/cosmic-swingset/src/params.js b/packages/cosmic-swingset/src/params.js index 655a6cd3d11..31f9c6e92ed 100644 --- a/packages/cosmic-swingset/src/params.js +++ b/packages/cosmic-swingset/src/params.js @@ -4,6 +4,13 @@ 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} @@ -37,14 +44,10 @@ const recordFromEntries = ( }), ); -/** @param {{key: string, size: number}[]} queueSizeEntries */ -export const parseQueueSizes = queueSizeEntries => - Object.fromEntries( - queueSizeEntries.map(({ key, size }) => { - typeof key === 'string' || Fail`Key ${key} must be a string`; - isNat(size) || Fail`Size ${size} is not a positive integer`; - return [key, size]; - }), +export const parseQueueSizes = entryRecords => + recordFromEntries( + entryRecords.map(({ key, size }) => [key, size]), + requireNat, ); /** @param {Record} queueSizes */ @@ -68,11 +71,9 @@ export const parseParams = 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) || From 1d5654d6838c835f0439ef9a9488b83e96055f5f Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Fri, 27 Sep 2024 02:05:53 -0400 Subject: [PATCH 08/34] refactor(cosmic-swingset): Prepare launch-chain.js for slow vat termination --- packages/cosmic-swingset/src/launch-chain.js | 84 +++++++++++++------- 1 file changed, 57 insertions(+), 27 deletions(-) diff --git a/packages/cosmic-swingset/src/launch-chain.js b/packages/cosmic-swingset/src/launch-chain.js index c2211d66a4c..865dd10f92e 100644 --- a/packages/cosmic-swingset/src/launch-chain.js +++ b/packages/cosmic-swingset/src/launch-chain.js @@ -87,6 +87,20 @@ 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' | 'intermission' | 'queued'} 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 + */ + +/** + * @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 * @@ -259,25 +273,26 @@ export async function buildSwingset( */ /** - * @param {BeansPerUnit} beansPerUnit + * @param {object} params + * @param {BeansPerUnit} params.beansPerUnit * @param {boolean} [ignoreBlockLimit] * @returns {ChainRunPolicy} */ function computronCounter( - { + { beansPerUnit }, + 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 policy = harden({ vatCreated() { @@ -303,11 +318,11 @@ function computronCounter( emptyCrank() { return shouldRun(); }, + shouldRun, - remainingBeans, - totalBeans() { - return totalBeans; - }, + remainingBeans: () => + ignoreBlockLimit ? undefined : blockComputeLimit - totalBeans, + totalBeans: () => totalBeans, }); return policy; } @@ -450,10 +465,11 @@ 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) { const startBeans = runPolicy.totalBeans(); controller.writeSlogObject({ type: 'cosmic-swingset-run-start', @@ -492,10 +508,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('queued'); } async function saveChainState() { @@ -687,15 +703,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 +722,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 {number} blockHeight + * @param {number} blockTime seconds since the POSIX epoch + */ + 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 +766,11 @@ 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('intermission'); if (!keepGoing) return; // Finally, process as much as we can from the actionQueue. - await processActions(actionQueue, runSwingset); + await processActions(actionQueue, runSwingset, 'queued'); } async function endBlock(blockHeight, blockTime, params) { @@ -759,11 +788,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 +979,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; From 508ea8e7fe4b7f92512973199c5aa85b17093694 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Fri, 27 Sep 2024 02:10:05 -0400 Subject: [PATCH 09/34] feat(cosmic-swingset): Use vat cleanup budget values to allow slow cleanup ...in otherwise empty blocks Fixes #8928 --- packages/cosmic-swingset/src/launch-chain.js | 63 +++++++++++++++++++- 1 file changed, 60 insertions(+), 3 deletions(-) diff --git a/packages/cosmic-swingset/src/launch-chain.js b/packages/cosmic-swingset/src/launch-chain.js index 865dd10f92e..998be2fccc6 100644 --- a/packages/cosmic-swingset/src/launch-chain.js +++ b/packages/cosmic-swingset/src/launch-chain.js @@ -52,6 +52,8 @@ import { makeQueue, makeQueueStorageMock } from './helpers/make-queue.js'; import { exportStorage } from './export-storage.js'; import { parseLocatedJson } from './helpers/json.js'; +const { hasOwn } = Object; + /** @import { Mailbox, RunPolicy, SwingSetConfig } from '@agoric/swingset-vat' */ /** @import { KVStore } from './helpers/bufferedStorage.js' */ @@ -88,14 +90,18 @@ const parseUpgradePlanInfo = (upgradePlan, prefix = '') => { */ /** - * @typedef {'leftover' | 'forced' | 'high-priority' | 'intermission' | 'queued'} CrankerPhase + * @typedef {'leftover' | 'forced' | 'high-priority' | 'intermission' | '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 @@ -262,6 +268,7 @@ export async function buildSwingset( * shouldRun(): boolean; * remainingBeans(): bigint | undefined; * totalBeans(): bigint; + * startCleanup(): boolean; * }} ChainRunPolicy */ @@ -273,13 +280,21 @@ export async function buildSwingset( */ /** + * 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 }, + { beansPerUnit, vatCleanupBudget }, ignoreBlockLimit = false, ) { const { @@ -294,6 +309,38 @@ function computronCounter( let totalBeans = 0n; const shouldRun = () => ignoreBlockLimit || totalBeans < blockComputeLimit; + 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)) { + 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() { totalBeans += vatCreation; @@ -318,11 +365,14 @@ function computronCounter( emptyCrank() { return shouldRun(); }, + allowCleanup, + didCleanup, shouldRun, remainingBeans: () => ignoreBlockLimit ? undefined : blockComputeLimit - totalBeans, totalBeans: () => totalBeans, + startCleanup, }); return policy; } @@ -469,7 +519,11 @@ export async function launch({ */ function makeRunSwingset(blockHeight, runPolicy) { let runNum = 0; - async function runSwingset(_phase) { + 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', @@ -771,6 +825,9 @@ export async function launch({ // Finally, process as much as we can from the actionQueue. await processActions(actionQueue, runSwingset, 'queued'); + + // Cleanup after terminated vats as allowed. + await runSwingset('cleanup'); } async function endBlock(blockHeight, blockTime, params) { From ee83f2fa4c4254248c61b5c1d32f633b6d3f6890 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Wed, 2 Oct 2024 15:12:17 -0400 Subject: [PATCH 10/34] refactor(cosmic-swingset): Improve CrankerPhase values --- packages/cosmic-swingset/src/launch-chain.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/cosmic-swingset/src/launch-chain.js b/packages/cosmic-swingset/src/launch-chain.js index 998be2fccc6..e90827bf675 100644 --- a/packages/cosmic-swingset/src/launch-chain.js +++ b/packages/cosmic-swingset/src/launch-chain.js @@ -90,7 +90,7 @@ const parseUpgradePlanInfo = (upgradePlan, prefix = '') => { */ /** - * @typedef {'leftover' | 'forced' | 'high-priority' | 'intermission' | 'queued' | 'cleanup'} CrankerPhase + * @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 @@ -565,7 +565,7 @@ export async function launch({ const runPolicy = computronCounter(params, true); const runSwingset = makeRunSwingset(blockHeight, runPolicy); - await runSwingset('queued'); + await runSwingset('forced'); } async function saveChainState() { @@ -820,7 +820,7 @@ 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('intermission'); + keepGoing = await runSwingset('timer'); if (!keepGoing) return; // Finally, process as much as we can from the actionQueue. From d86ee6d5cf0882a53ac3a6e3b802e4002c4c1d12 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Wed, 2 Oct 2024 15:33:12 -0400 Subject: [PATCH 11/34] feat(x/swingset): Define default vat cleanup budget as { default: 5, kv: 50 } --- .../cosmos/x/swingset/types/default-params.go | 23 +++++++++---------- packages/cosmic-swingset/src/sim-params.js | 20 ++++++++-------- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/golang/cosmos/x/swingset/types/default-params.go b/golang/cosmos/x/swingset/types/default-params.go index 3cc11c881ea..cf58a6ee6cf 100644 --- a/golang/cosmos/x/swingset/types/default-params.go +++ b/golang/cosmos/x/swingset/types/default-params.go @@ -75,20 +75,19 @@ var ( NewQueueSize(QueueInbound, DefaultInboundQueueMax), } - // FIXME: Settle on default values - DefaultVatCleanupDefault = sdk.NewUint(0) // 5 - DefaultVatCleanupExports = sdk.NewUint(0) // 5 - DefaultVatCleanupImports = sdk.NewUint(0) // 5 - DefaultVatCleanupKv = sdk.NewUint(0) // 50 - DefaultVatCleanupSnapshots = sdk.NewUint(0) // 50 - DefaultVatCleanupTranscripts = sdk.NewUint(0) // 50 - DefaultVatCleanupBudget = []UintMapEntry{ + DefaultVatCleanupDefault = sdk.NewUint(5) + // DefaultVatCleanupExports = DefaultVatCleanupDefault + // DefaultVatCleanupImports = DefaultVatCleanupDefault + DefaultVatCleanupKv = sdk.NewUint(50) + // DefaultVatCleanupSnapshots = DefaultVatCleanupDefault + // DefaultVatCleanupTranscripts = DefaultVatCleanupDefault + DefaultVatCleanupBudget = []UintMapEntry{ UintMapEntry{VatCleanupDefault, DefaultVatCleanupDefault}, - UintMapEntry{VatCleanupExports, DefaultVatCleanupExports}, - UintMapEntry{VatCleanupImports, DefaultVatCleanupImports}, + // UintMapEntry{VatCleanupExports, DefaultVatCleanupExports}, + // UintMapEntry{VatCleanupImports, DefaultVatCleanupImports}, UintMapEntry{VatCleanupKv, DefaultVatCleanupKv}, - UintMapEntry{VatCleanupSnapshots, DefaultVatCleanupSnapshots}, - UintMapEntry{VatCleanupTranscripts, DefaultVatCleanupTranscripts}, + // UintMapEntry{VatCleanupSnapshots, DefaultVatCleanupSnapshots}, + // UintMapEntry{VatCleanupTranscripts, DefaultVatCleanupTranscripts}, } ) diff --git a/packages/cosmic-swingset/src/sim-params.js b/packages/cosmic-swingset/src/sim-params.js index 332febc1f6e..b6639a3d293 100644 --- a/packages/cosmic-swingset/src/sim-params.js +++ b/packages/cosmic-swingset/src/sim-params.js @@ -75,24 +75,24 @@ export const defaultQueueMax = [ ]; export const vatCleanupDefault = 'default'; -export const defaultVatCleanupDefault = 0; +export const defaultVatCleanupDefault = 5; export const vatCleanupExports = 'exports'; -export const defaultVatCleanupExports = 0; +// export const defaultVatCleanupExports = defaultVatCleanupDefault; export const vatCleanupImports = 'imports'; -export const defaultVatCleanupImports = 0; +// export const defaultVatCleanupImports = defaultVatCleanupDefault; export const vatCleanupKv = 'kv'; -export const defaultVatCleanupKv = 0; +export const defaultVatCleanupKv = 50; export const vatCleanupSnapshots = 'snapshots'; -export const defaultVatCleanupSnapshots = 0; +// export const defaultVatCleanupSnapshots = defaultVatCleanupDefault; export const vatCleanupTranscripts = 'transcripts'; -export const defaultVatCleanupTranscripts = 0; +// export const defaultVatCleanupTranscripts = defaultVatCleanupDefault; export const defaultVatCleanupBudget = [ { key: vatCleanupDefault, value: `${defaultVatCleanupDefault}` }, - { key: vatCleanupExports, value: `${defaultVatCleanupExports}` }, - { key: vatCleanupImports, value: `${defaultVatCleanupImports}` }, + // { key: vatCleanupExports, value: `${defaultVatCleanupExports}` }, + // { key: vatCleanupImports, value: `${defaultVatCleanupImports}` }, { key: vatCleanupKv, value: `${defaultVatCleanupKv}` }, - { key: vatCleanupSnapshots, value: `${defaultVatCleanupSnapshots}` }, - { key: vatCleanupTranscripts, value: `${defaultVatCleanupTranscripts}` }, + // { key: vatCleanupSnapshots, value: `${defaultVatCleanupSnapshots}` }, + // { key: vatCleanupTranscripts, value: `${defaultVatCleanupTranscripts}` }, ]; /** From c7969489464e2552d0d06d4dad8ab9f0e38affa6 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Thu, 24 Oct 2024 13:38:10 -0400 Subject: [PATCH 12/34] chore(cosmic-swingset): Simplify sim-params.js --- packages/cosmic-swingset/src/sim-params.js | 58 ++++++++++++++-------- 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/packages/cosmic-swingset/src/sim-params.js b/packages/cosmic-swingset/src/sim-params.js index b6639a3d293..4ff73ba6949 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)}` }); @@ -74,26 +75,43 @@ export const defaultQueueMax = [ makeQueueSize(QueueInbound, defaultInboundQueueMax), ]; -export const vatCleanupDefault = 'default'; -export const defaultVatCleanupDefault = 5; -export const vatCleanupExports = 'exports'; -// export const defaultVatCleanupExports = defaultVatCleanupDefault; -export const vatCleanupImports = 'imports'; -// export const defaultVatCleanupImports = defaultVatCleanupDefault; -export const vatCleanupKv = 'kv'; -export const defaultVatCleanupKv = 50; -export const vatCleanupSnapshots = 'snapshots'; -// export const defaultVatCleanupSnapshots = defaultVatCleanupDefault; -export const vatCleanupTranscripts = 'transcripts'; -// export const defaultVatCleanupTranscripts = defaultVatCleanupDefault; -export const defaultVatCleanupBudget = [ - { key: vatCleanupDefault, value: `${defaultVatCleanupDefault}` }, - // { key: vatCleanupExports, value: `${defaultVatCleanupExports}` }, - // { key: vatCleanupImports, value: `${defaultVatCleanupImports}` }, - { key: vatCleanupKv, value: `${defaultVatCleanupKv}` }, - // { key: vatCleanupSnapshots, value: `${defaultVatCleanupSnapshots}` }, - // { key: vatCleanupTranscripts, value: `${defaultVatCleanupTranscripts}` }, -]; +/** + * @enum {(typeof VatCleanupPhase)[keyof typeof VatCleanupPhase]} + */ +export const VatCleanupPhase = /** @type {const} */ ({ + Default: 'default', + Exports: 'exports', + Imports: 'imports', + 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} From 28c4d8bf9897f1ff744e64ea0e681ee41064aafd Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Wed, 2 Oct 2024 16:05:17 -0400 Subject: [PATCH 13/34] feat(x/swingset): Require a non-empty vat cleanup budget to include `default` --- golang/cosmos/x/swingset/types/params.go | 12 ++++++- golang/cosmos/x/swingset/types/params_test.go | 34 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/golang/cosmos/x/swingset/types/params.go b/golang/cosmos/x/swingset/types/params.go index 30b12d19be3..9b53ae58fbf 100644 --- a/golang/cosmos/x/swingset/types/params.go +++ b/golang/cosmos/x/swingset/types/params.go @@ -172,10 +172,20 @@ func validateQueueMax(i interface{}) error { } func validateVatCleanupBudget(i interface{}) error { - _, ok := i.([]UintMapEntry) + 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 } diff --git a/golang/cosmos/x/swingset/types/params_test.go b/golang/cosmos/x/swingset/types/params_test.go index b43e08351f3..ff3a7cbe149 100644 --- a/golang/cosmos/x/swingset/types/params_test.go +++ b/golang/cosmos/x/swingset/types/params_test.go @@ -146,3 +146,37 @@ func TestUpdateParamsFromExisting(t *testing.T) { 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) + } +} From bfc29d8540906f1da18b0c195dbed90b853e04c7 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Wed, 23 Oct 2024 14:55:25 -0400 Subject: [PATCH 14/34] chore(cosmic-swingset): Add JSDoc for `launch` parameters --- .../src/helpers/bufferedStorage.js | 23 +++++--- packages/cosmic-swingset/src/launch-chain.js | 55 ++++++++++++++++--- 2 files changed, 61 insertions(+), 17 deletions(-) diff --git a/packages/cosmic-swingset/src/helpers/bufferedStorage.js b/packages/cosmic-swingset/src/helpers/bufferedStorage.js index 1b58c69b1b4..c2cb1cad6b8 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 } @@ -37,17 +42,17 @@ export function insistStorageAPI(kvStore) { * 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 +64,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 +77,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 +123,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 +197,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/launch-chain.js b/packages/cosmic-swingset/src/launch-chain.js index e90827bf675..2cc4fa51ab5 100644 --- a/packages/cosmic-swingset/src/launch-chain.js +++ b/packages/cosmic-swingset/src/launch-chain.js @@ -55,7 +55,7 @@ import { parseLocatedJson } from './helpers/json.js'; const { hasOwn } = Object; /** @import { Mailbox, RunPolicy, SwingSetConfig } from '@agoric/swingset-vat' */ -/** @import { KVStore } from './helpers/bufferedStorage.js' */ +/** @import { KVStore, BufferedKVStore } from './helpers/bufferedStorage.js' */ const console = anylogger('launch-chain'); const blockManagerConsole = anylogger('block-manager'); @@ -116,7 +116,7 @@ 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 {unknown} bootstrapArgs JSON-serializable data @@ -377,6 +377,41 @@ function computronCounter( 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 {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 {string | (() => string | Promise)} vatconfig + * @property {unknown} argv for the bootstrap vat + * @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, @@ -428,14 +463,16 @@ 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); + }; + })(); const { kernelStorage, hostStorage } = openSwingStore(kernelStateDBDir, { traceFile: swingStoreTraceFile, From b7ed256696a04bf4676a093503c30d72db5c0ad1 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Wed, 23 Oct 2024 17:52:47 -0400 Subject: [PATCH 15/34] refactor(cosmic-swingset): Update `launch`/`buildSwingset` to directly accept config objects The old type was basically {ERef | () => path}, while the new one is {ERef} and eliminates the redundant thunk. --- .../src/controller/initializeSwingset.js | 129 ++++++++++-------- packages/SwingSet/src/index.js | 1 + packages/cosmic-swingset/src/chain-main.js | 6 +- packages/cosmic-swingset/src/launch-chain.js | 33 +++-- packages/cosmic-swingset/src/sim-chain.js | 10 +- 5 files changed, 100 insertions(+), 79 deletions(-) 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/cosmic-swingset/src/chain-main.js b/packages/cosmic-swingset/src/chain-main.js index 10b669da82a..192ff4c802f 100644 --- a/packages/cosmic-swingset/src/chain-main.js +++ b/packages/cosmic-swingset/src/chain-main.js @@ -457,13 +457,13 @@ export default async function main(progname, args, { env, homedir, agcc }) { bootMsg: makeBootMsg(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/launch-chain.js b/packages/cosmic-swingset/src/launch-chain.js index 2cc4fa51ab5..1dd15d6b48f 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'; @@ -118,7 +119,7 @@ const getHostKey = path => `host.${path}`; * @param {KVStore} mailboxStorage * @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 @@ -127,7 +128,7 @@ export async function buildSwingset( mailboxStorage, bridgeOutbound, kernelStorage, - vatconfig, + getVatConfig, bootstrapArgs, env, { @@ -159,13 +160,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: { @@ -389,8 +396,12 @@ function computronCounter( * @property {() => void} replayChainSends * @property {((destPort: string, msg: unknown) => unknown)} bridgeOutbound * @property {() => ({publish: (value: unknown) => Promise})} [makeInstallationPublisher] - * @property {string | (() => string | Promise)} vatconfig - * @property {unknown} argv for the bootstrap vat + * @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] 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, From c65e5b1c531c08026f5f11cf5d5dcdbe238b05ee Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Thu, 24 Oct 2024 13:40:25 -0400 Subject: [PATCH 16/34] feat(cosmic-swingset): Allow `launch` to accept an already-open swingStore ...exclusive with swingStoreExportCallback and kernelStateDBDir --- packages/cosmic-swingset/src/launch-chain.js | 29 ++++++++++++++------ packages/swing-store/src/exporter.js | 2 +- packages/swing-store/src/internal.js | 1 + packages/swing-store/src/swingStore.js | 1 + 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/packages/cosmic-swingset/src/launch-chain.js b/packages/cosmic-swingset/src/launch-chain.js index 1dd15d6b48f..b75e0176038 100644 --- a/packages/cosmic-swingset/src/launch-chain.js +++ b/packages/cosmic-swingset/src/launch-chain.js @@ -389,7 +389,8 @@ function computronCounter( * @typedef {object} LaunchOptions * @property {import('./helpers/make-queue.js').QueueStorage} actionQueueStorage * @property {import('./helpers/make-queue.js').QueueStorage} highPriorityQueueStorage - * @property {string} kernelStateDBDir + * @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 @@ -427,6 +428,7 @@ export async function launch({ actionQueueStorage, highPriorityQueueStorage, kernelStateDBDir, + swingStore, mailboxStorage, clearChainSends, replayChainSends, @@ -485,14 +487,23 @@ export async function launch({ }; })(); - const { kernelStorage, hostStorage } = openSwingStore(kernelStateDBDir, { - traceFile: swingStoreTraceFile, - exportCallback: swingStoreExportSyncCallback, - keepSnapshots, - keepTranscripts, - archiveSnapshot, - archiveTranscript, - }); + 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 { kvStore, commit } = hostStorage; /** @typedef {ReturnType>} InboundQueue */ 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, From 65c1523b5ee7015bdcbbeb036caea6a094cf6dca Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Thu, 24 Oct 2024 13:34:45 -0400 Subject: [PATCH 17/34] refactor: Move blockchain BootMsg code to packages/internal ...to avoid creating dependency cycles --- packages/cosmic-swingset/src/chain-main.js | 27 +-------- packages/cosmic-swingset/src/launch-chain.js | 5 +- packages/internal/README.md | 8 +-- packages/internal/package.json | 1 + packages/internal/src/chain-utils.js | 56 +++++++++++++++++++ packages/vats/src/core/basic-behaviors.js | 29 ++-------- .../vats/test/vat-bank-integration.test.js | 9 ++- 7 files changed, 74 insertions(+), 61 deletions(-) create mode 100644 packages/internal/src/chain-utils.js diff --git a/packages/cosmic-swingset/src/chain-main.js b/packages/cosmic-swingset/src/chain-main.js index 192ff4c802f..1eebb937c49 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 { makeBootMsg } 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 diff --git a/packages/cosmic-swingset/src/launch-chain.js b/packages/cosmic-swingset/src/launch-chain.js index b75e0176038..dd27acb46de 100644 --- a/packages/cosmic-swingset/src/launch-chain.js +++ b/packages/cosmic-swingset/src/launch-chain.js @@ -55,6 +55,7 @@ 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, BufferedKVStore } from './helpers/bufferedStorage.js' */ @@ -840,8 +841,8 @@ export async function launch({ * work or when instructed to (whichever comes first). * * @param {Cranker} runSwingset - * @param {number} blockHeight - * @param {number} blockTime seconds since the POSIX epoch + * @param {BlockInfo['blockHeight']} blockHeight + * @param {BlockInfo['blockTime']} blockTime */ async function processBlockActions(runSwingset, blockHeight, blockTime) { // First, complete leftover work, if any 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/chain-utils.js b/packages/internal/src/chain-utils.js new file mode 100644 index 00000000000..558d9d20bad --- /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 }[]; + * }} BootMsg + * cosmosInitAction fields that are subject to consensus. See cosmosInitAction + * in {@link ../../../golang/cosmos/app/app.go}. + */ + +/** + * @param {any} initAction + * @returns {BootMsg} + */ +export const makeBootMsg = 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/vats/src/core/basic-behaviors.js b/packages/vats/src/core/basic-behaviors.js index 4bbbdf69ad9..3c57a7a0d98 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').BootMsg; + * }; + * }; * }} powers */ export const mintInitialSupply = async ({ diff --git a/packages/vats/test/vat-bank-integration.test.js b/packages/vats/test/vat-bank-integration.test.js index 33cfe964744..e50afe0c2b2 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').BootMsg} */ + // @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. From 48b6405d23dc3dc051e3a9aea66ffb3f9749a82e Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Thu, 24 Oct 2024 17:40:17 -0400 Subject: [PATCH 18/34] feat(cosmic-swingset): Add support for testing blocks of a mock chain --- .../cosmic-swingset/test/run-policy.test.js | 92 +++++ packages/cosmic-swingset/tools/test-kit.js | 388 ++++++++++++++++++ packages/vats/tools/bootstrap-relay.js | 203 +++++++++ 3 files changed, 683 insertions(+) create mode 100644 packages/cosmic-swingset/test/run-policy.test.js create mode 100644 packages/cosmic-swingset/tools/test-kit.js create mode 100644 packages/vats/tools/bootstrap-relay.js 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..0522783fa6d --- /dev/null +++ b/packages/cosmic-swingset/test/run-policy.test.js @@ -0,0 +1,92 @@ +/* eslint-env node */ +import test from 'ava'; +import { Fail, q } from '@endo/errors'; +import { E } from '@endo/far'; +import { BridgeId, objectMap } from '@agoric/internal'; +import { CORE_EVAL } from '@agoric/internal/src/action-types.js'; +import { makeFakeStorageKit } from '@agoric/internal/src/storage-test-utils.js'; +import { makeCosmicSwingsetTestKit } from '../tools/test-kit.js'; +import { + DEFAULT_SIM_SWINGSET_PARAMS, + makeVatCleanupBudgetFromKeywords, +} from '../src/sim-params.js'; + +/** + * @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 => { + const { toStorage: handleVstorage } = makeFakeStorageKit(''); + const receiveBridgeSend = (destPort, msg) => { + switch (destPort) { + case BridgeId.STORAGE: { + return handleVstorage(msg); + } + default: + Fail`port ${q(destPort)} not implemented for message ${msg}`; + } + }; + const { + actionQueue, + getLastBlockInfo, + makeQueueRecord, + runNextBlock, + shutdown, + swingStore, + } = await makeCosmicSwingsetTestKit(receiveBridgeSend, { + bundles: makeSourceDescriptors({ + exporter: '@agoric/swingset-vat/test/vat-exporter.js', + }), + }); + await runNextBlock({ + params: makeCleanupBudgetParams({ Default: 0 }), + }); + + // We'll be interacting through core evals. + // TODO: But will probably also need controller access for deep inspection. + const pushCoreEval = fn => + actionQueue.push( + makeQueueRecord({ + type: CORE_EVAL, + evals: [ + { + json_permits: 'true', + js_code: String(fn), + }, + ], + }), + ); + + pushCoreEval(async powers => { + const bootstrap = powers.vats.bootstrap; + const vat = await E(bootstrap).createVat({ + name: 'doomed', + bundleCapName: 'exporter', + }); + // TODO: Give the vat a big footprint, then terminate it. + }); + await runNextBlock(); + // TODO: Assert lack of cleanup. + + await runNextBlock({ + params: makeCleanupBudgetParams({ Default: 2, Kv: 3 }), + }); + // TODO: Assert limited cleanup. + // TODO: Further cleanup assertions. +}); diff --git a/packages/cosmic-swingset/tools/test-kit.js b/packages/cosmic-swingset/tools/test-kit.js new file mode 100644 index 00000000000..3d724188d72 --- /dev/null +++ b/packages/cosmic-swingset/tools/test-kit.js @@ -0,0 +1,388 @@ +/* eslint-env node */ + +import * as fsPromises from 'node:fs/promises'; +import * as pathNamespace from 'node:path'; +import { assert, Fail } from '@endo/errors'; +import * as ActionType from '@agoric/internal/src/action-types.js'; +import { makeBootMsg } 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 } from '../src/helpers/bufferedStorage.js'; +import { makeQueue, makeQueueStorageMock } from '../src/helpers/make-queue.js'; + +/** @import { BlockInfo, BootMsg } from '@agoric/internal/src/chain-utils.js' */ +/** @import { SwingSetConfig } from '@agoric/swingset-vat' */ + +/** + * @template T + * @typedef {(input: T) => T} Replacer + */ + +/** @type {Replacer} */ +const clone = obj => JSON.parse(JSON.stringify(obj)); + +// TODO: Replace compareByCodePoints and makeKVStoreFromMap 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; + } +}; + +/** + * @param {Map} map + */ +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; +}; + +export const defaultBootMsg = harden( + makeBootMsg({ + type: ActionType.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({ + ...clone(defaultBootMsg), + 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 'relay' 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 {import('@agoric/swingset-vat').SwingSetConfig} + */ +const baseConfig = harden({ + defaultReapInterval: 'never', + defaultManagerType: undefined, + bootstrap: 'bootstrap', + vats: { + bootstrap: { + sourceSpec: '@agoric/vats/tools/bootstrap-relay.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 {import('@agoric/swingset-vat').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.fixupBootMsg] 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 {SwingSetConfig['vats']} [options.vats] extra static vat configuration + * @param {string} [options.baseBootstrapManifest] see {@link ../../vats/tools/bootstrap-relay.js} + * @param {string} [options.addBootstrapBehaviors] see {@link ../../vats/tools/bootstrap-relay.js} + * @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, + fixupBootMsg, + fixupConfig, + slogSender, + swingsetConfig = {}, + vats, + + // Options for vats (particularly the bootstrap-relay vat). + baseBootstrapManifest, + addBootstrapBehaviors, + }, + { fsp = fsPromises, resolvePath = pathNamespace.resolve } = {}, +) => { + await null; + /** @type {SwingSetConfig} */ + let config = { + ...clone(baseConfig), + ...configOverrides, + 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 }; + + const bootstrapVatDesc = config.vats[config.bootstrap]; + const bootstrapVatParams = bootstrapVatDesc.parameters; + if (baseBootstrapManifest) { + bootstrapVatParams.baseManifest = baseBootstrapManifest; + } + if (addBootstrapBehaviors) { + bootstrapVatParams.addBehaviors = addBootstrapBehaviors; + } + + if (fixupConfig) config = fixupConfig(config); + + const swingStore = initSwingStore(); // in-memory + const { hostStorage } = swingStore; + + const actionQueueStorage = makeQueueStorageMock().storage; + const highPriorityQueueStorage = makeQueueStorageMock().storage; + const mailboxStorage = makeBufferedStorage(makeKVStoreFromMap(new Map())); + + const savedChainSends = []; + const clearChainSends = async () => savedChainSends.splice(0); + const replayChainSends = (..._args) => { + throw Error('not implemented'); + }; + + let bootMsg = clone(defaultBootMsg); + if (fixupBootMsg) bootMsg = fixupBootMsg(bootMsg); + let { + blockHeight: lastBlockHeight, + blockTime: lastBlockTime, + params: lastBlockParams, + } = bootMsg; + let lastBlockWalltime = Date.now(); + + // 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); + }; + + if (!slogSender && (env.SLOGFILE || env.SLOGSENDER)) { + slogSender = await makeSlogSender({ env }); + } + + const { blockingSend, shutdown: shutdownKernel } = await launch({ + swingStore, + actionQueueStorage, + highPriorityQueueStorage, + mailboxStorage, + clearChainSends, + replayChainSends, + receiveBridgeSend, + vatconfig: config, + argv: { bootMsg }, + env, + debugName, + slogSender, + swingsetConfig, + }); + const shutdown = async () => { + await Promise.all([shutdownKernel, hostStorage.close()]); + }; + + /** + * @returns {BlockInfo} + */ + const getLastBlockInfo = () => ({ + blockHeight: lastBlockHeight, + blockTime: lastBlockTime, + params: lastBlockParams, + }); + + let blockTxCount = 0; + + /** + * @param {Partial} [blockInfo] + */ + const runNextBlock = async ({ + blockHeight = lastBlockHeight + 1, + blockTime = nextBlockTime(), + params = lastBlockParams, + } = {}) => { + blockHeight > lastBlockHeight || + Fail`blockHeight ${blockHeight} must be greater than ${lastBlockHeight}`; + blockTime > lastBlockTime || + Fail`blockTime ${blockTime} must be greater than ${lastBlockTime}`; + lastBlockWalltime = Date.now(); + lastBlockHeight = blockHeight; + lastBlockTime = blockTime; + lastBlockParams = params; + blockTxCount = 0; + const context = { blockHeight, blockTime }; + await blockingSend({ + type: ActionType.BEGIN_BLOCK, + ...context, + params, + }); + await blockingSend({ type: ActionType.END_BLOCK, ...context }); + await blockingSend({ type: ActionType.COMMIT_BLOCK, ...context }); + await blockingSend({ type: ActionType.AFTER_COMMIT_BLOCK, ...context }); + return getLastBlockInfo(); + }; + + const makeQueueRecord = action => { + blockTxCount += 1; + return { + action, + context: { + blockHeight: lastBlockHeight + 1, + txHash: blockTxCount, + msgIdx: '', + }, + }; + }; + + return { + // SwingSet-oriented references. + actionQueue: makeQueue(actionQueueStorage), + highPriorityActionQueue: makeQueue(highPriorityQueueStorage), + mailboxStorage, + shutdown, + swingStore, + + // Functions specific to this kit. + getLastBlockInfo, + makeQueueRecord, + runNextBlock, + }; +}; diff --git a/packages/vats/tools/bootstrap-relay.js b/packages/vats/tools/bootstrap-relay.js new file mode 100644 index 00000000000..c4f7aa122fe --- /dev/null +++ b/packages/vats/tools/bootstrap-relay.js @@ -0,0 +1,203 @@ +import { Fail, q } from '@endo/errors'; +import { Far, E } from '@endo/far'; +import { makePromiseKit } from '@endo/promise-kit'; +import { objectMap } from '@agoric/internal'; +import { buildManualTimer } from '@agoric/swingset-vat/tools/manual-timer.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; + * logger: (msg) => void; + * }} vatPowers + * @param {{ + * coreProposalCodeSteps?: string[]; + * baseManifest?: string; + * addBehaviors?: 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(); + vatAdminP.then(value => (vatAdmin = value)); // for better debugging + const vatData = new Map(); + const devicesByName = new Map(); + const callLogsByRemotable = new Map(); + + const { baseManifest: manifestName = 'MINIMAL', addBehaviors = [] } = + bootstrapParameters; + Object.hasOwn(manifests, manifestName) || + Fail`missing manifest ${manifestName}`; + Array.isArray(addBehaviors) || Fail`addBehaviors must be an array of names`; + 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, + bootstrapParameters, + manifest, + allBehaviors, + modules, + makeDurableZone(baggage), + ); + + return Far('root', { + ...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 devices and static vats. + for (const [name, root] of Object.entries(vats)) { + if (name !== 'vatAdmin') { + vatData.set(name, { root }); + } + } + for (const [name, device] of Object.entries(devices)) { + devicesByName.set(name, device); + } + }, + + getDevice: async deviceName => devicesByName.get(deviceName), + + getVatAdmin: async () => vatAdmin || vatAdminP, + + /** @deprecated in favor of getManualTimer */ + getTimer: async () => manualTimer, + + getManualTimer: async () => manualTimer, + + getVatRoot: async vatName => { + const vat = vatData.get(vatName) || Fail`unknown vat name: ${q(vatName)}`; + const { root } = vat; + return root; + }, + + createVat: async ( + { name, bundleCapName, vatParameters = {} }, + options = {}, + ) => { + const bcap = await E(vatAdminP).getNamedBundleCap(bundleCapName); + const vatOptions = { ...options, vatParameters }; + const { adminNode, root } = await E(vatAdminP).createVat( + bcap, + vatOptions, + ); + vatData.set(name, { adminNode, root }); + return root; + }, + + upgradeVat: async ({ name, bundleCapName, vatParameters = {} }) => { + const vat = vatData.get(name) || Fail`unknown vat name: ${q(name)}`; + const bcap = await E(vatAdminP).getNamedBundleCap(bundleCapName); + const options = { vatParameters }; + const incarnationNumber = await E(vat.adminNode).upgrade(bcap, options); + vat.incarnationNumber = incarnationNumber; + return incarnationNumber; + }, + + /** + * Derives a remotable from an object by mapping each object property into a + * method that returns the value. + * + * @param {string} label + * @param {Record} returnValues + */ + makeRemotable: (label, returnValues) => { + const callLogs = []; + const makeGetterFunction = (value, name) => { + const getValue = (...args) => { + callLogs.push([name, ...args]); + return value; + }; + return getValue; + }; + // `objectMap` hardens its result, but... + const methods = objectMap(returnValues, makeGetterFunction); + // ... `Far` requires its methods argument not to be hardened. + const remotable = Far(label, { ...methods }); + callLogsByRemotable.set(remotable, callLogs); + return remotable; + }, + + makePromiseKit: () => { + const { promise, ...resolverMethods } = makePromiseKit(); + const resolver = Far('resolver', resolverMethods); + return harden({ promise, resolver }); + }, + + /** + * Returns a copy of a remotable's logs. + * + * @param {object} remotable + */ + getLogForRemotable: remotable => { + const logs = + callLogsByRemotable.get(remotable) || + Fail`logs not found for ${q(remotable)}`; + // Return a copy so that the original remains mutable. + return harden([...logs]); + }, + }); +}; +harden(buildRootObject); From fd534ab3eecd90d2b7658eb24b27d9fece89367e Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Sun, 27 Oct 2024 19:16:04 -0400 Subject: [PATCH 19/34] refactor(cosmic-swingset): Simplify mock-chain testing infrastructure --- packages/SwingSet/tools/bootstrap-relay.js | 9 + packages/SwingSet/tools/vat-puppet.js | 111 +++++++++ .../src/helpers/bufferedStorage.js | 167 ++++++++++++++ packages/cosmic-swingset/src/launch-chain.js | 3 +- .../cosmic-swingset/test/run-policy.test.js | 91 ++++---- packages/cosmic-swingset/tools/test-kit.js | 211 ++++++++---------- packages/internal/src/action-types.js | 85 +++++-- packages/vats/src/core/lib-boot.js | 2 +- ...y.js => vat-bootstrap-chain-reflective.js} | 142 ++++++------ 9 files changed, 566 insertions(+), 255 deletions(-) create mode 100644 packages/SwingSet/tools/vat-puppet.js rename packages/vats/tools/{bootstrap-relay.js => vat-bootstrap-chain-reflective.js} (56%) 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-swingset/src/helpers/bufferedStorage.js b/packages/cosmic-swingset/src/helpers/bufferedStorage.js index c2cb1cad6b8..8d860e06f58 100644 --- a/packages/cosmic-swingset/src/helpers/bufferedStorage.js +++ b/packages/cosmic-swingset/src/helpers/bufferedStorage.js @@ -38,6 +38,173 @@ 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. diff --git a/packages/cosmic-swingset/src/launch-chain.js b/packages/cosmic-swingset/src/launch-chain.js index dd27acb46de..6898d6af310 100644 --- a/packages/cosmic-swingset/src/launch-chain.js +++ b/packages/cosmic-swingset/src/launch-chain.js @@ -59,6 +59,8 @@ const { hasOwn } = Object; /** @import { Mailbox, RunPolicy, SwingSetConfig } from '@agoric/swingset-vat' */ /** @import { KVStore, BufferedKVStore } from './helpers/bufferedStorage.js' */ +/** @typedef {ReturnType>} InboundQueue */ + const console = anylogger('launch-chain'); const blockManagerConsole = anylogger('block-manager'); @@ -507,7 +509,6 @@ export async function launch({ }); const { kvStore, commit } = hostStorage; - /** @typedef {ReturnType>} InboundQueue */ /** @type {InboundQueue} */ const actionQueue = makeQueue(actionQueueStorage); /** @type {InboundQueue} */ diff --git a/packages/cosmic-swingset/test/run-policy.test.js b/packages/cosmic-swingset/test/run-policy.test.js index 0522783fa6d..eb57d8da3cd 100644 --- a/packages/cosmic-swingset/test/run-policy.test.js +++ b/packages/cosmic-swingset/test/run-policy.test.js @@ -1,18 +1,26 @@ /* eslint-env node */ import test from 'ava'; -import { Fail, q } from '@endo/errors'; +import { assert, q, Fail } from '@endo/errors'; import { E } from '@endo/far'; import { BridgeId, objectMap } from '@agoric/internal'; -import { CORE_EVAL } from '@agoric/internal/src/action-types.js'; import { makeFakeStorageKit } from '@agoric/internal/src/storage-test-utils.js'; -import { makeCosmicSwingsetTestKit } from '../tools/test-kit.js'; +import { + defaultBootstrapMessage, + 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' */ + /** - * @param {Record} src + * Converts a Record into Record + * for defining e.g. a `bundles` group. + * + * @param {Record} src * @returns {import('@agoric/swingset-vat').SwingSetConfigDescriptor} */ const makeSourceDescriptors = src => { @@ -42,44 +50,47 @@ test('cleanup work must be limited by vat_cleanup_budget', async t => { Fail`port ${q(destPort)} not implemented for message ${msg}`; } }; - const { - actionQueue, - getLastBlockInfo, - makeQueueRecord, - runNextBlock, - shutdown, - swingStore, - } = await makeCosmicSwingsetTestKit(receiveBridgeSend, { - bundles: makeSourceDescriptors({ - exporter: '@agoric/swingset-vat/test/vat-exporter.js', - }), - }); - await runNextBlock({ - params: makeCleanupBudgetParams({ Default: 0 }), - }); - - // We'll be interacting through core evals. - // TODO: But will probably also need controller access for deep inspection. - const pushCoreEval = fn => - actionQueue.push( - makeQueueRecord({ - type: CORE_EVAL, - evals: [ - { - json_permits: 'true', - js_code: String(fn), - }, - ], + const { pushCoreEval, runNextBlock, shutdown, swingStore } = + await makeCosmicSwingsetTestKit(receiveBridgeSend, { + bundles: makeSourceDescriptors({ + puppet: '@agoric/swingset-vat/tools/vat-puppet.js', }), - ); + fixupBootMsg: () => ({ + ...defaultBootstrapMessage, + params: makeCleanupBudgetParams({ Default: 0 }), + }), + }); + 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.bootstrap; - const vat = await E(bootstrap).createVat({ - name: 'doomed', - bundleCapName: 'exporter', - }); - // TODO: Give the vat a big footprint, then terminate it. + 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)?`, + ); + + // Terminate the vat and assert lack of cleanup. + pushCoreEval(async powers => { + const { bootstrap } = powers.vats; + const vat = await E(bootstrap).getVatRoot('doomed'); + // TODO: Give the vat a big footprint, similar to + // packages/SwingSet/test/vat-admin/slow-termination/slow-termination.test.js + await E(vat).dieHappy(); }); await runNextBlock(); // TODO: Assert lack of cleanup. @@ -89,4 +100,6 @@ test('cleanup work must be limited by vat_cleanup_budget', async t => { }); // TODO: Assert limited cleanup. // TODO: Further cleanup assertions. + + await shutdown(); }); diff --git a/packages/cosmic-swingset/tools/test-kit.js b/packages/cosmic-swingset/tools/test-kit.js index 3d724188d72..b4509bec1db 100644 --- a/packages/cosmic-swingset/tools/test-kit.js +++ b/packages/cosmic-swingset/tools/test-kit.js @@ -2,18 +2,26 @@ import * as fsPromises from 'node:fs/promises'; import * as pathNamespace from 'node:path'; -import { assert, Fail } from '@endo/errors'; -import * as ActionType from '@agoric/internal/src/action-types.js'; +import { Fail } from '@endo/errors'; +import { + SwingsetMessageType, + QueuedActionType, +} from '@agoric/internal/src/action-types.js'; import { makeBootMsg } 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 } from '../src/helpers/bufferedStorage.js'; +import { + makeBufferedStorage, + makeKVStoreFromMap, +} from '../src/helpers/bufferedStorage.js'; import { makeQueue, makeQueueStorageMock } from '../src/helpers/make-queue.js'; /** @import { BlockInfo, BootMsg } from '@agoric/internal/src/chain-utils.js' */ -/** @import { SwingSetConfig } from '@agoric/swingset-vat' */ +/** @import { Mailbox, ManagerType, SwingSetConfig } from '@agoric/swingset-vat' */ +/** @import { KVStore } from '../src/helpers/bufferedStorage.js' */ +/** @import { InboundQueue } from '../src/launch-chain.js'; */ /** * @template T @@ -23,95 +31,9 @@ import { makeQueue, makeQueueStorageMock } from '../src/helpers/make-queue.js'; /** @type {Replacer} */ const clone = obj => JSON.parse(JSON.stringify(obj)); -// TODO: Replace compareByCodePoints and makeKVStoreFromMap 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; - } -}; - -/** - * @param {Map} map - */ -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; -}; - export const defaultBootMsg = harden( makeBootMsg({ - type: ActionType.AG_COSMOS_INIT, + type: SwingsetMessageType.AG_COSMOS_INIT, blockHeight: 100, blockTime: Math.floor(Date.parse('2020-01-01T00:00Z') / 1000), chainID: 'localtest', @@ -147,13 +69,13 @@ export const defaultBootstrapMessage = harden({ /** * 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 'relay' that exposes reflective methods for inspecting and + * 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 {import('@agoric/swingset-vat').SwingSetConfig} + * @type {SwingSetConfig} */ const baseConfig = harden({ defaultReapInterval: 'never', @@ -161,7 +83,7 @@ const baseConfig = harden({ bootstrap: 'bootstrap', vats: { bootstrap: { - sourceSpec: '@agoric/vats/tools/bootstrap-relay.js', + sourceSpec: '@agoric/vats/tools/vat-bootstrap-chain-reflective.js', creationOptions: { critical: true, }, @@ -202,10 +124,9 @@ const baseConfig = harden({ * default SwingSet configuration (may be overridden by more specific options * such as `defaultManagerType`) * @param {string} [options.debugName] - * @param {import('@agoric/swingset-vat').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 {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.fixupBootMsg] a final opportunity to make * any changes @@ -214,11 +135,18 @@ const baseConfig = harden({ * @param {import('@agoric/telemetry').SlogSender} [options.slogSender] * @param {import('../src/chain-main.js').CosmosSwingsetConfig} [options.swingsetConfig] * @param {SwingSetConfig['vats']} [options.vats] extra static vat configuration - * @param {string} [options.baseBootstrapManifest] see {@link ../../vats/tools/bootstrap-relay.js} - * @param {string} [options.addBootstrapBehaviors] see {@link ../../vats/tools/bootstrap-relay.js} + * @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/vat-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] + * @param {typeof import('node:path').resolve} [powers.resolvePath] */ export const makeCosmicSwingsetTestKit = async ( receiveBridgeSend, @@ -236,10 +164,11 @@ export const makeCosmicSwingsetTestKit = async ( swingsetConfig = {}, vats, - // Options for vats (particularly the bootstrap-relay vat). + // Options for vats (particularly the reflective bootstrap vat). baseBootstrapManifest, addBootstrapBehaviors, - }, + bootstrapCoreEvals, + } = {}, { fsp = fsPromises, resolvePath = pathNamespace.resolve } = {}, ) => { await null; @@ -257,14 +186,13 @@ export const makeCosmicSwingsetTestKit = async ( 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]; - const bootstrapVatParams = bootstrapVatDesc.parameters; - if (baseBootstrapManifest) { - bootstrapVatParams.baseManifest = baseBootstrapManifest; - } - if (addBootstrapBehaviors) { - bootstrapVatParams.addBehaviors = addBootstrapBehaviors; - } + Object.assign(bootstrapVatDesc.parameters, { + baseManifest: baseBootstrapManifest, + addBehaviors: addBootstrapBehaviors, + coreProposalCodeSteps: bootstrapCoreEvals, + }); if (fixupConfig) config = fixupConfig(config); @@ -273,7 +201,11 @@ export const makeCosmicSwingsetTestKit = async ( const actionQueueStorage = makeQueueStorageMock().storage; const highPriorityQueueStorage = makeQueueStorageMock().storage; - const mailboxStorage = makeBufferedStorage(makeKVStoreFromMap(new Map())); + const { kvStore: mailboxKVStore, ...mailboxBufferMethods } = + makeBufferedStorage( + /** @type {KVStore} */ (makeKVStoreFromMap(new Map())), + ); + const mailboxStorage = { ...mailboxKVStore, ...mailboxBufferMethods }; const savedChainSends = []; const clearChainSends = async () => savedChainSends.splice(0); @@ -308,7 +240,7 @@ export const makeCosmicSwingsetTestKit = async ( mailboxStorage, clearChainSends, replayChainSends, - receiveBridgeSend, + bridgeOutbound: receiveBridgeSend, vatconfig: config, argv: { bootMsg }, env, @@ -350,39 +282,76 @@ export const makeCosmicSwingsetTestKit = async ( blockTxCount = 0; const context = { blockHeight, blockTime }; await blockingSend({ - type: ActionType.BEGIN_BLOCK, + type: SwingsetMessageType.BEGIN_BLOCK, ...context, params, }); - await blockingSend({ type: ActionType.END_BLOCK, ...context }); - await blockingSend({ type: ActionType.COMMIT_BLOCK, ...context }); - await blockingSend({ type: ActionType.AFTER_COMMIT_BLOCK, ...context }); + await blockingSend({ type: SwingsetMessageType.END_BLOCK, ...context }); + await blockingSend({ type: SwingsetMessageType.COMMIT_BLOCK, ...context }); + await blockingSend({ + type: SwingsetMessageType.AFTER_COMMIT_BLOCK, + ...context, + }); return getLastBlockInfo(); }; + await runNextBlock(); - const makeQueueRecord = action => { + /** @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; - return { + queue.push({ action, context: { blockHeight: lastBlockHeight + 1, txHash: blockTxCount, msgIdx: '', }, + }); + }; + /** + * @param {string | ((...args: any[]) => void)} fn + * @param {string} [jsonPermits] should deserialize into a BootstrapManifestPermit + * @param {InboundQueue} [queue] + */ + const pushCoreEval = ( + fn, + jsonPermits = 'true', + queue = highPriorityQueue, + ) => { + /** @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: String(fn), + }; + const action = { + type: QueuedActionType.CORE_EVAL, + evals: [coreEvalDesc], }; + pushQueueRecord(action, queue); }; return { // SwingSet-oriented references. - actionQueue: makeQueue(actionQueueStorage), - highPriorityActionQueue: makeQueue(highPriorityQueueStorage), + actionQueue, + highPriorityQueue, mailboxStorage, shutdown, swingStore, // Functions specific to this kit. getLastBlockInfo, - makeQueueRecord, + pushQueueRecord, + pushCoreEval, runNextBlock, }; }; 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/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/tools/bootstrap-relay.js b/packages/vats/tools/vat-bootstrap-chain-reflective.js similarity index 56% rename from packages/vats/tools/bootstrap-relay.js rename to packages/vats/tools/vat-bootstrap-chain-reflective.js index c4f7aa122fe..7bbd7ed32be 100644 --- a/packages/vats/tools/bootstrap-relay.js +++ b/packages/vats/tools/vat-bootstrap-chain-reflective.js @@ -1,8 +1,15 @@ +/** + * @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 { objectMap } from '@agoric/internal'; 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'; @@ -51,14 +58,11 @@ manifests.MINIMAL = makeManifestForBehaviors([ ]); /** - * @param {VatPowers & { - * D: DProxy; - * logger: (msg) => void; - * }} vatPowers + * @param {VatPowers & { D: DProxy; testLog: typeof console.log }} vatPowers * @param {{ - * coreProposalCodeSteps?: string[]; * baseManifest?: string; * addBehaviors?: string[]; + * coreProposalCodeSteps?: string[]; * }} bootstrapParameters * @param {import('@agoric/vat-data').Baggage} baggage */ @@ -66,16 +70,22 @@ export const buildRootObject = (vatPowers, bootstrapParameters, baggage) => { const manualTimer = buildManualTimer(); let vatAdmin; const { promise: vatAdminP, resolve: captureVatAdmin } = makePromiseKit(); - vatAdminP.then(value => (vatAdmin = value)); // for better debugging - const vatData = new Map(); + 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 callLogsByRemotable = new Map(); const { baseManifest: manifestName = 'MINIMAL', addBehaviors = [] } = bootstrapParameters; Object.hasOwn(manifests, manifestName) || Fail`missing manifest ${manifestName}`; - Array.isArray(addBehaviors) || Fail`addBehaviors must be an array of names`; const manifest = { ...manifests[manifestName], ...makeManifestForBehaviors(addBehaviors), @@ -91,7 +101,7 @@ export const buildRootObject = (vatPowers, bootstrapParameters, baggage) => { * V]> */ const bootstrapBase = makeBootstrap( - vatPowers, + { ...vatPowers, logger: vatPowers.testLog }, bootstrapParameters, manifest, allBehaviors, @@ -99,7 +109,11 @@ export const buildRootObject = (vatPowers, bootstrapParameters, baggage) => { makeDurableZone(baggage), ); + const reflectionMethods = makeReflectionMethods(vatPowers, baggage); + return Far('root', { + ...reflectionMethods, + ...bootstrapBase, bootstrap: async (vats, devices) => { await bootstrapBase.bootstrap(vats, devices); @@ -107,10 +121,10 @@ export const buildRootObject = (vatPowers, bootstrapParameters, baggage) => { // createVatAdminService is idempotent (despite the name). captureVatAdmin(E(vats.vatAdmin).createVatAdminService(devices.vatAdmin)); - // Capture references to devices and static vats. + // Capture references to static vats and devices. for (const [name, root] of Object.entries(vats)) { if (name !== 'vatAdmin') { - vatData.set(name, { root }); + vatRecords.set(name, { root }); } } for (const [name, device] of Object.entries(devices)) { @@ -118,85 +132,55 @@ export const buildRootObject = (vatPowers, bootstrapParameters, baggage) => { } }, - getDevice: async deviceName => devicesByName.get(deviceName), + getDevice: deviceName => devicesByName.get(deviceName), - getVatAdmin: async () => vatAdmin || vatAdminP, + getManualTimer: () => manualTimer, - /** @deprecated in favor of getManualTimer */ - getTimer: async () => manualTimer, + getVatAdmin: () => vatAdmin || vatAdminP, - getManualTimer: async () => manualTimer, - - getVatRoot: async vatName => { - const vat = vatData.get(vatName) || Fail`unknown vat name: ${q(vatName)}`; + getVatRoot: vatName => { + const vat = + vatRecords.get(vatName) || Fail`unknown vat name: ${q(vatName)}`; const { root } = vat; return root; }, - createVat: async ( - { name, bundleCapName, vatParameters = {} }, - options = {}, - ) => { - const bcap = await E(vatAdminP).getNamedBundleCap(bundleCapName); - const vatOptions = { ...options, vatParameters }; - const { adminNode, root } = await E(vatAdminP).createVat( - bcap, - vatOptions, - ); - vatData.set(name, { adminNode, root }); - return root; - }, - - upgradeVat: async ({ name, bundleCapName, vatParameters = {} }) => { - const vat = vatData.get(name) || Fail`unknown vat name: ${q(name)}`; - const bcap = await E(vatAdminP).getNamedBundleCap(bundleCapName); - const options = { vatParameters }; - const incarnationNumber = await E(vat.adminNode).upgrade(bcap, options); - vat.incarnationNumber = incarnationNumber; - return incarnationNumber; - }, - /** - * Derives a remotable from an object by mapping each object property into a - * method that returns the value. - * - * @param {string} label - * @param {Record} returnValues + * @param {string} vatName + * @param {string} [bundleCapName] + * @param {{ vatParameters?: object } & Record} [vatOptions] + * @returns {Promise} root object of the new vat */ - makeRemotable: (label, returnValues) => { - const callLogs = []; - const makeGetterFunction = (value, name) => { - const getValue = (...args) => { - callLogs.push([name, ...args]); - return value; - }; - return getValue; - }; - // `objectMap` hardens its result, but... - const methods = objectMap(returnValues, makeGetterFunction); - // ... `Far` requires its methods argument not to be hardened. - const remotable = Far(label, { ...methods }); - callLogsByRemotable.set(remotable, callLogs); - return remotable; - }, - - makePromiseKit: () => { - const { promise, ...resolverMethods } = makePromiseKit(); - const resolver = Far('resolver', resolverMethods); - return harden({ promise, resolver }); + 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; }, /** - * Returns a copy of a remotable's logs. - * - * @param {object} remotable + * @param {string} vatName + * @param {string} [bundleCapName] + * @param {{ vatParameters?: object } & Record} [vatOptions] + * @returns {Promise} the resulting incarnation number */ - getLogForRemotable: remotable => { - const logs = - callLogsByRemotable.get(remotable) || - Fail`logs not found for ${q(remotable)}`; - // Return a copy so that the original remains mutable. - return harden([...logs]); + 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; }, }); }; From 700373b0ea07720087cceaf8a53676fba2e4d00b Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Mon, 28 Oct 2024 06:09:14 -0400 Subject: [PATCH 20/34] test(cosmic-swingset): Cover slow vat termination in a blockchain context --- .../cosmic-swingset/test/run-policy.test.js | 111 ++++++++++++++++-- 1 file changed, 101 insertions(+), 10 deletions(-) diff --git a/packages/cosmic-swingset/test/run-policy.test.js b/packages/cosmic-swingset/test/run-policy.test.js index eb57d8da3cd..9786c56164a 100644 --- a/packages/cosmic-swingset/test/run-policy.test.js +++ b/packages/cosmic-swingset/test/run-policy.test.js @@ -55,6 +55,12 @@ test('cleanup work must be limited by vat_cleanup_budget', async t => { bundles: makeSourceDescriptors({ puppet: '@agoric/swingset-vat/tools/vat-puppet.js', }), + configOverrides: { + // Ensure multiple spans and snapshots. + defaultManagerType: 'xsnap', + snapshotInitial: 2, + snapshotInterval: 4, + }, fixupBootMsg: () => ({ ...defaultBootstrapMessage, params: makeCleanupBudgetParams({ Default: 0 }), @@ -70,7 +76,7 @@ test('cleanup work must be limited by vat_cleanup_budget', async t => { return value; }; - // Launch the new vat and capture its ID. + // Launch the new vat and capture its ID and store snapshotting. pushCoreEval(async powers => { const { bootstrap } = powers.vats; await E(bootstrap).createVat('doomed', 'puppet'); @@ -83,23 +89,108 @@ test('cleanup work must be limited by vat_cleanup_budget', async t => { 'v8', `time to update expected vatID to ${JSON.stringify(vatIDs)}.at(-1)?`, ); + const getKV = () => + [...mapStore].filter(([key]) => key.startsWith(`${vatID}.`)); + // console.log(swingStore.kernelStorage); - // Terminate the vat and assert lack of cleanup. + t.false( + JSON.parse(mustGet('vats.terminated')).includes(vatID), + 'must not be terminated', + ); + 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 vat = await E(bootstrap).getVatRoot('doomed'); - // TODO: Give the vat a big footprint, similar to - // packages/SwingSet/test/vat-admin/slow-termination/slow-termination.test.js - await E(vat).dieHappy(); + 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(); - // TODO: Assert lack of cleanup. + 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 doomed = await E(bootstrap).getVatRoot('doomed'); + await E(doomed).dieHappy(); + }); + 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'); + await runNextBlock(); + t.is(getKV().length, onlyKV.length - 6, 'further kvStore deletion'); + + // Allow remaining cleanup. await runNextBlock({ - params: makeCleanupBudgetParams({ Default: 2, Kv: 3 }), + params: makeCleanupBudgetParams({ Default: 2 ** 32 }), }); - // TODO: Assert limited cleanup. - // TODO: Further cleanup assertions. + await runNextBlock(); + t.is(getKV().length, 0, 'cleanup complete'); await shutdown(); }); From ebd2910e3880fa6c2c04610d0072a7f4e05c5a01 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Mon, 28 Oct 2024 06:10:32 -0400 Subject: [PATCH 21/34] chore(cosmic-swingset): Fix bug uncovered by testing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🥳 --- packages/cosmic-swingset/src/launch-chain.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cosmic-swingset/src/launch-chain.js b/packages/cosmic-swingset/src/launch-chain.js index 6898d6af310..9500a380a12 100644 --- a/packages/cosmic-swingset/src/launch-chain.js +++ b/packages/cosmic-swingset/src/launch-chain.js @@ -337,7 +337,7 @@ function computronCounter( return totalBeans === 0n && !cleanupDone; }; const didCleanup = details => { - for (const [phase, count] of Object.entries(details)) { + for (const [phase, count] of Object.entries(details.cleanups)) { if (phase === 'total') continue; if (!hasOwn(remainingCleanups, phase)) { // TODO: log unknown phases? From cd1a3eccd843db45b916e2712a776f89e49ed59e Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Mon, 28 Oct 2024 06:31:33 -0400 Subject: [PATCH 22/34] chore: Add the "promises" slow vat cleanup phase --- golang/cosmos/x/swingset/types/default-params.go | 3 +++ packages/cosmic-swingset/src/sim-params.js | 1 + 2 files changed, 4 insertions(+) diff --git a/golang/cosmos/x/swingset/types/default-params.go b/golang/cosmos/x/swingset/types/default-params.go index cf58a6ee6cf..d267b48d659 100644 --- a/golang/cosmos/x/swingset/types/default-params.go +++ b/golang/cosmos/x/swingset/types/default-params.go @@ -35,6 +35,7 @@ const ( VatCleanupDefault = "default" VatCleanupExports = "exports" VatCleanupImports = "imports" + VatCleanupPromises = "promises" VatCleanupKv = "kv" VatCleanupSnapshots = "snapshots" VatCleanupTranscripts = "transcripts" @@ -78,6 +79,7 @@ var ( DefaultVatCleanupDefault = sdk.NewUint(5) // DefaultVatCleanupExports = DefaultVatCleanupDefault // DefaultVatCleanupImports = DefaultVatCleanupDefault + // DefaultVatCleanupPromises = DefaultVatCleanupDefault DefaultVatCleanupKv = sdk.NewUint(50) // DefaultVatCleanupSnapshots = DefaultVatCleanupDefault // DefaultVatCleanupTranscripts = DefaultVatCleanupDefault @@ -85,6 +87,7 @@ var ( UintMapEntry{VatCleanupDefault, DefaultVatCleanupDefault}, // UintMapEntry{VatCleanupExports, DefaultVatCleanupExports}, // UintMapEntry{VatCleanupImports, DefaultVatCleanupImports}, + // UintMapEntry{VatCleanupPromises, DefaultVatCleanupPromises}, UintMapEntry{VatCleanupKv, DefaultVatCleanupKv}, // UintMapEntry{VatCleanupSnapshots, DefaultVatCleanupSnapshots}, // UintMapEntry{VatCleanupTranscripts, DefaultVatCleanupTranscripts}, diff --git a/packages/cosmic-swingset/src/sim-params.js b/packages/cosmic-swingset/src/sim-params.js index 4ff73ba6949..4971fe6ed20 100644 --- a/packages/cosmic-swingset/src/sim-params.js +++ b/packages/cosmic-swingset/src/sim-params.js @@ -82,6 +82,7 @@ export const VatCleanupPhase = /** @type {const} */ ({ Default: 'default', Exports: 'exports', Imports: 'imports', + Promises: 'promises', Kv: 'kv', Snapshots: 'snapshots', Transcripts: 'transcripts', From a50844e87eacd82815d595d2bedc9891303ee44e Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Mon, 28 Oct 2024 12:19:54 -0400 Subject: [PATCH 23/34] chore(cosmic-swingset): Appease tsc --- .../src/helpers/json-stable-stringify.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) 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); } From 687d439b7e3333e9df7a41c92f8462679257dfad Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Mon, 28 Oct 2024 16:37:32 -0400 Subject: [PATCH 24/34] build(cosmic-swingset): Work around CI issue --- packages/vats/tsconfig.build.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/vats/tsconfig.build.json b/packages/vats/tsconfig.build.json index fe5ea57d603..1843a3ccdb4 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/vat-bootstrap-chain-reflective.js"] } From d7c287264541614d6d7a5c113dbe8c83a1495474 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Mon, 28 Oct 2024 23:34:33 -0400 Subject: [PATCH 25/34] test(cosmic-swingset): Introduce a swingset instantiation discontinuity to the run policy test An unrelated CI test experienced a failure when budgeted vat cleanup had already removed a strict subset of a slowly-terminating vat's kvStore entries that included the one with key "${vatID}.o.nextID"... `provideVatKeeper` uses such keys as a "vat is alive" predicate that must pass before it spins up any new keeper (whose primary purpose is providing an interface for a *live* vat). This is not an issue within a single swingset instantiation, in which `provideVatKeeper` is memoized. But that cache starts empty with each process, and the attempt to instantiate a new keeper for a slowly- terminating vat in this state fails. run-policy.test.js now covers this scenario. --- .../cosmic-swingset/test/run-policy.test.js | 72 +++++++++++++------ packages/cosmic-swingset/tools/test-kit.js | 15 ++-- 2 files changed, 59 insertions(+), 28 deletions(-) diff --git a/packages/cosmic-swingset/test/run-policy.test.js b/packages/cosmic-swingset/test/run-policy.test.js index 9786c56164a..f6f027cfa91 100644 --- a/packages/cosmic-swingset/test/run-policy.test.js +++ b/packages/cosmic-swingset/test/run-policy.test.js @@ -50,22 +50,23 @@ test('cleanup work must be limited by vat_cleanup_budget', async t => { Fail`port ${q(destPort)} not implemented for message ${msg}`; } }; + const options = { + bundles: makeSourceDescriptors({ + puppet: '@agoric/swingset-vat/tools/vat-puppet.js', + }), + configOverrides: { + // Ensure multiple spans and snapshots. + defaultManagerType: 'xsnap', + snapshotInitial: 2, + snapshotInterval: 4, + }, + fixupBootMsg: () => ({ + ...defaultBootstrapMessage, + params: makeCleanupBudgetParams({ Default: 0 }), + }), + }; const { pushCoreEval, runNextBlock, shutdown, swingStore } = - await makeCosmicSwingsetTestKit(receiveBridgeSend, { - bundles: makeSourceDescriptors({ - puppet: '@agoric/swingset-vat/tools/vat-puppet.js', - }), - configOverrides: { - // Ensure multiple spans and snapshots. - defaultManagerType: 'xsnap', - snapshotInitial: 2, - snapshotInterval: 4, - }, - fixupBootMsg: () => ({ - ...defaultBootstrapMessage, - params: makeCleanupBudgetParams({ Default: 0 }), - }), - }); + await makeCosmicSwingsetTestKit(receiveBridgeSend, options); const mapStore = provideEnhancedKVStore( /** @type {KVStore} */ (swingStore.kernelStorage.kvStore), ); @@ -89,6 +90,10 @@ test('cleanup work must be limited by vat_cleanup_budget', async t => { 'v8', `time to update expected vatID to ${JSON.stringify(vatIDs)}.at(-1)?`, ); + // 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)); const getKV = () => [...mapStore].filter(([key]) => key.startsWith(`${vatID}.`)); // console.log(swingStore.kernelStorage); @@ -182,15 +187,36 @@ test('cleanup work must be limited by vat_cleanup_budget', async t => { params: makeCleanupBudgetParams({ Default: 2 ** 32, Kv: 3 }), }); t.is(getKV().length, onlyKV.length - 3, 'initial kvStore deletion'); - await runNextBlock(); + let lastBlockInfo = await runNextBlock(); t.is(getKV().length, onlyKV.length - 6, 'further kvStore deletion'); - // Allow remaining cleanup. - await runNextBlock({ - params: makeCleanupBudgetParams({ Default: 2 ** 32 }), - }); - await runNextBlock(); - t.is(getKV().length, 0, 'cleanup complete'); + // 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 }); + { + // Pick up where we left off with the same data and block, + // but with a new budget. + const newOptions = { + ...options, + swingStore, + fixupBootMsg: () => ({ + ...defaultBootstrapMessage, + ...lastBlockInfo, + params: makeCleanupBudgetParams({ Default: 2 ** 32 }), + }), + }; + // eslint-disable-next-line no-shadow + const { runNextBlock, shutdown } = await makeCosmicSwingsetTestKit( + receiveBridgeSend, + newOptions, + ); + + await runNextBlock(); + t.is(getKV().length, 0, 'cleanup complete'); - await shutdown(); + await shutdown(); + } }); diff --git a/packages/cosmic-swingset/tools/test-kit.js b/packages/cosmic-swingset/tools/test-kit.js index b4509bec1db..32002882db4 100644 --- a/packages/cosmic-swingset/tools/test-kit.js +++ b/packages/cosmic-swingset/tools/test-kit.js @@ -134,6 +134,8 @@ const baseConfig = harden({ * 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 @@ -162,6 +164,7 @@ export const makeCosmicSwingsetTestKit = async ( fixupConfig, slogSender, swingsetConfig = {}, + swingStore = initSwingStore(), // in-memory vats, // Options for vats (particularly the reflective bootstrap vat). @@ -196,7 +199,6 @@ export const makeCosmicSwingsetTestKit = async ( if (fixupConfig) config = fixupConfig(config); - const swingStore = initSwingStore(); // in-memory const { hostStorage } = swingStore; const actionQueueStorage = makeQueueStorageMock().storage; @@ -233,7 +235,7 @@ export const makeCosmicSwingsetTestKit = async ( slogSender = await makeSlogSender({ env }); } - const { blockingSend, shutdown: shutdownKernel } = await launch({ + const launchResult = await launch({ swingStore, actionQueueStorage, highPriorityQueueStorage, @@ -248,8 +250,12 @@ export const makeCosmicSwingsetTestKit = async ( slogSender, swingsetConfig, }); - const shutdown = async () => { - await Promise.all([shutdownKernel, hostStorage.close()]); + const { blockingSend, shutdown: shutdownKernel } = launchResult; + /** @type {(options?: { kernelOnly?: boolean }) => Promise} */ + const shutdown = async ({ kernelOnly = false } = {}) => { + await shutdownKernel(); + if (kernelOnly) return; + await hostStorage.close(); }; /** @@ -294,7 +300,6 @@ export const makeCosmicSwingsetTestKit = async ( }); return getLastBlockInfo(); }; - await runNextBlock(); /** @type {InboundQueue} */ const actionQueue = makeQueue(actionQueueStorage); From b786414a987cb850b35cdbf26351ace4109684c8 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Mon, 28 Oct 2024 23:44:04 -0400 Subject: [PATCH 26/34] fix(SwingSet): Introduce a termination-dedicated "VatUndertaker" analog to "VatKeeper" --- .../SwingSet/src/kernel/state/kernelKeeper.js | 79 ++++++++++++------ .../SwingSet/src/kernel/state/vatKeeper.js | 80 ++++++++++--------- 2 files changed, 96 insertions(+), 63 deletions(-) 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); From 21fb12cc202c733e0542614800115a114d0c60b1 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Tue, 29 Oct 2024 14:43:59 -0400 Subject: [PATCH 27/34] test(cosmic-swingset): Minor cleanup --- .../cosmic-swingset/test/run-policy.test.js | 32 ++++--- packages/cosmic-swingset/tools/test-kit.js | 84 ++++++++++++------- 2 files changed, 74 insertions(+), 42 deletions(-) diff --git a/packages/cosmic-swingset/test/run-policy.test.js b/packages/cosmic-swingset/test/run-policy.test.js index f6f027cfa91..8c0dc50c0cf 100644 --- a/packages/cosmic-swingset/test/run-policy.test.js +++ b/packages/cosmic-swingset/test/run-policy.test.js @@ -40,7 +40,11 @@ const makeCleanupBudgetParams = budget => { }; test('cleanup work must be limited by vat_cleanup_budget', async t => { - const { toStorage: handleVstorage } = makeFakeStorageKit(''); + 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: { @@ -65,8 +69,12 @@ test('cleanup work must be limited by vat_cleanup_budget', async t => { params: makeCleanupBudgetParams({ Default: 0 }), }), }; - const { pushCoreEval, runNextBlock, shutdown, swingStore } = - await makeCosmicSwingsetTestKit(receiveBridgeSend, options); + 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), ); @@ -77,7 +85,7 @@ test('cleanup work must be limited by vat_cleanup_budget', async t => { return value; }; - // Launch the new vat and capture its ID and store snapshotting. + // Launch the new vat and capture its ID. pushCoreEval(async powers => { const { bootstrap } = powers.vats; await E(bootstrap).createVat('doomed', 'puppet'); @@ -90,18 +98,18 @@ test('cleanup work must be limited by vat_cleanup_budget', async t => { '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}.`)); - // console.log(swingStore.kernelStorage); - - t.false( - JSON.parse(mustGet('vats.terminated')).includes(vatID), - 'must not be terminated', - ); const initialEntries = new Map(getKV()); t.not(initialEntries.size, 0, 'initial kvStore entries must exist'); @@ -196,6 +204,7 @@ test('cleanup work must be limited by vat_cleanup_budget', async t => { 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. @@ -213,10 +222,9 @@ test('cleanup work must be limited by vat_cleanup_budget', async t => { receiveBridgeSend, newOptions, ); + finish = shutdown; await runNextBlock(); t.is(getKV().length, 0, 'cleanup complete'); - - await shutdown(); } }); diff --git a/packages/cosmic-swingset/tools/test-kit.js b/packages/cosmic-swingset/tools/test-kit.js index 32002882db4..9bc89ce0cbf 100644 --- a/packages/cosmic-swingset/tools/test-kit.js +++ b/packages/cosmic-swingset/tools/test-kit.js @@ -29,7 +29,13 @@ import { makeQueue, makeQueueStorageMock } from '../src/helpers/make-queue.js'; */ /** @type {Replacer} */ -const clone = obj => JSON.parse(JSON.stringify(obj)); +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 defaultBootMsg = harden( makeBootMsg({ @@ -56,7 +62,7 @@ export const defaultBootMsg = harden( }), ); export const defaultBootstrapMessage = harden({ - ...clone(defaultBootMsg), + ...deepCopyData(defaultBootMsg), blockHeight: 1, blockTime: Math.floor(Date.parse('2010-01-01T00:00Z') / 1000), isBootstrap: true, @@ -79,6 +85,7 @@ export const defaultBootstrapMessage = harden({ */ const baseConfig = harden({ defaultReapInterval: 'never', + defaultReapGCKrefs: 'never', defaultManagerType: undefined, bootstrap: 'bootstrap', vats: { @@ -164,7 +171,7 @@ export const makeCosmicSwingsetTestKit = async ( fixupConfig, slogSender, swingsetConfig = {}, - swingStore = initSwingStore(), // in-memory + swingStore, vats, // Options for vats (particularly the reflective bootstrap vat). @@ -177,9 +184,9 @@ export const makeCosmicSwingsetTestKit = async ( await null; /** @type {SwingSetConfig} */ let config = { - ...clone(baseConfig), + ...deepCopyData(baseConfig), ...configOverrides, - defaultManagerType, + ...stripUndefined({ defaultManagerType }), }; if (bundleDir) { bundleDir = resolvePath(bundleDir); @@ -191,14 +198,26 @@ export const makeCosmicSwingsetTestKit = async ( // @ts-expect-error we assume that config.bootstrap is not undefined const bootstrapVatDesc = config.vats[config.bootstrap]; - Object.assign(bootstrapVatDesc.parameters, { - baseManifest: baseBootstrapManifest, - addBehaviors: addBootstrapBehaviors, - coreProposalCodeSteps: bootstrapCoreEvals, - }); + Object.assign( + bootstrapVatDesc.parameters, + stripUndefined({ + baseManifest: baseBootstrapManifest, + addBehaviors: addBootstrapBehaviors, + coreProposalCodeSteps: bootstrapCoreEvals, + }), + ); if (fixupConfig) config = fixupConfig(config); + let bootMsg = deepCopyData(defaultBootMsg); + if (fixupBootMsg) bootMsg = fixupBootMsg(bootMsg); + bootMsg?.type === SwingsetMessageType.AG_COSMOS_INIT || + Fail`bootMsg must be AG_COSMOS_INIT`; + if (bootMsg.isBootstrap === undefined && !swingStore) { + bootMsg.isBootstrap = true; + } + + if (!swingStore) swingStore = initSwingStore(); // in-memory const { hostStorage } = swingStore; const actionQueueStorage = makeQueueStorageMock().storage; @@ -215,22 +234,6 @@ export const makeCosmicSwingsetTestKit = async ( throw Error('not implemented'); }; - let bootMsg = clone(defaultBootMsg); - if (fixupBootMsg) bootMsg = fixupBootMsg(bootMsg); - let { - blockHeight: lastBlockHeight, - blockTime: lastBlockTime, - params: lastBlockParams, - } = bootMsg; - let lastBlockWalltime = Date.now(); - - // 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); - }; - if (!slogSender && (env.SLOGFILE || env.SLOGSENDER)) { slogSender = await makeSlogSender({ env }); } @@ -258,6 +261,17 @@ export const makeCosmicSwingsetTestKit = async ( await hostStorage.close(); }; + // Remember information about the current block, starting with the boot + // message. + let { + isBootstrap: needsBootstrap, + blockHeight: lastBlockHeight, + blockTime: lastBlockTime, + params: lastBlockParams, + } = bootMsg; + let lastBlockWalltime = Date.now(); + await blockingSend(bootMsg); + /** * @returns {BlockInfo} */ @@ -267,20 +281,30 @@ export const makeCosmicSwingsetTestKit = async ( 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 = lastBlockHeight + 1, - blockTime = nextBlockTime(), + blockHeight = needsBootstrap ? lastBlockHeight : lastBlockHeight + 1, + blockTime = needsBootstrap ? lastBlockTime : nextBlockTime(), params = lastBlockParams, } = {}) => { - blockHeight > lastBlockHeight || + needsBootstrap || + blockHeight > lastBlockHeight || Fail`blockHeight ${blockHeight} must be greater than ${lastBlockHeight}`; - blockTime > lastBlockTime || + needsBootstrap || + blockTime > lastBlockTime || Fail`blockTime ${blockTime} must be greater than ${lastBlockTime}`; + needsBootstrap = false; lastBlockWalltime = Date.now(); lastBlockHeight = blockHeight; lastBlockTime = blockTime; From 0c9057ce2004ab710620e7e73abffa13d47b9cdc Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Tue, 29 Oct 2024 21:58:08 -0400 Subject: [PATCH 28/34] chore: Rename BootMsg to InitMsg, reserving the former for `isBootstrap: true` --- packages/cosmic-swingset/src/chain-main.js | 4 +-- .../cosmic-swingset/test/run-policy.test.js | 7 ++-- packages/cosmic-swingset/tools/test-kit.js | 34 +++++++++---------- packages/internal/src/chain-utils.js | 6 ++-- packages/vats/src/core/basic-behaviors.js | 2 +- .../vats/test/vat-bank-integration.test.js | 2 +- 6 files changed, 28 insertions(+), 27 deletions(-) diff --git a/packages/cosmic-swingset/src/chain-main.js b/packages/cosmic-swingset/src/chain-main.js index 1eebb937c49..0221529fbf5 100644 --- a/packages/cosmic-swingset/src/chain-main.js +++ b/packages/cosmic-swingset/src/chain-main.js @@ -30,7 +30,7 @@ import { } from '@agoric/internal/src/lib-chainStorage.js'; import { makeShutdown } from '@agoric/internal/src/node/shutdown.js'; -import { makeBootMsg } from '@agoric/internal/src/chain-utils.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'; @@ -429,7 +429,7 @@ export default async function main(progname, args, { env, homedir, agcc }) { }; const argv = { - bootMsg: makeBootMsg(initAction), + bootMsg: makeInitMsg(initAction), }; const getVatConfig = async () => { const href = await importMetaResolve( diff --git a/packages/cosmic-swingset/test/run-policy.test.js b/packages/cosmic-swingset/test/run-policy.test.js index 8c0dc50c0cf..ec81c1723a8 100644 --- a/packages/cosmic-swingset/test/run-policy.test.js +++ b/packages/cosmic-swingset/test/run-policy.test.js @@ -6,6 +6,7 @@ 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'; @@ -64,7 +65,7 @@ test('cleanup work must be limited by vat_cleanup_budget', async t => { snapshotInitial: 2, snapshotInterval: 4, }, - fixupBootMsg: () => ({ + fixupInitMessage: () => ({ ...defaultBootstrapMessage, params: makeCleanupBudgetParams({ Default: 0 }), }), @@ -211,8 +212,8 @@ test('cleanup work must be limited by vat_cleanup_budget', async t => { const newOptions = { ...options, swingStore, - fixupBootMsg: () => ({ - ...defaultBootstrapMessage, + fixupInitMessage: () => ({ + ...defaultInitMessage, ...lastBlockInfo, params: makeCleanupBudgetParams({ Default: 2 ** 32 }), }), diff --git a/packages/cosmic-swingset/tools/test-kit.js b/packages/cosmic-swingset/tools/test-kit.js index 9bc89ce0cbf..960368992f6 100644 --- a/packages/cosmic-swingset/tools/test-kit.js +++ b/packages/cosmic-swingset/tools/test-kit.js @@ -7,7 +7,7 @@ import { SwingsetMessageType, QueuedActionType, } from '@agoric/internal/src/action-types.js'; -import { makeBootMsg } from '@agoric/internal/src/chain-utils.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'; @@ -18,7 +18,7 @@ import { } from '../src/helpers/bufferedStorage.js'; import { makeQueue, makeQueueStorageMock } from '../src/helpers/make-queue.js'; -/** @import { BlockInfo, BootMsg } from '@agoric/internal/src/chain-utils.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'; */ @@ -37,8 +37,8 @@ const stripUndefined = obj => Object.entries(obj).filter(([_key, value]) => value !== undefined), ); -export const defaultBootMsg = harden( - makeBootMsg({ +export const defaultInitMessage = harden( + makeInitMsg({ type: SwingsetMessageType.AG_COSMOS_INIT, blockHeight: 100, blockTime: Math.floor(Date.parse('2020-01-01T00:00Z') / 1000), @@ -62,7 +62,7 @@ export const defaultBootMsg = harden( }), ); export const defaultBootstrapMessage = harden({ - ...deepCopyData(defaultBootMsg), + ...deepCopyData(defaultInitMessage), blockHeight: 1, blockTime: Math.floor(Date.parse('2010-01-01T00:00Z') / 1000), isBootstrap: true, @@ -135,7 +135,7 @@ const baseConfig = harden({ * {@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.fixupBootMsg] a final opportunity to make + * @param {Replacer} [options.fixupInitMessage] a final opportunity to make * any changes * @param {Replacer} [options.fixupConfig] a final opportunity * to make any changes @@ -167,7 +167,7 @@ export const makeCosmicSwingsetTestKit = async ( defaultManagerType, debugName, env = process.env, - fixupBootMsg, + fixupInitMessage, fixupConfig, slogSender, swingsetConfig = {}, @@ -209,12 +209,12 @@ export const makeCosmicSwingsetTestKit = async ( if (fixupConfig) config = fixupConfig(config); - let bootMsg = deepCopyData(defaultBootMsg); - if (fixupBootMsg) bootMsg = fixupBootMsg(bootMsg); - bootMsg?.type === SwingsetMessageType.AG_COSMOS_INIT || - Fail`bootMsg must be AG_COSMOS_INIT`; - if (bootMsg.isBootstrap === undefined && !swingStore) { - bootMsg.isBootstrap = true; + 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 @@ -247,7 +247,7 @@ export const makeCosmicSwingsetTestKit = async ( replayChainSends, bridgeOutbound: receiveBridgeSend, vatconfig: config, - argv: { bootMsg }, + argv: { bootMsg: initMessage }, env, debugName, slogSender, @@ -261,16 +261,16 @@ export const makeCosmicSwingsetTestKit = async ( await hostStorage.close(); }; - // Remember information about the current block, starting with the boot + // Remember information about the current block, starting with the init // message. let { isBootstrap: needsBootstrap, blockHeight: lastBlockHeight, blockTime: lastBlockTime, params: lastBlockParams, - } = bootMsg; + } = initMessage; let lastBlockWalltime = Date.now(); - await blockingSend(bootMsg); + await blockingSend(initMessage); /** * @returns {BlockInfo} diff --git a/packages/internal/src/chain-utils.js b/packages/internal/src/chain-utils.js index 558d9d20bad..fe1716a6016 100644 --- a/packages/internal/src/chain-utils.js +++ b/packages/internal/src/chain-utils.js @@ -26,16 +26,16 @@ import * as _ActionType from './action-types.js'; * type: typeof _ActionType.AG_COSMOS_INIT; * chainID: string; * supplyCoins: { denom: string; amount: NatString }[]; - * }} BootMsg + * }} InitMsg * cosmosInitAction fields that are subject to consensus. See cosmosInitAction * in {@link ../../../golang/cosmos/app/app.go}. */ /** * @param {any} initAction - * @returns {BootMsg} + * @returns {InitMsg} */ -export const makeBootMsg = initAction => { +export const makeInitMsg = initAction => { const { type, blockHeight, diff --git a/packages/vats/src/core/basic-behaviors.js b/packages/vats/src/core/basic-behaviors.js index 3c57a7a0d98..5783e87d0fc 100644 --- a/packages/vats/src/core/basic-behaviors.js +++ b/packages/vats/src/core/basic-behaviors.js @@ -539,7 +539,7 @@ export const installBootContracts = async ({ * @param {BootstrapPowers & { * vatParameters: { * argv: { - * bootMsg?: import('@agoric/internal/src/chain-utils.js').BootMsg; + * bootMsg?: import('@agoric/internal/src/chain-utils.js').InitMsg; * }; * }; * }} powers diff --git a/packages/vats/test/vat-bank-integration.test.js b/packages/vats/test/vat-bank-integration.test.js index e50afe0c2b2..e06fb358a21 100644 --- a/packages/vats/test/vat-bank-integration.test.js +++ b/packages/vats/test/vat-bank-integration.test.js @@ -46,7 +46,7 @@ test('mintInitialSupply, addBankAssets bootstrap actions', async t => { }); // Genesis RUN supply: 50 - /** @type {import('@agoric/internal/src/chain-utils.js').BootMsg} */ + /** @type {import('@agoric/internal/src/chain-utils.js').InitMsg} */ // @ts-expect-error missing properties const bootMsg = { type: AG_COSMOS_INIT, From 874644fb753c52bc2a16a202d153110d8db0f9fe Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Tue, 29 Oct 2024 22:00:49 -0400 Subject: [PATCH 29/34] test(cosmic-swingset): Clean up doomed vat garbage before terminating it --- packages/cosmic-swingset/test/run-policy.test.js | 6 ++++-- packages/vats/tools/vat-bootstrap-chain-reflective.js | 7 +++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/cosmic-swingset/test/run-policy.test.js b/packages/cosmic-swingset/test/run-policy.test.js index ec81c1723a8..6058bccf08b 100644 --- a/packages/cosmic-swingset/test/run-policy.test.js +++ b/packages/cosmic-swingset/test/run-policy.test.js @@ -60,6 +60,8 @@ test('cleanup work must be limited by vat_cleanup_budget', async t => { puppet: '@agoric/swingset-vat/tools/vat-puppet.js', }), configOverrides: { + // Aggressive GC. + defaultReapInterval: 1, // Ensure multiple spans and snapshots. defaultManagerType: 'xsnap', snapshotInitial: 2, @@ -167,8 +169,8 @@ test('cleanup work must be limited by vat_cleanup_budget', async t => { // Terminate the vat and verify lack of cleanup. pushCoreEval(async powers => { const { bootstrap } = powers.vats; - const doomed = await E(bootstrap).getVatRoot('doomed'); - await E(doomed).dieHappy(); + const adminNode = await E(bootstrap).getVatAdminNode('doomed'); + await E(adminNode).terminateWithFailure(); }); await runNextBlock(); t.true( diff --git a/packages/vats/tools/vat-bootstrap-chain-reflective.js b/packages/vats/tools/vat-bootstrap-chain-reflective.js index 7bbd7ed32be..74bdc8259b0 100644 --- a/packages/vats/tools/vat-bootstrap-chain-reflective.js +++ b/packages/vats/tools/vat-bootstrap-chain-reflective.js @@ -138,6 +138,13 @@ export const buildRootObject = (vatPowers, bootstrapParameters, baggage) => { 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)}`; From 315cdd56e0955ba26d624ca3a4997888abc1d635 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Wed, 30 Oct 2024 00:57:36 -0400 Subject: [PATCH 30/34] fix(x/swingset): Let migration see incomplete Params structs --- golang/cosmos/x/swingset/keeper/keeper.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/golang/cosmos/x/swingset/keeper/keeper.go b/golang/cosmos/x/swingset/keeper/keeper.go index dcd4790c445..385cc482d03 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 } From a805191513b7b6e46b98fcf3a362d77d8e5ac2d0 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Wed, 30 Oct 2024 03:02:27 -0400 Subject: [PATCH 31/34] chore: Per convention, remove leading "vat-" from "vat-bootstrap-chain-reflective.js" --- packages/cosmic-swingset/tools/test-kit.js | 4 ++-- ...trap-chain-reflective.js => bootstrap-chain-reflective.js} | 0 packages/vats/tsconfig.build.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename packages/vats/tools/{vat-bootstrap-chain-reflective.js => bootstrap-chain-reflective.js} (100%) diff --git a/packages/cosmic-swingset/tools/test-kit.js b/packages/cosmic-swingset/tools/test-kit.js index 960368992f6..e19e5c0ea9c 100644 --- a/packages/cosmic-swingset/tools/test-kit.js +++ b/packages/cosmic-swingset/tools/test-kit.js @@ -90,7 +90,7 @@ const baseConfig = harden({ bootstrap: 'bootstrap', vats: { bootstrap: { - sourceSpec: '@agoric/vats/tools/vat-bootstrap-chain-reflective.js', + sourceSpec: '@agoric/vats/tools/bootstrap-chain-reflective.js', creationOptions: { critical: true, }, @@ -147,7 +147,7 @@ const baseConfig = harden({ * @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/vat-bootstrap-chain-reflective.js}) + * {@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}) diff --git a/packages/vats/tools/vat-bootstrap-chain-reflective.js b/packages/vats/tools/bootstrap-chain-reflective.js similarity index 100% rename from packages/vats/tools/vat-bootstrap-chain-reflective.js rename to packages/vats/tools/bootstrap-chain-reflective.js diff --git a/packages/vats/tsconfig.build.json b/packages/vats/tsconfig.build.json index 1843a3ccdb4..1487c804a7b 100644 --- a/packages/vats/tsconfig.build.json +++ b/packages/vats/tsconfig.build.json @@ -4,5 +4,5 @@ "../../tsconfig-build-options.json" ], // FIXME: https://github.com/Agoric/agoric-sdk/issues/10351 - "exclude": ["tools/vat-bootstrap-chain-reflective.js"] + "exclude": ["tools/bootstrap-chain-reflective.js"] } From b9b190364da5af0dd28b67eed3635385d0a623af Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Wed, 6 Nov 2024 00:41:05 -0500 Subject: [PATCH 32/34] test(x/vstorage): Supply testing function with unlimited gas --- golang/cosmos/x/vstorage/testing/queue.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 } From 55b9b49d5f77be9db33ae132a8f7b7017a90d0a9 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Fri, 8 Nov 2024 13:27:51 -0500 Subject: [PATCH 33/34] feat(x/swingset): Read beansPerUnit in each message handler and pass down to helpers This minimizes gas consumption vs. helpers re-reading parameters, making it less likely for an unestimated transaction to run out. --- golang/cosmos/ante/inbound_test.go | 4 +- golang/cosmos/x/swingset/keeper/keeper.go | 18 +++++--- .../x/swingset/types/expected_keepers.go | 4 +- golang/cosmos/x/swingset/types/msgs.go | 42 +++++++++++++------ 4 files changed, 46 insertions(+), 22 deletions(-) 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/x/swingset/keeper/keeper.go b/golang/cosmos/x/swingset/keeper/keeper.go index 385cc482d03..28c7723b752 100644 --- a/golang/cosmos/x/swingset/keeper/keeper.go +++ b/golang/cosmos/x/swingset/keeper/keeper.go @@ -307,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) @@ -342,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/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. From db8108ab1cac57b9fab91115a517f7a61b1c9299 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Sat, 9 Nov 2024 12:09:00 -0500 Subject: [PATCH 34/34] test(cosmic-swingset): Update mock-chain pushCoreEval to require source text input This reduces the risk of lexical shear between definition in a *.test.js file and evaluation in a core-eval compartment. --- .../cosmic-swingset/test/run-policy.test.js | 98 +++++++++++-------- packages/cosmic-swingset/tools/test-kit.js | 14 ++- 2 files changed, 66 insertions(+), 46 deletions(-) diff --git a/packages/cosmic-swingset/test/run-policy.test.js b/packages/cosmic-swingset/test/run-policy.test.js index 6058bccf08b..7a368621b76 100644 --- a/packages/cosmic-swingset/test/run-policy.test.js +++ b/packages/cosmic-swingset/test/run-policy.test.js @@ -89,10 +89,12 @@ test('cleanup work must be limited by vat_cleanup_budget', async t => { }; // Launch the new vat and capture its ID. - pushCoreEval(async powers => { - const { bootstrap } = powers.vats; - await E(bootstrap).createVat('doomed', 'puppet'); - }); + 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); @@ -117,39 +119,49 @@ test('cleanup work must be limited by vat_cleanup_budget', async t => { 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)); - }); + 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), @@ -167,11 +179,13 @@ test('cleanup work must be limited by vat_cleanup_budget', async t => { ); // 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(); - }); + 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), diff --git a/packages/cosmic-swingset/tools/test-kit.js b/packages/cosmic-swingset/tools/test-kit.js index e19e5c0ea9c..88018ef591d 100644 --- a/packages/cosmic-swingset/tools/test-kit.js +++ b/packages/cosmic-swingset/tools/test-kit.js @@ -345,22 +345,28 @@ export const makeCosmicSwingsetTestKit = async ( }); }; /** - * @param {string | ((...args: any[]) => void)} fn - * @param {string} [jsonPermits] should deserialize into a BootstrapManifestPermit + * @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 = ( - fn, + 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: String(fn), + js_code: fnText, }; const action = { type: QueuedActionType.CORE_EVAL,