Skip to content

Commit f482812

Browse files
committed
Preserve ABI revert evidence for pinned NFT lifecycle reads
1 parent 0fde169 commit f482812

3 files changed

Lines changed: 80 additions & 1 deletion

File tree

docs/RPC_OPERATIONS.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ contract outcome, not a provider outage. The caller receives the RPC error and
2424
the provider remains available for other calls. Transport failures, malformed
2525
responses, and missing archive state still trigger capability-specific failover.
2626

27+
Routed RPC exceptions retain the redacted JSON-RPC error payload, including
28+
ABI revert data. The pinned-state decoder uses that evidence to recognize
29+
`Invalid token ID` only at a verified same-receipt NFT mint/burn boundary.
30+
Keeping only the provider message loses this proof and retries valid receipts
31+
indefinitely. Unproven absence and unrelated errors still fail enrichment.
32+
2733
## Goldsky measurements and limits
2834

2935
A small anonymous-output probe from the production host verified the donated provider privately: chain 4663; exact matching block/header and log digests against the local node; old block headers; USDG `decimals()` at blocks 30,000,000 and 56,400,000; receipts; `debug_traceTransaction` with `callTracer`; and a four-item JSON-RPC batch. The local pruned node could not answer those archive-state calls. Individual successful Goldsky requests in this probe took roughly 76–352 ms; these are samples, not percentile/SLA claims.

src/rhpools/lp_rpc.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -659,8 +659,10 @@ def _validate_item(self, source: _Source, item: Any, request_id: int, method: st
659659
if error is not None:
660660
code = error.get("code") if isinstance(error, Mapping) else None
661661
message = error.get("message") if isinstance(error, Mapping) else error
662+
# Pinned NFT lifecycle reads need the ABI revert payload, not just
663+
# its message. Keep the full error while redacting provider secrets.
662664
failure = self._error(
663-
f"{source.name} {method}: {self._registry._safe_error(source, RuntimeError(str(message)))}",
665+
f"{source.name} {method}: {self._registry._safe_error(source, RuntimeError(str(error)))}",
664666
code=code if isinstance(code, int) else None,
665667
)
666668
if method in {"eth_call", "eth_estimateGas"} and (

tests/test_lp_rpc.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,3 +217,74 @@ def response(item):
217217
assert client.batch([good]) == ["0x01"]
218218
finally:
219219
factory.close()
220+
221+
222+
@pytest.mark.parametrize("lifecycle", ["mint", "burn"])
223+
def test_nfpm_lifecycle_absence_survives_routed_batch_fallback(
224+
provider, monkeypatch, tmp_path, lifecycle):
225+
from eth_abi import encode
226+
from rhpools.lp_market_index import MarketIndexer, RpcError
227+
from rhpools.lp_market_protocols import (
228+
ProtocolDecodeError, UNISWAP_V3_POSITION_MANAGER,
229+
decode_position_state_results, position_state_requests,
230+
)
231+
from rhpools.lp_market_store import MarketStore
232+
233+
monkeypatch.setattr(provider._registry, "error_type", RpcError)
234+
zero, owner = "0x" + "0" * 40, "0x" + "1" * 40
235+
missing_pin = "0x9" if lifecycle == "mint" else "0xa"
236+
liquidity = 100 if lifecycle == "mint" else 0
237+
revert_data = "0x08c379a0" + encode(["string"], ["Invalid token ID"]).hex()
238+
position_data = "0x" + encode(
239+
["uint96", "address", "address", "address", "uint24", "int24",
240+
"int24", "uint128", "uint256", "uint256", "uint128", "uint128"],
241+
[0, zero, owner, "0x" + "2" * 40, 500, -60, 60, liquidity, 0, 0, 0, 0],
242+
).hex()
243+
244+
def post(_client, _source, payload):
245+
def response(item):
246+
result = {"jsonrpc": "2.0", "id": item["id"]}
247+
if item["method"] == "eth_chainId":
248+
result["result"] = hex(CHAIN_ID)
249+
elif item["params"][1] == missing_pin:
250+
result["error"] = {
251+
"code": 3,
252+
"message": "RuntimeError: execution reverted: Invalid token ID",
253+
"data": revert_data,
254+
}
255+
else:
256+
result["result"] = position_data
257+
return result
258+
return [response(item) for item in payload] if isinstance(payload, list) else response(payload)
259+
260+
monkeypatch.setattr(lp_rpc.RoutedRpc, "_post", post)
261+
event = {
262+
"protocol": "nft", "kind": "transfer", "block_number": 10,
263+
"block_hash": "0x" + "a" * 64, "tx_hash": "0x" + "b" * 64,
264+
"tx_index": 0, "log_index": 1, "token_id": "42",
265+
"custody": UNISWAP_V3_POSITION_MANAGER,
266+
"data": {
267+
"manager_protocol": "v3", "mint": lifecycle == "mint",
268+
"burn": lifecycle == "burn",
269+
"prior_owner": zero if lifecycle == "mint" else owner,
270+
"new_owner": owner if lifecycle == "mint" else zero,
271+
},
272+
}
273+
with MarketStore(tmp_path / "market.sqlite") as store:
274+
scanner = MarketIndexer(store, SimpleNamespace(), "", rpc=provider)
275+
try:
276+
requests = position_state_requests([event])
277+
results = scanner._rpc_state_batch(scanner._state_rpc_calls(requests))
278+
update = decode_position_state_results(requests, results)[0]["data"]
279+
missing = "position_before" if lifecycle == "mint" else "position_after"
280+
present = "position_after" if lifecycle == "mint" else "position_before"
281+
assert update[missing]["exists"] is False
282+
assert update[missing]["claims_empty"] is True
283+
assert update[present]["exists"] is True
284+
assert update[present]["liquidity"] == str(liquidity)
285+
286+
event["data"].update(mint=False, burn=False)
287+
with pytest.raises(ProtocolDecodeError, match="pinned eth_call failed"):
288+
decode_position_state_results(position_state_requests([event]), results)
289+
finally:
290+
scanner.close()

0 commit comments

Comments
 (0)