Skip to content

feat(coordinator): add debug_executionWitness JSON-RPC client#3248

Open
gauravahuja wants to merge 4 commits into
mainfrom
feature/execution-witness-client
Open

feat(coordinator): add debug_executionWitness JSON-RPC client#3248
gauravahuja wants to merge 4 commits into
mainfrom
feature/execution-witness-client

Conversation

@gauravahuja
Copy link
Copy Markdown
Contributor

@gauravahuja gauravahuja commented Jun 1, 2026

Introduce ExecutionWitnessClient in jvm-libs with a coordinator implementation that calls Besu's debug_executionWitness RPC. Extend BlockParameter with BlockHash for hash-based block references aligned with Besu's string param parsing.

This PR implements issue(s) #3085

Checklist

  • I wrote new tests for my new core changes.
  • I have successfully ran tests, style checker and build against my new changes locally.
  • If this change is deployed to any environment (including Devnet), E2E test coverage exists or is included in this
    PR.
  • I have informed the team of any breaking changes if there are any.

Note

Medium Risk
Shared BlockParameter gains a new variant with explicit UnsupportedOperationException on some code paths; otherwise additive client and RPC parsing with good test coverage.

Overview
Adds ExecutionWitnessClient and an ExecutionWitness model in jvm-libs, plus a new coordinator:clients:execution-witness-client module that calls Besu’s debug_executionWitness JSON-RPC (with retries, hex parsing, and typed errors for null results, RPC failures, and parse errors).

Extends BlockParameter with BlockHash (fromHash, parse for 64-hex 0x… strings) and wires hash-aware behavior where needed: RPC param encoding for the witness client, in-process state recovery header lookup by hash, and fake ETH/witness test clients. getBlockParameterNumber and toWeb3j() now fail fast on block hash because those stacks don’t resolve hashes the same way.

Includes WireMock tests for the JSON-RPC client and expanded BlockParameter / web3j mapping tests; registers the new Gradle subproject.

Reviewed by Cursor Bugbot for commit 0464554. Bugbot is set up for automated code reviews on this repo. Configure here.

Introduce ExecutionWitnessClient in jvm-libs with a coordinator
implementation that calls Besu's debug_executionWitness RPC. Extend
BlockParameter with BlockHash for hash-based block references aligned
with Besu's string param parsing.
@gauravahuja gauravahuja marked this pull request as ready for review June 2, 2026 01:09
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Jun 2, 2026

Codecov Report

❌ Patch coverage is 26.78571% with 82 lines in your changes missing coverage. Please review.
✅ Project coverage is 76.75%. Comparing base (2b020d9) to head (0464554).
⚠️ Report is 2 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
.../executionwitness/ExecutionWitnessJsonRpcClient.kt 0.00% 26 Missing ⚠️
...executionwitness/ExecutionWitnessResponseParser.kt 0.00% 25 Missing ⚠️
.../kotlin/linea/executionwitness/ExecutionWitness.kt 21.73% 17 Missing and 1 partial ⚠️
...els/src/main/kotlin/linea/domain/BlockParameter.kt 73.91% 3 Missing and 3 partials ⚠️
...ients/executionwitness/BlockParameterExtensions.kt 0.00% 4 Missing ⚠️
...a/ethapi/extensions/EthApiBlockClientExtensions.kt 62.50% 2 Missing and 1 partial ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##               main    #3248       +/-   ##
=============================================
+ Coverage     55.41%   76.75%   +21.33%     
- Complexity     5264     7010     +1746     
=============================================
  Files          1126     1130        +4     
  Lines         44644    44745      +101     
  Branches       5356     5372       +16     
