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

feat: [ADR-070] Unordered Transactions (2/2) #18739

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,81 @@ Note, always read the **SimApp** section for more information on application wir

## [Unreleased]

### Unordered Transactions

The Cosmos SDK now supports unordered transactions. This means that transactions
can be executed in any order and doesn't require the client to deal with or manage
nonces. This also means the order of execution is not guaranteed. To enable unordered
transactions in your application:

* Update the `App` constructor to create, load, and save the unordered transaction
manager.

```go
func NewApp(...) *App {
// ...

// create, start, and load the unordered tx manager
utxDataDir := filepath.Join(cast.ToString(appOpts.Get(flags.FlagHome)), "data")
app.UnorderedTxManager = unorderedtx.NewManager(utxDataDir)
app.UnorderedTxManager.Start()

if err := app.UnorderedTxManager.OnInit(); err != nil {
panic(fmt.Errorf("failed to initialize unordered tx manager: %w", err))
}
}
```

* Add the decorator to the existing AnteHandler chain, which should be as early
as possible.

```go
anteDecorators := []sdk.AnteDecorator{
ante.NewSetUpContextDecorator(),
// ...
ante.NewUnorderedTxDecorator(unorderedtx.DefaultMaxUnOrderedTTL, app.UnorderedTxManager),
// ...
}

return sdk.ChainAnteDecorators(anteDecorators...), nil
```

* If the App has a SnapshotManager defined, you must also register the extension
for the TxManager.

```go
if manager := app.SnapshotManager(); manager != nil {
err := manager.RegisterExtensions(unorderedtx.NewSnapshotter(app.UnorderedTxManager))
if err != nil {
panic(fmt.Errorf("failed to register snapshot extension: %s", err))
}
}
```

* Create or update the App's `Close()` method to close the unordered tx manager.
Note, this is critical as it ensures the manager's state is written to file
such that when the node restarts, it can recover the state to provide replay
protection.

```go
func (app *App) Close() error {
// ...

// close the unordered tx manager
if e := app.UnorderedTxManager.Close(); e != nil {
err = errors.Join(err, e)
}

return err
}
```

