Reading the Web3j Codebase: What I Learned About Ethereum Transactions
A walkthrough of how transactions actually work inside a Java Ethereum library, and the bugs I fixed along the way.
Reading the Web3j Codebase: What I Learned About Ethereum Transactions
A walkthrough of how transactions actually work inside a Java Ethereum library, and the bugs I fixed along the way.
How This Started
Most developers who work with Ethereum from Java use Web3j but never really open the jar. You call contract.transfer(...), it works, and you move on. I was the same for a while.
Then I started preparing for the LFDT Web3j mentorship project under the Linux Foundation Decentralized Trust program. The mentorship is about maintaining and improving the Web3j library itself. Specifically keeping it compatible with the latest Ethereum Improvement Proposals (EIPs), fixing bugs, improving documentation, and generally helping the maintainers who do not have enough time to do all of this alone.
So I had to actually understand the library. Not just use it. I had to understand what happens between the line of Java code you write and the moment a transaction lands on the Ethereum network.
I started by cloning the repo and reading the module structure. Then I picked one thing to follow: a transaction. Where does it start, what touches it, what transforms it, and what finally goes out on the wire.
Once I had a rough map in my head, I started looking at open issues. I found some real bugs. I fixed a few of them. That is when things started making sense properly. You read code to understand it but you fix bugs to really understand it.
This blog is about what I found. I am going to explain it the way I wish someone had explained it to me when I first opened the codebase.
What Web3j Actually Does
Ethereum nodes expose a JSON-RPC API. You send an HTTP POST with a JSON body, the node responds with a JSON result. That is all. There is no special protocol. It is just HTTP.
The problem is that JSON-RPC alone is quite low level. If you want to call a smart contract function, you have to ABI-encode the function selector and arguments yourself. If you want to send a transaction, you have to RLP-encode it, sign it with ECDSA, and hex-encode the result. Then you have to handle nonces, gas estimation, receipt polling, and error parsing.
Web3j handles all of that for you in Java. It gives you typed contract wrappers, a signing system, an encoding system, and a transport layer that talks JSON-RPC. You get a library that feels like working with any other Java API while it handles all the Ethereum specifics underneath.
That is the pitch. But the interesting part is how the pieces fit together inside.
The Module Structure
Web3j is a multi-module Gradle project. Each module is fairly focused. Here is what each one does:
- core: The main entry point. Has the
Web3jinterface, JSON-RPC request/response objects, transaction managers, and receipt processors. - crypto: Handles key pairs, ECDSA signing, and
Credentials. This is where private keys live and where signatures are computed. - rlp: Implements Recursive Length Prefix encoding. This is the binary format Ethereum uses to serialize transactions.
- abi: Handles ABI encoding and decoding. Maps Java types to Solidity types so function calls can be encoded correctly.
- tx: Higher level transaction management. Has
TransactionManager, gas providers, and receipt polling. - utils: Hex conversion, numeric utilities, and the
Asyncclass that runs things off the main thread. - protocol: Low level JSON-RPC method definitions that map to Ethereum node API calls.
The dependency direction is mostly: core depends on crypto, abi, and rlp. The crypto module is a leaf, it does not depend on the others. The diagram below shows this more clearly.
Figure : Web3j High Level Architecture

The application talks to core. Core coordinates everything. The encoding layers (abi, rlp) and the signing layer (crypto) are used during transaction preparation. The protocol layer finally sends the JSON-RPC request to the node.
How a Transaction Actually Works Inside Web3j
This is the part I spent the most time on. Let me walk through it step by step because it is not obvious just from looking at the surface API.
Step 1: The contract call
You call a method on a generated contract wrapper. Something like token.transfer(to, amount).send(). That wrapper class is generated from the Solidity ABI JSON. It knows the function name and argument types.
Step 2: ABI encoding
The FunctionEncoder in the abi module takes the function and its arguments and produces a hex string. The first 4 bytes are the function selector (keccak256 of the function signature, truncated). The rest is the encoded arguments. For example, a uint256 value is padded to 32 bytes.
Step 3: Creating the raw transaction
RawTransaction.createTransaction() is called with the nonce, gas parameters, the target contract address, any ETH value, and the encoded function data from step 2.
Step 4: Gas and nonce
The TransactionManager fetches the current nonce from the node using eth_getTransactionCount. Gas price or fee parameters come from the configured ContractGasProvider. If gas limit is not set manually, it calls eth_estimateGas on the node.
Step 5: Signing
TransactionEncoder.signMessage() is called. This first builds a signing payload using RLP encoding (more on that below). Then it calls Sign.signMessage() with that payload and the private key. This runs ECDSA signing and returns the v, r, s components of the signature.
Step 6: Final encoding
The transaction fields plus the signature are RLP encoded together into a final byte array. This gets hex encoded into a string like 0x02f8cb.... The 0x02 prefix is the transaction type for EIP-1559.
Step 7: Sending
eth_sendRawTransaction is called with the hex string. The node validates the signature, checks the nonce, and broadcasts to the network if everything is fine.
Step 8: Receipt
The TransactionReceiptProcessor polls eth_getTransactionReceipt until the transaction is mined. It returns the receipt with the block number, gas used, and any event logs.
Figure : Transaction Flow Inside Web3j

