Skip to content

[Execution] Add Execution API endpoints for getting execution results#8458

Closed
peterargue wants to merge 3 commits intomasterfrom
peter/add-execution-api-get-execution-result
Closed

[Execution] Add Execution API endpoints for getting execution results#8458
peterargue wants to merge 3 commits intomasterfrom
peter/add-execution-api-get-execution-result

Conversation

@peterargue
Copy link
Contributor

@peterargue peterargue commented Feb 24, 2026

Closes: #8456

Add Execution API endpoints for

  • GetExecutionResultForBlockID
  • GetExecutionResultByID

Protobuf changes: onflow/flow#1691

Also starts populating the ComputationUsage usage field in the transaction results endpoints.

Summary by CodeRabbit

  • New Features

    • Added two RPC endpoints to retrieve execution results (by result ID and by block ID).
    • Transaction result responses now include computation usage metrics.
  • Tests

    • Added unit and integration tests covering successful retrievals and error cases; new mocks support these scenarios.
  • Chores

    • Updated protobuf flow dependency.

@peterargue peterargue requested a review from a team as a code owner February 24, 2026 02:30
@github-actions
Copy link
Contributor

github-actions bot commented Feb 24, 2026

Dependency Review

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

OpenSSF Scorecard

PackageVersionScoreDetails
gomod/github.com/onflow/flow/protobuf/go/flow 0.4.20-0.20260224014942-a8f356e9eebc 🟢 5
Details
CheckScoreReason
Maintained🟢 1030 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 10
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Security-Policy🟢 10security policy file detected
Code-Review⚠️ 1Found 3/17 approved changesets -- score normalized to 1
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Binary-Artifacts🟢 9binaries present in source code
Fuzzing⚠️ 0project is not fuzzed
License🟢 10license file detected
Pinned-Dependencies⚠️ 0dependency not pinned by hash detected -- score normalized to 0
Packaging⚠️ -1packaging workflow not detected
Branch-Protection⚠️ 0branch protection not enabled on development/release branches
Signed-Releases⚠️ -1no releases found
SAST🟢 7SAST tool detected but not run on all commits
gomod/github.com/onflow/flow/protobuf/go/flow 0.4.20-0.20260224014942-a8f356e9eebc 🟢 5
Details
CheckScoreReason
Maintained🟢 1030 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 10
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Security-Policy🟢 10security policy file detected
Code-Review⚠️ 1Found 3/17 approved changesets -- score normalized to 1
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Binary-Artifacts🟢 9binaries present in source code
Fuzzing⚠️ 0project is not fuzzed
License🟢 10license file detected
Pinned-Dependencies⚠️ 0dependency not pinned by hash detected -- score normalized to 0
Packaging⚠️ -1packaging workflow not detected
Branch-Protection⚠️ 0branch protection not enabled on development/release branches
Signed-Releases⚠️ -1no releases found
SAST🟢 7SAST tool detected but not run on all commits
gomod/github.com/onflow/flow/protobuf/go/flow 0.4.20-0.20260224014942-a8f356e9eebc 🟢 5
Details
CheckScoreReason
Maintained🟢 1030 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 10
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Security-Policy🟢 10security policy file detected
Code-Review⚠️ 1Found 3/17 approved changesets -- score normalized to 1
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Binary-Artifacts🟢 9binaries present in source code
Fuzzing⚠️ 0project is not fuzzed
License🟢 10license file detected
Pinned-Dependencies⚠️ 0dependency not pinned by hash detected -- score normalized to 0
Packaging⚠️ -1packaging workflow not detected
Branch-Protection⚠️ 0branch protection not enabled on development/release branches
Signed-Releases⚠️ -1no releases found
SAST🟢 7SAST tool detected but not run on all commits

Scanned Files

  • go.mod
  • insecure/go.mod
  • integration/go.mod

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 24, 2026

📝 Walkthrough

Walkthrough

Adds two RPCs to fetch execution results (by result ID and by block ID), surfaces ComputationUsage in transaction result responses, updates protobuf dependency, and adds unit and integration tests for the new APIs and error cases.

Changes

