Skip to content

[Access] Fix executor address matching in scheduled tx indexer#8539

Open
peterargue wants to merge 3 commits intomasterfrom
peter/scheduled-tx-executor-match
Open

[Access] Fix executor address matching in scheduled tx indexer#8539
peterargue wants to merge 3 commits intomasterfrom
peter/scheduled-tx-executor-match

Conversation

@peterargue
Copy link
Copy Markdown
Contributor

@peterargue peterargue commented Apr 9, 2026

Summary

  • Bug: isExecutorTransaction in the scheduled transactions indexer only matched ScheduledTransactionExecutor.Address, but v0 system collections (mainnet blocks before 133408444, testnet before 290050888) produce executor transactions with FlowServiceAccount.Address as the authorizer. This caused failed scheduled transactions to throw an exception because they could not be matched to the PendingExecution event.
  • Fix: Made executor address matching height-aware using the same version boundaries as the system collection builder. Also added an Arguments >= 1 check to prevent matching other v0 system transactions (ProcessCallbacksTransaction, SystemChunkTransaction) that share the service account authorizer but have no arguments.
  • Added test for v1-era failed transaction detection and updated existing tests to use the correct address for their block heights.

Test plan

  • All existing scheduled transaction indexer tests pass with updated addresses
  • New TestScheduledTransactionsIndexer_FailedTransactionV1 verifies v1 behavior
  • Existing TestScheduledTransactionsIndexer_FailedTransaction now verifies v0 behavior
  • TestScheduledTransactionsIndexer_ExecutorTxNoArguments verifies that zero-argument system txs are not matched

Summary by CodeRabbit

  • Bug Fixes

    • Improved executor transaction identification to account for block height variations, ensuring accurate transaction categorization across different protocol versions.
  • Tests

    • Added comprehensive test coverage for height-aware transaction processing, including new v1 protocol version tests.
    • Enhanced error handling verification to ensure transactions are properly skipped rather than causing failures.

@peterargue peterargue requested a review from a team as a code owner April 9, 2026 17:06
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 9, 2026

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 9, 2026

📝 Walkthrough

Walkthrough

The changes implement height-aware executor address mapping for scheduled transactions. The executor address transitions from a fixed field to a version-dependent structure using the chain height versioning system. Transaction identification now requires height context, and error handling is relaxed for executor transactions with missing scheduled-transaction arguments.

Changes

Cohort / File(s) Summary
Height-Aware Executor Address
module/state_synchronization/indexer/extended/scheduled_transactions.go
Replaced fixed scheduledExecutorAddr with height-dependent executorAddr *access.Versioned[flow.Address]. Updated isExecutorTransaction() to accept block height and match against height-specific executor address. Modified failed-matching loop to skip executor transactions lacking scheduled-transaction arguments instead of erroring.
Expanded Test Coverage
module/state_synchronization/indexer/extended/scheduled_transactions_test.go
Added v1-specific test constant and three new v1 tests (TestScheduledTransactionsIndexer_FailedTransactionV1, TestScheduledTransactionsIndexer_MixedFailedAndExecutedV1, TestScheduledTransactionsIndexer_NonExecutorTxSkippedV1). Updated v0 tests to use FlowServiceAccount authorizer. Extended existing tests with additional assertions for executor transaction matching and error conditions.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • zhangchiqing
  • janezpodhostnik
  • turbolent

Poem

🐰 Hopping through heights, addresses now versioned,
Where executor transactions are height-assessed,
No more fixed bounds, just boundaries that shift,
Each block gets its match—a versioned gift!
Tests in v0 and v1 now run side by side, 🎯✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: making executor address matching height-aware in the scheduled transaction indexer to fix a bug where v0 system transactions were being incorrectly matched.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch peter/scheduled-tx-executor-match

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@peterargue peterargue changed the base branch from master to peter/nft-group April 9, 2026 17:07
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Apr 9, 2026

Codecov Report

❌ Patch coverage is 88.23529% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...ization/indexer/extended/scheduled_transactions.go 88.23% 1 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@peterargue peterargue changed the title Fix executor address matching in scheduled tx indexer [Access] Fix executor address matching in scheduled tx indexer Apr 9, 2026
Base automatically changed from peter/nft-group to master April 17, 2026 20:14
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
module/state_synchronization/indexer/extended/scheduled_transactions_test.go (1)

