fix: Skip an ABI function that cannot be indexed - #351
Merged
Uxío (Uxio0) merged 3 commits intoSep 18, 2026
Merged
Uxío (Uxio0) merged 3 commits into
Uxío (Uxio0) merged 3 commits into
Conversation
Rows in the abi table come from third party sources, so an element can be
missing the fields a selector needs. selectors_from_abis read fn_abi["type"]
and let the exception out, so one bad row aborted the whole batch. At startup
that reached the background loader in main.py, which retries forever, so
/health/ready stayed at 503 and the pod never joined the Service. The reload
path already tolerated a bad row since PLA-1954, startup did not.
The guard is now per element and catches everything. Which exception a
malformed element raises is a detail of the libraries that parse it, and the
invariant is that no single element can leave the decoder without a selector
map. Per element also means losing one method instead of every function of a
contract, which is what a per row guard would drop.
A missing type now means "function", as the ABI spec says. It has to be filled
in because function_abi_to_4byte_selector reads the key and raises without it.
Elements with no name are left out: abi_to_signature invents a signature for
them, and decoding one raises KeyError later, outside DataDecoderException, so
the request returns 500.
The per row handler in load_new_abis is gone. selectors_from_abis is the single
funnel for startup, reload and the per address lookup, so the guard belongs
there and only there.
The malformed_abi mock was [{"name": "buyDroid"}], which is now a valid ABI, so
it moves to a tuple input without components.
Refs PLA-2002
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
2 tasks
Felipe Alvarado (falvaradorodriguez)
previously approved these changes
Sep 18, 2026
Co-authored-by: Felipe Alvarado <felipe@safe.global>
Felipe Alvarado (falvaradorodriguez)
approved these changes
Sep 18, 2026
Uxío (Uxio0)
deleted the
uxio/pla-2002-decoder-service-stop-one-malformed-abi-row-from-blocking
branch
September 18, 2026 08:58
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Make sure these boxes are checked! 📦✅
./run_tests.sh(98 passed; ranalembic upgrade head+pytestagainst local containers on alternative ports because the compose ports were taken)pre-commit run -a(clean on the changed files;insert-licensealso wants to add SPDX headers to 51 unrelated files that are already missing them onmain, left alone)What was wrong? 👾
Closes https://linear.app/safe-global/issue/PLA-2002
Rows in the
abitable come from third party sources, so an element can be missing the fields a selector needs.selectors_from_abisreadfn_abi["type"]and let the exception out, so one bad row aborted the whole batch.At startup that exception reaches the background loader in
app/main.py, which logs it and retries everyDECODER_LOAD_RETRY_SECONDSforever./health/readystays at 503 and the pod never joins the Service. It is silent until the next restart, which may be days after the row arrived, and nothing alerts on it.The reload path already tolerated a bad row since PLA-1954. Startup did not, because it hands a whole batch to a single thread call.
Raised by Felipe Alvarado (@falvaradorodriguez) while reviewing #350.
How was it fixed? 🎯
The guard is now per element and catches everything:
get_contract_abi_selectors_with_functionsreturned{}for that contract, so it lostFULL_MATCHandPARTIAL_MATCHdecoding entirely.malformed_abimock stopped raising oncetypewas defaulted, so it had to be rewritten to keep failing.Two related fixes in the same filter:
typemeans"function", as the ABI spec says. It has to be filled in rather than just accepted, becausefunction_abi_to_4byte_selectorreads the key and raises without it.nameare left out.abi_to_signatureinvents a signature for them (function(uint256)), and decoding one later raisesKeyErroroutsideDataDecoderException, so the request returns 500. That path already existed for elements with an explicit"type": "function"and no name; defaulting the type would have widened it.The per row handler in
load_new_abisis gone.selectors_from_abisis the single funnel for startup, reload and the per address lookup, so the guard belongs there and only there.Behaviour change worth noting: ABIs whose elements omit
typeused to be dropped, and are now indexed. That is why themalformed_abimock changed shape:[{"name": "buyDroid"}]is a valid ABI under the spec default.Numbers
The restructuring was benchmarked against the previous shape on 500 production-shaped ABIs:
+0.25%, about +5ms at 100k rows. The per element
trycosts ~0.2ns per element thanks to zero-cost exceptions in 3.13, and the dict copy only happens whentypeis absent, which real ABIs never are.The short-circuit in
_generate_selectors_with_abis_from_abiis kept and now goes throughfunction_abis. It saves a 68us thread hop for an ABI with no functions, against 0.29us to check. It can sit outside the guard becausefunction_abiscannot raise: it ignores anabi_jsonthat is not a list and skips any element that is not a dict.Follow-ups, not in this PR
load_new_abiscallsadd_abiper row, so each row pays a ~68us thread hop for ~18us of work, and theprevious_last_abi_id is Nonebranch feeds the entire table through it. At 300k rows that is ~20s of thread overhead against ~5.7s of real work. Routing it through the batched path would make it ~41ms. Same optimisation that took startup from 14.4s to 1.9s.typeat write time instead of read time.abi_hashhashes the raw json, so[{"name":"f"}]and[{"type":"function","name":"f"}]are stored as two rows for the same ABI.