Skip to content

Commit

Permalink
Merge pull request #8703 from Agoric/mfig-vm-action-defaults
Browse files Browse the repository at this point in the history
feat(cosmos): contextual and declarative VM action field defaults
  • Loading branch information
mergify[bot] authored Jan 6, 2024
2 parents 6466eec + 021fd0f commit 75ef233
Show file tree
Hide file tree
Showing 13 changed files with 405 additions and 231 deletions.
36 changes: 16 additions & 20 deletions golang/cosmos/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -866,18 +866,17 @@ func normalizeModuleAccount(ctx sdk.Context, ak authkeeper.AccountKeeper, name s
}

type cosmosInitAction struct {
Type string `json:"type"`
ChainID string `json:"chainID"`
BlockTime int64 `json:"blockTime,omitempty"`
IsBootstrap bool `json:"isBootstrap"`
Params swingset.Params `json:"params"`
SupplyCoins sdk.Coins `json:"supplyCoins"`
UpgradePlan *upgradetypes.Plan `json:"upgradePlan,omitempty"`
LienPort int `json:"lienPort"`
StoragePort int `json:"storagePort"`
SwingsetPort int `json:"swingsetPort"`
VbankPort int `json:"vbankPort"`
VibcPort int `json:"vibcPort"`
vm.ActionHeader `actionType:"AG_COSMOS_INIT"`
ChainID string `json:"chainID"`
IsBootstrap bool `json:"isBootstrap"`
Params swingset.Params `json:"params"`
SupplyCoins sdk.Coins `json:"supplyCoins"`
UpgradePlan *upgradetypes.Plan `json:"upgradePlan,omitempty"`
LienPort int `json:"lienPort"`
StoragePort int `json:"storagePort"`
SwingsetPort int `json:"swingsetPort"`
VbankPort int `json:"vbankPort"`
VibcPort int `json:"vibcPort"`
}