=============================================
+ Hits          24741    34344     +9603     
+ Misses        19182     9023    -10159     
- Partials        721     1378      +657     
Flag Coverage Δ *Carryforward flag
hardhat 96.17% <ø> (ø) Carriedforward from ab8aa98
kotlin 55.11% <26.78%> (+54.77%) ⬆️
lido-governance-monitor 97.61% <ø> (ø) Carriedforward from ab8aa98
linea-native-libs 90.69% <ø> (ø) Carriedforward from ab8aa98
linea-shared-utils 96.18% <ø> (ø) Carriedforward from ab8aa98
native-yield-automation-service 97.68% <ø> (ø) Carriedforward from ab8aa98
postman 99.92% <ø> (ø) Carriedforward from ab8aa98
sdk-core 98.09% <ø> (ø) Carriedforward from ab8aa98
sdk-ethers 89.83% <ø> (ø) Carriedforward from ab8aa98
sdk-viem 99.45% <ø> (ø) Carriedforward from ab8aa98
tracer 88.50% <ø> (ø) Carriedforward from ab8aa98

*This pull request uses carry forward flags. Click here to find out more.

Files with missing lines Coverage Δ
...lin/linea/web3j/domain/BlockParameterExtensions.kt 100.00% <100.00%> (+100.00%) ⬆️
...a/ethapi/extensions/EthApiBlockClientExtensions.kt 76.92% <62.50%> (+76.92%) ⬆️
...ients/executionwitness/BlockParameterExtensions.kt 0.00% <0.00%> (ø)
...els/src/main/kotlin/linea/domain/BlockParameter.kt 77.55% <73.91%> (+77.55%) ⬆️
.../kotlin/linea/executionwitness/ExecutionWitness.kt 21.73% <21.73%> (ø)
...executionwitness/ExecutionWitnessResponseParser.kt 0.00% <0.00%> (ø)
.../executionwitness/ExecutionWitnessJsonRpcClient.kt 0.00% <0.00%> (ø)

... and 307 files with indirect coverage changes

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

require(hash.size == 32) { "block hash must be 32 bytes, got ${hash.size}" }
}

fun getHash(): ByteArray = hash
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.

BlockHash.getHash() exposes mutable internal array

Medium Severity

BlockHash.getHash() returns the internal ByteArray reference directly without a defensive copy. Since equals and hashCode depend on the array contents, any caller mutating the returned array silently corrupts the BlockHash instance — breaking map lookups, set membership, and equality checks. This is especially risky because BlockHash is used as a Map key in FakeExecutionWitnessClient.

Fix in Cursor Fix in Web

Triggered by project rule: Hardhat signer UI (browser signing)

Reviewed by Cursor Bugbot for commit ab8aa98. Configure here.

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.

Valid comment

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.

    val a = BlockParameter.fromHash(ByteArray(32) { 7 })
    val b = a.getHash()
    println("BEFORE a: ${a.getHash().encodeHex()}")
    println("BEFORE b: ${b.encodeHex()}")
    for (i in b.indices) {
      b[i] = (b[i] + 1).toByte()
    }
    println("AFTER a: ${a.getHash().encodeHex()}")
    println("AFTER b: ${b.encodeHex()}")

Output:

BEFORE a: 0x0707070707070707070707070707070707070707070707070707070707070707
BEFORE b: 0x0707070707070707070707070707070707070707070707070707070707070707
AFTER a: 0x0808080808080808080808080808080808080808080808080808080808080808
AFTER b: 0x0808080808080808080808080808080808080808080808080808080808080808

val hexBody = normalized.drop(2)
if (hexBody.length == BLOCK_HASH_HEX_LENGTH) {
return BlockHash("0x$hexBody".decodeHex())
}
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.

Parse ambiguity: valid hex number misidentified as hash

Medium Severity

BlockParameter.parse treats any 64-character hex string as a BlockHash, but a 64-hex-char string is also a valid 256-bit hex number. For instance, "0x0000000000000000000000000000000000000000000000000000000000000001" parses as a BlockHash rather than BlockNumber(1). Callers using parse for block numbers from sources that zero-pad to 32 bytes will silently get the wrong BlockParameter variant, leading to UnsupportedOperationException when getNumber() is called.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ab8aa98. Configure here.

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.

This comment seems relevant and the implementation can be simplified as well IMHO

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The comment while theoretically true is not practical. We don't pad numbers to 32 bytes. If we happen to use padded numbers, it will not be possible to differentiate if it is a block number or block hash.

Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 3 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 0464554. Configure here.

