Skip to content

Commit

Permalink
feat(cosmos): impose defaults when sending VM actions
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelfig committed Jan 6, 2024
1 parent 6466eec commit a710d68
Show file tree
Hide file tree
Showing 4 changed files with 281 additions and 10 deletions.
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
26 changes: 22 additions & 4 deletions golang/cosmos/x/swingset/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,16 @@ func NewKeeper(
}
}

func populateAction(ctx sdk.Context, action vm.Action) (vm.Action, error) {
action = vm.PopulateAction(ctx, action)
ah := action.GetActionHeader()
if len(ah.Type) == 0 {
return nil, fmt.Errorf("action %q cannot have an empty ActionHeader.Type", action)
}

return action, nil
}

// pushAction appends an action to the controller's specified inbound queue.
// The queue is kept in the kvstore so that changes are properly reverted if the
// kvstore is rolled back. By the time the block manager runs, it can commit
Expand All @@ -118,7 +128,11 @@ func NewKeeper(
//
// The inbound queue's format is documented by `makeChainQueue` in
// `packages/cosmic-swingset/src/helpers/make-queue.js`.
func (k Keeper) pushAction(ctx sdk.Context, inboundQueuePath string, action vm.Jsonable) error {
func (k Keeper) pushAction(ctx sdk.Context, inboundQueuePath string, action vm.Action) error {
action, err := populateAction(ctx, action)
if err != nil {
return err
}
txHash, txHashOk := ctx.Context().Value(baseapp.TxHashContextKey).(string)
if !txHashOk {
txHash = "unknown"
Expand All @@ -143,12 +157,12 @@ func (k Keeper) pushAction(ctx sdk.Context, inboundQueuePath string, action vm.J
}

// PushAction appends an action to the controller's actionQueue.
func (k Keeper) PushAction(ctx sdk.Context, action vm.Jsonable) error {
func (k Keeper) PushAction(ctx sdk.Context, action vm.Action) error {
return k.pushAction(ctx, StoragePathActionQueue, action)
}

// PushAction appends an action to the controller's highPriorityQueue.
func (k Keeper) PushHighPriorityAction(ctx sdk.Context, action vm.Jsonable) error {
func (k Keeper) PushHighPriorityAction(ctx sdk.Context, action vm.Action) error {
return k.pushAction(ctx, StoragePathHighPriorityQueue, action)
}

Expand Down Expand Up @@ -234,7 +248,11 @@ func (k Keeper) UpdateQueueAllowed(ctx sdk.Context) error {
// until the response. It is orthogonal to PushAction, and should only be used
// by SwingSet to perform block lifecycle events (BEGIN_BLOCK, END_BLOCK,
// COMMIT_BLOCK).
func (k Keeper) BlockingSend(ctx sdk.Context, action vm.Jsonable) (string, error) {
func (k Keeper) BlockingSend(ctx sdk.Context, action vm.Action) (string, error) {
action, err := populateAction(ctx, action)
if err != nil {
return "", err
}
bz, err := json.Marshal(action)
if err != nil {
return "", err
Expand Down

0 comments on commit a710d68

Please sign in to comment.