Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[chain/transaction] Introduce Multi-Action Support #698

Closed
wants to merge 16 commits into from
6 changes: 6 additions & 0 deletions chain/dependencies.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@ type Rules interface {
GetWarpConfig(sourceChainID ids.ID) (bool, uint64, uint64)

FetchCustom(string) (any, bool)

GetMaxActionsPerTx() uint8
}

type MetadataManager interface {
Expand Down Expand Up @@ -229,6 +231,10 @@ type Object interface {
type Action interface {
Object

// GetActionID returns the ActionID for an [Action] in a [Transaction]. There may be
// multiple [Action]s, so we pass its index in the [Action] array along with the txID.
GetActionID(idx uint8, txID ids.ID) codec.Address

// MaxComputeUnits is the maximum amount of compute a given [Action] could use. This is
// used to determine whether the [Action] can be included in a given block and to compute
// the required fee to execute.
Expand Down
200 changes: 124 additions & 76 deletions chain/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
"github.com/ava-labs/hypersdk/utils"
)

const defaultStateKeySize = 4

var (
_ emap.Item = (*Transaction)(nil)
_ mempool.Item = (*Transaction)(nil)
Expand All @@ -33,9 +35,8 @@
Base *Base `json:"base"`
WarpMessage *warp.Message `json:"warpMessage"`

// TODO: turn [Action] into an array (#335)
Action Action `json:"action"`
Auth Auth `json:"auth"`
Actions []Action `json:"actions"`
Auth Auth `json:"auth"`

digest []byte
bytes []byte
Expand All @@ -55,31 +56,37 @@
VerifyErr error
}

func NewTx(base *Base, wm *warp.Message, act Action) *Transaction {
func NewTx(base *Base, wm *warp.Message, actions []Action) *Transaction {
return &Transaction{
Base: base,
WarpMessage: wm,
Action: act,
Actions: actions,
}
}

func (t *Transaction) Digest() ([]byte, error) {
if len(t.digest) > 0 {
return t.digest, nil
}
actionID := t.Action.GetTypeID()
var warpBytes []byte
if t.WarpMessage != nil {
warpBytes = t.WarpMessage.Bytes()
}
size := t.Base.Size() +
codec.BytesLen(warpBytes) +
consts.ByteLen + t.Action.Size()
consts.IntLen
for _, action := range t.Actions {
size += consts.ByteLen + action.Size()
}
p := codec.NewWriter(size, consts.NetworkSizeLimit)
t.Base.Marshal(p)
p.PackBytes(warpBytes)
p.PackByte(actionID)
t.Action.Marshal(p)
p.PackInt(len(t.Actions))
for idx, action := range t.Actions {
actionID := action.GetActionID(idx, t.id)

Check failure on line 86 in chain/transaction.go

View workflow job for this annotation

GitHub Actions / hypersdk-lint

cannot use idx (variable of type int) as uint8 value in argument to action.GetActionID

Check failure on line 86 in chain/transaction.go

View workflow job for this annotation

GitHub Actions / hypersdk-unit-tests

cannot use idx (variable of type int) as uint8 value in argument to action.GetActionID
p.PackByte(actionID)

Check failure on line 87 in chain/transaction.go

View workflow job for this annotation

GitHub Actions / hypersdk-lint

cannot use actionID (variable of type "github.com/ava-labs/hypersdk/codec".Address) as byte value in argument to p.PackByte

Check failure on line 87 in chain/transaction.go

View workflow job for this annotation

GitHub Actions / hypersdk-unit-tests

cannot use actionID (variable of type "github.com/ava-labs/hypersdk/codec".Address) as byte value in argument to p.PackByte
action.Marshal(p)
}
return p.Bytes(), p.Err()
}

Expand Down Expand Up @@ -126,13 +133,38 @@
if t.stateKeys != nil {
return t.stateKeys, nil
}
stateKeys := set.NewSet[string](defaultStateKeySize)

Check failure on line 136 in chain/transaction.go

View workflow job for this annotation

GitHub Actions / hypersdk-lint

undefined: set

Check failure on line 136 in chain/transaction.go

View workflow job for this annotation

GitHub Actions / hypersdk-unit-tests

undefined: set

// Verify the formatting of state keys passed by the controller
actionKeys := t.Action.StateKeys(t.Auth.Actor(), t.ID())

Check failure on line 139 in chain/transaction.go

View workflow job for this annotation

GitHub Actions / hypersdk-lint

t.Action undefined (type *Transaction has no field or method Action)

Check failure on line 139 in chain/transaction.go

View workflow job for this annotation

GitHub Actions / hypersdk-unit-tests

t.Action undefined (type *Transaction has no field or method Action)
sponsorKeys := sm.SponsorStateKeys(t.Auth.Sponsor())
stateKeys := make(state.Keys)

Check failure on line 141 in chain/transaction.go

View workflow job for this annotation

GitHub Actions / hypersdk-lint

no new variables on left side of :=

Check failure on line 141 in chain/transaction.go

View workflow job for this annotation

GitHub Actions / hypersdk-unit-tests

no new variables on left side of :=
for _, m := range []state.Keys{actionKeys, sponsorKeys} {
for k, v := range m {
// Handle incoming warp message keys
if t.WarpMessage != nil {
p := sm.IncomingWarpKeyPrefix(t.WarpMessage.SourceChainID, t.warpID)
k := keys.EncodeChunks(p, MaxIncomingWarpChunks)
stateKeys.Add(string(k))
}
}
}

// Handle action/auth keys
var outputsWarp bool
for _, action := range t.Actions {
if action.OutputsWarpMessage() {
if outputsWarp {
// TODO: handle multiple outgoing warp messages (use actionID instead of txID)
return nil, errors.New("can't ouput multiple messaages")
}
p := sm.OutgoingWarpKeyPrefix(t.id)
k := keys.EncodeChunks(p, MaxOutgoingWarpChunks)
stateKeys.Add(string(k))
outputsWarp = true
}
// TODO: may need to use an ActionID instead of a TxID (if creating 2 assets in same tx, could lead to conflicts)
for _, k := range action.StateKeys(t.Auth.Actor(), t.ID()) {
if !keys.Valid(k) {
return nil, ErrInvalidKeyValue
}
Expand All @@ -147,10 +179,17 @@
stateKeys.Add(string(k), state.All)
}
if t.Action.OutputsWarpMessage() {
// TODO: handle multiple outgoing warp messages
p := sm.OutgoingWarpKeyPrefix(t.id)
k := keys.EncodeChunks(p, MaxOutgoingWarpChunks)
stateKeys.Add(string(k), state.Allocate|state.Write)
}
for _, k := range sm.SponsorStateKeys(t.Auth.Sponsor()) {
if !keys.Valid(k) {
return nil, ErrInvalidKeyValue
}
stateKeys.Add(k)
}

// Cache keys if called again
t.stateKeys = stateKeys
Expand All @@ -165,16 +204,18 @@
func (t *Transaction) MaxUnits(sm StateManager, r Rules) (fees.Dimensions, error) {
// Cacluate max compute costs
maxComputeUnitsOp := math.NewUint64Operator(r.GetBaseComputeUnits())
maxComputeUnitsOp.Add(t.Action.MaxComputeUnits(r))
maxComputeUnitsOp.Add(t.Auth.ComputeUnits(r))
if t.WarpMessage != nil {
maxComputeUnitsOp.Add(r.GetBaseWarpComputeUnits())
maxComputeUnitsOp.MulAdd(uint64(t.numWarpSigners), r.GetWarpComputeUnitsPerSigner())
}
if t.Action.OutputsWarpMessage() {
// Chunks later accounted for by call to [StateKeys]
maxComputeUnitsOp.Add(r.GetOutgoingWarpComputeUnits())
for _, action := range t.Actions {
maxComputeUnitsOp.Add(action.MaxComputeUnits(r))
if action.OutputsWarpMessage() {
// Chunks later accounted for by call to [StateKeys]
maxComputeUnitsOp.Add(r.GetOutgoingWarpComputeUnits())
}
}
maxComputeUnitsOp.Add(t.Auth.ComputeUnits(r))
maxComputeUnits, err := maxComputeUnitsOp.Value()
if err != nil {
return fees.Dimensions{}, err
Expand Down Expand Up @@ -298,14 +339,16 @@
if err := t.Base.Execute(r.ChainID(), r, timestamp); err != nil {
return err
}
start, end := t.Action.ValidRange(r)
if start >= 0 && timestamp < start {
return ErrActionNotActivated
}
if end >= 0 && timestamp > end {
return ErrActionNotActivated
for _, action := range t.Actions {
start, end := action.ValidRange(r)
if start >= 0 && timestamp < start {
return ErrActionNotActivated
}
if end >= 0 && timestamp > end {
return ErrActionNotActivated
}
}
start, end = t.Auth.ValidRange(r)
start, end := t.Auth.ValidRange(r)
if start >= 0 && timestamp < start {
return ErrAuthNotActivated
}
Expand Down Expand Up @@ -339,7 +382,7 @@
ts *tstate.TStateView,
timestamp int64,
warpVerified bool,
) (*Result, error) {
) ([]*Result, error) {
// Always charge fee first (in case [Action] moves funds)
maxUnits, err := t.MaxUnits(s, r)
if err != nil {
Expand Down Expand Up @@ -371,63 +414,65 @@
case err != nil:
// An error here can indicate there is an issue with the database or that
// the key was not properly specified.
return &Result{false, utils.ErrBytes(err), maxUnits, maxFee, nil}, nil
return []*Result{{false, utils.ErrBytes(err), maxUnits, maxFee, nil}}, nil
}
}

// We create a temp state checkpoint to ensure we don't commit failed actions to state.
actionStart := ts.OpIndex()
handleRevert := func(rerr error) (*Result, error) {
handleRevert := func(rerr error) ([]*Result, error) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we revert, return all results up to the failure (with success true)

// Be warned that the variables captured in this function
// are set when this function is defined. If any of them are
// modified later, they will not be used here.
ts.Rollback(ctx, actionStart)
return &Result{false, utils.ErrBytes(rerr), maxUnits, maxFee, nil}, nil
}
success, actionCUs, output, warpMessage, err := t.Action.Execute(ctx, r, ts, timestamp, t.Auth.Actor(), t.id, warpVerified)
if err != nil {
return handleRevert(err)
}
if len(output) == 0 && output != nil {
// Enforce object standardization (this is a VM bug and we should fail
// fast)
return handleRevert(ErrInvalidObject)
}
outputsWarp := t.Action.OutputsWarpMessage()
if !success {
ts.Rollback(ctx, actionStart)
warpMessage = nil // warp messages can only be emitted on success
} else {
// Ensure constraints hold if successful
if (warpMessage == nil && outputsWarp) || (warpMessage != nil && !outputsWarp) {
for _, action := range t.Actions {

Check warning

Code scanning / CodeQL

Useless assignment to local variable Warning

This definition of action is never used.
success, actionCUs, output, warpMessage, err := action.Execute(ctx, r, ts, timestamp, t.Auth.Actor(), t.id, warpVerified)
if err != nil {
return handleRevert(err)
}
if len(output) == 0 && output != nil {
// Enforce object standardization (this is a VM bug and we should fail
// fast)
return handleRevert(ErrInvalidObject)
}

// Store incoming warp messages in state by their ID to prevent replays
if t.WarpMessage != nil {
p := s.IncomingWarpKeyPrefix(t.WarpMessage.SourceChainID, t.warpID)
k := keys.EncodeChunks(p, MaxIncomingWarpChunks)
if err := ts.Insert(ctx, k, nil); err != nil {
return handleRevert(err)
outputsWarp := action.OutputsWarpMessage()
if !success {
ts.Rollback(ctx, actionStart)
warpMessage = nil // warp messages can only be emitted on success
} else {
// Ensure constraints hold if successful
if (warpMessage == nil && outputsWarp) || (warpMessage != nil && !outputsWarp) {
return handleRevert(ErrInvalidObject)
}
}

// Store newly created warp messages in state by their txID to ensure we can
// always sign for a message
if warpMessage != nil {
// Enforce we are the source of our own messages
warpMessage.NetworkID = r.NetworkID()
warpMessage.SourceChainID = r.ChainID()
// Initialize message (compute bytes) now that everything is populated
if err := warpMessage.Initialize(); err != nil {
return handleRevert(err)
// Store incoming warp messages in state by their ID to prevent replays
if t.WarpMessage != nil {
p := s.IncomingWarpKeyPrefix(t.WarpMessage.SourceChainID, t.warpID)
k := keys.EncodeChunks(p, MaxIncomingWarpChunks)
if err := ts.Insert(ctx, k, nil); err != nil {
return handleRevert(err)
}
}
// We use txID here because did not know the warpID before execution (and
// we pre-reserve this key for the processor).
p := s.OutgoingWarpKeyPrefix(t.id)
k := keys.EncodeChunks(p, MaxOutgoingWarpChunks)
if err := ts.Insert(ctx, k, warpMessage.Bytes()); err != nil {
return handleRevert(err)

// Store newly created warp messages in state by their txID to ensure we can
// always sign for a message
if warpMessage != nil {
// Enforce we are the source of our own messages
warpMessage.NetworkID = r.NetworkID()
warpMessage.SourceChainID = r.ChainID()
// Initialize message (compute bytes) now that everything is populated
if err := warpMessage.Initialize(); err != nil {
return handleRevert(err)
}
// We use txID here because did not know the warpID before execution (and
// we pre-reserve this key for the processor).
p := s.OutgoingWarpKeyPrefix(t.id)
k := keys.EncodeChunks(p, MaxOutgoingWarpChunks)
if err := ts.Insert(ctx, k, warpMessage.Bytes()); err != nil {
return handleRevert(err)
}
}
}
}
Expand Down Expand Up @@ -535,21 +580,24 @@
return p.Err()
}

actionID := t.Action.GetTypeID()
authID := t.Auth.GetTypeID()
t.Base.Marshal(p)
var warpBytes []byte
if t.WarpMessage != nil {
warpBytes = t.WarpMessage.Bytes()
if len(warpBytes) == 0 {
return ErrWarpMessageNotInitialized
// TODO: do I need all this within the loop?
for idx, action := range t.Action {
Fixed Show fixed Hide fixed
actionID := t.Action.GetActionID(idx, t.id)
authID := t.Auth.GetTypeID()
t.Base.Marshal(p)
var warpBytes []byte
if t.WarpMessage != nil {
warpBytes = t.WarpMessage.Bytes()
if len(warpBytes) == 0 {
return ErrWarpMessageNotInitialized
}
}
p.PackBytes(warpBytes)
p.PackByte(actionID)
t.Action.Marshal(p)
p.PackByte(authID)
t.Auth.Marshal(p)
}
p.PackBytes(warpBytes)
p.PackByte(actionID)
t.Action.Marshal(p)
p.PackByte(authID)
t.Auth.Marshal(p)
return p.Err()
}

Expand Down
4 changes: 4 additions & 0 deletions examples/morpheusvm/actions/transfer.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ func (*Transfer) GetTypeID() uint8 {
return mconsts.TransferID
}

func (*BurnAsset) GetActionID(idx uint8, txID ids.ID) codec.Address {
return codec.CreateAddress(idx, txID)
}

func (t *Transfer) StateKeys(actor codec.Address, _ ids.ID) state.Keys {
return state.Keys{
string(storage.BalanceKey(actor)): state.Read | state.Write,
Expand Down
6 changes: 6 additions & 0 deletions examples/morpheusvm/genesis/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ type Genesis struct {
StorageKeyWriteUnits uint64 `json:"storageKeyWriteUnits"`
StorageValueWriteUnits uint64 `json:"storageValueWriteUnits"` // per chunk

// Action Per Tx
MaxActionsPerTx uint8 `json:"maxActionsPerTx"`

// Allocates
CustomAllocation []*CustomAllocation `json:"customAllocation"`
}
Expand Down Expand Up @@ -94,6 +97,9 @@ func Default() *Genesis {
StorageValueAllocateUnits: 5,
StorageKeyWriteUnits: 10,
StorageValueWriteUnits: 3,

// Action Per Tx
MaxActionsPerTx: 1,
}
}

Expand Down
4 changes: 4 additions & 0 deletions examples/morpheusvm/genesis/rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,7 @@ func (r *Rules) GetWindowTargetUnits() fees.Dimensions {
func (*Rules) FetchCustom(string) (any, bool) {
return nil, false
}

func (*Rules) GetMaxActionsPerTx() uint8 {
return r.g.MaxActionsPerTx
}
Loading
Loading