private fun byteArrayListHashCode(list: List<ByteArray>): Int {
return list.fold(0) { acc, bytes -> 31 * acc + bytes.contentHashCode() }
}
}
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.

ExecutionWitness data class missing toString() override

Low Severity

ExecutionWitness is a data class with List<ByteArray> fields but does not override toString(). The auto-generated data class toString() will produce unreadable output like ExecutionWitness(state=[[B@1a2b, [B@3c4d], ...) since ByteArray uses identity-based toString(). This violates the Kotlin/Java checklist item requiring toString() overrides on data classes with ByteArray fields.

Fix in Cursor Fix in Web

Triggered by project rule: Bugbot Review Instructions

Reviewed by Cursor Bugbot for commit 0464554. Configure here.

@gauravahuja gauravahuja requested a review from a team June 2, 2026 13:38
@@ -0,0 +1,41 @@
package linea.executionwitness
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.

I recommend placing this under the linea.ethapi package.

The rationale is that debug_executionWitness is (or will be) part of the Ethereum standard API, even if it's in debug_ name space. forced transaction's won't be hence they have their own packave.

@jonesho @Filter94 WDYT?

return result
}

private fun byteArrayListsEqual(a: List<ByteArray>, b: List<ByteArray>): Boolean {
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.

IIRC, I have already seen something with same intent elsewhere before. Maybe this shall be in ByteArrayExtensions.kt ?

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.

FYI byteArrayListEquals in jvm-libs/generic/extensions/kotlin/src/main/kotlin/linea/kotlin/ListExtensions.kt

interface ExecutionWitnessClient {
fun getExecutionWitness(
block: BlockParameter,
): SafeFuture<Result<ExecutionWitness, ErrorResponse<ExecutionWitnessError>>>
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.

I suggest we return the value directly instead of the a Result monad. For errors, you can use the JsonRpcErrorResponseException

Suggested change
): SafeFuture<Result<ExecutionWitness, ErrorResponse<ExecutionWitnessError>>>
): SafeFuture<ExecutionWitness>


class FakeExecutionWitnessClient(
private val witnessesByBlock: Map<BlockParameter, ExecutionWitness> = emptyMap(),
) : ExecutionWitnessClient {
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.

What use cases do you envision for this isolated class? Why not part of FakeEthApiClient with auto-generation of fake execution witness ?

val hexBody = normalized.drop(2)
if (hexBody.length == BLOCK_HASH_HEX_LENGTH) {
return BlockHash("0x$hexBody".decodeHex())
}
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.

This comment seems relevant and the implementation can be simplified as well IMHO

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.

I don't much value/motivation for the existence of this file. It feels generated by AI with obvious info.

The non-obvious part is Node prerequisites section, which can be added as comment ExecutionWitnessClient.kt directly.

@Filter94 @jonesho Any take on this?


constructor(
vertx: Vertx,
rpcClient: JsonRpcClient,
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.

use JsonRpcV2Client instead. V1 is meant to be deprecated/removed once Shomei and Traces API get removed.

}

enum class ExecutionWitnessError {
NULL_RESULT,
Copy link
Copy Markdown
Contributor

@jonesho jonesho Jun 3, 2026

Choose a reason for hiding this comment

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

@gauravahuja how does "NULL_RESULT" occurs? like a server side failure where the response somehow was null? what scenario from Besu could that happen? e.g. the target block number is in the future?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

val b = BlockParameter.fromHash(hashBytes.copyOf())
assertThat(a).isEqualTo(b)
assertThat(a.hashCode()).isEqualTo(b.hashCode())
assertThat(BlockParameter.fromHash(ByteArray(32) { 8 })).isNotEqualTo(a)
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.

I don't see how the above assertion related to "content-based equality"


private val sampleWitnessJson = """
{
"state": ["f902"],
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.

}

@Test
fun `getExecutionWitness returns NULL_RESULT when result is null`() {
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.

Is it speculative or an actually possible response in case a block is unavailable?

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.

I see you answered it here

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.

5 participants