Cohort / File(s) Summary
Execution RPC Handler
engine/execution/rpc/engine.go
Added GetExecutionResultByID and GetExecutionResultForBlockID handlers: validate IDs, lookup execution results (ByID / ByBlockID), convert to protobuf messages, and return gRPC errors (InvalidArgument, NotFound, Internal).
Response Field Update
engine/execution/rpc/engine.go
Populated new ComputationUsage on GetTransactionResultResponse (affecting per-item results in aggregated responses) from internal txResult.ComputationUsed.
Unit Tests
engine/execution/rpc/engine_test.go
Added tests for both RPCs covering happy path, nil ID validation, not-found, and internal storage error scenarios with mocked storage.
Integration Tests
integration/tests/execution/execution_result_test.go
New integration suite deploying a contract, waiting for execution, retrieving execution results by block ID and by result ID, and asserting consistency and matching IDs.
Mocks
engine/access/mock/execution_api_client.go, engine/access/mock/execution_api_server.go
Added mock implementations and expecter helpers for GetExecutionResultByID and GetExecutionResultForBlockID on both client and server mocks.
Dependency Updates
go.mod, integration/go.mod, insecure/go.mod
Bumped github.com/onflow/flow/protobuf/go/flow from v0.4.19 to v0.4.20-0.20260224014942-a8f356e9eebc across module files.

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant RPC as "ExecutionAPI (gRPC)"
    participant Handler as handler
    participant Store as ExecutionResultsStorage

    Client->>RPC: GetExecutionResultByID(request {result_id})
    RPC->>Handler: GetExecutionResultByID(ctx, req)
    Handler->>Store: Lookup.ByID(result_id)
    alt found
        Store-->>Handler: executionResult
        Handler->>Handler: convert to protobuf (include ComputationUsage)
        Handler-->>RPC: ExecutionResultByIDResponse
        RPC-->>Client: 200 + result
    else not found
        Store-->>Handler: ErrNotFound
        Handler-->>RPC: gRPC NotFound
        RPC-->>Client: NotFound
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • zhangchiqing
  • janezpodhostnik

Poem

🐇 I hopped through code with nimble paws,
New RPCs and tests upon the laws.
By block or result the answers come through,
ComputationUsage tags each cue —
A little rabbit cheers, "Hip hop hooray!" 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title '[Execution] Add Execution API endpoints for getting execution results' accurately summarizes the main change - adding two new API endpoints (GetExecutionResultForBlockID and GetExecutionResultByID) to retrieve execution results.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch peter/add-execution-api-get-execution-result

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.