To submit an unordered transaction, the client must set the `unordered` flag to
`true` and ensure a reasonable `timeout_height` is set. The `timeout_height` is
used as a TTL for the transaction and is used to provide replay protection. See
[ADR-070](https://github.com/cosmos/cosmos-sdk/blob/main/docs/architecture/adr-070-unordered-account.md)
for more details.

### Params

* Params Migrations were removed. It is required to migrate to 0.50 prior to upgrading to .51.
Expand Down
20 changes: 14 additions & 6 deletions docs/architecture/adr-070-unordered-account.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,14 +278,22 @@ func (d *DedupTxDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool,

Wire the `OnNewBlock` method of `UnorderedTxManager` into the BaseApp's ABCI `Commit` event.

### Start Up
### State Management

On start up, the node needs to re-fill the tx hash dictionary of `UnorderedTxManager`
by scanning `MaxUnOrderedTTL` number of historical blocks for existing un-expired
un-ordered transactions.
On start up, the node needs to ensure the TxManager's state contains all un-expired
transactions that have been committed to the chain. This is critical since if the
state is not properly initialized, the node will not reject duplicate transactions
and thus will not provide replay protection, and will likely get an app hash mismatch error.

An alternative design is to store the tx hash dictionary in kv store, then no need
to warm up on start up.
We propose to write all un-expired unordered transactions from the TxManager's to
file on disk. On start up, the node will read this file and re-populate the TxManager's
map. The write to file will happen when the node gracefully shuts down on `Close()`.

Note, this is not a perfect solution, in the context of store v1. With store v2,
we can omit explicit file handling altogether and simply write the all the transactions
to non-consensus state, i.e State Storage (SS).

Alternatively, we can write all the transactions to consensus state.

## Consequences

Expand Down
3 changes: 3 additions & 0 deletions simapp/ante.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"errors"

"cosmossdk.io/x/auth/ante"
"cosmossdk.io/x/auth/ante/unorderedtx"
circuitante "cosmossdk.io/x/circuit/ante"

sdk "github.com/cosmos/cosmos-sdk/types"
Expand All @@ -13,6 +14,7 @@ import (
type HandlerOptions struct {
ante.HandlerOptions
CircuitKeeper circuitante.CircuitBreaker
TxManager *unorderedtx.Manager
}

// NewAnteHandler returns an AnteHandler that checks and increments sequence
Expand All @@ -37,6 +39,7 @@ func NewAnteHandler(options HandlerOptions) (sdk.AnteHandler, error) {
ante.NewExtensionOptionsDecorator(options.ExtensionOptionChecker),
ante.NewValidateBasicDecorator(),
ante.NewTxTimeoutHeightDecorator(),
ante.NewUnorderedTxDecorator(unorderedtx.DefaultMaxUnOrderedTTL, options.TxManager),
ante.NewValidateMemoDecorator(options.AccountKeeper),
ante.NewConsumeGasForTxSizeDecorator(options.AccountKeeper),
ante.NewDeductFeeDecorator(options.AccountKeeper, options.BankKeeper, options.FeegrantKeeper, options.TxFeeChecker),
Expand Down
29 changes: 29 additions & 0 deletions simapp/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"cosmossdk.io/x/accounts/testing/counter"
"cosmossdk.io/x/auth"
"cosmossdk.io/x/auth/ante"
"cosmossdk.io/x/auth/ante/unorderedtx"
authcodec "cosmossdk.io/x/auth/codec"
authkeeper "cosmossdk.io/x/auth/keeper"
"cosmossdk.io/x/auth/posthandler"
Expand Down Expand Up @@ -169,6 +170,8 @@ type SimApp struct {
ModuleManager *module.Manager
BasicModuleManager module.BasicManager

UnorderedTxManager *unorderedtx.Manager

// simulation manager
sm *module.SimulationManager

Expand Down Expand Up @@ -519,6 +522,25 @@ func NewSimApp(
}
app.sm = module.NewSimulationManagerFromAppModules(app.ModuleManager.Modules, overrideModules)

// create, start, and load the unordered tx manager
utxDataDir := filepath.Join(cast.ToString(appOpts.Get(flags.FlagHome)), "data")
alexanderbez marked this conversation as resolved.
Show resolved Hide resolved
app.UnorderedTxManager = unorderedtx.NewManager(utxDataDir)
app.UnorderedTxManager.Start()

if err := app.UnorderedTxManager.OnInit(); err != nil {
panic(fmt.Errorf("failed to initialize unordered tx manager: %w", err))
}

// register custom snapshot extensions (if any)
if manager := app.SnapshotManager(); manager != nil {
err := manager.RegisterExtensions(
unorderedtx.NewSnapshotter(app.UnorderedTxManager),
)
if err != nil {
panic(fmt.Errorf("failed to register snapshot extension: %s", err))
}
}

app.sm.RegisterStoreDecoders()

// initialize stores
Expand Down Expand Up @@ -579,6 +601,7 @@ func (app *SimApp) setAnteHandler(txConfig client.TxConfig) {
SigGasConsumer: ante.DefaultSigVerificationGasConsumer,
},
&app.CircuitKeeper,
app.UnorderedTxManager,
},
)
if err != nil {
Expand All @@ -600,6 +623,12 @@ func (app *SimApp) setPostHandler() {
app.SetPostHandler(postHandler)
}

// Close implements the Application interface and closes all necessary application
// resources.
func (app *SimApp) Close() error {
return app.UnorderedTxManager.Close()
}

// Name returns the name of the App
func (app *SimApp) Name() string { return app.BaseApp.Name() }

Expand Down
31 changes: 31 additions & 0 deletions simapp/app_v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,19 @@
package simapp

import (
"fmt"
"io"
"os"
"path/filepath"

dbm "github.com/cosmos/cosmos-db"
"github.com/spf13/cast"

"cosmossdk.io/depinject"
"cosmossdk.io/log"
storetypes "cosmossdk.io/store/types"
"cosmossdk.io/x/auth"
"cosmossdk.io/x/auth/ante/unorderedtx"
authkeeper "cosmossdk.io/x/auth/keeper"
authsims "cosmossdk.io/x/auth/simulation"
authtypes "cosmossdk.io/x/auth/types"
Expand All @@ -34,6 +37,7 @@ import (

"github.com/cosmos/cosmos-sdk/baseapp"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/codec"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
"github.com/cosmos/cosmos-sdk/runtime"
Expand Down Expand Up @@ -64,6 +68,8 @@ type SimApp struct {
txConfig client.TxConfig
interfaceRegistry codectypes.InterfaceRegistry

UnorderedTxManager *unorderedtx.Manager

// keepers
AuthKeeper authkeeper.AccountKeeper
BankKeeper bankkeeper.Keeper
Expand Down Expand Up @@ -256,13 +262,38 @@ func NewSimApp(
// return app.App.InitChainer(ctx, req)
// })

// create, start, and load the unordered tx manager
utxDataDir := filepath.Join(cast.ToString(appOpts.Get(flags.FlagHome)), "data")
app.UnorderedTxManager = unorderedtx.NewManager(utxDataDir)
app.UnorderedTxManager.Start()

if err := app.UnorderedTxManager.OnInit(); err != nil {
panic(fmt.Errorf("failed to initialize unordered tx manager: %w", err))
}

// register custom snapshot extensions (if any)
if manager := app.SnapshotManager(); manager != nil {
err := manager.RegisterExtensions(
unorderedtx.NewSnapshotter(app.UnorderedTxManager),
)
if err != nil {
panic(fmt.Errorf("failed to register snapshot extension: %s", err))
}
}

if err := app.Load(loadLatest); err != nil {
panic(err)
}

return app
}

// Close implements the Application interface and closes all necessary application
// resources.
func (app *SimApp) Close() error {
return app.UnorderedTxManager.Close()
}

// LegacyAmino returns SimApp's amino codec.
//
// NOTE: This is solely to be used for testing purposes as it may be desirable
Expand Down
13 changes: 9 additions & 4 deletions x/auth/ante/unordered.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,16 @@ func (d *UnorderedTxDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate b
return next(ctx, tx, simulate)
}

if unorderedTx.GetTimeoutHeight() == 0 {
// TTL is defined as a specific block height at which this tx is no longer valid
ttl := unorderedTx.GetTimeoutHeight()

if ttl == 0 {
alexanderbez marked this conversation as resolved.
Show resolved Hide resolved
return ctx, errorsmod.Wrap(sdkerrors.ErrInvalidRequest, "unordered transaction must have timeout_height set")
}

if unorderedTx.GetTimeoutHeight() > uint64(ctx.BlockHeight())+d.maxUnOrderedTTL {
if ttl < uint64(ctx.BlockHeight()) {
return ctx, errorsmod.Wrap(sdkerrors.ErrInvalidRequest, "unordered transaction has a timeout_height that has already passed")
}
if ttl > uint64(ctx.BlockHeight())+d.maxUnOrderedTTL {
return ctx, errorsmod.Wrapf(sdkerrors.ErrInvalidRequest, "unordered tx ttl exceeds %d", d.maxUnOrderedTTL)
}

Expand All @@ -62,7 +67,7 @@ func (d *UnorderedTxDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate b

if ctx.ExecMode() == sdk.ExecModeFinalize {
// a new tx included in the block, add the hash to the unordered tx manager
d.txManager.Add(txHash, unorderedTx.GetTimeoutHeight())
d.txManager.Add(txHash, ttl)
}

return next(ctx, tx, simulate)
Expand Down
36 changes: 24 additions & 12 deletions x/auth/ante/unordered_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ import (
)

func TestUnorderedTxDecorator_OrderedTx(t *testing.T) {
txm := unorderedtx.NewManager()
defer txm.Close()
txm := unorderedtx.NewManager(t.TempDir())
defer func() {
require.NoError(t, txm.Close())
}()

txm.Start()

Expand All @@ -31,8 +33,10 @@ func TestUnorderedTxDecorator_OrderedTx(t *testing.T) {
}

func TestUnorderedTxDecorator_UnorderedTx_NoTTL(t *testing.T) {
txm := unorderedtx.NewManager()
defer txm.Close()
txm := unorderedtx.NewManager(t.TempDir())
defer func() {
require.NoError(t, txm.Close())
}()

txm.Start()

Expand All @@ -46,8 +50,10 @@ func TestUnorderedTxDecorator_UnorderedTx_NoTTL(t *testing.T) {
}

func TestUnorderedTxDecorator_UnorderedTx_InvalidTTL(t *testing.T) {
txm := unorderedtx.NewManager()
defer txm.Close()
txm := unorderedtx.NewManager(t.TempDir())
defer func() {
require.NoError(t, txm.Close())
}()

txm.Start()

Expand All @@ -61,8 +67,10 @@ func TestUnorderedTxDecorator_UnorderedTx_InvalidTTL(t *testing.T) {
}

func TestUnorderedTxDecorator_UnorderedTx_AlreadyExists(t *testing.T) {
txm := unorderedtx.NewManager()
defer txm.Close()
txm := unorderedtx.NewManager(t.TempDir())
defer func() {
require.NoError(t, txm.Close())
}()

txm.Start()

Expand All @@ -79,8 +87,10 @@ func TestUnorderedTxDecorator_UnorderedTx_AlreadyExists(t *testing.T) {
}

func TestUnorderedTxDecorator_UnorderedTx_ValidCheckTx(t *testing.T) {
txm := unorderedtx.NewManager()
defer txm.Close()
txm := unorderedtx.NewManager(t.TempDir())
defer func() {
require.NoError(t, txm.Close())
}()

txm.Start()

Expand All @@ -94,8 +104,10 @@ func TestUnorderedTxDecorator_UnorderedTx_ValidCheckTx(t *testing.T) {
}

func TestUnorderedTxDecorator_UnorderedTx_ValidDeliverTx(t *testing.T) {
txm := unorderedtx.NewManager()
defer txm.Close()
txm := unorderedtx.NewManager(t.TempDir())
defer func() {
require.NoError(t, txm.Close())
}()

txm.Start()

Expand Down
Loading
Loading