// Name returns the name of the App
Expand All @@ -900,16 +899,9 @@ func (app *GaiaApp) initController(ctx sdk.Context, bootstrap bool) {
app.CheckControllerInited(false)
app.controllerInited = true

var blockTime int64 = 0
if bootstrap || app.upgradePlan != nil {
blockTime = ctx.BlockTime().Unix()
}

// Begin initializing the controller here.
action := &cosmosInitAction{
Type: "AG_COSMOS_INIT",
ChainID: ctx.ChainID(),
BlockTime: blockTime,
IsBootstrap: bootstrap,
Params: app.SwingSetKeeper.GetParams(ctx),
SupplyCoins: sdk.NewCoins(app.BankKeeper.GetSupply(ctx, "uist")),
Expand All @@ -920,7 +912,11 @@ func (app *GaiaApp) initController(ctx sdk.Context, bootstrap bool) {
VbankPort: app.vbankPort,
VibcPort: app.vibcPort,
}
// This really abuses `BlockingSend` to get back at `sendToController`
// This uses `BlockingSend` as a friendly wrapper for `sendToController`
//
// CAVEAT: we are restarting after an in-consensus halt or just because this
// node felt like it. The controller must be able to handle either case
// (inConsensus := action.IsBootstrap || action.UpgradePlan != nil).
out, err := app.SwingSetKeeper.BlockingSend(ctx, action)

// fmt.Fprintf(os.Stderr, "AG_COSMOS_INIT Returned from SwingSet: %s, %v\n", out, err)
Expand Down
130 changes: 130 additions & 0 deletions golang/cosmos/vm/action.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package vm

import (
"reflect"
"strconv"
"time"

sdk "github.com/cosmos/cosmos-sdk/types"
)

var (
zeroTime = time.Time{}
actionHeaderType = reflect.TypeOf(ActionHeader{})
_ Action = &ActionHeader{}
)

// Jsonable is a value, j, that can be passed through json.Marshal(j).
type Jsonable interface{}

type Action interface {
GetActionHeader() ActionHeader
}

// ActionPusher enqueues data for later consumption by the controller.
type ActionPusher func(ctx sdk.Context, action Action) error

// ActionHeader should be embedded in all actions. It is populated by PopulateAction.
type ActionHeader struct {
// Type defaults to the `actionType:"..."` tag of the embedder's ActionHeader field.
Type string `json:"type"`
// BlockHeight defaults to sdk.Context.BlockHeight().
BlockHeight int64 `json:"blockHeight,omitempty"`
// BlockTime defaults to sdk.Context.BlockTime().Unix().
BlockTime int64 `json:"blockTime,omitempty"`
}

func (ah ActionHeader) GetActionHeader() ActionHeader {
return ah
}

// SetActionHeaderFromContext provides defaults to an ActionHeader.
func SetActionHeaderFromContext(ctx sdk.Context, actionType string, ah *ActionHeader) {
// Default the action type.
if len(ah.Type) == 0 {
ah.Type = actionType
}

// Default the block height.
if ah.BlockHeight == 0 {
ah.BlockHeight = ctx.BlockHeight()
}

if ah.BlockTime == 0 {
// Only default to a non-zero block time.
if blockTime := ctx.BlockTime(); blockTime != zeroTime {
ah.BlockTime = blockTime.Unix()
}
}
}

// PopulateAction interprets `default:"..."` tags and specially handles an
// embedded ActionHeader struct.
func PopulateAction(ctx sdk.Context, action Action) Action {
oldRv := reflect.Indirect(reflect.ValueOf(action))
if oldRv.Kind() != reflect.Struct {
return action
}

// Shallow copy to a new value.
rp := reflect.New(oldRv.Type())
rv := reflect.Indirect(rp)
for i := 0; i < rv.NumField(); i++ {
oldField := oldRv.Field(i)
field := rv.Field(i)
fieldType := rv.Type().Field(i)
if !field.CanSet() {
continue
}

// Copy from original.
field.Set(oldField)

// Populate any ActionHeader struct.
var ahp *ActionHeader
if fieldType.Type == actionHeaderType {
ahp = field.Addr().Interface().(*ActionHeader)
} else if fieldType.Type == reflect.PtrTo(actionHeaderType) {
if field.IsNil() {
ahp = &ActionHeader{}
} else {
ahp = field.Interface().(*ActionHeader)
}
}
if ahp != nil {
actionTypeTag, _ := fieldType.Tag.Lookup("actionType")
ah := *ahp
SetActionHeaderFromContext(ctx, actionTypeTag, &ah)
if field.Kind() == reflect.Ptr {
field.Set(reflect.ValueOf(&ah))
} else {
field.Set(reflect.ValueOf(ah))
}
continue
}

// Still zero value, try default struct field tag.
defaultTag, _ := fieldType.Tag.Lookup("default")
if !field.IsZero() || len(defaultTag) == 0 {
continue
}

switch field.Kind() {
case reflect.String:
field.SetString(defaultTag)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if val, err := strconv.ParseInt(defaultTag, 0, 64); err == nil {
field.SetInt(val)
}
case reflect.Bool:
if val, err := strconv.ParseBool(defaultTag); err == nil {
field.SetBool(val)
}
case reflect.Float32, reflect.Float64:
if val, err := strconv.ParseFloat(defaultTag, 64); err == nil {
field.SetFloat(val)
}
}
}
return rv.Interface().(Action)
}
129 changes: 129 additions & 0 deletions golang/cosmos/vm/action_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package vm_test

import (
"encoding/json"
"testing"
"time"

"github.com/Agoric/agoric-sdk/golang/cosmos/vm"
sdk "github.com/cosmos/cosmos-sdk/types"
)

var (
_ vm.Action = &Trivial{}
_ vm.Action = &Defaults{}
_ vm.Action = &dataAction{}
)

type Trivial struct {
vm.ActionHeader
Abc int
def string
}

type Defaults struct {
String string `default:"abc"`
Int int `default:"123"`
Float float64 `default:"4.56"`
Bool bool `default:"true"`
Any interface{}
}

func (d Defaults) GetActionHeader() vm.ActionHeader {
return vm.ActionHeader{}
}

type dataAction struct {
*vm.ActionHeader `actionType:"DATA_ACTION"`
Data []byte
}

func TestActionContext(t *testing.T) {
emptyCtx := sdk.Context{}

testCases := []struct {
name string
ctx sdk.Context
in vm.Action
expectedOut interface{}
}{
{"nil", emptyCtx, nil, nil},
{"no context", emptyCtx,
&Trivial{Abc: 123, def: "zot"},
&Trivial{Abc: 123, def: "zot"},
},
{"block height",
emptyCtx.WithBlockHeight(998),
&Trivial{Abc: 123, def: "zot"},
&Trivial{ActionHeader: vm.ActionHeader{BlockHeight: 998}, Abc: 123, def: "zot"},
},
{"block time",
emptyCtx.WithBlockTime(time.UnixMicro(1_000_000)),
&Trivial{Abc: 123, def: "zot"},
&Trivial{Abc: 123, def: "zot", ActionHeader: vm.ActionHeader{BlockTime: 1}},
},
{"default tags",
emptyCtx,
&Defaults{},
&Defaults{"abc", 123, 4.56, true, nil},
},
{"data action defaults",
emptyCtx.WithBlockHeight(998).WithBlockTime(time.UnixMicro(1_000_000)),
&dataAction{Data: []byte("hello")},
&dataAction{Data: []byte("hello"),
ActionHeader: &vm.ActionHeader{Type: "DATA_ACTION", BlockHeight: 998, BlockTime: 1}},
},
{"data action override Type",
emptyCtx.WithBlockHeight(998).WithBlockTime(time.UnixMicro(1_000_000)),
&dataAction{Data: []byte("hello2"),
ActionHeader: &vm.ActionHeader{Type: "DATA_ACTION2"}},
&dataAction{Data: []byte("hello2"),
ActionHeader: &vm.ActionHeader{Type: "DATA_ACTION2", BlockHeight: 998, BlockTime: 1}},
},
{"data action override BlockHeight",
emptyCtx.WithBlockHeight(998).WithBlockTime(time.UnixMicro(1_000_000)),
&dataAction{Data: []byte("hello2"),
ActionHeader: &vm.ActionHeader{BlockHeight: 999}},
&dataAction{Data: []byte("hello2"),
ActionHeader: &vm.ActionHeader{Type: "DATA_ACTION", BlockHeight: 999, BlockTime: 1}},
},
{"data action override BlockTime",
emptyCtx.WithBlockHeight(998).WithBlockTime(time.UnixMicro(1_000_000)),
&dataAction{Data: []byte("hello2"),
ActionHeader: &vm.ActionHeader{BlockTime: 2}},
&dataAction{Data: []byte("hello2"),
ActionHeader: &vm.ActionHeader{Type: "DATA_ACTION", BlockHeight: 998, BlockTime: 2}},
},
{"data action override all defaults",
emptyCtx.WithBlockHeight(998).WithBlockTime(time.UnixMicro(1_000_000)),
&dataAction{Data: []byte("hello2"),
ActionHeader: &vm.ActionHeader{Type: "DATA_ACTION2", BlockHeight: 999, BlockTime: 2}},
&dataAction{Data: []byte("hello2"),
ActionHeader: &vm.ActionHeader{Type: "DATA_ACTION2", BlockHeight: 999, BlockTime: 2}},
},
}

for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
jstr := func(in interface{}) string {
bz, err := json.Marshal(in)
if err != nil {
t.Fatal(err)
}
return string(bz)
}
jsonIn := jstr(tc.in)
out := vm.PopulateAction(tc.ctx, tc.in)
jsonIn2 := jstr(tc.in)
if jsonIn != jsonIn2 {
t.Errorf("unexpected mutated input: %s to %s", jsonIn, jsonIn2)
}
jsonOut := jstr(out)
jsonExpectedOut := jstr(tc.expectedOut)
if jsonOut != jsonExpectedOut {
t.Errorf("expected %s, got %s", jsonExpectedOut, jsonOut)
}
})
}
}
6 changes: 0 additions & 6 deletions golang/cosmos/vm/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,6 @@ type ControllerAdmissionMsg interface {
IsHighPriority(sdk.Context, interface{}) (bool, error)
}

// Jsonable is a value, j, that can be passed through json.Marshal(j).
type Jsonable interface{}

// ActionPusher enqueues data for later consumption by the controller.
type ActionPusher func(ctx sdk.Context, action Jsonable) error

var wrappedEmptySDKContext = sdk.WrapSDKContext(
sdk.Context{}.WithContext(context.Background()),
)
Expand Down
2 changes: 1 addition & 1 deletion golang/cosmos/x/lien/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ func makeTestKit() testKit {
sk := stakingkeeper.NewKeeper(cdc, stakingStoreKey, wak, bk, stakingSpace)

// lien keeper
pushAction := func(sdk.Context, vm.Jsonable) error {
pushAction := func(sdk.Context, vm.Action) error {
return nil
}
keeper := NewKeeper(cdc, lienStoreKey, wak, bk, sk, pushAction)
Expand Down
Loading

0 comments on commit 75ef233

Please sign in to comment.