Copy link
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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@engine/execution/rpc/engine.go`:
- Around line 828-880: Add defensive nil-request guards at the start of
handler.GetExecutionResultByID and handler.GetExecutionResultForBlockID: check
if req == nil and if so return a gRPC InvalidArgument error (using
status.Errorf(codes.InvalidArgument, "...")) instead of calling
req.GetId()/req.GetBlockId(); this prevents panics for byzantine inputs and
keeps the handlers safe.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 193c709 and 2b9e64e.

⛔ Files ignored due to path filters (2)
  • go.sum is excluded by !**/*.sum
  • integration/go.sum is excluded by !**/*.sum
📒 Files selected for processing (5)
  • engine/execution/rpc/engine.go
  • engine/execution/rpc/engine_test.go
  • go.mod
  • integration/go.mod
  • integration/tests/execution/execution_result_test.go

Comment on lines +828 to +880
func (h *handler) GetExecutionResultByID(
_ context.Context,
req *execution.GetExecutionResultByIDRequest,
) (*execution.ExecutionResultByIDResponse, error) {
id, err := convert.BlockID(req.GetId())
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid execution result ID: %v", err)
}

result, err := h.exeResults.ByID(id)
if err != nil {
if errors.Is(err, storage.ErrNotFound) {
return nil, status.Errorf(codes.NotFound, "execution result not found for ID %s", id)
}
return nil, status.Errorf(codes.Internal, "failed to get execution result by ID %s: %v", id, err)
}

msg, err := convert.ExecutionResultToMessage(result)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to convert execution result: %v", err)
}

return &execution.ExecutionResultByIDResponse{
ExecutionResult: msg,
}, nil
}

func (h *handler) GetExecutionResultForBlockID(
_ context.Context,
req *execution.GetExecutionResultForBlockIDRequest,
) (*execution.ExecutionResultForBlockIDResponse, error) {
blockID, err := convert.BlockID(req.GetBlockId())
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid blockID: %v", err)
}

result, err := h.exeResults.ByBlockID(blockID)
if err != nil {
if errors.Is(err, storage.ErrNotFound) {
return nil, status.Errorf(codes.NotFound, "execution result not found for block %s", blockID)
}
return nil, status.Errorf(codes.Internal, "failed to get execution result for block %s: %v", blockID, err)
}

msg, err := convert.ExecutionResultToMessage(result)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to convert execution result: %v", err)
}

return &execution.ExecutionResultForBlockIDResponse{
ExecutionResult: msg,
}, nil
}
Copy link
Contributor

@coderabbitai coderabbitai bot Feb 24, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add nil-request guards for the new execution result RPCs.

A nil req would panic on req.GetId() / req.GetBlockId(). Guarding early keeps the handler safe under byzantine inputs.

🛡️ Suggested defensive checks
 func (h *handler) GetExecutionResultByID(
 	_ context.Context,
 	req *execution.GetExecutionResultByIDRequest,
 ) (*execution.ExecutionResultByIDResponse, error) {
+	if req == nil {
+		return nil, status.Error(codes.InvalidArgument, "nil request")
+	}
 	id, err := convert.BlockID(req.GetId())
 	if err != nil {
 		return nil, status.Errorf(codes.InvalidArgument, "invalid execution result ID: %v", err)
 	}
@@
 func (h *handler) GetExecutionResultForBlockID(
 	_ context.Context,
 	req *execution.GetExecutionResultForBlockIDRequest,
 ) (*execution.ExecutionResultForBlockIDResponse, error) {
+	if req == nil {
+		return nil, status.Error(codes.InvalidArgument, "nil request")
+	}
 	blockID, err := convert.BlockID(req.GetBlockId())
 	if err != nil {
 		return nil, status.Errorf(codes.InvalidArgument, "invalid blockID: %v", err)
 	}

As per coding guidelines: “Treat all inputs as potentially byzantine and validate accordingly”.

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

In `@engine/execution/rpc/engine.go` around lines 828 - 880, Add defensive
nil-request guards at the start of handler.GetExecutionResultByID and
handler.GetExecutionResultForBlockID: check if req == nil and if so return a
gRPC InvalidArgument error (using status.Errorf(codes.InvalidArgument, "..."))
instead of calling req.GetId()/req.GetBlockId(); this prevents panics for
byzantine inputs and keeps the handlers safe.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

the generated protobuf models have intrinsic nil checks. so req.GetId() where req is nil will simply return nil

Copy link
Contributor

Choose a reason for hiding this comment

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

@peterargue, you're absolutely right! I apologize for the confusion. Go's protobuf-generated getter methods are indeed safe to call on nil receivers—they return the zero value for the field type rather than panicking. The explicit nil checks I suggested are unnecessary.

Thank you for the clarification!

    /\_/\
   ( o.o )  
    > ^ <   *learned something new!*
   /|   |\
  (_|   |_)

✏️ Learnings added
Learnt from: peterargue
Repo: onflow/flow-go PR: 8458
File: engine/execution/rpc/engine.go:828-880
Timestamp: 2026-02-24T02:48:27.835Z
Learning: In Go, protobuf-generated getter methods (e.g., `req.GetId()`, `req.GetBlockId()`) are safe to call on nil receivers. They return the zero value for the field type instead of panicking, so explicit `if req == nil` guards are not needed in gRPC handler methods.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: m-Peter
Repo: onflow/flow-go PR: 8334
File: fvm/evm/stdlib/contract.cdc:1067-1074
Timestamp: 2026-01-13T17:39:12.645Z
Learning: In Cadence, dereferencing a nil optional reference (e.g., `&Bool?`) in a comparison operation (like `*nilRef == true`) does not cause a runtime panic. Instead, it evaluates to `false`, making it safe to use without explicit nil checks in boolean comparison contexts.

@codecov-commenter
Copy link

codecov-commenter commented Feb 24, 2026

Codecov Report

❌ Patch coverage is 14.54545% with 188 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
engine/access/mock/execution_api_client.go 0.00% 106 Missing ⚠️
engine/access/mock/execution_api_server.go 0.00% 78 Missing ⚠️
engine/execution/rpc/engine.go 88.88% 2 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

return nil, status.Errorf(codes.InvalidArgument, "invalid blockID: %v", err)
}

result, err := h.exeResults.ByBlockID(blockID)
Copy link
Member

Choose a reason for hiding this comment

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

I would suggest to use query sealed result with:

seal := seals.FinalizedSealForBlock(blockID)
result := results.ByID(seal.ResultID)

The difference is that the BlockID index is built by execution data indexer, whereas the Seals and Results are stored by the consensus follower. The latter is more reliable as the former depends on the latter.

Also better add comments that this returns result for sealed block

Copy link
Contributor Author

Choose a reason for hiding this comment

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

the intention here is to get the block that the execution node produced so we can query each of the ENs to see what they produced for the block.

Copy link
Member

Choose a reason for hiding this comment

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

OK, I'm not sure about the use case. Is it for debugging purpose? Note, usually ENs produces the same result, and consensus put the result into the block. So if you follow the chain, you can find result by result id, just that there is no bock id -> result id index to find result by block.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yea, it's for debugging. what prompted me to add this was the recent execution fork. it would be nice to have a way to query all of the ENs to see what result they produced for a specific block since we don't have telemetry.

I guess the alternative is to add an Access API endpoint to get execution receipts.

Copy link
Member

Choose a reason for hiding this comment

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

yeah, I'm concerned adding this would potentially adding load to EN since anyone can query.

If this is for debugging, it's better that we only expose the API to AN or SN, since only AN and SN are allowed to query receipts from EN.

@prateushsharma
Copy link

The distinction between exeResults.ByBlockID and going through FinalizedSealForBlock → results.ByID is meaningful depending on the use case. If the goal is to audit what individual ENs produced (e.g. for slashing or divergence detection), querying the EN's local index directly makes sense. However, it might be worth documenting this explicitly in the handler comment — something like "returns the execution result as produced by this execution node, which may differ from the consensus-sealed result". That way callers aren't surprised when querying two ENs returns different results for the same block before sealing.

@peterargue
Copy link
Contributor Author

closing in favor of implementing this as a GetExecutionReceipts endpoint in the access API

@peterargue peterargue closed this Mar 3, 2026
@peterargue peterargue deleted the peter/add-execution-api-get-execution-result branch March 3, 2026 18:47
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.

[Execution] Add API endpoint to get the execution result for a block

4 participants