The key thing to notice is that RLP encoding happens twice. Once for the signing payload and once for the final encoded transaction. They are not the same. That distinction matters a lot, especially for newer transaction types.
RLP Encoding: What It Is and Why It Matters
RLP stands for Recursive Length Prefix. It is the encoding format Ethereum uses to serialize arbitrary data structures into bytes. Everything that goes on the wire or gets hashed for signing uses RLP under the hood.
The rules are simple. A single byte value under 0x80 is encoded as itself. Longer byte strings are prefixed with their length. Lists of items are encoded recursively with a prefix for the total length of the encoded list. That is basically it.
In Web3j, the rlp module has two types: RlpString for leaf values and RlpList for collections. A transaction is a list of fields. Each field is an RlpString. The whole thing goes through RlpEncoder.encode() which recursively walks the structure and produces bytes.
The tricky part is knowing which fields go into which encoding.
For signing, you take a subset of the transaction fields, RLP encode them, hash the result with keccak256, and sign the hash. The node later decodes the signed transaction and verifies the signature by doing the same thing on its end. If your signing payload does not match what the node expects, the signature verification fails and the transaction is rejected.
For the network payload (what you actually send), you take all the transaction fields plus the signature components and RLP encode them together. For typed transactions like EIP-1559 or EIP-4844, you prepend a transaction type byte.
Getting this split wrong is a real bug. And it is the kind of bug that is hard to spot because the code looks correct until you compare it field by field against the Ethereum spec.
Blob Transactions and EIP-4844
EIP-4844, called Proto-Danksharding, added a new transaction type called Type 3 (prefix 0x03). The main purpose is to allow rollups to post large amounts of data to Ethereum cheaply using "blobs". A blob is roughly 128 KB of data. It is stored by nodes only temporarily and is not executed by the EVM.
A Type 3 transaction has some extra fields compared to a normal EIP-1559 transaction:
maxFeePerBlobGas: the maximum fee the sender is willing to pay per unit of blob gasblobVersionedHashes: a list of hashes that identify the blobs attached to the transactionblobs: the actual blob datacommitments: KZG polynomial commitments to the blob dataproofs: KZG proofs that the commitments are correct
Here is the part that matters for encoding. The signing payload for a Type 3 transaction includes the core fields including blobVersionedHashes but does NOT include the actual blobs, commitments, or proofs. Those go only in the network payload. The "sidecar" (blobs + commitments + proofs) is attached to the outer wrapper that gets sent to the node, but is not signed.
If your code includes blobs in the signing payload, you compute the wrong hash. Your signature is over the wrong data. The node rejects it. No helpful error message will tell you exactly why. You just get a signature verification failure.
Figure : EIP-4844 Signing Payload vs Network Payload

