Skip to content

Commit

Permalink
[chain] Introduce Multi-Actions and Outputs in Transactions (#858)
Browse files Browse the repository at this point in the history
* add generic

* emap passes

* eheap passes

* fix tokenvm orderbook

* intro ActionID

* intro max actions

* enforce max actions + loop thru statekeys

* fix some tx errors

* fix builder errors

* fix processor errors

* fix more tx errors

* fix client rpc error

* fix morpheus transfer action

* fix more morpheus

* morpheusVM compiles

* fix mock gen

* fix morpheus static lint

* fix lint and function signatures

* fix rpc GenerateTx

* import chain into morpheus actions

* more lints

* marhsal unmarshal Actions

* totalUnits fees.Add

* fix invalid signature

* fix some tx fees and packing bytes

* check maxActions earlier

* morpheus integration passes

* make action.Execute use actionID

* intro codec.ActionID

* fix tokenvm

* tokevnm integration errors

* fix tokenvm integration error

* tokenvm integration passes

* create long ID

* fix token-cli lint

* fix token fauct and feed lint

* introduce multiple result.Outputs and morpheus integration passes

* tokenvm integration passes using multiple result.Outputs

* increase maxActions for tokenvm to 2

* add multiple transfers in 1 tx

* add create and mint multiple assets

* add multiple trades

* add failed fill order

* address lints

* add hrp param to LIDFromString

* fix x/programs lint

* getmaxactionspertx should be uint8

* update readme

* self review

* fix rust ci

* make get action id a helper

* move GetMaxActionsPerTx

* add MaxOutputsPerAction

* actions instead of action

* do sponsorkeys after action iteration

* add action idx and type to ErrActionNotActivated

* dont override prev outputs in handleRevert

* skip all if one action is failed, do fee comp at the end, refund once

* introduce LIDLen

* fix type alias

* use PackLID

* use codec.LIDLen

* EstimateMaxUnits over all actions

* revert is populated in last output of last action

* fix action Size

* fix LID to and from String

* make EmptyAddress const more generic

* if not successful error is last output in last action

* add comments in transaction

* fix mock gen

* fix rust ci

* [chain] fix result size (#889)

* include r.Outputs size in calculation above

* fix packing of Outputs for each Action

* [chain] Remove execute success (#894)

* remove err from Action

* mock gen

* add more TODOs

* add context on why revert is better

* add back error

* cleanup interface

* cleanup tx loop

* update error marshaling

* update transfer op

* cleaning up access

* finish tokenvm

* update mocks

* fix programs

* update create order

* cleanup cmd program

* backend.go compiles

* more cleanup

* morpheusvm integration passing

* fix tokenvm integration

* add nolint

* require lint

---------

Co-authored-by: Patrick O'Grady <prohb125@gmail.com>

* remove trailing enter

* revert `codec.LID` (#920)

* remove LID definition

* remove optional packer

* update cli

* update heap

* eheap + emap

* making progress on morpheusvm

* cleanup load test error

* tokenvm actions

* update token-cli

* update token-feed + token-faucet

* update token-wallet

* more progress in tokenvm

* update token storage

* use id const

* update programs

* unit tests passing

* remove consts form simulator

* fix lint

* pass integration tests

* integration test passing

* reduce size of digest

* fix load test

* e2e lint

* reuse nodes

* reuse nodes

* fix lint

* run gci

* multi-action nits (#922)

* add key validity check to Keys set

* cleanup state key validity checking

* reviewed chain/transaction

* cli changes

* add multi-send example to morpheus

* cleanup tokenvm

* remove heap casting

* fix program execute

* fix return

* cleanup var names

* unify error

* update fetcher tests

* remove unnecessary conversion

* update programs code

* update action batches section

---------

Co-authored-by: Patrick O'Grady <prohb125@gmail.com>
  • Loading branch information
wlawt and patrick-ogrady authored May 22, 2024
1 parent b139da2 commit af85e97
Show file tree
Hide file tree
Showing 96 changed files with 1,755 additions and 1,697 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/hypersdk-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ jobs:
MODE: 'test'

tokenvm-load-tests:
needs: [tokenvm-unit-tests]
needs: [tokenvm-lint, tokenvm-unit-tests]
strategy:
matrix:
level: [v1, v2, v3] # v4 is not supported
Expand All @@ -161,7 +161,7 @@ jobs:
run: GOAMD64=${{ matrix.level }} scripts/tests.load.sh

tokenvm-sync-tests:
needs: [tokenvm-unit-tests]
needs: [tokenvm-lint, tokenvm-unit-tests]
runs-on: ubuntu-20.04-32
timeout-minutes: 25
steps:
Expand Down Expand Up @@ -262,7 +262,7 @@ jobs:
MODE: 'test'

morpheusvm-load-tests:
needs: [morpheusvm-unit-tests]
needs: [morpheusvm-lint, morpheusvm-unit-tests]
strategy:
matrix:
level: [v1, v2, v3] # v4 is not supported
Expand All @@ -283,7 +283,7 @@ jobs:
run: GOAMD64=${{ matrix.level }} scripts/tests.load.sh

morpheusvm-sync-tests:
needs: [morpheusvm-unit-tests]
needs: [morpheusvm-lint, morpheusvm-unit-tests]
runs-on: ubuntu-20.04-32
timeout-minutes: 25
steps:
Expand Down
51 changes: 28 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,26 @@ for a single account and ensure they are ordered) and makes the network layer
more efficient (we can gossip any valid transaction to any node instead of just
the transactions for each account that can be executed at the moment).

### Action Batches and Arbitrary Outputs
Each `hypersdk` transaction specifies an array of `Actions` that
must all execute successfully for any state changes to be committed.
Additionally, each `Action` is permitted to return an array of outputs (each
output is arbitrary bytes defined by the `hypervm`) upon successful execution.

The `tokenvm` uses `Action` batches to offer complex, atomic interactions over simple
primitives (i.e. create order, fill order, and cancel order). For example, a user
can create a transaction that fills 8 orders. If any of the fills fail, all pending
state changes in the transaction are rolled back. The `tokenvm` uses `Action` outputs to
return the remaining units on any partially filled order to power an in-memory orderbook.

The outcome of execution is not stored/indexed by the `hypersdk`. Unlike most other
blockchains/blockchain frameworks, which provide an optional "archival mode" for historical access,
the `hypersdk` only stores what is necessary to validate the next valid block and to help new nodes
sync to the current state. Rather, the `hypersdk` invokes the `hypervm` with all execution
results whenever a block is accepted for it to perform arbitrary operations (as
required by a developer's use case). In this callback, a `hypervm` could store
results in a SQL database or write to a Kafka stream.

### Easy Functionality Upgrades
Every object that can appear on-chain (i.e. `Actions` and/or `Auth`) and every chain
parameter (i.e. `Unit Price`) is scoped by block timestamp. This makes it
Expand All @@ -417,23 +437,6 @@ override the default gossip technique with your own. For example, you may wish
to not have any node-to-node gossip and just require validators to propose
blocks only with the transactions they've received over RPC.

### Transaction Results and Execution Rollback
The `hypersdk` allows for any `Action` to return a result from execution
(which can be any arbitrary bytes), the amount of fee units it consumed, and
whether or not it was successful (if unsuccessful, all state changes are rolled
back). This support is typically required by anyone using the `hypersdk` to
implement a smart contract-based runtime that allows for cost-effective
conditional execution (exiting early if a condition does not hold can be much
cheaper than the full execution of the transaction).

The outcome of execution is not stored/indexed by the `hypersdk`. Unlike most other
blockchains/blockchain frameworks, which provide an optional "archival mode" for historical access,
the `hypersdk` only stores what is necessary to validate the next valid block and to help new nodes
sync to the current state. Rather, the `hypersdk` invokes the `hypervm` with all execution
results whenever a block is accepted for it to perform arbitrary operations (as
required by a developer's use case). In this callback, a `hypervm` could store
results in a SQL database or write to a Kafka stream.

### Support for Generic Storage Backends
When initializing a `hypervm`, the developer explicitly specifies which storage backends
to use for each object type (state vs blocks vs metadata). As noted above, this
Expand Down Expand Up @@ -648,24 +651,23 @@ type Action interface {
// key (formatted as a big-endian uint16). This is used to automatically calculate storage usage.
//
// If any key is removed and then re-created, this will count as a creation instead of a modification.
StateKeys(actor codec.Address, txID ids.ID) state.Keys
StateKeys(actor codec.Address, actionID ids.ID) state.Keys

// Execute actually runs the [Action]. Any state changes that the [Action] performs should
// be done here.
//
// If any keys are touched during [Execute] that are not specified in [StateKeys], the transaction
// will revert and the max fee will be charged.
//
// An error should only be returned if a fatal error was encountered, otherwise [success] should
// be marked as false and fees will still be charged.
//
// If [Execute] returns an error, execution will halt and any state changes will revert.
Execute(
ctx context.Context,
r Rules,
mu state.Mutable,
timestamp int64,
actor codec.Address,
txID ids.ID,
) (success bool, computeUnits uint64, output []byte, err error)
actionID ids.ID,
) (computeUnits uint64, outputs [][]byte, err error)
}
```

Expand Down Expand Up @@ -743,6 +745,9 @@ type Rules interface {
GetMinBlockGap() int64 // in milliseconds
GetMinEmptyBlockGap() int64 // in milliseconds
GetValidityWindow() int64 // in milliseconds

GetMaxActionsPerTx() uint8
GetMaxOutputsPerAction() uint8

GetMinUnitPrice() Dimensions
GetUnitPriceChangeDenominator() Dimensions
Expand Down
2 changes: 1 addition & 1 deletion chain/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
"github.com/ava-labs/hypersdk/consts"
)

const BaseSize = consts.Uint64Len*2 + consts.IDLen
const BaseSize = consts.Uint64Len*2 + ids.IDLen

type Base struct {
// Timestamp is the expiry of the transaction (inclusive). Once this time passes and the
Expand Down
4 changes: 2 additions & 2 deletions chain/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -799,10 +799,10 @@ func (b *StatelessBlock) FeeManager() *fees.Manager {
}

func (b *StatefulBlock) Marshal() ([]byte, error) {
size := consts.IDLen + consts.Uint64Len + consts.Uint64Len +
size := ids.IDLen + consts.Uint64Len + consts.Uint64Len +
consts.Uint64Len + window.WindowSliceSize +
consts.IntLen + codec.CummSize(b.Txs) +
consts.IDLen + consts.Uint64Len + consts.Uint64Len
ids.IDLen + consts.Uint64Len + consts.Uint64Len

p := codec.NewWriter(size, consts.NetworkSizeLimit)

Expand Down
12 changes: 7 additions & 5 deletions chain/dependencies.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@ type Rules interface {
GetMinEmptyBlockGap() int64 // in milliseconds
GetValidityWindow() int64 // in milliseconds

GetMaxActionsPerTx() uint8
GetMaxOutputsPerAction() uint8

GetMinUnitPrice() fees.Dimensions
GetUnitPriceChangeDenominator() fees.Dimensions
GetWindowTargetUnits() fees.Dimensions
Expand Down Expand Up @@ -238,24 +241,23 @@ type Action interface {
// key (formatted as a big-endian uint16). This is used to automatically calculate storage usage.
//
// If any key is removed and then re-created, this will count as a creation instead of a modification.
StateKeys(actor codec.Address, txID ids.ID) state.Keys
StateKeys(actor codec.Address, actionID ids.ID) state.Keys

// Execute actually runs the [Action]. Any state changes that the [Action] performs should
// be done here.
//
// If any keys are touched during [Execute] that are not specified in [StateKeys], the transaction
// will revert and the max fee will be charged.
//
// An error should only be returned if a fatal error was encountered, otherwise [success] should
// be marked as false and fees will still be charged.
// If [Execute] returns an error, execution will halt and any state changes will revert.
Execute(
ctx context.Context,
r Rules,
mu state.Mutable,
timestamp int64,
actor codec.Address,
txID ids.ID,
) (success bool, computeUnits uint64, output []byte, err error)
actionID ids.ID,
) (computeUnits uint64, outputs [][]byte, err error)
}

type Auth interface {
Expand Down
2 changes: 2 additions & 0 deletions chain/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ var (
ErrMisalignedTime = errors.New("misaligned time")
ErrInvalidActor = errors.New("invalid actor")
ErrInvalidSponsor = errors.New("invalid sponsor")
ErrTooManyActions = errors.New("too many actions")
ErrTooManyOutputs = errors.New("too many outputs")

// Execution Correctness
ErrInvalidBalance = errors.New("invalid balance")
Expand Down
11 changes: 5 additions & 6 deletions chain/mock_action.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 28 additions & 0 deletions chain/mock_rules.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

43 changes: 33 additions & 10 deletions chain/result.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,35 @@ import (

type Result struct {
Success bool
Output []byte
Error []byte

Outputs [][][]byte

Consumed fees.Dimensions
Fee uint64
}

func (r *Result) Size() int {
return consts.BoolLen + codec.BytesLen(r.Output) + fees.DimensionsLen + consts.Uint64Len
outputSize := consts.Uint8Len // actions
for _, action := range r.Outputs {
outputSize += consts.Uint8Len
for _, output := range action {
outputSize += codec.BytesLen(output)
}
}
return consts.BoolLen + codec.BytesLen(r.Error) + outputSize + fees.DimensionsLen + consts.Uint64Len
}

func (r *Result) Marshal(p *codec.Packer) error {
p.PackBool(r.Success)
p.PackBytes(r.Output)
p.PackBytes(r.Error)
p.PackByte(uint8(len(r.Outputs)))
for _, outputs := range r.Outputs {
p.PackByte(uint8(len(outputs)))
for _, output := range outputs {
p.PackBytes(output)
}
}
p.PackFixedBytes(r.Consumed.Bytes())
p.PackUint64(r.Fee)
return nil
Expand All @@ -45,11 +61,20 @@ func UnmarshalResult(p *codec.Packer) (*Result, error) {
result := &Result{
Success: p.UnpackBool(),
}
p.UnpackBytes(consts.MaxInt, false, &result.Output)
if len(result.Output) == 0 {
// Enforce object standardization
result.Output = nil
p.UnpackBytes(consts.MaxInt, false, &result.Error)
outputs := [][][]byte{}
numActions := p.UnpackByte()
for i := uint8(0); i < numActions; i++ {
numOutputs := p.UnpackByte()
actionOutputs := [][]byte{}
for j := uint8(0); j < numOutputs; j++ {
var output []byte
p.UnpackBytes(consts.MaxInt, false, &output)
actionOutputs = append(actionOutputs, output)
}
outputs = append(outputs, actionOutputs)
}
result.Outputs = outputs
consumedRaw := make([]byte, fees.DimensionsLen)
p.UnpackFixedBytes(fees.DimensionsLen, &consumedRaw)
consumed, err := fees.UnpackDimensions(consumedRaw)
Expand All @@ -58,9 +83,7 @@ func UnmarshalResult(p *codec.Packer) (*Result, error) {
}
result.Consumed = consumed
result.Fee = p.UnpackUint64(false)
if !p.Empty() {
return nil, p.Err()
}
// Wait to check if empty until after all results are unpacked.
return result, p.Err()
}

Expand Down
Loading

0 comments on commit af85e97

Please sign in to comment.