783-811: Minor: test name is slightly misleading now.

TestScheduledTransactionsIndexer_ExecutorTxNoArguments no longer exercises an executor tx per se — after the fix, a zero-argument FlowServiceAccount-authorized tx is explicitly not considered an executor tx. The test correctly validates that behavior by asserting the "have no corresponding executor transaction" error, but the name implies the tx is still an executor tx with missing args.

Consider renaming to something like TestScheduledTransactionsIndexer_SystemTxWithNoArgumentsNotMatched to reflect the actual intent (this is primarily about distinguishing v0 ProcessCallbacksTransaction/SystemChunkTransaction from the executor tx).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@module/state_synchronization/indexer/extended/scheduled_transactions_test.go`
around lines 783 - 811, The test name
TestScheduledTransactionsIndexer_ExecutorTxNoArguments is misleading because it
verifies that a zero-argument FlowServiceAccount-authorized system transaction
is NOT treated as an executor tx; rename the test function to a clearer name
(e.g., TestScheduledTransactionsIndexer_SystemTxWithNoArgumentsNotMatched) and
update any references/imports/mentions to that symbol so the test declaration,
any calls, and logging reflect the new name while leaving the body (creating
noArgTx, pendingEvt, and the assertions) unchanged.
module/state_synchronization/indexer/extended/scheduled_transactions.go (2)

125-129: Double-check the 3-entry version map is intentional (and keep the assertion working).

Including access.VersionLatest alongside Version0/Version1 is required here because the fallback access.NewStaticHeightVersionMapper(access.LatestBoundary) only defines VersionLatest, and access.NewVersioned panics if any version returned by the mapper is missing from the map (see model/access/versioned.go lines 24-29). Worth a brief inline comment so the extra VersionLatest entry isn't "cleaned up" as a duplicate of Version1 by a future refactor.

💡 Proposed note
 	executorAddr := access.NewVersioned(map[access.Version]flow.Address{
-		systemcollection.Version0:  sc.FlowServiceAccount.Address,
-		systemcollection.Version1:  sc.ScheduledTransactionExecutor.Address,
-		access.VersionLatest:       sc.ScheduledTransactionExecutor.Address,
+		systemcollection.Version0: sc.FlowServiceAccount.Address,
+		systemcollection.Version1: sc.ScheduledTransactionExecutor.Address,
+		// VersionLatest is required for chains without an explicit mapping
+		// (fallback mapper emits only VersionLatest).
+		access.VersionLatest: sc.ScheduledTransactionExecutor.Address,
 	}, versionMapper)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@module/state_synchronization/indexer/extended/scheduled_transactions.go`
around lines 125 - 129, The version map passed to access.NewVersioned for
executorAddr intentionally includes three keys (systemcollection.Version0,
systemcollection.Version1, access.VersionLatest) because the fallback mapper
(versionMapper created via
access.NewStaticHeightVersionMapper(access.LatestBoundary)) can return
access.VersionLatest and access.NewVersioned will panic if a returned version is
missing; add a brief inline comment next to the executorAddr/access.NewVersioned
call explaining that the extra access.VersionLatest entry is required to satisfy
the fallback mapper and must not be removed as a duplicate of Version1 to keep
the existing assertion behavior intact.

119-129: Optional: consider centralizing the height-version fallback.

The if !ok { versionMapper = access.NewStaticHeightVersionMapper(access.LatestBoundary) } pattern here duplicates the fallback logic in systemcollection.Default (see model/access/systemcollection/system_collection.go lines 32-40). If additional call sites emerge that need a version-aware address/type keyed off ChainHeightVersions, extracting a small helper like systemcollection.VersionMapperFor(chainID) would keep the fallback behavior consistent across the codebase.

