AirgapTransactionError type from RPC#6435
Conversation
|
If this PR represents a change to the public RPC API:
Thank you for keeping the RPC clients in sync with the server API @steveluscher. |
6f9a63c to
12c3fc7
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #6435 +/- ##
=========================================
- Coverage 82.8% 82.8% -0.1%
=========================================
Files 849 849
Lines 379208 379314 +106
=========================================
+ Hits 314085 314161 +76
- Misses 65123 65153 +30 🚀 New features to boost your workflow:
|
12c3fc7 to
04e1299
Compare
joncinque
left a comment
There was a problem hiding this comment.
Looks great overall, mostly small things
| } | ||
|
|
||
| #[derive(Clone, Debug, PartialEq, Eq)] | ||
| pub struct UiTransactionError(pub TransactionError); |
There was a problem hiding this comment.
To really airgap this, it would be best to keep the inner type private. That way, users must use from / into to go between the two types
There was a problem hiding this comment.
Yeah, this was all in service of this wild match:
match e.kind() {
ErrorKind::Io(_) | ErrorKind::Reqwest(_) => {
// fall through on io error, we will retry the transaction
}
ErrorKind::TransactionError(TransactionError::BlockhashNotFound)
| ErrorKind::RpcError(RpcError::RpcResponseError {
data:
RpcResponseErrorData::SendTransactionPreflightFailure(
RpcSimulateTransactionResult {
err: Some(UiTransactionError(TransactionError::BlockhashNotFound)),
..
},
),
..
}) => {
// fall through so that we will resend with another blockhash
}
ErrorKind::TransactionError(transaction_error)
| ErrorKind::RpcError(RpcError::RpcResponseError {
data:
RpcResponseErrorData::SendTransactionPreflightFailure(
RpcSimulateTransactionResult {
err: Some(UiTransactionError(transaction_error)),
..
},
),
..
}) => {
// if we get other than blockhash not found error the transaction is invalid
context.error_map.insert(index, transaction_error.clone());
}
_ => {
return Err(TpuSenderError::from(e));
}
}Which produced this error when I made the inner type private:
error[E0532]: cannot match against a tuple struct which contains private fields
--> client/src/send_and_confirm_transactions_in_parallel.rs:293:43
|
293 | ... err: Some(UiTransactionError(TransactionError::BlockhashNotFound)),
| ^^^^^^^^^^^^^^^^^^
|
note: constructor is not visible here due to private fields
--> client/src/send_and_confirm_transactions_in_parallel.rs:293:62
|
293 | ... err: Some(UiTransactionError(TransactionError::BlockhashNotFound)),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ private field
I've forced it to work by changing the match.
23b3c33 to
474dcb3
Compare
joncinque
left a comment
There was a problem hiding this comment.
Just one last little thing, then this is good to go from my side!
| solana-tps-client = { workspace = true } | ||
| solana-tpu-client = { workspace = true } | ||
| solana-transaction = { workspace = true } | ||
| solana-transaction-error = { workspace = true } |
| transaction, | ||
| get_transaction_error, | ||
| err: transaction_status.err.clone(), | ||
| err: transaction_status.err.clone().map(Into::into), |
There was a problem hiding this comment.
Since we're making breaking changes with 3.0, it might be worth having rpc-client return the new error type in follow-up work
There was a problem hiding this comment.
Should we? I think people interacting with the rpc-client want to feed the TransactionError they get into {some code} where it's appropriate to feed it the ‘raw’ thing. UiTransactionError really just exists as a wrapper in which to locate stupid serialization logic like ‘you have to produce exactly this JSON for {reasons}.’ I think it's fine and appropriate to keep the Rust RPC client speaking in the base Rust types?
| let outer_instruction_index: u8 = arr[0] | ||
| .as_u64() | ||
| .expect("Expected the first element to be the `outer_instruction_index`") | ||
| as u8; | ||
| let err = InstructionError::deserialize(&arr[1]) | ||
| .expect("Expected the second element to deserialize as an `InstructionError`"); |
There was a problem hiding this comment.
IMO, this function is already fallible, so it doesn't hurt to make these index accessors (arr[0]) as well as the expect calls fallible as well, rather than panics.
There was a problem hiding this comment.
I struggled with this @buffalojoec. Is that what you had in mind? a9273c0
There was a problem hiding this comment.
Yes! Just wanted to avoid the crashes. Calling .expect() or .unwrap() will crash the program if the value returns empty.
There was a problem hiding this comment.
If you want, you can take a look at .and_then(), which could help you stack Result values together and simplify some of that. Up to you.
There was a problem hiding this comment.
It works, and now I'm afraid to touch it.
* Changed CLI types to use `UiTransactionError` * Eliminated `UiTransactionResult` in favour of `Result<(), UiTransactionError>` * Implemented `std::error::Error` on `UiTransactionError` which just forwards to `TransactionError` * Implemented `fmt::Display` on `UiTransactionError` which just forwards to `TransactionError` * Made the inner error private on `UiTransactionError` * Eliminated `Deref` implementation, requiring explicit conversion
* Make the `InstructionError` deserialize actually work by taking all of the bytes
474dcb3 to
a9273c0
Compare
| DeserializeError::custom("Expected the first element to be a u64") | ||
| })? as u8; | ||
| let rest_bytes: Vec<u8> = arr | ||
| .get(1..) |
There was a problem hiding this comment.
Fixed this. Completely didn't work before.
There was a problem hiding this comment.
SVM stamp. 🤝
Please have a look at @joncinque's two comments.
b78abe1 to
992a206
Compare
|
Oof. I don't know what I was thinking with that deserializer update. Pushed 992a206 just now to simply take the second element in the array and pass it to |
* Add `UiTransactionError` and `UiTransactionResult` * Fixup callsites and tests * Review feedback * Changed CLI types to use `UiTransactionError` * Eliminated `UiTransactionResult` in favour of `Result<(), UiTransactionError>` * Implemented `std::error::Error` on `UiTransactionError` which just forwards to `TransactionError` * Implemented `fmt::Display` on `UiTransactionError` which just forwards to `TransactionError` * Made the inner error private on `UiTransactionError` * Eliminated `Deref` implementation, requiring explicit conversion * * Results, all the way down * Make the `InstructionError` deserialize actually work by taking all of the bytes * Remove stray dependency * Fix the deserializer
* Add `UiTransactionError` and `UiTransactionResult` * Fixup callsites and tests * Review feedback * Changed CLI types to use `UiTransactionError` * Eliminated `UiTransactionResult` in favour of `Result<(), UiTransactionError>` * Implemented `std::error::Error` on `UiTransactionError` which just forwards to `TransactionError` * Implemented `fmt::Display` on `UiTransactionError` which just forwards to `TransactionError` * Made the inner error private on `UiTransactionError` * Eliminated `Deref` implementation, requiring explicit conversion * * Results, all the way down * Make the `InstructionError` deserialize actually work by taking all of the bytes * Remove stray dependency * Fix the deserializer
* Add `UiTransactionError` and `UiTransactionResult` * Fixup callsites and tests * Review feedback * Changed CLI types to use `UiTransactionError` * Eliminated `UiTransactionResult` in favour of `Result<(), UiTransactionError>` * Implemented `std::error::Error` on `UiTransactionError` which just forwards to `TransactionError` * Implemented `fmt::Display` on `UiTransactionError` which just forwards to `TransactionError` * Made the inner error private on `UiTransactionError` * Eliminated `Deref` implementation, requiring explicit conversion * * Results, all the way down * Make the `InstructionError` deserialize actually work by taking all of the bytes * Remove stray dependency * Fix the deserializer
|
To go along with the changes in #8625, we'll need to backport this PR to v2.3 for forward compatibility with v3 of The airgapped types allow us to modify deserialization so that v2.3 clients / RPCs can deserialize the new error type into the old one. Otherwise RPCs are blocked from upgrading their fleet. I tested this locally, and it works. |
Problem
At present, we leak the serialization format of
TransactionErrorstraight out through the RPC. Any change to the serialization format ofTransactionError(this being an example) could change the response format of the RPC API. Application clients that expect responses to be in a certain format (eg.{ "InstructionError": [number, string | { [name: string]: unknown }] }) could break if the serialization ever changed (eg.{ "InstructionError": { index: number, err: { __type: string } & Record<string, unknown> }).Summary of Changes
In this PR, we create ‘airgaps’ between
TransactionErrorand the type that is emitted from the RPC API:UiTransactionErrorUiTransactionResultThose provide a locus for custom serialization logic. Now you can conceive of a change to the structure of
TransactionErrorthat can be handled by the custom serialization logic to produce JSON that is backward compatible with existing clients.We also add tests to verify that the current JSON output for named, struct, tuple, and particularly
InstructionErrorvariants ofTransactionErrordon't change without a test breaking.This is a prerequisite for #6083