| name | brane-code-review |
| description | Review Brane SDK code for correctness, Java 21 patterns, type safety, and architectural consistency. Use when reviewing PRs, checking code changes, or validating implementations against Brane standards. |
Brane SDK Code Review
Architecture Overview
Brane is a type-safe Ethereum SDK for Java 21. The codebase follows these principles:
- Zero external dependencies in public APIs - Only JDK and Brane types exposed
- Immutable value types - Records for data, no mutable state in public types
- Explicit error handling - Typed exceptions, no silent failures
- Thread-safe by default - Safe for concurrent use without external synchronization
Module Structure
| Module | Purpose | Dependencies |
|---|
brane-primitives | Low-level Hex/RLP encoding | None (foundation) |
brane-core | Types, ABI, Crypto, Models, Errors | brane-primitives |
brane-rpc | JSON-RPC clients (Brane.Reader, Brane.Signer) | brane-core |
brane-contract | High-level contract binding via dynamic proxy | brane-core, brane-rpc |
brane-examples | Integration tests and usage examples | All modules |
brane-benchmark | Performance benchmarks (may use external libs) | All modules |
Dependency Rule: Lower modules MUST NOT depend on higher modules.
Reference Code (Study These First)
Before reviewing, understand these exemplary implementations:
- Record + validation:
brane-core/.../types/Address.java
- Exception hierarchy:
brane-core/.../error/RpcException.java
- Complex client:
brane-rpc/.../DefaultSigner.java
- Dynamic proxy:
brane-contract/.../BraneContract.java
- Javadoc style:
brane-contract/.../BraneContract.java
Java 21 Patterns (Required)
Full Reference: See JAVA21.md in project root for comprehensive patterns including:
sealed types, guarded patterns (when clause), null handling rules (lazy eval vs constant defaults),
virtual thread pinning, RpcUtils.toRpcException(), and static constants (Wei.ZERO, HexData.EMPTY).
Records for Data Types
public record Address(@JsonValue String value) {
public Address {
Objects.requireNonNull(value, "address");
if (!HEX.matcher(value).matches()) {
throw new IllegalArgumentException("Invalid address: " + value);
}
value = value.toLowerCase(Locale.ROOT);
}
}
public class Address {
private String value;
public void setValue(String v) { this.value = v; }
}
Switch Expressions
return switch (type) {
case UINT -> decodeUint(data);
case ADDRESS -> decodeAddress(data);
case BOOL -> decodeBool(data);
};
switch (type) {
case UINT:
return decodeUint(data);
case ADDRESS:
return decodeAddress(data);
default:
throw new IllegalArgumentException();
}
Pattern Matching
if (value instanceof Address addr) {
return addr.value();
}
if (value instanceof Address) {
Address addr = (Address) value;
return addr.value();
}
Text Blocks for Multi-line Strings
private static final String ABI_JSON = """
[
{"type": "function", "name": "transfer"}
]
""";
private static final String ABI_JSON =
"[\n" +
" {\"type\": \"function\"}\n" +
"]";
var for Obvious Types
var address = new Address("0x...");
var mapper = new ObjectMapper();
var logs = new ArrayList<LogEntry>();
var result = process(input);
TransactionReceipt result = process(input);
Stream.toList() over Collectors
return topics.stream().map(Hash::new).toList();
return topics.stream().map(Hash::new).collect(Collectors.toList());
Type Safety Rules
Public API Types
Public methods/constructors/fields MUST only use:
- Java standard types:
String, BigInteger, List, Map, byte[]
- Brane types:
Address, Hash, HexData, Wei, Transaction, TransactionReceipt
- Brane exceptions:
RpcException, RevertException, AbiEncodingException, AbiDecodingException
Null Handling
Objects.requireNonNull(provider, "provider");
public Optional<Long> nonceOpt() { ... }
public Address getAddress() { return address; }
return optional.get();
Raw Types
List<LogEntry> logs = new ArrayList<>();
Map<String, Object> params = new LinkedHashMap<>();
List logs = new ArrayList();
Map params = new HashMap();
Exception Handling
Exception Hierarchy
BraneException (base)
├── RpcException (JSON-RPC errors)
├── RevertException (contract reverts with decoded reason)
├── AbiEncodingException (encoding failures)
├── AbiDecodingException (decoding failures)
├── ChainMismatchException (wrong chain ID)
├── InvalidSenderException (signer mismatch)
└── TxnException (transaction failures)
Exception Wrapping
try {
return provider.send(method, params);
} catch (IOException e) {
throw new RpcException(-32000, "Connection failed: " + endpoint, null, e);
}
catch (IOException e) {
throw new RpcException(-32000, "Connection failed", null);
}
catch (IOException e) {
return null;
}
Never Catch Generic Exception in Public API
try {
process(input);
} catch (Exception e) {
}
try {
process(input);
} catch (RpcException e) {
handleRpcError(e);
} catch (AbiDecodingException e) {
handleDecodingError(e);
}
Concurrency Patterns
Thread-Safe Caching
private final AtomicReference<Long> cachedChainId = new AtomicReference<>();
public long getChainId() {
Long cached = cachedChainId.get();
if (cached != null) {
return cached;
}
long actual = fetchChainId();
cachedChainId.set(actual);
return actual;
}
private Long cachedChainId;
public long getChainId() {
if (cachedChainId == null) {
cachedChainId = fetchChainId();
}
return cachedChainId;
}
Virtual Threads (When Applicable)
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
futures.forEach(f -> executor.submit(f));
}
ExecutorService executor = Executors.newFixedThreadPool(10);
Documentation Standards
Javadoc Requirements
Public classes and methods MUST have Javadoc with:
- Summary sentence - What it does (imperative mood)
- @param - For each parameter
- @return - What is returned
- @throws - Each checked and significant unchecked exception
TransactionReceipt sendTransactionAndWait(
TransactionRequest request,
long timeoutMillis,
long pollIntervalMillis);
Code Examples in Javadoc
Use {@code} for inline code, <pre>{@code ...}</pre> for blocks:
Review Checklist
Correctness
Java 21 Patterns
Type Safety
Exceptions
Architecture
Style
Common Review Findings
Finding: Missing Null Check
public void process(Address addr) {
return addr.value();
}
public void process(Address addr) {
Objects.requireNonNull(addr, "addr");
return addr.value();
}
Finding: Mutable Return Type
public List<LogEntry> getLogs() {
return logs;
}
public List<LogEntry> getLogs() {
return List.copyOf(logs);
}
Finding: Resource Leak
InputStream is = new FileInputStream(file);
byte[] data = is.readAllBytes();
try (InputStream is = new FileInputStream(file)) {
byte[] data = is.readAllBytes();
}
Finding: Old Collection Patterns
List<String> result = new ArrayList<>();
for (Hash h : hashes) {
result.add(h.value());
}
return result;
return hashes.stream().map(Hash::value).toList();
Finding: Magic Numbers
if (data.length() != 42) { ... }
private static final int ADDRESS_HEX_LENGTH = 42;
if (data.length() != ADDRESS_HEX_LENGTH) { ... }
Performance Considerations
Avoid Unnecessary Allocations
public String getValue() {
return "0x" + Hex.encode(bytes);
}
private final String cachedValue;
public String getValue() {
return cachedValue;
}
Prefer Primitive Streams for Numeric Operations
long total = values.stream().mapToLong(Wei::toLong).sum();
Long total = values.stream().map(Wei::value).reduce(0L, Long::sum);
StringBuilder for String Concatenation in Loops
var sb = new StringBuilder();
for (var item : items) {
sb.append(item.value());
}
String result = "";
for (var item : items) {
result += item.value();
}
Security Considerations
Input Validation
- Validate all external input (RPC responses, user parameters)
- Reject invalid hex strings early
- Check array bounds before access
- Validate address format before use
Sensitive Data
- Never log private keys
- Sanitize transaction data in logs
- Use
LogSanitizer for debug output
Integer Overflow
BigInteger value = new BigInteger(hexValue, 16);
long value = Long.parseLong(hexValue, 16);
Verification Protocol (CRITICAL)
Goal: Every finding must be grounded in truth. No hallucinations. No vague claims.
Finding Classification
Classify each finding into one of four tiers, each with different evidence requirements:
| Tier | Type | Evidence Required | Action |
|---|
| T1 | Confirmed Bug | Failing test OR concrete execution trace proving the failure | Must fix before merge |
| T2 | Potential Bug | Code path trace + specific scenario description | Requires investigation |
| T3 | Design Concern | Explanation with rationale, reference to standards/patterns | Discuss with author |
| T4 | Suggestion | Brief explanation of improvement | Optional enhancement |
T1: Confirmed Bug - Evidence Requirements
For a finding to be classified as Confirmed Bug, you MUST provide ONE of:
Option A: Failing Test
@Test
void shouldRejectNullAddress() {
var client = new DefaultClient(provider);
assertThrows(IllegalArgumentException.class, () -> client.call(null));
}
Option B: Execution Trace
EXECUTION TRACE:
1. User calls: client.call(null)
2. → DefaultClient.call(Address addr) at line 42
3. → [NO null check] proceeds to line 47
4. → addr.value() called at line 47
5. → NullPointerException thrown (unintended)
EXPECTED: IllegalArgumentException at step 3
ACTUAL: NPE at step 5
T2: Potential Bug - Evidence Requirements
For Potential Bug, you MUST provide:
- Code Path Trace - Show the exact execution path
- Trigger Scenario - Specific conditions that trigger the issue
- Why Existing Tests Miss It - Explain the coverage gap
POTENTIAL BUG: Race condition in cached chain ID
CODE PATH:
1. Thread A calls getChainId(), sees cachedChainId == null
2. Thread A enters if-block, starts fetchChainId() (slow RPC call)
3. Thread B calls getChainId(), sees cachedChainId == null (not set yet)
4. Thread B also enters if-block, starts fetchChainId()
5. Both threads make redundant RPC calls
TRIGGER SCENARIO:
- Multiple virtual threads calling getChainId() on cold start
- High latency RPC endpoint (>100ms)
WHY TESTS MISS IT:
- Unit tests are single-threaded
- Integration tests use fast local Anvil, hiding the race window
T3: Design Concern - Evidence Requirements
For Design Concern, provide:
- What: Clear description of the concern
- Why It Matters: Impact on maintainability/readability/extensibility
- Reference: Link to standard, pattern, or existing code that demonstrates preferred approach
DESIGN CONCERN: Method does too many things (violates SRP)
WHAT: sendTransactionAndWait() handles signing, sending, polling, and timeout
WHY IT MATTERS:
- Hard to test individual behaviors
- Timeout logic duplicated if user wants different polling strategy
- 150 lines in single method reduces readability
REFERENCE: See viem's separation: signTransaction() + sendRawTransaction() + waitForTransactionReceipt()
T4: Suggestion - Evidence Requirements
Brief explanation only:
SUGGESTION: Use switch expression instead of if-else chain at line 87
Currently 15 lines, could be 8 with switch expression for better readability.
Counter-Argument Requirement
For every T1 and T2 finding, you MUST include a counter-argument section that argues why it might NOT be a bug:
FINDING: Null pointer risk in processTransaction(tx)
EVIDENCE: [execution trace as above]
COUNTER-ARGUMENT (Why this might NOT be a bug):
- The public API `submitTransaction()` validates tx != null before calling processTransaction()
- processTransaction() is private and only called from submitTransaction()
- Therefore, null can never reach this code path in practice
VERDICT: After tracing all call sites, confirmed processTransaction() is ONLY called from
submitTransaction() which has null check. This is NOT a bug - the internal method can
safely assume non-null. Downgrading to T4 Suggestion: add @Nullable annotation or
Objects.requireNonNull for defensive coding.
This forces you to:
- Consider if the issue is actually reachable
- Check preconditions enforced elsewhere
- Avoid false positives from analyzing code in isolation
Existing Test Verification
Before claiming any bug (T1 or T2), you MUST:
Step 1: Find Related Tests
./gradlew test --tests "*ClassName*" --dry-run
grep -r "methodName\|ClassName" */src/test/
Step 2: Analyze Test Coverage
Ask yourself:
- Do tests cover this code path?
- If tests exist and pass, why don't they catch this bug?
- Is the test incorrect, or is my analysis wrong?
Step 3: Document in Finding
EXISTING TEST ANALYSIS:
- Found: DefaultClientTest.java lines 45-67 test call() method
- Coverage gap: Tests only pass Address objects, never test null input
- Conclusion: Bug is real, tests have coverage gap
OR
EXISTING TEST ANALYSIS:
- Found: DefaultClientTest.shouldRejectNullAddress() at line 89
- Test passes and expects IllegalArgumentException
- Re-checking my analysis...
CORRECTION: I misread the code. Null check exists at line 41, I was looking at wrong method.
No bug here.
Finding Report Template
Use this template for each finding:
### [T1/T2/T3/T4] [Short Title]
**Location**: `module/path/to/File.java:LINE`
**Classification**: [Confirmed Bug | Potential Bug | Design Concern | Suggestion]
**Description**:
[1-2 sentences describing the issue]
**Evidence**:
[For T1: Failing test or execution trace]
[For T2: Code path + trigger scenario + test gap analysis]
[For T3: What + Why + Reference]
[For T4: Brief explanation]
**Existing Test Analysis**: (Required for T1/T2)
[What tests exist? Why don't they catch this?]
**Counter-Argument**: (Required for T1/T2)
[Why might this NOT be a bug? What would make my analysis wrong?]
**Verdict**:
[Final assessment after considering counter-argument]
**Recommended Fix**: (If applicable)
[Code snippet or description of fix]
Review Process
Phase 1: Discovery
- Read the code changes carefully
- Note potential issues without classifying yet
- DO NOT jump to conclusions
Phase 2: Verification
For each potential issue:
- Trace the code path
- Check existing tests
- Formulate counter-argument
- Classify into T1/T2/T3/T4
Phase 3: Report
- Use the finding template
- Order by severity (T1 first, T4 last)
- Be specific about locations (file:line)
Phase 4: Self-Check
Before submitting review, ask:
Anti-Patterns (What NOT to Do)
❌ Vague Claims
BAD: "This might have null pointer issues"
GOOD: "Line 47 dereferences `addr.value()` without null check. Trace shows
null can reach here via path X→Y→Z"
❌ Assumed Bugs Without Tracing
BAD: "The cache isn't thread-safe"
GOOD: "The cache uses plain field without synchronization. Trace shows Thread A
can see partial write from Thread B when [specific scenario]"
❌ Ignoring Test Evidence
BAD: Claim bug exists without checking if tests cover it
GOOD: "Tests exist but don't cover this case because [reason]"
❌ Single-Path Thinking
BAD: "This input validation is missing" (without checking all entry points)
GOOD: "Checked all 3 call sites: submitTx(), batchSubmit(), internal retry().
Only retry() lacks validation, but it's only called after validation
in submitTx(). Not a bug."
Confidence Calibration
After completing a review, rate your confidence:
| Confidence | Meaning | When to Use |
|---|
| High | Would bet money on it | Failing test exists, or exhaustive trace completed |
| Medium | Likely correct but edge cases unclear | Traced main path, some branches unchecked |
| Low | Uncertain, needs more investigation | Pattern-matched without deep trace |
Rule: Only report T1 (Confirmed Bug) with HIGH confidence. If confidence is Medium/Low, downgrade to T2 or investigate further.