feat: probe Aptos providers by ledger chain id - #4069
Conversation
67d79d3 to
05e2c85
Compare
301c4ea to
672d398
Compare
fbf1c4e to
1ef3e16
Compare
Pull request overviewAdds an Aptos arm to the network-fingerprint probe. Changes:
Reviewed changesPer-file summary
I did not build or run the test suite ( FindingsNon-blocking (nits, follow-ups, suggestions):
Nothing else stood out: the config templates already ship ✅ Approved |
1ef3e16 to
d5ee2e1
Compare
d5ee2e1 to
a8946bf
Compare
a8946bf to
f6eca1f
Compare
0718161 to
3061134
Compare
|
|
||
| #[rstest] | ||
| #[case::mainnet("1", "1")] | ||
| #[case::padded("0002", "2")] |
There was a problem hiding this comment.
Optional nit: if we treat 0002 as 2, which means testnet, then perhaps it should be:
| #[case::padded("0002", "2")] | |
| #[case::testnet("0002", "2")] |
or
| #[case::padded("0002", "2")] | |
| #[case::padded_testnet("0002", "2")] |
| ForeignChainInspectionError::RpcRequestRejected(message) | ||
| } | ||
| }, | ||
| // Rate limits and server errors are provider hiccups → transient, so the |
There was a problem hiding this comment.
Nit: It was a bit confusing to me to see the comment explaining that this error is transient (until I consulted Claude), because the transient/non-transient split isn't decided in this file at all. The mapping in classified() only picks a ForeignChainInspectionError variant, while each variant's transientness is defined centrally in is_transient() in lib.rs. Same goes for:
| .expect("network_fingerprint should succeed"); | ||
|
|
||
| // Then | ||
| assert_eq!(fingerprint.to_string(), "2"); |
There was a problem hiding this comment.
We could reuse TESTNET_CHAIN_ID here.
| // Rate limits and server errors are provider hiccups → transient, so the | ||
| // affected provider is dropped from the quorum instead of blocking it. | ||
| AptosRpcError::ApiError { | ||
| status: 408 | 429, .. |
There was a problem hiding this comment.
FWIW, by Claude:
These two arms re-encode the policy that already exists as
is_retryable_statusin the crate root (408 | 429, or>= 500), which is reachable from this module. Collapsing them into oneAptosRpcError::ApiError { status, .. } if is_retryable_status(status)arm keeps the two classifiers from drifting when one of them learns a new status.
| /// Aptos mainnet's ledger chain id, as shipped in the node config file | ||
| /// `foreign_chains.aptos.expected_network_fingerprint`. | ||
| const EXPECTED_NETWORK_FINGERPRINT: &str = "1"; |
There was a problem hiding this comment.
How does this comment guarantee it won't get stale? Why is it relevant that this is set in the config? If so, do we have a DEFAULT_* parameter somewhere we could use from prod, instead of hard-coding this in the test?
There was a problem hiding this comment.
No hard guarantee, but Aptos mainnet's chain id is 1 and is not supposed to ever change, so I don't expect this to get stale. "As shipped in the node config file" refers to the default config file we ship with values already filled in. The operators can freely change this value. They can for instance change this to 2 to target Aptos testnet, for a testnet deployment.
There is deliberately no DEFAULT_* in code. We wanted a clean separation between config and code, the node just runs with whatever the config file says. By that logic the test should read the config file as well, but that felt like overkill for a sanity check. Since the chain id is fixed, I think hard coding it here is fine.
There was a problem hiding this comment.
We wanted a clean separation between config and code, the node just runs with whatever the config file says
Thanks for the explanation. I feel like the comment "as shipped in the node config file is at odds with that goal, but since this is in a test-file, this is not a blocker.
| const EXPECTED_NETWORK_FINGERPRINT: &str = "1"; | ||
|
|
||
| #[tokio::test] | ||
| #[ignore = "manual test to sanity check against live Aptos RPC provider"] |
There was a problem hiding this comment.
This test might be more useful if we could activate it with a flag, together with all the other live-RPC tests.
As it stands, I don't know how we will remember / know that it exists and periodically run it.
There was a problem hiding this comment.
The flag already exists as the crate convention: every *_rpc_manual.rs test is #[ignore = "manual …"], and they all run together with cargo nextest run -p foreign-chain-inspector --run-ignored only.
Running them periodically is a fair point, but idealy I would like to retire these tests completely once the probing logic is landed and picked up in the Foreign chain config tester CLI. The periodic run should use that CLI tool to exercise the same code as used in the Tx validation path, instead of these half baked tests.
My understanding is that these are more of a "trust me bro, I did test the request code against the real provider for my change" kind of test atm.
There was a problem hiding this comment.
idealy I would like to retire these tests completely once the probing logic is landed and picked up in the Foreign chain config tester CLI.
that does sound nice. Good plan 👍
The chain id lives in the ledger info at the REST root, so `AptosRpcClient` gains a call for it. The status mapping `extract` already had is shared, except for a 404, which on the root means the URL serves no Aptos API rather than a missing transaction.
`probe_chain` hands it to the inspector factory, so a chain whose client carries its own deadline cannot drift from the one the probe enforces.
Threading the deadline through the factory keeps client construction inside the probe, which is the thing to move. Leaves a TODO(#4043) where it belongs.
A 404 reads differently per endpoint, so the response type carries the verdict as an associated const and the call site passes nothing.
…esource reqwest reports both as a decode error, so the transport step and the decode step now fail with their own types: a truncated or timed out body stays transient, while a body that is not the resource is a verdict about the endpoint. Also folds the status table into `classified`, so the absence meaning is only ever read from the response type.
`ClassifyRpcOutcome::Response` now requires `HasAbsenceMeaning`, so a transport cannot classify a response type that never declared what a "not found" answer means for it. Also restores the `#[from]` conversions on `AptosRpcError` and sweeps the comments this stack added.
Name the config field the manual test's fingerprint mirrors, and drop the comments that restate the code they sit on.
3061134 to
46ae5c4
Compare
|
Sorry, I committed to re-reviewing it today, but I won’t make it. There were more review comments and follow-up commits than I expected. I’ll try to get to it tomorrow. Sorry about that. 🙏🏼 |
There was a problem hiding this comment.
I skimmed through it and it LGTM. I didn’t do a deep re-review because, as discussed offline, it looks like you force-pushed some changes after the reviews, which makes it much harder to tell what changed since then. Not sure if that was caused by the automatic restack or something else.
Let’s try to avoid force-pushing once reviews are in when possible, since the diff against the commit I reviewed is pretty noisy, presumably because of the force-push: https://github.com/near/mpc/compare/30611349572c5c843cdf7443237be3a30f3746fa..a4e826bc0fd99eb930007a499f56e0050c14177f.
kevindeforth
left a comment
There was a problem hiding this comment.
Thank you, this looks good!
Closes #4092.
Aptos reports its chain id in the ledger info every node serves at the REST root, so
AptosRpcClientgains aget_ledger_infocall and the inspector readschain_idfrom it.Notes for review
A 404 means different things depending on which method was called.
get_transaction_by_hash: the transaction is absent.get_ledger_info: the endpoint does not serve an Aptos API.foreign-chain-instpectorcrate root because Sui needs the same distinction for gRPCNOT_FOUND.The transport step and the decode step fail with their own types. Splitting the parsing into two steps to identify network errors from permanent faults.