Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

fix(unorderedtx): issues reported in audit #21467

Merged
merged 28 commits into from
Sep 11, 2024
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
7aba2e7
small fixes
facundomedica Aug 28, 2024
8f3e2cb
update upgrading md
facundomedica Aug 29, 2024
83fa1f0
do not chekc the sequence in prepareproposal
facundomedica Aug 30, 2024
75e79c0
fix
facundomedica Aug 30, 2024
33665d5
double-check this, not sure
facundomedica Sep 3, 2024
0661845
update
facundomedica Sep 4, 2024
c100377
merge main
facundomedica Sep 4, 2024
af42e6c
use mkdirall instead of just mkdir
facundomedica Sep 4, 2024
32ec055
revert to re-do
facundomedica Sep 4, 2024
a06cc3f
re-add skip of seq in prepareproposal
facundomedica Sep 4, 2024
8a5018e
fix close
facundomedica Sep 4, 2024
5109b45
fix
facundomedica Sep 5, 2024
d86bfd4
remove benchmark
facundomedica Sep 5, 2024
8031960
update upgrading.md
facundomedica Sep 5, 2024
9be58bd
use preblock on simapp v1
facundomedica Sep 5, 2024
7ccca5d
rollback change in test
facundomedica Sep 5, 2024
efee58b
Merge branch 'main' into facu/fixunorderedtx-audit
facundomedica Sep 5, 2024
74c1faa
Merge branch 'main' of https://github.com/cosmos/cosmos-sdk into facu…
facundomedica Sep 5, 2024
d29c921
Merge branch 'facu/fixunorderedtx-audit' of https://github.com/cosmos…
facundomedica Sep 5, 2024
7b3ffef
Merge branch 'main' into facu/fixunorderedtx-audit
facundomedica Sep 6, 2024
804415f
merge
facundomedica Sep 9, 2024
0b9141a
timestamp and height
facundomedica Sep 9, 2024
dcc9705
Revert "timestamp and height"
facundomedica Sep 9, 2024
4b30492
we don't care about timestamp in snapshot
facundomedica Sep 9, 2024
cf9fd70
finally fix this
facundomedica Sep 10, 2024
274c596
fix test
facundomedica Sep 11, 2024
0fcc166
comment
facundomedica Sep 11, 2024
9974f61
remove test prints
facundomedica Sep 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,20 @@ If you are still using the legacy wiring, you must enable unordered transactions
}
```

* Create or update the App's `Preblocker()` method to call the unordered tx
manager's `OnNewBlock()` method.

```go
...
app.SetPreblocker(app.PreBlocker)
...

func (app *SimApp) PreBlocker(ctx sdk.Context, req *abci.RequestFinalizeBlock) (*sdk.ResponsePreBlock, error) {
app.UnorderedTxManager.OnNewBlock(ctx.BlockTime())
return app.ModuleManager.PreBlock(ctx, req)
}
Comment on lines +288 to +291
Copy link
Contributor

Choose a reason for hiding this comment

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

Review: Integration of Unordered Transaction Manager in PreBlocker()

The integration of the unordered transaction manager within the PreBlocker() method is crucial for handling unordered transactions effectively. The method now calls OnNewBlock() on the unordered transaction manager, which is a significant change to ensure that the manager is notified upon each new block. This allows the manager to process transactions that do not require a specific order.

However, the documentation should clarify what specific errors might trigger the panic and under what conditions. This would help developers understand the potential risks and how to mitigate them.

Consider adding more detailed error handling documentation to clarify under what conditions the panic is triggered and how to handle these errors gracefully.