Not blocking — this file is currently the only second consumer.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@module/state_synchronization/indexer/extended/scheduled_transactions.go`
around lines 119 - 129, Duplicate fallback logic for ChainHeightVersions is used
here; replace the inline check and
access.NewStaticHeightVersionMapper(access.LatestBoundary) with a centralized
helper (e.g. add systemcollection.VersionMapperFor(chainID) that returns the
mapper or the static LatestBoundary fallback) and then call that helper to
assign versionMapper before building executorAddr (referencing
systemcollection.ChainHeightVersions, versionMapper, and
access.NewStaticHeightVersionMapper in the change).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@module/state_synchronization/indexer/extended/scheduled_transactions.go`:
- Around line 437-446: The current isExecutorTransaction(tx
*flow.TransactionBody, height uint64) incorrectly classifies v0
ProcessCallbacksTransaction as an executor transaction; update
isExecutorTransaction to add a discriminator that ensures the tx script/content
matches the executor scheduled-transaction script (or explicitly excludes the
ProcessCallbacksTransaction pattern) before returning true—use tx.Script (or its
byte signature/known hash or specific entrypoint/name in the script) alongside
the existing checks and keep executorAddr.ByHeight(height) usage; alternatively,
implement a helper like isProcessCallbacksTransaction(tx) and return false from
isExecutorTransaction when that helper detects a callback transaction so
argument decoding for scheduled tx IDs is only attempted for true executor
transactions.

---

Nitpick comments:
In
`@module/state_synchronization/indexer/extended/scheduled_transactions_test.go`:
- Around line 783-811: The test name
TestScheduledTransactionsIndexer_ExecutorTxNoArguments is misleading because it
verifies that a zero-argument FlowServiceAccount-authorized system transaction
is NOT treated as an executor tx; rename the test function to a clearer name
(e.g., TestScheduledTransactionsIndexer_SystemTxWithNoArgumentsNotMatched) and
update any references/imports/mentions to that symbol so the test declaration,
any calls, and logging reflect the new name while leaving the body (creating
noArgTx, pendingEvt, and the assertions) unchanged.

In `@module/state_synchronization/indexer/extended/scheduled_transactions.go`:
- Around line 125-129: The version map passed to access.NewVersioned for
executorAddr intentionally includes three keys (systemcollection.Version0,
systemcollection.Version1, access.VersionLatest) because the fallback mapper
(versionMapper created via
access.NewStaticHeightVersionMapper(access.LatestBoundary)) can return
access.VersionLatest and access.NewVersioned will panic if a returned version is
missing; add a brief inline comment next to the executorAddr/access.NewVersioned
call explaining that the extra access.VersionLatest entry is required to satisfy
the fallback mapper and must not be removed as a duplicate of Version1 to keep
the existing assertion behavior intact.
- Around line 119-129: Duplicate fallback logic for ChainHeightVersions is used
here; replace the inline check and
access.NewStaticHeightVersionMapper(access.LatestBoundary) with a centralized
helper (e.g. add systemcollection.VersionMapperFor(chainID) that returns the
mapper or the static LatestBoundary fallback) and then call that helper to
assign versionMapper before building executorAddr (referencing
systemcollection.ChainHeightVersions, versionMapper, and
access.NewStaticHeightVersionMapper in the change).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c4377f2a-cb8d-447b-9efb-cb372f1a4a5e

📥 Commits

Reviewing files that changed from the base of the PR and between 9636eb5 and d370d15.

📒 Files selected for processing (2)
  • module/state_synchronization/indexer/extended/scheduled_transactions.go
  • module/state_synchronization/indexer/extended/scheduled_transactions_test.go

