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

Co-authored-by: yihuang <huang@crypto.com>
Co-authored-by: Facundo <facundomedica@gmail.com>
This commit is contained in:
Aleksandr Bezobchuk 2024-01-04 11:58:52 -05:00 committed by GitHub
parent d520fcf7cc
commit 6ec53aaf54
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 1518 additions and 311 deletions

View File

@ -40,6 +40,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
### Features
* (x/auth) Support the ability to broadcast unordered transactions per ADR-070. See UPGRADING.md for more details on integration.
* (client) [#18557](https://github.com/cosmos/cosmos-sdk/pull/18557) Add `--qrcode` flag to `keys show` command to support displaying keys address QR code.
* (x/staking) [#18142](https://github.com/cosmos/cosmos-sdk/pull/18142) Introduce `key_rotation_fee` param to calculate fees while rotating the keys
* (client) [#18101](https://github.com/cosmos/cosmos-sdk/pull/18101) Add a `keyring-default-keyname` in `client.toml` for specifying a default key name, and skip the need to use the `--from` flag when signing transactions.
@ -189,7 +190,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
* (x/auth) [#18351](https://github.com/cosmos/cosmos-sdk/pull/18351) Auth module was moved to its own go.mod `cosmossdk.io/x/auth`
* (types) [#18372](https://github.com/cosmos/cosmos-sdk/pull/18372) Removed global configuration for coin type and purpose. Setters and getters should be removed and access directly to defined types.
* (types) [#18695](https://github.com/cosmos/cosmos-sdk/pull/18695) Removed global configuration for txEncoder.
* (server) [#18909](https://github.com/cosmos/cosmos-sdk/pull/18909) Remove configuration endpoint on grpc reflection endpoint in favour of auth module bech32prefix endpoint already exposed.
* (server) [#18909](https://github.com/cosmos/cosmos-sdk/pull/18909) Remove configuration endpoint on grpc reflection endpoint in favour of auth module bech32prefix endpoint already exposed.
### CLI Breaking Changes

View File

@ -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.

View File

@ -2767,6 +2767,7 @@ var (
fd_TxBody_messages protoreflect.FieldDescriptor
fd_TxBody_memo protoreflect.FieldDescriptor
fd_TxBody_timeout_height protoreflect.FieldDescriptor
fd_TxBody_unordered protoreflect.FieldDescriptor
fd_TxBody_extension_options protoreflect.FieldDescriptor
fd_TxBody_non_critical_extension_options protoreflect.FieldDescriptor
)
@ -2777,6 +2778,7 @@ func init() {
fd_TxBody_messages = md_TxBody.Fields().ByName("messages")
fd_TxBody_memo = md_TxBody.Fields().ByName("memo")
fd_TxBody_timeout_height = md_TxBody.Fields().ByName("timeout_height")
fd_TxBody_unordered = md_TxBody.Fields().ByName("unordered")
fd_TxBody_extension_options = md_TxBody.Fields().ByName("extension_options")
fd_TxBody_non_critical_extension_options = md_TxBody.Fields().ByName("non_critical_extension_options")
}
@ -2864,6 +2866,12 @@ func (x *fastReflection_TxBody) Range(f func(protoreflect.FieldDescriptor, proto
return
}
}
if x.Unordered != false {
value := protoreflect.ValueOfBool(x.Unordered)
if !f(fd_TxBody_unordered, value) {
return
}
}
if len(x.ExtensionOptions) != 0 {
value := protoreflect.ValueOfList(&_TxBody_1023_list{list: &x.ExtensionOptions})
if !f(fd_TxBody_extension_options, value) {
@ -2897,6 +2905,8 @@ func (x *fastReflection_TxBody) Has(fd protoreflect.FieldDescriptor) bool {
return x.Memo != ""
case "cosmos.tx.v1beta1.TxBody.timeout_height":
return x.TimeoutHeight != uint64(0)
case "cosmos.tx.v1beta1.TxBody.unordered":
return x.Unordered != false
case "cosmos.tx.v1beta1.TxBody.extension_options":
return len(x.ExtensionOptions) != 0
case "cosmos.tx.v1beta1.TxBody.non_critical_extension_options":
@ -2923,6 +2933,8 @@ func (x *fastReflection_TxBody) Clear(fd protoreflect.FieldDescriptor) {
x.Memo = ""
case "cosmos.tx.v1beta1.TxBody.timeout_height":
x.TimeoutHeight = uint64(0)
case "cosmos.tx.v1beta1.TxBody.unordered":
x.Unordered = false
case "cosmos.tx.v1beta1.TxBody.extension_options":
x.ExtensionOptions = nil
case "cosmos.tx.v1beta1.TxBody.non_critical_extension_options":
@ -2955,6 +2967,9 @@ func (x *fastReflection_TxBody) Get(descriptor protoreflect.FieldDescriptor) pro
case "cosmos.tx.v1beta1.TxBody.timeout_height":
value := x.TimeoutHeight
return protoreflect.ValueOfUint64(value)
case "cosmos.tx.v1beta1.TxBody.unordered":
value := x.Unordered
return protoreflect.ValueOfBool(value)
case "cosmos.tx.v1beta1.TxBody.extension_options":
if len(x.ExtensionOptions) == 0 {
return protoreflect.ValueOfList(&_TxBody_1023_list{})
@ -2995,6 +3010,8 @@ func (x *fastReflection_TxBody) Set(fd protoreflect.FieldDescriptor, value proto
x.Memo = value.Interface().(string)
case "cosmos.tx.v1beta1.TxBody.timeout_height":
x.TimeoutHeight = value.Uint()
case "cosmos.tx.v1beta1.TxBody.unordered":
x.Unordered = value.Bool()
case "cosmos.tx.v1beta1.TxBody.extension_options":
lv := value.List()
clv := lv.(*_TxBody_1023_list)
@ -3045,6 +3062,8 @@ func (x *fastReflection_TxBody) Mutable(fd protoreflect.FieldDescriptor) protore
panic(fmt.Errorf("field memo of message cosmos.tx.v1beta1.TxBody is not mutable"))
case "cosmos.tx.v1beta1.TxBody.timeout_height":
panic(fmt.Errorf("field timeout_height of message cosmos.tx.v1beta1.TxBody is not mutable"))
case "cosmos.tx.v1beta1.TxBody.unordered":
panic(fmt.Errorf("field unordered of message cosmos.tx.v1beta1.TxBody is not mutable"))
default:
if fd.IsExtension() {
panic(fmt.Errorf("proto3 declared messages do not support extensions: cosmos.tx.v1beta1.TxBody"))
@ -3065,6 +3084,8 @@ func (x *fastReflection_TxBody) NewField(fd protoreflect.FieldDescriptor) protor
return protoreflect.ValueOfString("")
case "cosmos.tx.v1beta1.TxBody.timeout_height":
return protoreflect.ValueOfUint64(uint64(0))
case "cosmos.tx.v1beta1.TxBody.unordered":
return protoreflect.ValueOfBool(false)
case "cosmos.tx.v1beta1.TxBody.extension_options":
list := []*anypb.Any{}
return protoreflect.ValueOfList(&_TxBody_1023_list{list: &list})
@ -3153,6 +3174,9 @@ func (x *fastReflection_TxBody) ProtoMethods() *protoiface.Methods {
if x.TimeoutHeight != 0 {
n += 1 + runtime.Sov(uint64(x.TimeoutHeight))
}
if x.Unordered {
n += 2
}
if len(x.ExtensionOptions) > 0 {
for _, e := range x.ExtensionOptions {
l = options.Size(e)
@ -3230,6 +3254,16 @@ func (x *fastReflection_TxBody) ProtoMethods() *protoiface.Methods {
dAtA[i] = 0xfa
}
}
if x.Unordered {
i--
if x.Unordered {
dAtA[i] = 1
} else {
dAtA[i] = 0
}
i--
dAtA[i] = 0x20
}
if x.TimeoutHeight != 0 {
i = runtime.EncodeVarint(dAtA, i, uint64(x.TimeoutHeight))
i--
@ -3392,6 +3426,26 @@ func (x *fastReflection_TxBody) ProtoMethods() *protoiface.Methods {
break
}
}
case 4:
if wireType != 0 {
return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field Unordered", wireType)
}
var v int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrIntOverflow
}
if iNdEx >= l {
return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
v |= int(b&0x7F) << shift
if b < 0x80 {
break
}
}
x.Unordered = bool(v != 0)
case 1023:
if wireType != 2 {
return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field ExtensionOptions", wireType)
@ -8414,11 +8468,26 @@ type TxBody struct {
Messages []*anypb.Any `protobuf:"bytes,1,rep,name=messages,proto3" json:"messages,omitempty"`
// memo is any arbitrary note/comment to be added to the transaction.
// WARNING: in clients, any publicly exposed text should not be called memo,
// but should be called `note` instead (see https://github.com/cosmos/cosmos-sdk/issues/9122).
// but should be called `note` instead (see
// https://github.com/cosmos/cosmos-sdk/issues/9122).
Memo string `protobuf:"bytes,2,opt,name=memo,proto3" json:"memo,omitempty"`
// timeout is the block height after which this transaction will not
// be processed by the chain
// timeout_height is the block height after which this transaction will not
// be processed by the chain.
//
// Note, if unordered=true this value MUST be set
// and will act as a short-lived TTL in which the transaction is deemed valid
// and kept in memory to prevent duplicates.
TimeoutHeight uint64 `protobuf:"varint,3,opt,name=timeout_height,json=timeoutHeight,proto3" json:"timeout_height,omitempty"`
// unordered, when set to true, indicates that the transaction signer(s)
// intend for the transaction to be evaluated and executed in an un-ordered
// fashion. Specifically, the account's nonce will NOT be checked or
// incremented, which allows for fire-and-forget as well as concurrent
// transaction execution.
//
// Note, when set to true, the existing 'timeout_height' value must be set and
// will be used to correspond to a height in which the transaction is deemed
// valid.
Unordered bool `protobuf:"varint,4,opt,name=unordered,proto3" json:"unordered,omitempty"`
// extension_options are arbitrary options that can be added by chains
// when the default options are not sufficient. If any of these are present
// and can't be handled, the transaction will be rejected
@ -8470,6 +8539,13 @@ func (x *TxBody) GetTimeoutHeight() uint64 {
return 0
}
func (x *TxBody) GetUnordered() bool {
if x != nil {
return x.Unordered
}
return false
}
func (x *TxBody) GetExtensionOptions() []*anypb.Any {
if x != nil {
return x.ExtensionOptions
@ -8703,13 +8779,15 @@ type Fee struct {
// gas_limit is the maximum gas that can be used in transaction processing
// before an out of gas error occurs
GasLimit uint64 `protobuf:"varint,2,opt,name=gas_limit,json=gasLimit,proto3" json:"gas_limit,omitempty"`
// if unset, the first signer is responsible for paying the fees. If set, the specified account must pay the fees.
// the payer must be a tx signer (and thus have signed this field in AuthInfo).
// setting this field does *not* change the ordering of required signers for the transaction.
// if unset, the first signer is responsible for paying the fees. If set, the
// specified account must pay the fees. the payer must be a tx signer (and
// thus have signed this field in AuthInfo). setting this field does *not*
// change the ordering of required signers for the transaction.
Payer string `protobuf:"bytes,3,opt,name=payer,proto3" json:"payer,omitempty"`
// if set, the fee payer (either the first signer or the value of the payer field) requests that a fee grant be used
// to pay fees instead of the fee payer's own balance. If an appropriate fee grant does not exist or the chain does
// not support fee grants, this will fail
// if set, the fee payer (either the first signer or the value of the payer
// field) requests that a fee grant be used to pay fees instead of the fee
// payer's own balance. If an appropriate fee grant does not exist or the
// chain does not support fee grants, this will fail
Granter string `protobuf:"bytes,4,opt,name=granter,proto3" json:"granter,omitempty"`
}
@ -9030,119 +9108,121 @@ var file_cosmos_tx_v1beta1_tx_proto_rawDesc = []byte{
0x63, 0x65, 0x12, 0x2c, 0x0a, 0x03, 0x74, 0x69, 0x70, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32,
0x16, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x74, 0x78, 0x2e, 0x76, 0x31, 0x62, 0x65,
0x74, 0x61, 0x31, 0x2e, 0x54, 0x69, 0x70, 0x42, 0x02, 0x18, 0x01, 0x52, 0x03, 0x74, 0x69, 0x70,
0x22, 0x95, 0x02, 0x0a, 0x06, 0x54, 0x78, 0x42, 0x6f, 0x64, 0x79, 0x12, 0x30, 0x0a, 0x08, 0x6d,
0x22, 0xb3, 0x02, 0x0a, 0x06, 0x54, 0x78, 0x42, 0x6f, 0x64, 0x79, 0x12, 0x30, 0x0a, 0x08, 0x6d,
0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e,
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
0x41, 0x6e, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x12, 0x12, 0x0a,
0x04, 0x6d, 0x65, 0x6d, 0x6f, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6d, 0x65, 0x6d,
0x6f, 0x12, 0x25, 0x0a, 0x0e, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x5f, 0x68, 0x65, 0x69,
0x67, 0x68, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0d, 0x74, 0x69, 0x6d, 0x65, 0x6f,
0x75, 0x74, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x42, 0x0a, 0x11, 0x65, 0x78, 0x74, 0x65,
0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xff, 0x07,
0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x10, 0x65, 0x78, 0x74, 0x65,
0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x5a, 0x0a, 0x1e,
0x6e, 0x6f, 0x6e, 0x5f, 0x63, 0x72, 0x69, 0x74, 0x69, 0x63, 0x61, 0x6c, 0x5f, 0x65, 0x78, 0x74,
0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xff,
0x0f, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x1b, 0x6e, 0x6f, 0x6e,
0x43, 0x72, 0x69, 0x74, 0x69, 0x63, 0x61, 0x6c, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f,
0x6e, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xa4, 0x01, 0x0a, 0x08, 0x41, 0x75, 0x74,
0x68, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x40, 0x0a, 0x0c, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x5f,
0x69, 0x6e, 0x66, 0x6f, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x63, 0x6f,
0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x74, 0x78, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e,
0x53, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0b, 0x73, 0x69, 0x67, 0x6e,
0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x73, 0x12, 0x28, 0x0a, 0x03, 0x66, 0x65, 0x65, 0x18, 0x02,
0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x74, 0x78,
0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x46, 0x65, 0x65, 0x52, 0x03, 0x66, 0x65,
0x65, 0x12, 0x2c, 0x0a, 0x03, 0x74, 0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16,
0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x74, 0x78, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74,
0x61, 0x31, 0x2e, 0x54, 0x69, 0x70, 0x42, 0x02, 0x18, 0x01, 0x52, 0x03, 0x74, 0x69, 0x70, 0x22,
0x97, 0x01, 0x0a, 0x0a, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x33,
0x0a, 0x0a, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01,
0x75, 0x74, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x75, 0x6e, 0x6f, 0x72,
0x64, 0x65, 0x72, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x75, 0x6e, 0x6f,
0x72, 0x64, 0x65, 0x72, 0x65, 0x64, 0x12, 0x42, 0x0a, 0x11, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73,
0x69, 0x6f, 0x6e, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xff, 0x07, 0x20, 0x03,
0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63,
0x4b, 0x65, 0x79, 0x12, 0x38, 0x0a, 0x09, 0x6d, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x6e, 0x66, 0x6f,
0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e,
0x74, 0x78, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x4d, 0x6f, 0x64, 0x65, 0x49,
0x6e, 0x66, 0x6f, 0x52, 0x08, 0x6d, 0x6f, 0x64, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1a, 0x0a,
0x08, 0x73, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52,
0x08, 0x73, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, 0x22, 0xe0, 0x02, 0x0a, 0x08, 0x4d, 0x6f,
0x64, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x3c, 0x0a, 0x06, 0x73, 0x69, 0x6e, 0x67, 0x6c, 0x65,
0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e,
0x74, 0x78, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x4d, 0x6f, 0x64, 0x65, 0x49,
0x6e, 0x66, 0x6f, 0x2e, 0x53, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x48, 0x00, 0x52, 0x06, 0x73, 0x69,
0x6e, 0x67, 0x6c, 0x65, 0x12, 0x39, 0x0a, 0x05, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x18, 0x02, 0x20,
0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x74, 0x78, 0x2e,
0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x4d, 0x6f, 0x64, 0x65, 0x49, 0x6e, 0x66, 0x6f,
0x2e, 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x48, 0x00, 0x52, 0x05, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x1a,
0x41, 0x0a, 0x06, 0x53, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x12, 0x37, 0x0a, 0x04, 0x6d, 0x6f, 0x64,
0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x23, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73,
0x2e, 0x74, 0x78, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x62, 0x65,
0x74, 0x61, 0x31, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x04, 0x6d, 0x6f,
0x64, 0x65, 0x1a, 0x90, 0x01, 0x0a, 0x05, 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x12, 0x4b, 0x0a, 0x08,
0x62, 0x69, 0x74, 0x61, 0x72, 0x72, 0x61, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2f,
0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x2e, 0x6d,
0x75, 0x6c, 0x74, 0x69, 0x73, 0x69, 0x67, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e,
0x43, 0x6f, 0x6d, 0x70, 0x61, 0x63, 0x74, 0x42, 0x69, 0x74, 0x41, 0x72, 0x72, 0x61, 0x79, 0x52,
0x08, 0x62, 0x69, 0x74, 0x61, 0x72, 0x72, 0x61, 0x79, 0x12, 0x3a, 0x0a, 0x0a, 0x6d, 0x6f, 0x64,
0x65, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e,
0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x74, 0x78, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61,
0x31, 0x2e, 0x4d, 0x6f, 0x64, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x09, 0x6d, 0x6f, 0x64, 0x65,
0x49, 0x6e, 0x66, 0x6f, 0x73, 0x42, 0x05, 0x0a, 0x03, 0x73, 0x75, 0x6d, 0x22, 0x81, 0x02, 0x0a,
0x03, 0x46, 0x65, 0x65, 0x12, 0x79, 0x0a, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01,
0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x62, 0x61,
0x73, 0x65, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x43, 0x6f, 0x69, 0x6e, 0x42,
0x46, 0xc8, 0xde, 0x1f, 0x00, 0xaa, 0xdf, 0x1f, 0x28, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e,
0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2f, 0x63, 0x6f, 0x73, 0x6d, 0x6f,
0x73, 0x2d, 0x73, 0x64, 0x6b, 0x2f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x43, 0x6f, 0x69, 0x6e,
0x73, 0x9a, 0xe7, 0xb0, 0x2a, 0x0c, 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x5f, 0x63, 0x6f, 0x69,
0x6e, 0x73, 0xa8, 0xe7, 0xb0, 0x2a, 0x01, 0x52, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12,
0x1b, 0x0a, 0x09, 0x67, 0x61, 0x73, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01,
0x28, 0x04, 0x52, 0x08, 0x67, 0x61, 0x73, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x2e, 0x0a, 0x05,
0x70, 0x61, 0x79, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x18, 0xd2, 0xb4, 0x2d,
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x10, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73,
0x69, 0x6f, 0x6e, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x5a, 0x0a, 0x1e, 0x6e, 0x6f,
0x6e, 0x5f, 0x63, 0x72, 0x69, 0x74, 0x69, 0x63, 0x61, 0x6c, 0x5f, 0x65, 0x78, 0x74, 0x65, 0x6e,
0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xff, 0x0f, 0x20,
0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x1b, 0x6e, 0x6f, 0x6e, 0x43, 0x72,
0x69, 0x74, 0x69, 0x63, 0x61, 0x6c, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x4f,
0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xa4, 0x01, 0x0a, 0x08, 0x41, 0x75, 0x74, 0x68, 0x49,
0x6e, 0x66, 0x6f, 0x12, 0x40, 0x0a, 0x0c, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x5f, 0x69, 0x6e,
0x66, 0x6f, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x63, 0x6f, 0x73, 0x6d,
0x6f, 0x73, 0x2e, 0x74, 0x78, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x53, 0x69,
0x67, 0x6e, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x72,
0x49, 0x6e, 0x66, 0x6f, 0x73, 0x12, 0x28, 0x0a, 0x03, 0x66, 0x65, 0x65, 0x18, 0x02, 0x20, 0x01,
0x28, 0x0b, 0x32, 0x16, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x74, 0x78, 0x2e, 0x76,
0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x46, 0x65, 0x65, 0x52, 0x03, 0x66, 0x65, 0x65, 0x12,
0x2c, 0x0a, 0x03, 0x74, 0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x63,
0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x74, 0x78, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31,
0x2e, 0x54, 0x69, 0x70, 0x42, 0x02, 0x18, 0x01, 0x52, 0x03, 0x74, 0x69, 0x70, 0x22, 0x97, 0x01,
0x0a, 0x0a, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x33, 0x0a, 0x0a,
0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b,
0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65,
0x79, 0x12, 0x38, 0x0a, 0x09, 0x6d, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x02,
0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x74, 0x78,
0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x4d, 0x6f, 0x64, 0x65, 0x49, 0x6e, 0x66,
0x6f, 0x52, 0x08, 0x6d, 0x6f, 0x64, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1a, 0x0a, 0x08, 0x73,
0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x73,
0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, 0x22, 0xe0, 0x02, 0x0a, 0x08, 0x4d, 0x6f, 0x64, 0x65,
0x49, 0x6e, 0x66, 0x6f, 0x12, 0x3c, 0x0a, 0x06, 0x73, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x18, 0x01,
0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x74, 0x78,
0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x4d, 0x6f, 0x64, 0x65, 0x49, 0x6e, 0x66,
0x6f, 0x2e, 0x53, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x48, 0x00, 0x52, 0x06, 0x73, 0x69, 0x6e, 0x67,
0x6c, 0x65, 0x12, 0x39, 0x0a, 0x05, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x18, 0x02, 0x20, 0x01, 0x28,
0x0b, 0x32, 0x21, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x74, 0x78, 0x2e, 0x76, 0x31,
0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x4d, 0x6f, 0x64, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x4d,
0x75, 0x6c, 0x74, 0x69, 0x48, 0x00, 0x52, 0x05, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x1a, 0x41, 0x0a,
0x06, 0x53, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x12, 0x37, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18,
0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x23, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x74,
0x78, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61,
0x31, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65,
0x1a, 0x90, 0x01, 0x0a, 0x05, 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x12, 0x4b, 0x0a, 0x08, 0x62, 0x69,
0x74, 0x61, 0x72, 0x72, 0x61, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x63,
0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x2e, 0x6d, 0x75, 0x6c,
0x74, 0x69, 0x73, 0x69, 0x67, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x43, 0x6f,
0x6d, 0x70, 0x61, 0x63, 0x74, 0x42, 0x69, 0x74, 0x41, 0x72, 0x72, 0x61, 0x79, 0x52, 0x08, 0x62,
0x69, 0x74, 0x61, 0x72, 0x72, 0x61, 0x79, 0x12, 0x3a, 0x0a, 0x0a, 0x6d, 0x6f, 0x64, 0x65, 0x5f,
0x69, 0x6e, 0x66, 0x6f, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x63, 0x6f,
0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x74, 0x78, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e,
0x4d, 0x6f, 0x64, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x09, 0x6d, 0x6f, 0x64, 0x65, 0x49, 0x6e,
0x66, 0x6f, 0x73, 0x42, 0x05, 0x0a, 0x03, 0x73, 0x75, 0x6d, 0x22, 0x81, 0x02, 0x0a, 0x03, 0x46,
0x65, 0x65, 0x12, 0x79, 0x0a, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x03,
0x28, 0x0b, 0x32, 0x19, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x62, 0x61, 0x73, 0x65,
0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x43, 0x6f, 0x69, 0x6e, 0x42, 0x46, 0xc8,
0xde, 0x1f, 0x00, 0xaa, 0xdf, 0x1f, 0x28, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f,
0x6d, 0x2f, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2f, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2d,
0x73, 0x64, 0x6b, 0x2f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x43, 0x6f, 0x69, 0x6e, 0x73, 0x9a,
0xe7, 0xb0, 0x2a, 0x0c, 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x5f, 0x63, 0x6f, 0x69, 0x6e, 0x73,
0xa8, 0xe7, 0xb0, 0x2a, 0x01, 0x52, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1b, 0x0a,
0x09, 0x67, 0x61, 0x73, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04,
0x52, 0x08, 0x67, 0x61, 0x73, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x2e, 0x0a, 0x05, 0x70, 0x61,
0x79, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x18, 0xd2, 0xb4, 0x2d, 0x14, 0x63,
0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x53, 0x74, 0x72,
0x69, 0x6e, 0x67, 0x52, 0x05, 0x70, 0x61, 0x79, 0x65, 0x72, 0x12, 0x32, 0x0a, 0x07, 0x67, 0x72,
0x61, 0x6e, 0x74, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x18, 0xd2, 0xb4, 0x2d,
0x14, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x53,
0x74, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x05, 0x70, 0x61, 0x79, 0x65, 0x72, 0x12, 0x32, 0x0a, 0x07,
0x67, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x18, 0xd2,
0xb4, 0x2d, 0x14, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73,
0x73, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x07, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x72,
0x22, 0xb6, 0x01, 0x0a, 0x03, 0x54, 0x69, 0x70, 0x12, 0x79, 0x0a, 0x06, 0x61, 0x6d, 0x6f, 0x75,
0x6e, 0x74, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f,
0x73, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x43,
0x6f, 0x69, 0x6e, 0x42, 0x46, 0xc8, 0xde, 0x1f, 0x00, 0xaa, 0xdf, 0x1f, 0x28, 0x67, 0x69, 0x74,
0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2f, 0x63,
0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2d, 0x73, 0x64, 0x6b, 0x2f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e,
0x43, 0x6f, 0x69, 0x6e, 0x73, 0x9a, 0xe7, 0xb0, 0x2a, 0x0c, 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79,
0x5f, 0x63, 0x6f, 0x69, 0x6e, 0x73, 0xa8, 0xe7, 0xb0, 0x2a, 0x01, 0x52, 0x06, 0x61, 0x6d, 0x6f,
0x75, 0x6e, 0x74, 0x12, 0x30, 0x0a, 0x06, 0x74, 0x69, 0x70, 0x70, 0x65, 0x72, 0x18, 0x02, 0x20,
0x01, 0x28, 0x09, 0x42, 0x18, 0xd2, 0xb4, 0x2d, 0x14, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e,
0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x06, 0x74,
0x69, 0x70, 0x70, 0x65, 0x72, 0x3a, 0x02, 0x18, 0x01, 0x22, 0xce, 0x01, 0x0a, 0x0d, 0x41, 0x75,
0x78, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x44, 0x61, 0x74, 0x61, 0x12, 0x32, 0x0a, 0x07, 0x61,
0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x18, 0xd2, 0xb4,
0x2d, 0x14, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73,
0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12,
0x3e, 0x0a, 0x08, 0x73, 0x69, 0x67, 0x6e, 0x5f, 0x64, 0x6f, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28,
0x0b, 0x32, 0x23, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x74, 0x78, 0x2e, 0x76, 0x31,
0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x44, 0x6f, 0x63, 0x44, 0x69, 0x72,
0x65, 0x63, 0x74, 0x41, 0x75, 0x78, 0x52, 0x07, 0x73, 0x69, 0x67, 0x6e, 0x44, 0x6f, 0x63, 0x12,
0x37, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x23, 0x2e,
0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x74, 0x78, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x69, 0x6e,
0x67, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x4d, 0x6f,
0x64, 0x65, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x69, 0x67, 0x18,
0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x73, 0x69, 0x67, 0x42, 0xb4, 0x01, 0x0a, 0x15, 0x63,
0x6f, 0x6d, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x74, 0x78, 0x2e, 0x76, 0x31, 0x62,
0x65, 0x74, 0x61, 0x31, 0x42, 0x07, 0x54, 0x78, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a,
0x2c, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x73, 0x64, 0x6b, 0x2e, 0x69, 0x6f, 0x2f, 0x61, 0x70,
0x69, 0x2f, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2f, 0x74, 0x78, 0x2f, 0x76, 0x31, 0x62, 0x65,
0x74, 0x61, 0x31, 0x3b, 0x74, 0x78, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0xa2, 0x02, 0x03,
0x43, 0x54, 0x58, 0xaa, 0x02, 0x11, 0x43, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x54, 0x78, 0x2e,
0x56, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0xca, 0x02, 0x11, 0x43, 0x6f, 0x73, 0x6d, 0x6f, 0x73,
0x5c, 0x54, 0x78, 0x5c, 0x56, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0xe2, 0x02, 0x1d, 0x43, 0x6f,
0x73, 0x6d, 0x6f, 0x73, 0x5c, 0x54, 0x78, 0x5c, 0x56, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x5c,
0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x13, 0x43, 0x6f,
0x73, 0x6d, 0x6f, 0x73, 0x3a, 0x3a, 0x54, 0x78, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x65, 0x74, 0x61,
0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x74, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x07, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x72, 0x22, 0xb6,
0x01, 0x0a, 0x03, 0x54, 0x69, 0x70, 0x12, 0x79, 0x0a, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74,
0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e,
0x62, 0x61, 0x73, 0x65, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x43, 0x6f, 0x69,
0x6e, 0x42, 0x46, 0xc8, 0xde, 0x1f, 0x00, 0xaa, 0xdf, 0x1f, 0x28, 0x67, 0x69, 0x74, 0x68, 0x75,
0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2f, 0x63, 0x6f, 0x73,
0x6d, 0x6f, 0x73, 0x2d, 0x73, 0x64, 0x6b, 0x2f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x43, 0x6f,
0x69, 0x6e, 0x73, 0x9a, 0xe7, 0xb0, 0x2a, 0x0c, 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x5f, 0x63,
0x6f, 0x69, 0x6e, 0x73, 0xa8, 0xe7, 0xb0, 0x2a, 0x01, 0x52, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e,
0x74, 0x12, 0x30, 0x0a, 0x06, 0x74, 0x69, 0x70, 0x70, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28,
0x09, 0x42, 0x18, 0xd2, 0xb4, 0x2d, 0x14, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x41, 0x64,
0x64, 0x72, 0x65, 0x73, 0x73, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x06, 0x74, 0x69, 0x70,
0x70, 0x65, 0x72, 0x3a, 0x02, 0x18, 0x01, 0x22, 0xce, 0x01, 0x0a, 0x0d, 0x41, 0x75, 0x78, 0x53,
0x69, 0x67, 0x6e, 0x65, 0x72, 0x44, 0x61, 0x74, 0x61, 0x12, 0x32, 0x0a, 0x07, 0x61, 0x64, 0x64,
0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x18, 0xd2, 0xb4, 0x2d, 0x14,
0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x53, 0x74,
0x72, 0x69, 0x6e, 0x67, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x3e, 0x0a,
0x08, 0x73, 0x69, 0x67, 0x6e, 0x5f, 0x64, 0x6f, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32,
0x23, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x74, 0x78, 0x2e, 0x76, 0x31, 0x62, 0x65,
0x74, 0x61, 0x31, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x44, 0x6f, 0x63, 0x44, 0x69, 0x72, 0x65, 0x63,
0x74, 0x41, 0x75, 0x78, 0x52, 0x07, 0x73, 0x69, 0x67, 0x6e, 0x44, 0x6f, 0x63, 0x12, 0x37, 0x0a,
0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x23, 0x2e, 0x63, 0x6f,
0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x74, 0x78, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x2e,
0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x4d, 0x6f, 0x64, 0x65,
0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x69, 0x67, 0x18, 0x04, 0x20,
0x01, 0x28, 0x0c, 0x52, 0x03, 0x73, 0x69, 0x67, 0x42, 0xb4, 0x01, 0x0a, 0x15, 0x63, 0x6f, 0x6d,
0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x74, 0x78, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74,
0x61, 0x31, 0x42, 0x07, 0x54, 0x78, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x2c, 0x63,
0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x73, 0x64, 0x6b, 0x2e, 0x69, 0x6f, 0x2f, 0x61, 0x70, 0x69, 0x2f,
0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2f, 0x74, 0x78, 0x2f, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61,
0x31, 0x3b, 0x74, 0x78, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0xa2, 0x02, 0x03, 0x43, 0x54,
0x58, 0xaa, 0x02, 0x11, 0x43, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x54, 0x78, 0x2e, 0x56, 0x31,
0x62, 0x65, 0x74, 0x61, 0x31, 0xca, 0x02, 0x11, 0x43, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x5c, 0x54,
0x78, 0x5c, 0x56, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0xe2, 0x02, 0x1d, 0x43, 0x6f, 0x73, 0x6d,
0x6f, 0x73, 0x5c, 0x54, 0x78, 0x5c, 0x56, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x5c, 0x47, 0x50,
0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x13, 0x43, 0x6f, 0x73, 0x6d,
0x6f, 0x73, 0x3a, 0x3a, 0x54, 0x78, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x62,
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (

View File

@ -74,6 +74,7 @@ const (
FlagOffset = "offset"
FlagCountTotal = "count-total"
FlagTimeoutHeight = "timeout-height"
FlagUnordered = "unordered"
FlagKeyAlgorithm = "algo"
FlagKeyType = "key-type"
FlagFeePayer = "fee-payer"
@ -136,6 +137,7 @@ func AddTxFlagsToCmd(cmd *cobra.Command) {
f.BoolP(FlagSkipConfirmation, "y", false, "Skip tx broadcasting prompt confirmation")
f.String(FlagSignMode, "", "Choose sign mode (direct|amino-json|direct-aux|textual), this is an advanced feature")
f.Uint64(FlagTimeoutHeight, 0, "Set a block timeout height to prevent the tx from being committed past a certain height")
f.Bool(FlagUnordered, false, "Enable unordered transaction delivery; must be used in conjunction with --timeout-height")
f.String(FlagFeePayer, "", "Fee payer pays fees for the transaction instead of deducting from the signer")
f.String(FlagFeeGranter, "", "Fee granter grants fees for the transaction")
f.String(FlagTip, "", "Tip is the amount that is going to be transferred to the fee payer on the target chain. This flag is only valid when used with --aux, and is ignored if the target chain didn't enable the TipDecorator")

View File

@ -36,6 +36,7 @@ type Factory struct {
gasAdjustment float64
chainID string
fromName string
unordered bool
offline bool
generateOnly bool
memo string
@ -86,6 +87,7 @@ func NewFactoryCLI(clientCtx client.Context, flagSet *pflag.FlagSet) (Factory, e
gasAdj := clientCtx.Viper.GetFloat64(flags.FlagGasAdjustment)
memo := clientCtx.Viper.GetString(flags.FlagNote)
timeoutHeight := clientCtx.Viper.GetUint64(flags.FlagTimeoutHeight)
unordered := clientCtx.Viper.GetBool(flags.FlagUnordered)
gasStr := clientCtx.Viper.GetString(flags.FlagGas)
gasSetting, _ := flags.ParseGasSetting(gasStr)
@ -103,6 +105,7 @@ func NewFactoryCLI(clientCtx client.Context, flagSet *pflag.FlagSet) (Factory, e
accountNumber: accNum,
sequence: accSeq,
timeoutHeight: timeoutHeight,
unordered: unordered,
gasAdjustment: gasAdj,
memo: memo,
signMode: signMode,
@ -132,6 +135,7 @@ func (f Factory) Fees() sdk.Coins { return f.fees }
func (f Factory) GasPrices() sdk.DecCoins { return f.gasPrices }
func (f Factory) AccountRetriever() client.AccountRetriever { return f.accountRetriever }
func (f Factory) TimeoutHeight() uint64 { return f.timeoutHeight }
func (f Factory) Unordered() bool { return f.unordered }
func (f Factory) FromName() string { return f.fromName }
// SimulateAndExecute returns the option to simulate and then execute the transaction
@ -245,6 +249,12 @@ func (f Factory) WithTimeoutHeight(height uint64) Factory {
return f
}
// WithUnordered returns a copy of the Factory with an updated unordered field.
func (f Factory) WithUnordered(v bool) Factory {
f.unordered = v
return f
}
// WithFeeGranter returns a copy of the Factory with an updated fee granter.
func (f Factory) WithFeeGranter(fg sdk.AccAddress) Factory {
f.feeGranter = fg

View File

@ -48,6 +48,7 @@ type (
SetFeePayer(feePayer sdk.AccAddress)
SetGasLimit(limit uint64)
SetTimeoutHeight(height uint64)
SetUnordered(v bool)
SetFeeGranter(feeGranter sdk.AccAddress)
AddAuxSignerData(tx.AuxSignerData) error
}

View File

@ -2,7 +2,7 @@
## Changelog
* Dec 4, 2023: Initial Draft
* Dec 4, 2023: Initial Draft (@yihuang, @tac0turtle, @alexanderbez)
## Status
@ -53,79 +53,140 @@ message TxBody {
}
```
### `DedupTxHashManager`
### Replay Protection
In order to provide replay protection, a user should ensure that the transaction's
TTL value is relatively short-lived but long enough to provide enough time to be
included in a block, e.g. ~H+50.
We facilitate this by storing the transaction's hash in a durable map, `UnorderedTxManager`,
to prevent duplicates, i.e. replay attacks. Upon transaction ingress during `CheckTx`,
we check if the transaction's hash exists in this map or if the TTL value is stale,
i.e. before the current block. If so, we reject it. Upon inclusion in a block
during `DeliverTx`, the transaction's hash is set in the map along with it's TTL
value.
This map is evaluated at the end of each block, e.g. ABCI `Commit`, and all stale
transactions, i.e. transactions's TTL value who's now beyond the committed block,
are purged from the map.
An important point to note is that in theory, it may be possible to submit an unordered
transaction twice, or multiple times, before the transaction is included in a block.
However, we'll note a few important layers of protection and mitigation:
* Assuming CometBFT is used as the underlying consensus engine and a non-noop mempool
is used, CometBFT will reject the duplicate for you.
* For applications that leverage ABCI++, `ProcessProposal` should evaluate and reject
malicious proposals with duplicate transactions.
* For applications that leverage their own application mempool, their mempool should
reject the duplicate for you.
* Finally, worst case if the duplicate transaction is somehow selected for a block
proposal, 2nd and all further attempts to evaluate it, will fail during `DeliverTx`,
so worst case you just end up filling up block space with a duplicate transaction.
```golang
type TxHash [32]byte
const PurgeLoopSleepMS = 500
// DedupTxHashManager contains the tx hash dictionary for duplicates checking,
// and expire them when block number progresses.
type DedupTxHashManager struct {
mutex sync.RWMutex
// tx hash -> expire block number
// for duplicates checking and expiration
hashes map[TxHash]uint64
// channel to receive latest block numbers
// UnorderedTxManager contains the tx hash dictionary for duplicates checking,
// and expire them when block production progresses.
type UnorderedTxManager struct {
// blockCh defines a channel to receive newly committed block heights
blockCh chan uint64
mu sync.RWMutex
// txHashes defines a map from tx hash -> TTL value, which is used for duplicate
// checking and replay protection, as well as purging the map when the TTL is
// expired.
txHashes map[TxHash]uint64
}
func NewDedupTxHashManager() *DedupTxHashManager {
m := &DedupTxHashManager{
hashes: make(map[TxHash]uint64),
blockCh: make(ch *uint64, 16),
func NewUnorderedTxManager() *UnorderedTxManager {
m := &UnorderedTxManager{
blockCh: make(chan uint64, 16),
txHashes: make(map[TxHash]uint64),
}
go m.purgeLoop()
return m
return m
}
func (dtm *DedupTxHashManager) Close() error {
close(dtm.blockCh)
dtm.blockCh = nil
func (m *UnorderedTxManager) Start() {
go m.purgeLoop()
}
func (m *UnorderedTxManager) Close() error {
close(m.blockCh)
m.blockCh = nil
return nil
}
func (dtm *DedupTxHashManager) Contains(hash TxHash) (ok bool) {
dtm.mutex.RLock()
defer dtm.mutex.RUnlock()
func (m *UnorderedTxManager) Contains(hash TxHash) bool{
m.mu.RLock()
defer m.mu.RUnlock()
_, ok = dtm.hashes[hash]
return
_, ok := m.txHashes[hash]
return ok
}
func (dtm *DedupTxHashManager) Size() int {
dtm.mutex.RLock()
defer dtm.mutex.RUnlock()
func (m *UnorderedTxManager) Size() int {
m.mu.RLock()
defer m.mu.RUnlock()
return len(dtm.hashes)
return len(m.txHashes)
}
func (dtm *DedupTxHashManager) Add(hash TxHash, expire uint64) (ok bool) {
dtm.mutex.Lock()
defer dtm.mutex.Unlock()
func (m *UnorderedTxManager) Add(hash TxHash, expire uint64) {
m.mu.Lock()
defer m.mu.Unlock()
dtm.hashes[hash] = expire
return
m.txHashes[hash] = expire
}
// OnNewBlock send the latest block number to the background purge loop,
// it should be called in abci commit event.
func (dtm *DedupTxHashManager) OnNewBlock(blockNumber uint64) {
dtm.blockCh <- &blockNumber
// OnNewBlock send the latest block number to the background purge loop, which
// should be called in ABCI Commit event.
func (m *UnorderedTxManager) OnNewBlock(blockHeight uint64) {
m.blockCh <- blockHeight
}
// purgeLoop removes expired tx hashes at background
func (dtm *DedupTxHashManager) purgeLoop() error {
// expiredTxs returns expired tx hashes based on the provided block height.
func (m *UnorderedTxManager) expiredTxs(blockHeight uint64) []TxHash {
m.mu.RLock()
defer m.mu.RUnlock()
var result []TxHash
for txHash, expire := range m.txHashes {
if blockHeight > expire {
result = append(result, txHash)
}
}
return result
}
func (m *UnorderedTxManager) purge(txHashes []TxHash) {
m.mu.Lock()
defer m.mu.Unlock()
for _, txHash := range txHashes {
delete(m.txHashes, txHash)
}
}
// purgeLoop removes expired tx hashes in the background
func (m *UnorderedTxManager) purgeLoop() error {
for {
blocks := channelBatchRecv(dtm.blockCh)
blocks := channelBatchRecv(m.blockCh)
if len(blocks) == 0 {
// channel closed
break
}
latest := *blocks[len(blocks)-1]
hashes := dtm.expired(latest)
hashes := m.expired(latest)
if len(hashes) > 0 {
dtm.purge(hashes)
m.purge(hashes)
}
// avoid burning cpu in catching up phase
@ -133,28 +194,6 @@ func (dtm *DedupTxHashManager) purgeLoop() error {
}
}
// expired find out expired tx hashes based on latest block number
func (dtm *DedupTxHashManager) expired(block uint64) []TxHash {
dtm.mutex.RLock()
defer dtm.mutex.RUnlock()
var result []TxHash
for h, expire := range dtm.hashes {
if block > expire {
result = append(result, h)
}
}
return result
}
func (dtm *DedupTxHashManager) purge(hashes []TxHash) {
dtm.mutex.Lock()
defer dtm.mutex.Unlock()
for _, hash := range hashes {
delete(dtm.hashes, hash)
}
}
// channelBatchRecv try to exhaust the channel buffer when it's not empty,
// and block when it's empty.
@ -176,9 +215,11 @@ func channelBatchRecv[T any](ch <-chan *T) []*T {
}
```
### Ante Handlers
### AnteHandler Decorator
Bypass the nonce decorator for un-ordered transactions.
In order to facilitate bypassing nonce verification, we have to modify the existing
`IncrementSequenceDecorator` AnteHandler decorator to skip the nonce verification
when the transaction is marked as un-ordered.
```golang
func (isd IncrementSequenceDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) {
@ -186,25 +227,26 @@ func (isd IncrementSequenceDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, sim
return next(ctx, tx, simulate)
}
// the previous logic
// ...
}
```
A decorator for the new logic.
In addition, we need to introduce a new decorator to perform the un-ordered transaction
verification and map lookup.
```golang
type TxHash [32]byte
const (
// MaxUnOrderedTTL defines the maximum ttl an un-order tx can set
MaxUnOrderedTTL = 1024
// DefaultMaxUnOrderedTTL defines the default maximum TTL an un-ordered transaction
// can set.
DefaultMaxUnOrderedTTL = 1024
)
type DedupTxDecorator struct {
m *DedupTxHashManager
m *UnorderedTxManager
maxUnOrderedTTL uint64
}
func (dtd *DedupTxDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) {
func (d *DedupTxDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) {
// only apply to un-ordered transactions
if !tx.UnOrdered() {
return next(ctx, tx, simulate)
@ -214,18 +256,18 @@ func (dtd *DedupTxDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate boo
return nil, errorsmod.Wrap(sdkerrors.ErrLogic, "unordered tx must set timeout-height")
}
if tx.TimeoutHeight() > ctx.BlockHeight() + MaxUnOrderedTTL {
return nil, errorsmod.Wrapf(sdkerrors.ErrLogic, "unordered tx ttl exceeds %d", MaxUnOrderedTTL)
if tx.TimeoutHeight() > ctx.BlockHeight() + d.maxUnOrderedTTL {
return nil, errorsmod.Wrapf(sdkerrors.ErrLogic, "unordered tx ttl exceeds %d", d.maxUnOrderedTTL)
}
// check for duplicates
if dtd.m.Contains(tx.Hash()) {
if d.m.Contains(tx.Hash()) {
return nil, errorsmod.Wrap(sdkerrors.ErrLogic, "tx is duplicated")
}
if !ctx.IsCheckTx() {
// a new tx included in the block, add the hash to the dictionary
dtd.m.Add(tx.Hash(), tx.TimeoutHeight())
// a new tx included in the block, add the hash to the unordered tx manager
d.m.Add(tx.Hash(), tx.TimeoutHeight())
}
return next(ctx, tx, simulate)
@ -234,16 +276,24 @@ func (dtd *DedupTxDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate boo
### `OnNewBlock`
Wire the `OnNewBlock` method of `DedupTxHashManager` into the BaseApp's ABCI Commit event.
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 `DedupTxHashManager`
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

View File

@ -105,13 +105,29 @@ message TxBody {
// memo is any arbitrary note/comment to be added to the transaction.
// WARNING: in clients, any publicly exposed text should not be called memo,
// but should be called `note` instead (see https://github.com/cosmos/cosmos-sdk/issues/9122).
// but should be called `note` instead (see
// https://github.com/cosmos/cosmos-sdk/issues/9122).
string memo = 2;
// timeout is the block height after which this transaction will not
// be processed by the chain
// timeout_height is the block height after which this transaction will not
// be processed by the chain.
//
// Note, if unordered=true this value MUST be set
// and will act as a short-lived TTL in which the transaction is deemed valid
// and kept in memory to prevent duplicates.
uint64 timeout_height = 3;
// unordered, when set to true, indicates that the transaction signer(s)
// intend for the transaction to be evaluated and executed in an un-ordered
// fashion. Specifically, the account's nonce will NOT be checked or
// incremented, which allows for fire-and-forget as well as concurrent
// transaction execution.
//
// Note, when set to true, the existing 'timeout_height' value must be set and
// will be used to correspond to a height in which the transaction is deemed
// valid.
bool unordered = 4;
// extension_options are arbitrary options that can be added by chains
// when the default options are not sufficient. If any of these are present
// and can't be handled, the transaction will be rejected
@ -211,14 +227,16 @@ message Fee {
// before an out of gas error occurs
uint64 gas_limit = 2;
// if unset, the first signer is responsible for paying the fees. If set, the specified account must pay the fees.
// the payer must be a tx signer (and thus have signed this field in AuthInfo).
// setting this field does *not* change the ordering of required signers for the transaction.
// if unset, the first signer is responsible for paying the fees. If set, the
// specified account must pay the fees. the payer must be a tx signer (and
// thus have signed this field in AuthInfo). setting this field does *not*
// change the ordering of required signers for the transaction.
string payer = 3 [(cosmos_proto.scalar) = "cosmos.AddressString"];
// if set, the fee payer (either the first signer or the value of the payer field) requests that a fee grant be used
// to pay fees instead of the fee payer's own balance. If an appropriate fee grant does not exist or the chain does
// not support fee grants, this will fail
// if set, the fee payer (either the first signer or the value of the payer
// field) requests that a fee grant be used to pay fees instead of the fee
// payer's own balance. If an appropriate fee grant does not exist or the
// chain does not support fee grants, this will fail
string granter = 4 [(cosmos_proto.scalar) = "cosmos.AddressString"];
}

View File

@ -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"
@ -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
@ -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),

View File

@ -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"
@ -169,6 +170,8 @@ type SimApp struct {
ModuleManager *module.Manager
BasicModuleManager module.BasicManager
UnorderedTxManager *unorderedtx.Manager
// simulation manager
sm *module.SimulationManager
@ -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")
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
@ -579,6 +601,7 @@ func (app *SimApp) setAnteHandler(txConfig client.TxConfig) {
SigGasConsumer: ante.DefaultSigVerificationGasConsumer,
},
&app.CircuitKeeper,
app.UnorderedTxManager,
},
)
if err != nil {
@ -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() }

View File

@ -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"
@ -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"
@ -64,6 +68,8 @@ type SimApp struct {
txConfig client.TxConfig
interfaceRegistry codectypes.InterfaceRegistry
UnorderedTxManager *unorderedtx.Manager
// keepers
AuthKeeper authkeeper.AccountKeeper
BankKeeper bankkeeper.Keeper
@ -256,6 +262,25 @@ 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)
}
@ -263,6 +288,12 @@ func NewSimApp(
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

View File

@ -290,7 +290,7 @@ message TestUpdatedTxBody {
repeated google.protobuf.Any messages = 1;
string memo = 2;
int64 timeout_height = 3;
uint64 some_new_field = 4;
uint64 some_new_field = 5;
string some_new_field_non_critical_field = 1050;
repeated google.protobuf.Any extension_options = 1023;
repeated google.protobuf.Any non_critical_extension_options = 2047;

View File

@ -23036,7 +23036,7 @@ func (x *fastReflection_TestUpdatedTxBody) ProtoMethods() *protoiface.Methods {
if x.SomeNewField != 0 {
i = runtime.EncodeVarint(dAtA, i, uint64(x.SomeNewField))
i--
dAtA[i] = 0x20
dAtA[i] = 0x28
}
if x.TimeoutHeight != 0 {
i = runtime.EncodeVarint(dAtA, i, uint64(x.TimeoutHeight))
@ -23200,7 +23200,7 @@ func (x *fastReflection_TestUpdatedTxBody) ProtoMethods() *protoiface.Methods {
break
}
}
case 4:
case 5:
if wireType != 0 {
return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field SomeNewField", wireType)
}
@ -26508,7 +26508,7 @@ type TestUpdatedTxBody struct {
Messages []*anypb.Any `protobuf:"bytes,1,rep,name=messages,proto3" json:"messages,omitempty"`
Memo string `protobuf:"bytes,2,opt,name=memo,proto3" json:"memo,omitempty"`
TimeoutHeight int64 `protobuf:"varint,3,opt,name=timeout_height,json=timeoutHeight,proto3" json:"timeout_height,omitempty"`
SomeNewField uint64 `protobuf:"varint,4,opt,name=some_new_field,json=someNewField,proto3" json:"some_new_field,omitempty"`
SomeNewField uint64 `protobuf:"varint,5,opt,name=some_new_field,json=someNewField,proto3" json:"some_new_field,omitempty"`
SomeNewFieldNonCriticalField string `protobuf:"bytes,1050,opt,name=some_new_field_non_critical_field,json=someNewFieldNonCriticalField,proto3" json:"some_new_field_non_critical_field,omitempty"`
ExtensionOptions []*anypb.Any `protobuf:"bytes,1023,rep,name=extension_options,json=extensionOptions,proto3" json:"extension_options,omitempty"`
NonCriticalExtensionOptions []*anypb.Any `protobuf:"bytes,2047,rep,name=non_critical_extension_options,json=nonCriticalExtensionOptions,proto3" json:"non_critical_extension_options,omitempty"`
@ -27410,7 +27410,7 @@ var file_testpb_unknonwnproto_proto_rawDesc = []byte{
0x6d, 0x65, 0x6f, 0x75, 0x74, 0x5f, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x03, 0x20, 0x01,
0x28, 0x03, 0x52, 0x0d, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x48, 0x65, 0x69, 0x67, 0x68,
0x74, 0x12, 0x24, 0x0a, 0x0e, 0x73, 0x6f, 0x6d, 0x65, 0x5f, 0x6e, 0x65, 0x77, 0x5f, 0x66, 0x69,
0x65, 0x6c, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x73, 0x6f, 0x6d, 0x65, 0x4e,
0x65, 0x6c, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x73, 0x6f, 0x6d, 0x65, 0x4e,
0x65, 0x77, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x48, 0x0a, 0x21, 0x73, 0x6f, 0x6d, 0x65, 0x5f,
0x6e, 0x65, 0x77, 0x5f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x5f, 0x6e, 0x6f, 0x6e, 0x5f, 0x63, 0x72,
0x69, 0x74, 0x69, 0x63, 0x61, 0x6c, 0x5f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x9a, 0x08, 0x20,

View File

@ -2578,7 +2578,7 @@ type TestUpdatedTxBody struct {
Messages []*types.Any `protobuf:"bytes,1,rep,name=messages,proto3" json:"messages,omitempty"`
Memo string `protobuf:"bytes,2,opt,name=memo,proto3" json:"memo,omitempty"`
TimeoutHeight int64 `protobuf:"varint,3,opt,name=timeout_height,json=timeoutHeight,proto3" json:"timeout_height,omitempty"`
SomeNewField uint64 `protobuf:"varint,4,opt,name=some_new_field,json=someNewField,proto3" json:"some_new_field,omitempty"`
SomeNewField uint64 `protobuf:"varint,5,opt,name=some_new_field,json=someNewField,proto3" json:"some_new_field,omitempty"`
SomeNewFieldNonCriticalField string `protobuf:"bytes,1050,opt,name=some_new_field_non_critical_field,json=someNewFieldNonCriticalField,proto3" json:"some_new_field_non_critical_field,omitempty"`
ExtensionOptions []*types.Any `protobuf:"bytes,1023,rep,name=extension_options,json=extensionOptions,proto3" json:"extension_options,omitempty"`
NonCriticalExtensionOptions []*types.Any `protobuf:"bytes,2047,rep,name=non_critical_extension_options,json=nonCriticalExtensionOptions,proto3" json:"non_critical_extension_options,omitempty"`
@ -2906,7 +2906,7 @@ var fileDescriptor_fe4560133be9209a = []byte{
0x1a, 0x5f, 0x6a, 0x70, 0x6d, 0xc5, 0x85, 0x3e, 0x73, 0x17, 0xf8, 0x0e, 0x14, 0x66, 0x84, 0x73,
0x7b, 0xac, 0x3c, 0xd0, 0x36, 0xa6, 0x56, 0x82, 0x92, 0xd5, 0x3c, 0x23, 0x33, 0x16, 0x57, 0xb3,
0x1c, 0x4b, 0x13, 0x84, 0x37, 0x23, 0x2c, 0x10, 0x83, 0x09, 0xf1, 0xc6, 0x13, 0x11, 0xf1, 0x78,
0x25, 0x92, 0x1e, 0x2a, 0x21, 0x7e, 0x1f, 0xca, 0x9c, 0xcd, 0xc8, 0x60, 0x79, 0x6d, 0xca, 0xaa,
0x25, 0x92, 0x1e, 0x2a, 0x21, 0x7e, 0x1f, 0xca, 0x9c, 0xcd, 0xc8, 0x60, 0x79, 0x6d, 0xca, 0xa9,
0x6b, 0x53, 0x49, 0x4a, 0x8f, 0x22, 0x63, 0xf1, 0x21, 0xfc, 0x60, 0x15, 0x35, 0x58, 0xd3, 0x82,
0x7f, 0x17, 0xb6, 0xe0, 0xf7, 0xd2, 0x3b, 0x8f, 0x5e, 0x6f, 0xc7, 0x7d, 0xb8, 0x46, 0xe6, 0x82,
0x50, 0x99, 0x23, 0x03, 0xa6, 0x3e, 0xe5, 0x72, 0xfd, 0xdf, 0xbb, 0xe7, 0xb8, 0x59, 0x49, 0xf0,
@ -2921,7 +2921,7 @@ var fileDescriptor_fe4560133be9209a = []byte{
0x3c, 0x7f, 0x55, 0xdb, 0xf9, 0xeb, 0xab, 0xda, 0xce, 0x67, 0xcd, 0xb1, 0x27, 0x26, 0xc1, 0xb0,
0xe9, 0xb0, 0x59, 0x2b, 0xfa, 0xc8, 0x1f, 0xfe, 0xdd, 0xe6, 0xee, 0x71, 0x4b, 0x56, 0x7d, 0x20,
0xbc, 0xa9, 0x1a, 0xb8, 0xb6, 0xb0, 0x87, 0x79, 0x45, 0x74, 0xe7, 0x3f, 0x01, 0x00, 0x00, 0xff,
0xff, 0x3a, 0xea, 0x0d, 0xa7, 0x67, 0x18, 0x00, 0x00,
0xff, 0x33, 0x3d, 0xcf, 0x3a, 0x67, 0x18, 0x00, 0x00,
}
func (m *Customer1) Marshal() (dAtA []byte, err error) {
@ -5261,7 +5261,7 @@ func (m *TestUpdatedTxBody) MarshalToSizedBuffer(dAtA []byte) (int, error) {
if m.SomeNewField != 0 {
i = encodeVarintUnknonwnproto(dAtA, i, uint64(m.SomeNewField))
i--
dAtA[i] = 0x20
dAtA[i] = 0x28
}
if m.TimeoutHeight != 0 {
i = encodeVarintUnknonwnproto(dAtA, i, uint64(m.TimeoutHeight))
@ -12602,7 +12602,7 @@ func (m *TestUpdatedTxBody) Unmarshal(dAtA []byte) error {
break
}
}
case 4:
case 5:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field SomeNewField", wireType)
}

View File

@ -356,11 +356,26 @@ type TxBody struct {
Messages []*types.Any `protobuf:"bytes,1,rep,name=messages,proto3" json:"messages,omitempty"`
// memo is any arbitrary note/comment to be added to the transaction.
// WARNING: in clients, any publicly exposed text should not be called memo,
// but should be called `note` instead (see https://github.com/cosmos/cosmos-sdk/issues/9122).
// but should be called `note` instead (see
// https://github.com/cosmos/cosmos-sdk/issues/9122).
Memo string `protobuf:"bytes,2,opt,name=memo,proto3" json:"memo,omitempty"`
// timeout is the block height after which this transaction will not
// be processed by the chain
// timeout_height is the block height after which this transaction will not
// be processed by the chain.
//
// Note, if unordered=true this value MUST be set
// and will act as a short-lived TTL in which the transaction is deemed valid
// and kept in memory to prevent duplicates.
TimeoutHeight uint64 `protobuf:"varint,3,opt,name=timeout_height,json=timeoutHeight,proto3" json:"timeout_height,omitempty"`
// unordered, when set to true, indicates that the transaction signer(s)
// intend for the transaction to be evaluated and executed in an un-ordered
// fashion. Specifically, the account's nonce will NOT be checked or
// incremented, which allows for fire-and-forget as well as concurrent
// transaction execution.
//
// Note, when set to true, the existing 'timeout_height' value must be set and
// will be used to correspond to a height in which the transaction is deemed
// valid.
Unordered bool `protobuf:"varint,4,opt,name=unordered,proto3" json:"unordered,omitempty"`
// extension_options are arbitrary options that can be added by chains
// when the default options are not sufficient. If any of these are present
// and can't be handled, the transaction will be rejected
@ -425,6 +440,13 @@ func (m *TxBody) GetTimeoutHeight() uint64 {
return 0
}
func (m *TxBody) GetUnordered() bool {
if m != nil {
return m.Unordered
}
return false
}
func (m *TxBody) GetExtensionOptions() []*types.Any {
if m != nil {
return m.ExtensionOptions
@ -789,13 +811,15 @@ type Fee struct {
// gas_limit is the maximum gas that can be used in transaction processing
// before an out of gas error occurs
GasLimit uint64 `protobuf:"varint,2,opt,name=gas_limit,json=gasLimit,proto3" json:"gas_limit,omitempty"`
// if unset, the first signer is responsible for paying the fees. If set, the specified account must pay the fees.
// the payer must be a tx signer (and thus have signed this field in AuthInfo).
// setting this field does *not* change the ordering of required signers for the transaction.
// if unset, the first signer is responsible for paying the fees. If set, the
// specified account must pay the fees. the payer must be a tx signer (and
// thus have signed this field in AuthInfo). setting this field does *not*
// change the ordering of required signers for the transaction.
Payer string `protobuf:"bytes,3,opt,name=payer,proto3" json:"payer,omitempty"`
// if set, the fee payer (either the first signer or the value of the payer field) requests that a fee grant be used
// to pay fees instead of the fee payer's own balance. If an appropriate fee grant does not exist or the chain does
// not support fee grants, this will fail
// if set, the fee payer (either the first signer or the value of the payer
// field) requests that a fee grant be used to pay fees instead of the fee
// payer's own balance. If an appropriate fee grant does not exist or the
// chain does not support fee grants, this will fail
Granter string `protobuf:"bytes,4,opt,name=granter,proto3" json:"granter,omitempty"`
}
@ -1020,74 +1044,75 @@ func init() {
func init() { proto.RegisterFile("cosmos/tx/v1beta1/tx.proto", fileDescriptor_96d1575ffde80842) }
var fileDescriptor_96d1575ffde80842 = []byte{
// 1059 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xc4, 0x55, 0x41, 0x6f, 0x1b, 0x45,
0x14, 0xf6, 0x7a, 0x6d, 0xc7, 0x7e, 0x4d, 0xda, 0x64, 0x14, 0x21, 0xc7, 0x51, 0xdd, 0xe0, 0xaa,
0x60, 0x55, 0x64, 0xb7, 0x4d, 0x0f, 0x94, 0x0a, 0x01, 0x76, 0x43, 0x94, 0xaa, 0x14, 0xa4, 0x4d,
0x4e, 0xbd, 0xac, 0xc6, 0xeb, 0xc9, 0x7a, 0x54, 0xef, 0xcc, 0xb2, 0x33, 0x0b, 0xde, 0x23, 0x3f,
0x00, 0xa9, 0x42, 0x42, 0x48, 0x9c, 0x39, 0x20, 0x4e, 0x3d, 0x20, 0x7e, 0x43, 0x4f, 0xa8, 0xe2,
0xc4, 0x09, 0xaa, 0xe4, 0xd0, 0x3b, 0x7f, 0x00, 0x34, 0xb3, 0xb3, 0x9b, 0xb4, 0xa4, 0x4e, 0x11,
0x48, 0x5c, 0xec, 0x99, 0xb7, 0xdf, 0x7b, 0xf3, 0xbd, 0x37, 0xdf, 0xbc, 0x07, 0x9d, 0x80, 0x8b,
0x88, 0x0b, 0x57, 0xce, 0xdc, 0xcf, 0xae, 0x8f, 0x88, 0xc4, 0xd7, 0x5d, 0x39, 0x73, 0xe2, 0x84,
0x4b, 0x8e, 0x56, 0xf2, 0x6f, 0x8e, 0x9c, 0x39, 0xe6, 0x5b, 0x67, 0x05, 0x47, 0x94, 0x71, 0x57,
0xff, 0xe6, 0xa8, 0xce, 0x6a, 0xc8, 0x43, 0xae, 0x97, 0xae, 0x5a, 0x19, 0xeb, 0xa6, 0x89, 0x1b,
0x24, 0x59, 0x2c, 0xb9, 0x1b, 0xa5, 0x53, 0x49, 0x05, 0x0d, 0xcb, 0x43, 0x0a, 0x83, 0x81, 0x77,
0x0d, 0x7c, 0x84, 0x05, 0x29, 0x31, 0x01, 0xa7, 0xcc, 0x7c, 0x7f, 0xf3, 0x98, 0xa6, 0xa0, 0x21,
0xa3, 0xec, 0x38, 0x92, 0xd9, 0x1b, 0xe0, 0x5a, 0xc8, 0x79, 0x38, 0x25, 0xae, 0xde, 0x8d, 0xd2,
0x03, 0x17, 0xb3, 0xac, 0xf8, 0x94, 0xc7, 0xf0, 0x73, 0xae, 0x26, 0x37, 0xbd, 0xe9, 0x7d, 0x69,
0x41, 0x75, 0x7f, 0x86, 0x36, 0xa1, 0x36, 0xe2, 0xe3, 0xac, 0x6d, 0x6d, 0x58, 0xfd, 0x73, 0x5b,
0x6b, 0xce, 0xdf, 0xf2, 0x77, 0xf6, 0x67, 0x43, 0x3e, 0xce, 0x3c, 0x0d, 0x43, 0x37, 0xa1, 0x85,
0x53, 0x39, 0xf1, 0x29, 0x3b, 0xe0, 0xed, 0xaa, 0xf6, 0x59, 0x3f, 0xc5, 0x67, 0x90, 0xca, 0xc9,
0x1d, 0x76, 0xc0, 0xbd, 0x26, 0x36, 0x2b, 0xd4, 0x05, 0x50, 0xb4, 0xb1, 0x4c, 0x13, 0x22, 0xda,
0xf6, 0x86, 0xdd, 0x5f, 0xf4, 0x4e, 0x58, 0x7a, 0x0c, 0xea, 0xfb, 0x33, 0x0f, 0x7f, 0x8e, 0x2e,
0x02, 0xa8, 0xa3, 0xfc, 0x51, 0x26, 0x89, 0xd0, 0xbc, 0x16, 0xbd, 0x96, 0xb2, 0x0c, 0x95, 0x01,
0xbd, 0x01, 0x17, 0x4a, 0x06, 0x06, 0x53, 0xd5, 0x98, 0xa5, 0xe2, 0xa8, 0x1c, 0x77, 0xd6, 0x79,
0x5f, 0x59, 0xb0, 0xb0, 0x47, 0x43, 0xb6, 0xcd, 0x83, 0xff, 0xea, 0xc8, 0x35, 0x68, 0x06, 0x13,
0x4c, 0x99, 0x4f, 0xc7, 0x6d, 0x7b, 0xc3, 0xea, 0xb7, 0xbc, 0x05, 0xbd, 0xbf, 0x33, 0x46, 0x57,
0xe0, 0x3c, 0x0e, 0x02, 0x9e, 0x32, 0xe9, 0xb3, 0x34, 0x1a, 0x91, 0xa4, 0x5d, 0xdb, 0xb0, 0xfa,
0x35, 0x6f, 0xc9, 0x58, 0x3f, 0xd6, 0xc6, 0xde, 0x1f, 0x16, 0x2c, 0x1b, 0x52, 0xdb, 0x34, 0x21,
0x81, 0x1c, 0xa4, 0xb3, 0xb3, 0xd8, 0xdd, 0x00, 0x88, 0xd3, 0xd1, 0x94, 0x06, 0xfe, 0x03, 0x92,
0x99, 0x3b, 0x59, 0x75, 0x72, 0x4d, 0x38, 0x85, 0x26, 0x9c, 0x01, 0xcb, 0xbc, 0x56, 0x8e, 0xbb,
0x4b, 0xb2, 0x7f, 0x4f, 0x15, 0x75, 0xa0, 0x29, 0xc8, 0xa7, 0x29, 0x61, 0x01, 0x69, 0xd7, 0x35,
0xa0, 0xdc, 0xa3, 0xb7, 0xc0, 0x96, 0x34, 0x6e, 0x37, 0x34, 0x97, 0xd7, 0x4e, 0xd3, 0x14, 0x8d,
0x87, 0xd5, 0xb6, 0xe5, 0x29, 0x58, 0xef, 0xeb, 0x2a, 0x34, 0x72, 0x91, 0xa1, 0x6b, 0xd0, 0x8c,
0x88, 0x10, 0x38, 0xd4, 0x89, 0xda, 0x2f, 0xcd, 0xa4, 0x44, 0x21, 0x04, 0xb5, 0x88, 0x44, 0xb9,
0x16, 0x5b, 0x9e, 0x5e, 0xab, 0x0c, 0x24, 0x8d, 0x08, 0x4f, 0xa5, 0x3f, 0x21, 0x34, 0x9c, 0x48,
0x9d, 0x62, 0xcd, 0x5b, 0x32, 0xd6, 0x5d, 0x6d, 0x44, 0x43, 0x58, 0x21, 0x33, 0x49, 0x98, 0xa0,
0x9c, 0xf9, 0x3c, 0x96, 0x94, 0x33, 0xd1, 0xfe, 0x73, 0x61, 0xce, 0xb1, 0xcb, 0x25, 0xfe, 0x93,
0x1c, 0x8e, 0xee, 0x43, 0x97, 0x71, 0xe6, 0x07, 0x09, 0x95, 0x34, 0xc0, 0x53, 0xff, 0x94, 0x80,
0x17, 0xe6, 0x04, 0x5c, 0x67, 0x9c, 0xdd, 0x36, 0xbe, 0x1f, 0xbe, 0x10, 0xbb, 0xf7, 0x9d, 0x05,
0xcd, 0xe2, 0x21, 0xa1, 0x0f, 0x60, 0x51, 0x89, 0x97, 0x24, 0x5a, 0x85, 0x45, 0x75, 0x2e, 0x9e,
0x52, 0xdb, 0x3d, 0x0d, 0xd3, 0xaf, 0xef, 0x9c, 0x28, 0xd7, 0x02, 0xf5, 0xc1, 0x3e, 0x20, 0xc4,
0x08, 0xe4, 0xb4, 0x4b, 0xd9, 0x21, 0xc4, 0x53, 0x90, 0xe2, 0xfa, 0xec, 0x57, 0xbb, 0xbe, 0x6f,
0x2c, 0x80, 0xe3, 0x33, 0x5f, 0x90, 0xa3, 0xf5, 0x6a, 0x72, 0xbc, 0x09, 0xad, 0x88, 0x8f, 0xc9,
0x59, 0x6d, 0xe5, 0x1e, 0x1f, 0x93, 0xbc, 0xad, 0x44, 0x66, 0xf5, 0x9c, 0x0c, 0xed, 0xe7, 0x65,
0xd8, 0x7b, 0x5a, 0x85, 0x66, 0xe1, 0x82, 0xde, 0x85, 0x86, 0xa0, 0x2c, 0x9c, 0x12, 0xc3, 0xa9,
0x37, 0x27, 0xbe, 0xb3, 0xa7, 0x91, 0xbb, 0x15, 0xcf, 0xf8, 0xa0, 0x77, 0xa0, 0xae, 0xdb, 0xb7,
0x21, 0xf7, 0xfa, 0x3c, 0xe7, 0x7b, 0x0a, 0xb8, 0x5b, 0xf1, 0x72, 0x8f, 0xce, 0x00, 0x1a, 0x79,
0x38, 0xf4, 0x36, 0xd4, 0x14, 0x6f, 0x4d, 0xe0, 0xfc, 0xd6, 0xe5, 0x13, 0x31, 0x8a, 0x86, 0x7e,
0xf2, 0x0e, 0x55, 0x3c, 0x4f, 0x3b, 0x74, 0x1e, 0x5a, 0x50, 0xd7, 0x51, 0xd1, 0x5d, 0x68, 0x8e,
0xa8, 0xc4, 0x49, 0x82, 0x8b, 0xda, 0xba, 0x45, 0x98, 0x7c, 0xec, 0x38, 0xe5, 0x94, 0x29, 0x62,
0xdd, 0xe6, 0x51, 0x8c, 0x03, 0x39, 0xa4, 0x72, 0xa0, 0xdc, 0xbc, 0x32, 0x00, 0xba, 0x05, 0x50,
0x56, 0x5d, 0xb5, 0x34, 0xfb, 0xac, 0xb2, 0xb7, 0x8a, 0xb2, 0x8b, 0x61, 0x1d, 0x6c, 0x91, 0x46,
0xbd, 0x2f, 0xaa, 0x60, 0xef, 0x10, 0x82, 0x32, 0x68, 0xe0, 0x48, 0x75, 0x07, 0x23, 0xcc, 0x72,
0x90, 0xa8, 0xe9, 0x76, 0x82, 0x0a, 0x65, 0xc3, 0x9d, 0xc7, 0xbf, 0x5d, 0xaa, 0xfc, 0xf0, 0xfb,
0xa5, 0x7e, 0x48, 0xe5, 0x24, 0x1d, 0x39, 0x01, 0x8f, 0xdc, 0x62, 0x72, 0xea, 0xbf, 0x4d, 0x31,
0x7e, 0xe0, 0xca, 0x2c, 0x26, 0x42, 0x3b, 0x88, 0x6f, 0x9f, 0x3d, 0xba, 0xba, 0x38, 0x25, 0x21,
0x0e, 0x32, 0x5f, 0xcd, 0x47, 0xf1, 0xfd, 0xb3, 0x47, 0x57, 0x2d, 0xcf, 0x1c, 0x88, 0xd6, 0xa1,
0x15, 0x62, 0xe1, 0x4f, 0x69, 0x44, 0xa5, 0xbe, 0x9e, 0x9a, 0xd7, 0x0c, 0xb1, 0xf8, 0x48, 0xed,
0x91, 0x03, 0xf5, 0x18, 0x67, 0x24, 0xc9, 0x9b, 0xdc, 0xb0, 0xfd, 0xcb, 0x8f, 0x9b, 0xab, 0x86,
0xd9, 0x60, 0x3c, 0x4e, 0x88, 0x10, 0x7b, 0x32, 0xa1, 0x2c, 0xf4, 0x72, 0x18, 0xda, 0x82, 0x85,
0x30, 0xc1, 0x4c, 0x9a, 0xae, 0x37, 0xcf, 0xa3, 0x00, 0xf6, 0x7e, 0xb2, 0xc0, 0xde, 0xa7, 0xf1,
0xff, 0x59, 0x83, 0x6b, 0xd0, 0x90, 0x34, 0x8e, 0x49, 0x92, 0xf7, 0xc1, 0x39, 0xac, 0x0d, 0xee,
0x56, 0xb5, 0x6d, 0xf5, 0x7e, 0xb6, 0x60, 0x69, 0x90, 0xce, 0xf2, 0xc7, 0xbb, 0x8d, 0x25, 0x56,
0xe9, 0xe3, 0x1c, 0xae, 0xd5, 0x35, 0x37, 0x7d, 0x03, 0x44, 0xef, 0x41, 0x53, 0xc9, 0xd7, 0x1f,
0xf3, 0xc0, 0xbc, 0x8e, 0xcb, 0x2f, 0xe9, 0x4a, 0x27, 0xa7, 0x9a, 0xb7, 0x20, 0xcc, 0xf0, 0x2d,
0x5e, 0x85, 0xfd, 0x0f, 0x5f, 0x05, 0x5a, 0x06, 0x5b, 0xd0, 0x50, 0xdf, 0xd3, 0xa2, 0xa7, 0x96,
0xc3, 0xf7, 0x1f, 0x1f, 0x76, 0xad, 0x27, 0x87, 0x5d, 0xeb, 0xe9, 0x61, 0xd7, 0x7a, 0x78, 0xd4,
0xad, 0x3c, 0x39, 0xea, 0x56, 0x7e, 0x3d, 0xea, 0x56, 0xee, 0x5f, 0x39, 0xbb, 0xd0, 0xae, 0x9c,
0x8d, 0x1a, 0xba, 0x41, 0xdd, 0xf8, 0x2b, 0x00, 0x00, 0xff, 0xff, 0x24, 0x52, 0x64, 0xe6, 0x23,
0x0a, 0x00, 0x00,
// 1076 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xc4, 0x55, 0x41, 0x6f, 0x1b, 0xc5,
0x17, 0xf7, 0x7a, 0x6d, 0xc7, 0x7e, 0x4d, 0xda, 0x64, 0x14, 0xfd, 0xe5, 0x38, 0xff, 0xba, 0xc1,
0x55, 0xc1, 0xaa, 0xc8, 0x6e, 0x9b, 0x1e, 0x28, 0x15, 0x02, 0xec, 0x86, 0x28, 0x55, 0x29, 0x48,
0x93, 0x9c, 0x7a, 0x59, 0x8d, 0x77, 0x27, 0xeb, 0x51, 0xbd, 0x33, 0xcb, 0xce, 0x2c, 0x78, 0x8f,
0x7c, 0x00, 0xa4, 0x8a, 0x0b, 0x12, 0x67, 0x0e, 0x88, 0x53, 0x25, 0x10, 0x9f, 0xa1, 0x27, 0x54,
0x71, 0xe2, 0x04, 0x55, 0x72, 0xe8, 0x9d, 0x2f, 0x00, 0xda, 0xd9, 0x59, 0x27, 0x2d, 0xa9, 0x53,
0x04, 0x12, 0x17, 0x7b, 0xe6, 0xed, 0xef, 0xbd, 0xf9, 0xbd, 0x37, 0xbf, 0x79, 0x0f, 0x3a, 0xbe,
0x90, 0x91, 0x90, 0xae, 0x9a, 0xba, 0x9f, 0x5e, 0x1f, 0x51, 0x45, 0xae, 0xbb, 0x6a, 0xea, 0xc4,
0x89, 0x50, 0x02, 0xad, 0x14, 0xdf, 0x1c, 0x35, 0x75, 0xcc, 0xb7, 0xce, 0x0a, 0x89, 0x18, 0x17,
0xae, 0xfe, 0x2d, 0x50, 0x9d, 0xd5, 0x50, 0x84, 0x42, 0x2f, 0xdd, 0x7c, 0x65, 0xac, 0x9b, 0x26,
0xae, 0x9f, 0x64, 0xb1, 0x12, 0x6e, 0x94, 0x4e, 0x14, 0x93, 0x2c, 0x9c, 0x1d, 0x52, 0x1a, 0x0c,
0xbc, 0x6b, 0xe0, 0x23, 0x22, 0xe9, 0x0c, 0xe3, 0x0b, 0xc6, 0xcd, 0xf7, 0x37, 0x8e, 0x69, 0x4a,
0x16, 0x72, 0xc6, 0x8f, 0x23, 0x99, 0xbd, 0x01, 0xae, 0x85, 0x42, 0x84, 0x13, 0xea, 0xea, 0xdd,
0x28, 0x3d, 0x70, 0x09, 0xcf, 0xca, 0x4f, 0x45, 0x0c, 0xaf, 0xe0, 0x6a, 0x72, 0xd3, 0x9b, 0xde,
0x17, 0x16, 0x54, 0xf7, 0xa7, 0x68, 0x13, 0x6a, 0x23, 0x11, 0x64, 0x6d, 0x6b, 0xc3, 0xea, 0x9f,
0xdb, 0x5a, 0x73, 0xfe, 0x92, 0xbf, 0xb3, 0x3f, 0x1d, 0x8a, 0x20, 0xc3, 0x1a, 0x86, 0x6e, 0x42,
0x8b, 0xa4, 0x6a, 0xec, 0x31, 0x7e, 0x20, 0xda, 0x55, 0xed, 0xb3, 0x7e, 0x8a, 0xcf, 0x20, 0x55,
0xe3, 0x3b, 0xfc, 0x40, 0xe0, 0x26, 0x31, 0x2b, 0xd4, 0x05, 0xc8, 0x69, 0x13, 0x95, 0x26, 0x54,
0xb6, 0xed, 0x0d, 0xbb, 0xbf, 0x88, 0x4f, 0x58, 0x7a, 0x1c, 0xea, 0xfb, 0x53, 0x4c, 0x3e, 0x43,
0x17, 0x01, 0xf2, 0xa3, 0xbc, 0x51, 0xa6, 0xa8, 0xd4, 0xbc, 0x16, 0x71, 0x2b, 0xb7, 0x0c, 0x73,
0x03, 0x7a, 0x1d, 0x2e, 0xcc, 0x18, 0x18, 0x4c, 0x55, 0x63, 0x96, 0xca, 0xa3, 0x0a, 0xdc, 0x59,
0xe7, 0x7d, 0x69, 0xc1, 0xc2, 0x1e, 0x0b, 0xf9, 0xb6, 0xf0, 0xff, 0xad, 0x23, 0xd7, 0xa0, 0xe9,
0x8f, 0x09, 0xe3, 0x1e, 0x0b, 0xda, 0xf6, 0x86, 0xd5, 0x6f, 0xe1, 0x05, 0xbd, 0xbf, 0x13, 0xa0,
0x2b, 0x70, 0x9e, 0xf8, 0xbe, 0x48, 0xb9, 0xf2, 0x78, 0x1a, 0x8d, 0x68, 0xd2, 0xae, 0x6d, 0x58,
0xfd, 0x1a, 0x5e, 0x32, 0xd6, 0x8f, 0xb4, 0xb1, 0xf7, 0xbb, 0x05, 0xcb, 0x86, 0xd4, 0x36, 0x4b,
0xa8, 0xaf, 0x06, 0xe9, 0xf4, 0x2c, 0x76, 0x37, 0x00, 0xe2, 0x74, 0x34, 0x61, 0xbe, 0xf7, 0x80,
0x66, 0xe6, 0x4e, 0x56, 0x9d, 0x42, 0x13, 0x4e, 0xa9, 0x09, 0x67, 0xc0, 0x33, 0xdc, 0x2a, 0x70,
0x77, 0x69, 0xf6, 0xcf, 0xa9, 0xa2, 0x0e, 0x34, 0x25, 0xfd, 0x24, 0xa5, 0xdc, 0xa7, 0xed, 0xba,
0x06, 0xcc, 0xf6, 0xe8, 0x4d, 0xb0, 0x15, 0x8b, 0xdb, 0x0d, 0xcd, 0xe5, 0x7f, 0xa7, 0x69, 0x8a,
0xc5, 0xc3, 0x6a, 0xdb, 0xc2, 0x39, 0xac, 0xf7, 0x7d, 0x15, 0x1a, 0x85, 0xc8, 0xd0, 0x35, 0x68,
0x46, 0x54, 0x4a, 0x12, 0xea, 0x44, 0xed, 0x97, 0x66, 0x32, 0x43, 0x21, 0x04, 0xb5, 0x88, 0x46,
0x85, 0x16, 0x5b, 0x58, 0xaf, 0xf3, 0x0c, 0x14, 0x8b, 0xa8, 0x48, 0x95, 0x37, 0xa6, 0x2c, 0x1c,
0x2b, 0x9d, 0x62, 0x0d, 0x2f, 0x19, 0xeb, 0xae, 0x36, 0xa2, 0xff, 0x43, 0x2b, 0xe5, 0x22, 0x09,
0x68, 0x42, 0x03, 0x9d, 0x63, 0x13, 0x1f, 0x1b, 0xd0, 0x10, 0x56, 0xe8, 0x54, 0x51, 0x2e, 0x99,
0xe0, 0x9e, 0x88, 0x15, 0x13, 0x5c, 0xb6, 0xff, 0x58, 0x98, 0x43, 0x6a, 0x79, 0x86, 0xff, 0xb8,
0x80, 0xa3, 0xfb, 0xd0, 0xe5, 0x82, 0x7b, 0x7e, 0xc2, 0x14, 0xf3, 0xc9, 0xc4, 0x3b, 0x25, 0xe0,
0x85, 0x39, 0x01, 0xd7, 0xb9, 0xe0, 0xb7, 0x8d, 0xef, 0x07, 0x2f, 0xc4, 0xee, 0x7d, 0x63, 0x41,
0xb3, 0x7c, 0x66, 0xe8, 0x7d, 0x58, 0xcc, 0xa5, 0x4d, 0x13, 0xad, 0xd1, 0xb2, 0x76, 0x17, 0x4f,
0xa9, 0xfc, 0x9e, 0x86, 0xe9, 0xb7, 0x79, 0x4e, 0xce, 0xd6, 0x12, 0xf5, 0xc1, 0x3e, 0xa0, 0xd4,
0xc8, 0xe7, 0xb4, 0x2b, 0xdb, 0xa1, 0x14, 0xe7, 0x90, 0xf2, 0x72, 0xed, 0x57, 0xbb, 0xdc, 0xaf,
0x2c, 0x80, 0xe3, 0x33, 0x5f, 0x10, 0xab, 0xf5, 0x6a, 0x62, 0xbd, 0x09, 0xad, 0x48, 0x04, 0xf4,
0xac, 0xa6, 0x73, 0x4f, 0x04, 0xb4, 0x68, 0x3a, 0x91, 0x59, 0x3d, 0x27, 0x52, 0xfb, 0x79, 0x91,
0xf6, 0x9e, 0x56, 0xa1, 0x59, 0xba, 0xa0, 0x77, 0xa0, 0x21, 0x19, 0x0f, 0x27, 0xd4, 0x70, 0xea,
0xcd, 0x89, 0xef, 0xec, 0x69, 0xe4, 0x6e, 0x05, 0x1b, 0x1f, 0xf4, 0x36, 0xd4, 0x75, 0x73, 0x37,
0xe4, 0x5e, 0x9b, 0xe7, 0x7c, 0x2f, 0x07, 0xee, 0x56, 0x70, 0xe1, 0xd1, 0x19, 0x40, 0xa3, 0x08,
0x87, 0xde, 0x82, 0x5a, 0xce, 0x5b, 0x13, 0x38, 0xbf, 0x75, 0xf9, 0x44, 0x8c, 0xb2, 0xdd, 0x9f,
0xbc, 0xc3, 0x3c, 0x1e, 0xd6, 0x0e, 0x9d, 0x87, 0x16, 0xd4, 0x75, 0x54, 0x74, 0x17, 0x9a, 0x23,
0xa6, 0x48, 0x92, 0x90, 0xb2, 0xb6, 0x6e, 0x19, 0xa6, 0x18, 0x4a, 0xce, 0x6c, 0x06, 0x95, 0xb1,
0x6e, 0x8b, 0x28, 0x26, 0xbe, 0x1a, 0x32, 0x35, 0xc8, 0xdd, 0xf0, 0x2c, 0x00, 0xba, 0x05, 0x30,
0xab, 0x7a, 0xde, 0xf0, 0xec, 0xb3, 0xca, 0xde, 0x2a, 0xcb, 0x2e, 0x87, 0x75, 0xb0, 0x65, 0x1a,
0xf5, 0x3e, 0xaf, 0x82, 0xbd, 0x43, 0x29, 0xca, 0xa0, 0x41, 0xa2, 0xbc, 0x77, 0x18, 0x61, 0xce,
0xc6, 0x4c, 0x3e, 0xfb, 0x4e, 0x50, 0x61, 0x7c, 0xb8, 0xf3, 0xf8, 0xd7, 0x4b, 0x95, 0xef, 0x7e,
0xbb, 0xd4, 0x0f, 0x99, 0x1a, 0xa7, 0x23, 0xc7, 0x17, 0x91, 0x5b, 0xce, 0x55, 0xfd, 0xb7, 0x29,
0x83, 0x07, 0xae, 0xca, 0x62, 0x2a, 0xb5, 0x83, 0xfc, 0xfa, 0xd9, 0xa3, 0xab, 0x8b, 0x13, 0x1a,
0x12, 0x3f, 0xf3, 0xf2, 0xe9, 0x29, 0xbf, 0x7d, 0xf6, 0xe8, 0xaa, 0x85, 0xcd, 0x81, 0x68, 0x1d,
0x5a, 0x21, 0x91, 0xde, 0x84, 0x45, 0x4c, 0xe9, 0xeb, 0xa9, 0xe1, 0x66, 0x48, 0xe4, 0x87, 0xf9,
0x1e, 0x39, 0x50, 0x8f, 0x49, 0x46, 0x93, 0xa2, 0x05, 0x0e, 0xdb, 0x3f, 0xff, 0xb0, 0xb9, 0x6a,
0x98, 0x0d, 0x82, 0x20, 0xa1, 0x52, 0xee, 0xa9, 0x84, 0xf1, 0x10, 0x17, 0x30, 0xb4, 0x05, 0x0b,
0x61, 0x42, 0xb8, 0x32, 0x3d, 0x71, 0x9e, 0x47, 0x09, 0xec, 0xfd, 0x68, 0x81, 0xbd, 0xcf, 0xe2,
0xff, 0xb2, 0x06, 0xd7, 0xa0, 0xa1, 0x58, 0x1c, 0xd3, 0xa4, 0xe8, 0x92, 0x73, 0x58, 0x1b, 0xdc,
0xad, 0x6a, 0xdb, 0xea, 0xfd, 0x64, 0xc1, 0xd2, 0x20, 0x9d, 0x16, 0x8f, 0x77, 0x9b, 0x28, 0x92,
0xa7, 0x4f, 0x0a, 0xb8, 0x56, 0xd7, 0xdc, 0xf4, 0x0d, 0x10, 0xbd, 0x0b, 0xcd, 0x5c, 0xbe, 0x5e,
0x20, 0x7c, 0xf3, 0x3a, 0x2e, 0xbf, 0xa4, 0x2b, 0x9d, 0x9c, 0x79, 0x78, 0x41, 0x9a, 0xd1, 0x5c,
0xbe, 0x0a, 0xfb, 0x6f, 0xbe, 0x0a, 0xb4, 0x0c, 0xb6, 0x64, 0xa1, 0xbe, 0xa7, 0x45, 0x9c, 0x2f,
0x87, 0xef, 0x3d, 0x3e, 0xec, 0x5a, 0x4f, 0x0e, 0xbb, 0xd6, 0xd3, 0xc3, 0xae, 0xf5, 0xf0, 0xa8,
0x5b, 0x79, 0x72, 0xd4, 0xad, 0xfc, 0x72, 0xd4, 0xad, 0xdc, 0xbf, 0x72, 0x76, 0xa1, 0x5d, 0x35,
0x1d, 0x35, 0x74, 0x83, 0xba, 0xf1, 0x67, 0x00, 0x00, 0x00, 0xff, 0xff, 0xb1, 0x48, 0xf7, 0x44,
0x41, 0x0a, 0x00, 0x00,
}
func (m *Tx) Marshal() (dAtA []byte, err error) {
@ -1364,6 +1389,16 @@ func (m *TxBody) MarshalToSizedBuffer(dAtA []byte) (int, error) {
dAtA[i] = 0xfa
}
}
if m.Unordered {
i--
if m.Unordered {
dAtA[i] = 1
} else {
dAtA[i] = 0
}
i--
dAtA[i] = 0x20
}
if m.TimeoutHeight != 0 {
i = encodeVarintTx(dAtA, i, uint64(m.TimeoutHeight))
i--
@ -1942,6 +1977,9 @@ func (m *TxBody) Size() (n int) {
if m.TimeoutHeight != 0 {
n += 1 + sovTx(uint64(m.TimeoutHeight))
}
if m.Unordered {
n += 2
}
if len(m.ExtensionOptions) > 0 {
for _, e := range m.ExtensionOptions {
l = e.Size()
@ -2955,6 +2993,26 @@ func (m *TxBody) Unmarshal(dAtA []byte) error {
break
}
}
case 4:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field Unordered", wireType)
}
var v int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowTx
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
v |= int(b&0x7F) << shift
if b < 0x80 {
break
}
}
m.Unordered = bool(v != 0)
case 1023:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field ExtensionOptions", wireType)

View File

@ -79,6 +79,14 @@ type (
GetTimeoutHeight() uint64
}
// TxWithUnordered extends the Tx interface by allowing a transaction to set
// the unordered field, which implicitly relies on TxWithTimeoutHeight.
TxWithUnordered interface {
TxWithTimeoutHeight
GetUnordered() bool
}
// HasValidateBasic defines a type that has a ValidateBasic method.
// ValidateBasic is deprecated and now facultative.
// Prefer validating messages directly in the msg server.

View File

@ -146,9 +146,14 @@ func (spkd SetPubKeyDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate b
return next(ctx, tx, simulate)
}
// SigVerificationDecorator verifies all signatures for a tx and return an error if any are invalid. Note,
// the SigVerificationDecorator will not check signatures on ReCheck.
// It will also increase the sequence number, and consume gas for signature verification.
// SigVerificationDecorator verifies all signatures for a tx and returns an
// error if any are invalid. Note, the SigVerificationDecorator will not check
// signatures on ReCheckTx. It will also increase the sequence number, and consume
// gas for signature verification.
//
// In cases where unordered or parallel transactions are desired, it is recommended
// to to set unordered=true with a reasonable timeout_height value, in which case
// this nonce verification and increment will be skipped.
//
// CONTRACT: Pubkeys are set in context for all signers before this decorator runs
// CONTRACT: Tx must implement SigVerifiableTx interface
@ -277,11 +282,15 @@ func (svd SigVerificationDecorator) authenticate(ctx sdk.Context, tx sdk.Tx, sim
return err
}
err = svd.increaseSequence(ctx, acc)
if err != nil {
return err
// Bypass incrementing sequence for transactions with unordered set to true.
// The actual parameters of the un-ordered tx will be checked in a separate
// decorator.
unorderedTx, ok := tx.(sdk.TxWithUnordered)
if ok && unorderedTx.GetUnordered() {
return nil
}
return nil
return svd.increaseSequence(ctx, acc)
}
// consumeSignatureGas will consume gas according to the pub-key being verified.
@ -419,8 +428,7 @@ func (vscd ValidateSigCountDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, sim
for _, pk := range pubKeys {
sigCount += CountSubKeys(pk)
if uint64(sigCount) > params.TxSigLimit {
return ctx, errorsmod.Wrapf(sdkerrors.ErrTooManySignatures,
"signatures: %d, limit: %d", sigCount, params.TxSigLimit)
return ctx, errorsmod.Wrapf(sdkerrors.ErrTooManySignatures, "signatures: %d, limit: %d", sigCount, params.TxSigLimit)
}
}
@ -430,10 +438,9 @@ func (vscd ValidateSigCountDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, sim
// DefaultSigVerificationGasConsumer is the default implementation of SignatureVerificationGasConsumer. It consumes gas
// for signature verification based upon the public key type. The cost is fetched from the given params and is matched
// by the concrete type.
func DefaultSigVerificationGasConsumer(
meter storetypes.GasMeter, sig signing.SignatureV2, params types.Params,
) error {
func DefaultSigVerificationGasConsumer(meter storetypes.GasMeter, sig signing.SignatureV2, params types.Params) error {
pubkey := sig.PubKey
switch pubkey := pubkey.(type) {
case *ed25519.PubKey:
meter.ConsumeGas(params.SigVerifyCostED25519, "ante verify: ed25519")
@ -452,10 +459,12 @@ func DefaultSigVerificationGasConsumer(
if !ok {
return fmt.Errorf("expected %T, got, %T", &signing.MultiSignatureData{}, sig.Data)
}
err := ConsumeMultisignatureVerificationGas(meter, multisignature, pubkey, params, sig.Sequence)
if err != nil {
return err
}
return nil
default:
@ -480,10 +489,12 @@ func ConsumeMultisignatureVerificationGas(
Data: sig.Signatures[sigIndex],
Sequence: accSeq,
}
err := DefaultSigVerificationGasConsumer(meter, sigV2, params)
if err != nil {
return err
}
sigIndex++
}
@ -507,6 +518,7 @@ func CountSubKeys(pub cryptotypes.PubKey) int {
if pub == nil {
return 0
}
v, ok := pub.(*kmultisig.LegacyAminoPubKey)
if !ok {
return 1
@ -532,6 +544,7 @@ func signatureDataToBz(data signing.SignatureData) ([][]byte, error) {
switch data := data.(type) {
case *signing.SingleSignatureData:
return [][]byte{data.Signature}, nil
case *signing.MultiSignatureData:
sigs := [][]byte{}
var err error
@ -541,19 +554,22 @@ func signatureDataToBz(data signing.SignatureData) ([][]byte, error) {
if err != nil {
return nil, err
}
sigs = append(sigs, nestedSigs...)
}
multiSignature := cryptotypes.MultiSignature{
Signatures: sigs,
}
aggregatedSig, err := multiSignature.Marshal()
if err != nil {
return nil, err
}
sigs = append(sigs, aggregatedSig)
sigs = append(sigs, aggregatedSig)
return sigs, nil
default:
return nil, sdkerrors.ErrInvalidType.Wrapf("unexpected signature data type %T", data)
}

74
x/auth/ante/unordered.go Normal file
View File

@ -0,0 +1,74 @@
package ante
import (
"crypto/sha256"
errorsmod "cosmossdk.io/errors"
"cosmossdk.io/x/auth/ante/unorderedtx"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)
var _ sdk.AnteDecorator = (*UnorderedTxDecorator)(nil)
// UnorderedTxDecorator defines an AnteHandler decorator that is responsible for
// checking if a transaction is intended to be unordered and if so, evaluates
// the transaction accordingly. An unordered transaction will bypass having it's
// nonce incremented, which allows fire-and-forget along with possible parallel
// transaction processing, without having to deal with nonces.
//
// The transaction sender must ensure that unordered=true and a timeout_height
// is appropriately set. The AnteHandler will check that the transaction is not
// a duplicate and will evict it from memory when the timeout is reached.
//
// The UnorderedTxDecorator should be placed as early as possible in the AnteHandler
// chain to ensure that during DeliverTx, the transaction is added to the UnorderedTxManager.
type UnorderedTxDecorator struct {
// maxUnOrderedTTL defines the maximum TTL a transaction can define.
maxUnOrderedTTL uint64
txManager *unorderedtx.Manager
}
func NewUnorderedTxDecorator(maxTTL uint64, m *unorderedtx.Manager) *UnorderedTxDecorator {
return &UnorderedTxDecorator{
maxUnOrderedTTL: maxTTL,
txManager: m,
}
}
func (d *UnorderedTxDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) {
unorderedTx, ok := tx.(sdk.TxWithUnordered)
if !ok || !unorderedTx.GetUnordered() {
// If the transaction does not implement unordered capabilities or has the
// unordered value as false, we bypass.
return next(ctx, tx, simulate)
}
// TTL is defined as a specific block height at which this tx is no longer valid
ttl := unorderedTx.GetTimeoutHeight()
if ttl == 0 {
return ctx, errorsmod.Wrap(sdkerrors.ErrInvalidRequest, "unordered transaction must have timeout_height set")
}
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)
}
txHash := sha256.Sum256(ctx.TxBytes())
// check for duplicates
if d.txManager.Contains(txHash) {
return ctx, errorsmod.Wrap(sdkerrors.ErrInvalidRequest, "tx %X is duplicated")
}
if ctx.ExecMode() == sdk.ExecModeFinalize {
// a new tx included in the block, add the hash to the unordered tx manager
d.txManager.Add(txHash, ttl)
}
return next(ctx, tx, simulate)
}

View File

@ -0,0 +1,154 @@
package ante_test
import (
"crypto/sha256"
"testing"
"github.com/stretchr/testify/require"
"cosmossdk.io/x/auth/ante"
"cosmossdk.io/x/auth/ante/unorderedtx"
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
"github.com/cosmos/cosmos-sdk/testutil/testdata"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/tx/signing"
)
func TestUnorderedTxDecorator_OrderedTx(t *testing.T) {
txm := unorderedtx.NewManager(t.TempDir())
defer func() {
require.NoError(t, txm.Close())
}()
txm.Start()
chain := sdk.ChainAnteDecorators(ante.NewUnorderedTxDecorator(unorderedtx.DefaultMaxUnOrderedTTL, txm))
tx, txBz := genUnorderedTx(t, false, 0)
ctx := sdk.Context{}.WithTxBytes(txBz).WithBlockHeight(100)
_, err := chain(ctx, tx, false)
require.NoError(t, err)
}
func TestUnorderedTxDecorator_UnorderedTx_NoTTL(t *testing.T) {
txm := unorderedtx.NewManager(t.TempDir())
defer func() {
require.NoError(t, txm.Close())
}()
txm.Start()
chain := sdk.ChainAnteDecorators(ante.NewUnorderedTxDecorator(unorderedtx.DefaultMaxUnOrderedTTL, txm))
tx, txBz := genUnorderedTx(t, true, 0)
ctx := sdk.Context{}.WithTxBytes(txBz).WithBlockHeight(100)
_, err := chain(ctx, tx, false)
require.Error(t, err)
}
func TestUnorderedTxDecorator_UnorderedTx_InvalidTTL(t *testing.T) {
txm := unorderedtx.NewManager(t.TempDir())
defer func() {
require.NoError(t, txm.Close())
}()
txm.Start()
chain := sdk.ChainAnteDecorators(ante.NewUnorderedTxDecorator(unorderedtx.DefaultMaxUnOrderedTTL, txm))
tx, txBz := genUnorderedTx(t, true, 100+unorderedtx.DefaultMaxUnOrderedTTL+1)
ctx := sdk.Context{}.WithTxBytes(txBz).WithBlockHeight(100)
_, err := chain(ctx, tx, false)
require.Error(t, err)
}
func TestUnorderedTxDecorator_UnorderedTx_AlreadyExists(t *testing.T) {
txm := unorderedtx.NewManager(t.TempDir())
defer func() {
require.NoError(t, txm.Close())
}()
txm.Start()
chain := sdk.ChainAnteDecorators(ante.NewUnorderedTxDecorator(unorderedtx.DefaultMaxUnOrderedTTL, txm))
tx, txBz := genUnorderedTx(t, true, 150)
ctx := sdk.Context{}.WithTxBytes(txBz).WithBlockHeight(100)
txHash := sha256.Sum256(txBz)
txm.Add(txHash, 150)
_, err := chain(ctx, tx, false)
require.Error(t, err)
}
func TestUnorderedTxDecorator_UnorderedTx_ValidCheckTx(t *testing.T) {
txm := unorderedtx.NewManager(t.TempDir())
defer func() {
require.NoError(t, txm.Close())
}()
txm.Start()
chain := sdk.ChainAnteDecorators(ante.NewUnorderedTxDecorator(unorderedtx.DefaultMaxUnOrderedTTL, txm))
tx, txBz := genUnorderedTx(t, true, 150)
ctx := sdk.Context{}.WithTxBytes(txBz).WithBlockHeight(100).WithExecMode(sdk.ExecModeCheck)
_, err := chain(ctx, tx, false)
require.NoError(t, err)
}
func TestUnorderedTxDecorator_UnorderedTx_ValidDeliverTx(t *testing.T) {
txm := unorderedtx.NewManager(t.TempDir())
defer func() {
require.NoError(t, txm.Close())
}()
txm.Start()
chain := sdk.ChainAnteDecorators(ante.NewUnorderedTxDecorator(unorderedtx.DefaultMaxUnOrderedTTL, txm))
tx, txBz := genUnorderedTx(t, true, 150)
ctx := sdk.Context{}.WithTxBytes(txBz).WithBlockHeight(100).WithExecMode(sdk.ExecModeFinalize)
_, err := chain(ctx, tx, false)
require.NoError(t, err)
txHash := sha256.Sum256(txBz)
require.True(t, txm.Contains(txHash))
}
func genUnorderedTx(t *testing.T, unordered bool, ttl uint64) (sdk.Tx, []byte) {
t.Helper()
s := SetupTestSuite(t, true)
s.txBuilder = s.clientCtx.TxConfig.NewTxBuilder()
// keys and addresses
priv1, _, addr1 := testdata.KeyTestPubAddr()
// msg and signatures
msg := testdata.NewTestMsg(addr1)
feeAmount := testdata.NewTestFeeAmount()
gasLimit := testdata.NewTestGasLimit()
require.NoError(t, s.txBuilder.SetMsgs(msg))
s.txBuilder.SetFeeAmount(feeAmount)
s.txBuilder.SetGasLimit(gasLimit)
s.txBuilder.SetUnordered(unordered)
s.txBuilder.SetTimeoutHeight(ttl)
privKeys, accNums, accSeqs := []cryptotypes.PrivKey{priv1}, []uint64{0}, []uint64{0}
tx, err := s.CreateTestTx(s.ctx, privKeys, accNums, accSeqs, s.ctx.ChainID(), signing.SignMode_SIGN_MODE_DIRECT)
require.NoError(t, err)
txBz, err := s.encCfg.TxConfig.TxEncoder()(tx)
require.NoError(t, err)
return tx, txBz
}

View File

@ -0,0 +1,285 @@
package unorderedtx
import (
"bufio"
"bytes"
"context"
"encoding/binary"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"sync"
"time"
"golang.org/x/exp/maps"
)
const (
// DefaultMaxUnOrderedTTL defines the default maximum TTL an un-ordered transaction
// can set.
DefaultMaxUnOrderedTTL = 1024
dirName = "unordered_txs"
fileName = "data"
)
// TxHash defines a transaction hash type alias, which is a fixed array of 32 bytes.
type TxHash [32]byte
// Manager contains the tx hash dictionary for duplicates checking, and expire
// them when block production progresses.
type Manager struct {
// blockCh defines a channel to receive newly committed block heights
blockCh chan uint64
// doneCh allows us to ensure the purgeLoop has gracefully terminated prior to closing
doneCh chan struct{}
// dataDir defines the directory to store unexpired unordered transactions
//
// XXX: Note, ideally we avoid the need to store unexpired unordered transactions
// directly to file. However, store v1 does not allow such a primitive. But,
// once store v2 is fully integrated, we can remove manual file handling and
// store the unexpired unordered transactions directly to SS.
//
// Ref: https://github.com/cosmos/cosmos-sdk/issues/18467
dataDir string
mu sync.RWMutex
// txHashes defines a map from tx hash -> TTL value, which is used for duplicate
// checking and replay protection, as well as purging the map when the TTL is
// expired.
txHashes map[TxHash]uint64
}
func NewManager(dataDir string) *Manager {
path := filepath.Join(dataDir, dirName)
if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
_ = os.Mkdir(path, os.ModePerm)
}
m := &Manager{
dataDir: dataDir,
blockCh: make(chan uint64, 16),
doneCh: make(chan struct{}),
txHashes: make(map[TxHash]uint64),
}
return m
}
func (m *Manager) Start() {
go m.purgeLoop()
}
// Close must be called when a node gracefully shuts down. Typically, this should
// be called in an application's Close() function, which is called by the server.
// Note, Start() must be called in order for Close() to not hang.
//
// It will free all necessary resources as well as writing all unexpired unordered
// transactions along with their TTL values to file.
func (m *Manager) Close() error {
close(m.blockCh)
<-m.doneCh
m.blockCh = nil
return m.flushToFile()
}
func (m *Manager) Contains(hash TxHash) bool {
m.mu.RLock()
defer m.mu.RUnlock()
_, ok := m.txHashes[hash]
return ok
}
func (m *Manager) Size() int {
m.mu.RLock()
defer m.mu.RUnlock()
return len(m.txHashes)
}
func (m *Manager) Add(txHash TxHash, ttl uint64) {
m.mu.Lock()
defer m.mu.Unlock()
m.txHashes[txHash] = ttl
}
// OnInit must be called when a node starts up. Typically, this should be called
// in an application's constructor, which is called by the server.
func (m *Manager) OnInit() error {
f, err := os.Open(filepath.Join(m.dataDir, dirName, fileName))
if err != nil {
if errors.Is(err, os.ErrNotExist) {
// File does not exist, which we can assume that there are no unexpired
// unordered transactions.
return nil
}
return fmt.Errorf("failed to open unconfirmed txs file: %w", err)
}
defer f.Close()
var (
r = bufio.NewReader(f)
buf = make([]byte, chunkSize)
)
for {
n, err := io.ReadFull(r, buf)
if err != nil {
if errors.Is(err, io.EOF) {
break
} else {
return fmt.Errorf("failed to read unconfirmed txs file: %w", err)
}
}
if n != 32+8 {
return fmt.Errorf("read unexpected number of bytes from unconfirmed txs file: %d", n)
}
var txHash TxHash
copy(txHash[:], buf[:txHashSize])
m.Add(txHash, binary.BigEndian.Uint64(buf[txHashSize:]))
}
return nil
}
// OnNewBlock sends the latest block number to the background purge loop, which
// should be called in ABCI Commit event.
func (m *Manager) OnNewBlock(blockHeight uint64) {
m.blockCh <- blockHeight
}
func (m *Manager) exportSnapshot(height uint64, snapshotWriter func([]byte) error) error {
var buf bytes.Buffer
w := bufio.NewWriter(&buf)
keys := maps.Keys(m.txHashes)
sort.Slice(keys, func(i, j int) bool { return bytes.Compare(keys[i][:], keys[j][:]) < 0 })
for _, txHash := range keys {
ttl := m.txHashes[txHash]
if height > ttl {
// skip expired txs that have yet to be purged
continue
}
chunk := unorderedTxToBytes(txHash, ttl)
if _, err := w.Write(chunk); err != nil {
return fmt.Errorf("failed to write unordered tx to buffer: %w", err)
}
}
if err := w.Flush(); err != nil {
return fmt.Errorf("failed to flush unordered txs buffer: %w", err)
}
return snapshotWriter(buf.Bytes())
}
// flushToFile writes all unexpired unordered transactions along with their TTL
// to file, overwriting the existing file if it exists.
func (m *Manager) flushToFile() error {
f, err := os.Create(filepath.Join(m.dataDir, dirName, fileName))
if err != nil {
return fmt.Errorf("failed to create unordered txs file: %w", err)
}
defer f.Close()
w := bufio.NewWriter(f)
for txHash, ttl := range m.txHashes {
chunk := unorderedTxToBytes(txHash, ttl)
if _, err = w.Write(chunk); err != nil {
return fmt.Errorf("failed to write unordered tx to buffer: %w", err)
}
}
if err = w.Flush(); err != nil {
return fmt.Errorf("failed to flush unordered txs buffer: %w", err)
}
return nil
}
// expiredTxs returns expired tx hashes based on the provided block height.
func (m *Manager) expiredTxs(blockHeight uint64) []TxHash {
m.mu.RLock()
defer m.mu.RUnlock()
var result []TxHash
for txHash, ttl := range m.txHashes {
if blockHeight > ttl {
result = append(result, txHash)
}
}
return result
}
func (m *Manager) purge(txHashes []TxHash) {
m.mu.Lock()
defer m.mu.Unlock()
for _, txHash := range txHashes {
delete(m.txHashes, txHash)
}
}
// purgeLoop removes expired tx hashes in the background
func (m *Manager) purgeLoop() {
for {
latestHeight, ok := m.batchReceive()
if !ok {
// channel closed
m.doneCh <- struct{}{}
return
}
hashes := m.expiredTxs(latestHeight)
if len(hashes) > 0 {
m.purge(hashes)
}
}
}
func (m *Manager) batchReceive() (uint64, bool) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
var latestHeight uint64
for {
select {
case <-ctx.Done():
return latestHeight, true
case blockHeight, ok := <-m.blockCh:
if !ok {
// channel is closed
return 0, false
}
if blockHeight > latestHeight {
latestHeight = blockHeight
}
}
}
}
func unorderedTxToBytes(txHash TxHash, ttl uint64) []byte {
chunk := make([]byte, chunkSize)
copy(chunk[:txHashSize], txHash[:])
ttlBz := make([]byte, ttlSize)
binary.BigEndian.PutUint64(ttlBz, ttl)
copy(chunk[txHashSize:], ttlBz)
return chunk
}

View File

@ -0,0 +1,150 @@
package unorderedtx_test
import (
"testing"
"time"
"github.com/stretchr/testify/require"
"cosmossdk.io/x/auth/ante/unorderedtx"
)
func TestUnorderedTxManager_Close(t *testing.T) {
txm := unorderedtx.NewManager(t.TempDir())
txm.Start()
require.NoError(t, txm.Close())
require.Panics(t, func() { txm.Close() })
}
func TestUnorderedTxManager_SimpleSize(t *testing.T) {
txm := unorderedtx.NewManager(t.TempDir())
defer func() {
require.NoError(t, txm.Close())
}()
txm.Start()
txm.Add([32]byte{0xFF}, 100)
txm.Add([32]byte{0xAA}, 100)
txm.Add([32]byte{0xCC}, 100)
require.Equal(t, 3, txm.Size())
}
func TestUnorderedTxManager_SimpleContains(t *testing.T) {
txm := unorderedtx.NewManager(t.TempDir())
defer func() {
require.NoError(t, txm.Close())
}()
txm.Start()
for i := 0; i < 10; i++ {
txHash := [32]byte{byte(i)}
txm.Add(txHash, 100)
require.True(t, txm.Contains(txHash))
}
for i := 10; i < 20; i++ {
txHash := [32]byte{byte(i)}
require.False(t, txm.Contains(txHash))
}
}
func TestUnorderedTxManager_InitEmpty(t *testing.T) {
txm := unorderedtx.NewManager(t.TempDir())
defer func() {
require.NoError(t, txm.Close())
}()
txm.Start()
require.NoError(t, txm.OnInit())
}
func TestUnorderedTxManager_CloseInit(t *testing.T) {
dataDir := t.TempDir()
txm := unorderedtx.NewManager(dataDir)
txm.Start()
// add a handful of unordered txs
for i := 0; i < 100; i++ {
txm.Add([32]byte{byte(i)}, 100)
}
// close the manager, which should flush all unexpired txs to file
require.NoError(t, txm.Close())
// create a new manager, start it
txm2 := unorderedtx.NewManager(dataDir)
defer func() {
require.NoError(t, txm2.Close())
}()
// start and execute OnInit, which should load the unexpired txs from file
txm2.Start()
require.NoError(t, txm2.OnInit())
require.Equal(t, 100, txm2.Size())
for i := 0; i < 100; i++ {
require.True(t, txm2.Contains([32]byte{byte(i)}))
}
}
func TestUnorderedTxManager_Flow(t *testing.T) {
txm := unorderedtx.NewManager(t.TempDir())
defer func() {
require.NoError(t, txm.Close())
}()
txm.Start()
// Seed the manager with a txs, some of which should eventually be purged and
// the others will remain. Txs with TTL less than or equal to 50 should be purged.
for i := 1; i <= 100; i++ {
txHash := [32]byte{byte(i)}
if i <= 50 {
txm.Add(txHash, uint64(i))
} else {
txm.Add(txHash, 100)
}
}
// start a goroutine that mimics new blocks being made every 500ms
doneBlockCh := make(chan bool)
go func() {
ticker := time.NewTicker(time.Millisecond * 500)
defer ticker.Stop()
var (
height uint64 = 1
i = 101
)
for range ticker.C {
txm.OnNewBlock(height)
height++
if height > 51 {
doneBlockCh <- true
return
} else {
txm.Add([32]byte{byte(i)}, 50)
}
}
}()
// Eventually all the txs that should be expired by block 50 should be purged.
// The remaining txs should remain.
require.Eventually(
t,
func() bool {
return txm.Size() == 50
},
2*time.Minute,
5*time.Second,
)
<-doneBlockCh
}

View File

@ -0,0 +1,92 @@
package unorderedtx
import (
"encoding/binary"
"errors"
"io"
snapshot "cosmossdk.io/store/snapshots/types"
)
const (
txHashSize = 32
ttlSize = 8
chunkSize = txHashSize + ttlSize
)
var _ snapshot.ExtensionSnapshotter = &Snapshotter{}
const (
// SnapshotFormat defines the snapshot format of exported unordered transactions.
// No protobuf envelope, no metadata.
SnapshotFormat = 1
// SnapshotName defines the snapshot name of exported unordered transactions.
SnapshotName = "unordered_txs"
)
type Snapshotter struct {
m *Manager
}
func NewSnapshotter(m *Manager) *Snapshotter {
return &Snapshotter{m: m}
}
func (s *Snapshotter) SnapshotName() string {
return SnapshotName
}
func (s *Snapshotter) SnapshotFormat() uint32 {
return SnapshotFormat
}
func (s *Snapshotter) SupportedFormats() []uint32 {
return []uint32{SnapshotFormat}
}
func (s *Snapshotter) SnapshotExtension(height uint64, payloadWriter snapshot.ExtensionPayloadWriter) error {
// export all unordered transactions as a single blob
return s.m.exportSnapshot(height, payloadWriter)
}
func (s *Snapshotter) RestoreExtension(height uint64, format uint32, payloadReader snapshot.ExtensionPayloadReader) error {
if format == SnapshotFormat {
return s.restore(height, payloadReader)
}
return snapshot.ErrUnknownFormat
}
func (s *Snapshotter) restore(height uint64, payloadReader snapshot.ExtensionPayloadReader) error {
// the payload should be the entire set of unordered transactions
payload, err := payloadReader()
if err != nil {
if errors.Is(err, io.EOF) {
return io.ErrUnexpectedEOF
}
return err
}
if len(payload)%chunkSize != 0 {
return errors.New("invalid unordered txs payload length")
}
var i int
for i < len(payload) {
var txHash TxHash
copy(txHash[:], payload[i:i+txHashSize])
ttl := binary.BigEndian.Uint64(payload[i+txHashSize : i+chunkSize])
if height < ttl {
// only add unordered transactions that are still valid, i.e. unexpired
s.m.Add(txHash, ttl)
}
i += chunkSize
}
return nil
}

View File

@ -0,0 +1,56 @@
package unorderedtx_test
import (
"testing"
"github.com/stretchr/testify/require"
"cosmossdk.io/x/auth/ante/unorderedtx"
)
func TestSnapshotter(t *testing.T) {
dataDir := t.TempDir()
txm := unorderedtx.NewManager(dataDir)
// add a handful of unordered txs
for i := 0; i < 100; i++ {
txm.Add([32]byte{byte(i)}, 100)
}
var unorderedTxBz []byte
s := unorderedtx.NewSnapshotter(txm)
w := func(bz []byte) error {
unorderedTxBz = bz
return nil
}
err := s.SnapshotExtension(50, w)
require.NoError(t, err)
require.NotEmpty(t, unorderedTxBz)
pr := func() ([]byte, error) {
return unorderedTxBz, nil
}
// restore with an invalid format which should result in an error
err = s.RestoreExtension(50, 2, pr)
require.Error(t, err)
// restore with height > ttl which should result in no unordered txs synced
txm2 := unorderedtx.NewManager(dataDir)
s2 := unorderedtx.NewSnapshotter(txm2)
err = s2.RestoreExtension(200, unorderedtx.SnapshotFormat, pr)
require.NoError(t, err)
require.Empty(t, txm2.Size())
// restore with with height < ttl which should result in all unordered txs synced
txm3 := unorderedtx.NewManager(dataDir)
s3 := unorderedtx.NewSnapshotter(txm3)
err = s3.RestoreExtension(50, unorderedtx.SnapshotFormat, pr)
require.NoError(t, err)
require.Equal(t, 100, txm3.Size())
for i := 0; i < 100; i++ {
require.True(t, txm3.Contains([32]byte{byte(i)}))
}
}

View File

@ -22,6 +22,6 @@ type Tx interface {
types.TxWithMemo
types.FeeTx
types.TxWithTimeoutHeight
types.TxWithUnordered
types.HasValidateBasic
}

View File

@ -221,6 +221,11 @@ func (w *wrapper) GetTimeoutHeight() uint64 {
return w.tx.Body.TimeoutHeight
}
// GetUnordered returns the transaction's unordered field (if set).
func (w *wrapper) GetUnordered() bool {
return w.tx.Body.Unordered
}
func (w *wrapper) GetSignaturesV2() ([]signing.SignatureV2, error) {
signerInfos := w.tx.AuthInfo.SignerInfos
sigs := w.tx.Signatures
@ -283,6 +288,13 @@ func (w *wrapper) SetTimeoutHeight(height uint64) {
w.bodyBz = nil
}
func (w *wrapper) SetUnordered(v bool) {
w.tx.Body.Unordered = v
// set bodyBz to nil because the cached bodyBz no longer matches tx.Body
w.bodyBz = nil
}
func (w *wrapper) SetMemo(memo string) {
w.tx.Body.Memo = memo

View File

@ -65,6 +65,8 @@ func TestUnknownFields(t *testing.T) {
shouldErr: false,
},
{
// If new fields are added to TxBody the number for some_new_field in the proto definition must be set to
// one that it's not used in TxBody.
name: "critical fields in TxBody should error on decode",
body: &testdata.TestUpdatedTxBody{
Memo: "foo",