```

* 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
Expand Down
85 changes: 47 additions & 38 deletions baseapp/abci_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -292,36 +292,42 @@ func (h *DefaultProposalHandler) PrepareProposalHandler() sdk.PrepareProposalHan
invalidTxs []sdk.Tx // invalid txs to be removed out of the loop to avoid dead lock
)
h.mempool.SelectBy(ctx, req.Txs, func(memTx sdk.Tx) bool {
signerData, err := h.signerExtAdapter.GetSigners(memTx)
if err != nil {
// propagate the error to the caller
resError = err
return false
}

// If the signers aren't in selectedTxsSignersSeqs then we haven't seen them before
// so we add them and continue given that we don't need to check the sequence.
shouldAdd := true
unorderedTx, ok := memTx.(sdk.TxWithUnordered)
isUnordered := ok && unorderedTx.GetUnordered()
txSignersSeqs := make(map[string]uint64)
for _, signer := range signerData {
signerKey := string(signer.Signer)
seq, ok := selectedTxsSignersSeqs[signerKey]
if !ok {
txSignersSeqs[signerKey] = signer.Sequence
continue

// if the tx is unordered, we don't need to check the sequence, we just add it
if !isUnordered {
signerData, err := h.signerExtAdapter.GetSigners(memTx)
if err != nil {
// propagate the error to the caller
resError = err
return false
}

// If we have seen this signer before in this block, we must make
// sure that the current sequence is seq+1; otherwise is invalid
// and we skip it.
if seq+1 != signer.Sequence {
shouldAdd = false
break
// If the signers aren't in selectedTxsSignersSeqs then we haven't seen them before
// so we add them and continue given that we don't need to check the sequence.
shouldAdd := true
for _, signer := range signerData {
signerKey := string(signer.Signer)
seq, ok := selectedTxsSignersSeqs[signerKey]
if !ok {
txSignersSeqs[signerKey] = signer.Sequence
continue
}

// If we have seen this signer before in this block, we must make
// sure that the current sequence is seq+1; otherwise is invalid
// and we skip it.
if seq+1 != signer.Sequence {
shouldAdd = false
break
}
txSignersSeqs[signerKey] = signer.Sequence
}
if !shouldAdd {
return true
}
txSignersSeqs[signerKey] = signer.Sequence
}
if !shouldAdd {
return true
}

// NOTE: Since transaction verification was already executed in CheckTx,
Expand All @@ -338,18 +344,21 @@ func (h *DefaultProposalHandler) PrepareProposalHandler() sdk.PrepareProposalHan
}

txsLen := len(h.txSelector.SelectedTxs(ctx))
for sender, seq := range txSignersSeqs {
// If txsLen != selectedTxsNums is true, it means that we've
// added a new tx to the selected txs, so we need to update
// the sequence of the sender.
if txsLen != selectedTxsNums {
selectedTxsSignersSeqs[sender] = seq
} else if _, ok := selectedTxsSignersSeqs[sender]; !ok {
// The transaction hasn't been added but it passed the
// verification, so we know that the sequence is correct.
// So we set this sender's sequence to seq-1, in order
// to avoid unnecessary calls to PrepareProposalVerifyTx.
selectedTxsSignersSeqs[sender] = seq - 1
// If the tx is unordered, we don't need to update the sender sequence.
if !isUnordered {
for sender, seq := range txSignersSeqs {
// If txsLen != selectedTxsNums is true, it means that we've
// added a new tx to the selected txs, so we need to update
// the sequence of the sender.
if txsLen != selectedTxsNums {
selectedTxsSignersSeqs[sender] = seq
} else if _, ok := selectedTxsSignersSeqs[sender]; !ok {
// The transaction hasn't been added but it passed the
// verification, so we know that the sequence is correct.
// So we set this sender's sequence to seq-1, in order
// to avoid unnecessary calls to PrepareProposalVerifyTx.
selectedTxsSignersSeqs[sender] = seq - 1
}
}
}
selectedTxsNums = txsLen
Expand Down
3 changes: 3 additions & 0 deletions runtime/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,9 @@ func (a *App) Close() error {

// PreBlocker application updates every pre block
func (a *App) PreBlocker(ctx sdk.Context, _ *abci.FinalizeBlockRequest) error {
if a.UnorderedTxManager != nil {
a.UnorderedTxManager.OnNewBlock(ctx.BlockTime())
}
return a.ModuleManager.PreBlock(ctx)
}

Expand Down
1 change: 1 addition & 0 deletions simapp/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,7 @@ func (app *SimApp) Name() string { return app.BaseApp.Name() }

// PreBlocker application updates every pre block
func (app *SimApp) PreBlocker(ctx sdk.Context, _ *abci.FinalizeBlockRequest) error {
app.UnorderedTxManager.OnNewBlock(ctx.BlockTime())
Copy link
Contributor

Choose a reason for hiding this comment

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

Ensure proper error handling in PreBlocker method.

The addition of app.UnorderedTxManager.OnNewBlock(ctx.BlockTime()) in the PreBlocker method is crucial for updating the transaction manager with the new block time. This change is aligned with the PR's objectives to enhance transaction management for unordered transactions.

However, it's important to ensure that any potential errors returned by OnNewBlock are handled appropriately. Currently, the method's return value is not checked, which could lead to unhandled errors that might affect the application's stability and reliability.

Consider modifying the implementation to handle potential errors returned by the OnNewBlock method. Here's a suggested change:

- app.UnorderedTxManager.OnNewBlock(ctx.BlockTime())
+ if err := app.UnorderedTxManager.OnNewBlock(ctx.BlockTime()); err != nil {
+     return fmt.Errorf("failed to notify UnorderedTxManager of new block: %w", err)
+ }

This modification ensures that errors are not silently ignored, enhancing the robustness of the transaction management process.

Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
app.UnorderedTxManager.OnNewBlock(ctx.BlockTime())
if err := app.UnorderedTxManager.OnNewBlock(ctx.BlockTime()); err != nil {
return fmt.Errorf("failed to notify UnorderedTxManager of new block: %w", err)
}

return app.ModuleManager.PreBlock(ctx)
}

Expand Down
19 changes: 19 additions & 0 deletions types/mempool/priority_nonce.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,16 @@ func (mp *PriorityNonceMempool[C]) Insert(ctx context.Context, tx sdk.Tx) error
sender := sig.Signer.String()
priority := mp.cfg.TxPriority.GetTxPriority(ctx, tx)
nonce := sig.Sequence

// if it's an unordered tx, we use the gas instead of the nonce
if unordered, ok := tx.(sdk.TxWithUnordered); ok && unordered.GetUnordered() {
gasLimit, err := unordered.GetGasLimit()
nonce = gasLimit
if err != nil {
return err
}
}

key := txMeta[C]{nonce: nonce, priority: priority, sender: sender}

senderIndex, ok := mp.senderIndices[sender]
Expand Down Expand Up @@ -459,6 +469,15 @@ func (mp *PriorityNonceMempool[C]) Remove(tx sdk.Tx) error {
sender := sig.Signer.String()
nonce := sig.Sequence

// if it's an unordered tx, we use the gas instead of the nonce
if unordered, ok := tx.(sdk.TxWithUnordered); ok && unordered.GetUnordered() {
gasLimit, err := unordered.GetGasLimit()
nonce = gasLimit
if err != nil {
return err
}
}

scoreKey := txMeta[C]{nonce: nonce, sender: sender}
score, ok := mp.scores[scoreKey]
if !ok {
Expand Down
18 changes: 18 additions & 0 deletions types/mempool/sender_nonce.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,15 @@ func (snm *SenderNonceMempool) Insert(_ context.Context, tx sdk.Tx) error {
snm.senders[sender] = senderTxs
}

// if it's an unordered tx, we use the gas instead of the nonce
if unordered, ok := tx.(sdk.TxWithUnordered); ok && unordered.GetUnordered() {
gasLimit, err := unordered.GetGasLimit()
nonce = gasLimit
if err != nil {
return err
}
}

senderTxs.Set(nonce, tx)

key := txKey{nonce: nonce, address: sender}
Expand Down Expand Up @@ -227,6 +236,15 @@ func (snm *SenderNonceMempool) Remove(tx sdk.Tx) error {
sender := sdk.AccAddress(sig.PubKey.Address()).String()
nonce := sig.Sequence

// if it's an unordered tx, we use the gas instead of the nonce
if unordered, ok := tx.(sdk.TxWithUnordered); ok && unordered.GetUnordered() {
gasLimit, err := unordered.GetGasLimit()
nonce = gasLimit
if err != nil {
return err
}
}

senderTxs, found := snm.senders[sender]
if !found {
return ErrTxNotFound
Expand Down
14 changes: 6 additions & 8 deletions x/auth/ante/unorderedtx/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ type Manager struct {
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)
if err = os.MkdirAll(path, os.ModePerm); err != nil {
panic(fmt.Errorf("failed to create unordered txs directory: %w", err))
}
}

m := &Manager{
Expand Down Expand Up @@ -162,13 +164,9 @@ func (m *Manager) exportSnapshot(height uint64, snapshotWriter func([]byte) erro
w := bufio.NewWriter(&buf)

keys := slices.SortedFunc(maps.Keys(m.txHashes), func(i, j TxHash) int { return bytes.Compare(i[:], j[:]) })
timestamp := time.Unix(int64(height), 0)
for _, txHash := range keys {
timeoutTime := m.txHashes[txHash]
if timestamp.After(timeoutTime) {
// skip expired txs that have yet to be purged
continue
}

// right now we dont have access block time at this flow, so we would just include the expired txs
// and let it be purge during purge loop
chunk := unorderedTxToBytes(txHash, uint64(timeoutTime.Unix()))
Expand All @@ -185,8 +183,8 @@ func (m *Manager) exportSnapshot(height uint64, snapshotWriter func([]byte) erro
return snapshotWriter(buf.Bytes())
}

// flushToFile writes all unexpired unordered transactions along with their TTL
// to file, overwriting the existing file if it exists.
// flushToFile writes all unordered transactions (including expired if not pruned yet)
// 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 {
Expand Down
5 changes: 3 additions & 2 deletions x/auth/ante/unorderedtx/snapshotter.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,9 @@ func (s *Snapshotter) restore(height uint64, payloadReader snapshot.ExtensionPay

timestamp := binary.BigEndian.Uint64(payload[i+txHashSize : i+chunkSize])

// purge any expired txs
if timestamp != 0 && timestamp > height {
// add all txs, we don't care at this point if they are expired,
// we'll let the purge loop handle that
if timestamp != 0 {
s.m.Add(txHash, time.Unix(int64(timestamp), 0))
}

Expand Down
Loading