In the Web3j codebase, this means TransactionEncoder needs two separate paths for Type 3 transactions. One method builds the signing RLP. Another builds the network RLP. They use different field lists. This was one of the things I worked on.
Bugs I Worked On
Reading code is one thing. Fixing bugs forces you to actually understand what the code is supposed to do. Here are the issues I dug into.
1. The Async executor memory leak
Web3j has an Async utility class that runs things off the main thread using a static ExecutorService. Static fields live as long as the class is loaded. In a web server like Tomcat or a Spring Boot app, when you redeploy the application, the old classloader is supposed to be garbage collected. But if a thread from a static executor still holds a reference to that classloader, the GC cannot collect it. Memory leaks. On repeated redeploys, the JVM runs out of memory.
The fix is to expose a shutdown() method on Async and call it from Web3j.shutdown(). This lets the application properly clean up when it stops. The executor gets shut down, threads terminate, classloader reference is released.
2. Gas estimation with payable functions
When you call eth_estimateGas, you build a Transaction object that describes what you want to execute. For payable functions, the function checks msg.value during execution. If you send an estimation request without including the ETH value in the transaction object, the simulated execution sees msg.value == 0. If the function reverts when value is zero (for example if it requires a minimum payment), the estimation call fails with a revert error.
The fix is simple. When building the gas estimation transaction, include the value field. Web3j was not forwarding it, so payable function gas estimation was broken in those cases.
3. EIP-4844 signing payload including blobs
The signing and network encoding for Type 3 transactions was not properly separated. The signing method was including blob-related data that should only be in the network payload. This produces a wrong transaction hash, which means the signature is computed over the wrong bytes. The node rejects the transaction. I worked on fixing the RLP encoding to correctly separate these two code paths.
4. Raw type generics in EthLog
The EthLog class had some raw type usage in Java generics. This causes unchecked warnings and in some cases type safety issues. The fix was straightforward: properly parameterize the generic types. But it required understanding what the class was doing with log results, which involved reading through the filter and event handling code.
What I Actually Learned
Let me be honest. I knew the theory of Ethereum transactions before this. But theory and code are very different things.
Reading the Web3j codebase gave me a much more concrete understanding of a few things:
RLP is not complicated but getting it wrong is easy. The spec is short. The implementation is a few hundred lines. But when you are encoding a Type 3 transaction and you have to match the exact field order and field inclusion against the Ethereum yellow paper, it gets fiddly fast.
ECDSA signing is used twice in one transaction. The hash you sign is a hash of the RLP-encoded signing payload. The signature components go back into the RLP for the final encoded transaction. Understanding this flow made everything about transaction validation on the node side click into place.
JSON-RPC is simpler than it looks. At the transport level, Web3j is just serializing Java objects into JSON and parsing the response. Jackson handles that. The complexity is all in preparing the data before it hits the network layer.
Large open source codebases have a logic to them. Once I understood the module boundaries, navigating the code became much easier. Core coordinates, crypto signs, rlp encodes, abi handles function data. Following one complete flow from top to bottom is the fastest way to build that mental model.
Fixing bugs teaches you more than reading. The gas estimation bug made me read through how eth_estimateGas is called, what fields matter, and how Solidity payable checks actually work. I would not have understood that from reading alone.
What I Want to Work on Next
I am applying for the LFDT Web3j mentorship under the Linux Foundation Decentralized Trust program for the June to November 2026 term. The mentors are George Tebrean and Nischal Sharma from Web3 Labs.
The project is about improving Web3j’s component libraries, keeping up with the latest EIPs, fixing known issues, improving documentation, and making the library more maintainable. A previous cohort did similar work and contributed real code that is now in the library.
The areas I am most interested in are transaction handling and encoding (especially getting EIP-4844 and EIP-7594 support right), improving test coverage for the encoding and signing code paths, and working on developer documentation that explains the internals more clearly than what exists today.
Web3j is one of the most practically important libraries in the Ethereum Java ecosystem. It deserves proper maintenance and I would like to help with that.
If you want to explore the codebase yourself, start by cloning the repo and following a single transaction from Contract.java down to TransactionEncoder.java. Once you understand that path, the rest of the library makes a lot more sense.
Written as part of preparation for LFDT Web3j Mentorship — LFDT / Linux Foundation, 2026. Web3j repository: github.com/web3j/web3j
메타데이터
- post_id
- d7cc906b64b3
- slug
- reading-the-web3j-codebase-what-i-learned-about-ethereum-transactions-d7cc906b64b3
- url
- https://medium.com/@Dev10-sys/reading-the-web3j-codebase-what-i-learned-about-ethereum-transactions-d7cc906b64b3
- canonical_url
- https://medium.com/@Dev10-sys/reading-the-web3j-codebase-what-i-learned-about-ethereum-transactions-d7cc906b64b3
- author_url
- https://medium.com/@Dev10-sys
- status
- ok
- fetched_at
- 2026-07-16 19:27:03