Comment on lines 437 to 446
// isExecutorTransaction returns true if the transaction was submitted by the scheduled executor
// account: sole authorizer is the scheduled executor address and payer is the empty address.
func (s *ScheduledTransactions) isExecutorTransaction(tx *flow.TransactionBody) bool {
// account for the given block height: sole authorizer matches the height-appropriate executor
// address, payer is the empty address, and the transaction has at least one argument (the
// scheduled tx ID).
func (s *ScheduledTransactions) isExecutorTransaction(tx *flow.TransactionBody, height uint64) bool {
return tx.Payer == flow.EmptyAddress &&
len(tx.Authorizers) == 1 &&
tx.Authorizers[0] == s.scheduledExecutorAddr
len(tx.Arguments) >= 1 &&
tx.Authorizers[0] == s.executorAddr.ByHeight(height)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Look for v0 system collection builder and enumerate transactions it emits,
# focusing on any that are authorized by the service account and carry arguments.
fd -t f 'system_collection' model/access/systemcollection/
echo "---- v0 builder ----"
fd -t f 'builder' model/access/systemcollection/ | xargs -r rg -n -C2 'Authorizers|Arguments|ServiceAccount|Payer'
echo "---- cross-reference ----"
rg -nP --type=go -C3 'AddAuthorizer|SetPayer' -g '!**/*_test.go' model/

Repository: onflow/flow-go

Length of output: 3928


🏁 Script executed:

#!/bin/bash
# Examine the full implementations of system transactions in v0
echo "=== ProcessCallbacksTransaction (v0) ==="
sed -n '70,85p' model/access/systemcollection/system_collection_v0.go

echo "=== ProcessScheduledTransactionsTransaction (v0) ==="
sed -n '40,51p' model/access/systemcollection/system_collection_v0.go

echo "=== SystemChunkTransaction (v0) ==="
sed -n '145,160p' model/access/systemcollection/system_collection_v0.go

echo "=== Check if Payer is ever set in v0 builders ==="
rg -n 'SetPayer' model/access/systemcollection/system_collection_v0.go

echo "=== Executor address initialization ==="
rg -n 'executorAddr' module/state_synchronization/indexer/extended/scheduled_transactions.go | head -20

Repository: onflow/flow-go

Length of output: 2170


🏁 Script executed:

#!/bin/bash
# Check the default Payer value in TransactionBodyBuilder
echo "=== TransactionBodyBuilder Build method ==="
rg -n 'type TransactionBodyBuilder' model/flow/transaction_body_builder.go -A 20

echo "=== TransactionBodyBuilder.Build() method ==="
rg -n 'func.*Build.*TransactionBody' model/flow/transaction_body_builder.go -A 10

echo "=== Executor address initialization for v0 vs v1 ==="
sed -n '125,135p' module/state_synchronization/indexer/extended/scheduled_transactions.go

echo "=== FlowServiceAccount address ==="
rg -n 'FlowServiceAccount' systemcontracts/ | head -10

Repository: onflow/flow-go

Length of output: 6130


🏁 Script executed:

#!/bin/bash
# Check the argument decoding logic at line 406
echo "=== Context around line 406 ==="
sed -n '400,420p' module/state_synchronization/indexer/extended/scheduled_transactions.go

echo "=== Full ProcessScheduledTransactions function signature ==="
rg -n 'func.*ProcessScheduledTransactions' module/state_synchronization/indexer/extended/scheduled_transactions.go -A 30 | head -40

Repository: onflow/flow-go

Length of output: 1043


Residual risk is real: ProcessCallbacksTransaction would be misidentified as executor transaction in v0 blocks.

The combination Payer == EmptyAddress && len(Authorizers) == 1 && Authorizers[0] == executor(height) && len(Arguments) >= 1 correctly handles ProcessScheduledTransactionsTransaction (zero args) and SystemChunkTransaction (zero args), and the height-aware executor address correctly maps to FlowServiceAccount in v0 blocks.

However, ProcessCallbacksTransaction in v0 satisfies all four conditions:

  • Payer: EmptyAddress (never set via SetPayer)
  • Authorizers: single FlowServiceAccount.Address
  • Arguments: ≥1 (contains callback event ID)
  • For v0 blocks, executorAddr.ByHeight() resolves to FlowServiceAccount.Address

When misidentified as an executor transaction, the code attempts to decode Arguments[0] as a scheduled transaction ID at line 406. Since ProcessCallbacksTransaction's argument is a callback event ID, the decoding fails and aborts block indexing. This is not theoretical: such transactions exist in v0 blocks.

A discriminator (e.g., script content comparison) is needed to distinguish executor transactions from ProcessCallbacksTransaction.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@module/state_synchronization/indexer/extended/scheduled_transactions.go`
around lines 437 - 446, The current isExecutorTransaction(tx
*flow.TransactionBody, height uint64) incorrectly classifies v0
ProcessCallbacksTransaction as an executor transaction; update
isExecutorTransaction to add a discriminator that ensures the tx script/content
matches the executor scheduled-transaction script (or explicitly excludes the
ProcessCallbacksTransaction pattern) before returning true—use tx.Script (or its
byte signature/known hash or specific entrypoint/name in the script) alongside
the existing checks and keep executorAddr.ByHeight(height) usage; alternatively,
implement a helper like isProcessCallbacksTransaction(tx) and return false from
isExecutorTransaction when that helper detects a callback transaction so
argument decoding for scheduled tx IDs is only attempted for true executor
transactions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants