Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: reintroduce RPC provider agreement logic #100

Merged
merged 21 commits into from
Dec 19, 2023
Merged

Conversation

rvanasa
Copy link
Collaborator

@rvanasa rvanasa commented Dec 15, 2023

The EVM RPC canister now calls several providers, returning either one consistent result or all individual results when calling relevant Candid-RPC methods. The agreement logic is the same as in the ckETH minter canister.

Progress:

  • Add consistency logic from ckETH
  • Update state machine tests
  • Add more tests for consistency logic

It's important to note that because this canister returns all JSON-RPC fields, it's possible to receive "inconsistent" results in cases where the results would otherwise be consistent for ckETH. This may also cause developers to write their own consistency checks. In this case, any built-in logic would further increase the complexity of processing RPC results. It's unclear how much of an issue this would become in practice, so developer feedback will be crucial here.

Context: internal design document

@rvanasa rvanasa marked this pull request as ready for review December 16, 2023 01:13
Copy link
Member

@gregorydemay gregorydemay left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @rvanasa for this nice MR! I have some minor comments and understanding questions but nothing major.

Comment on lines +83 to +85
type MultiRpcResult_1 = variant {
Consistent : Result_1;
Inconsistent : vec record { RpcService; Result_1 };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit puzzled by the naming MultiRpcResult_1/Result_1, MultiRpcResult_2/Result_2, which is not really indicative of what it represents. I'm guessing that candid doesn't really have a concept for generics and that's why such tricks are necessary but maybe we could have something a bot more user friendly, e.g. with fee history,

type MultiFeeHistoryResult = variant {
  Consistent : FeeHistoryResult;
  Inconsistent : vec record { RpcService; FeeHistoryResult };
};
type FeeHistoryResult = variant { Ok : opt FeeHistory; Err : RpcError };
...
eth_feeHistory : (CandidRpcSource, FeeHistoryArgs) -> (MultiFeeHistoryResult);

Does this make sense?

PS: I would also drop the Rpc in MultiRpcResult to have 2 types of results MultiResult/Result.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense for sure. This Candid file is automatically generated, but we could manually overwrite these names. The missing piece is a way to statically verify that the Candid interface matches the Rust source code. @dfx-json mentioned that a solution is currently in progress for this (as of a month or two ago).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way it was done for ckETH and some other canisters is to have the 2 (candid and Rust code) not auto-generated but then there is a unit test that controls that both are equivalent. Ideally, the candid file should be the source of truth and it should be able to generate the boilerplate Rust code, but I think that's out-of-scope for now. Maybe one easy way to modify this would be to avoid Rust generics in the types involved in the Candid interface and use struct like MultiFeeHistoryResult/FeeHistoryResult so that the generated Candid file is fixed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! This unit test is exactly what I was looking for. Included in #105.

eth_feeHistory : (CandidRpcSource, FeeHistoryArgs) -> (Result);
eth_getBlockByNumber : (CandidRpcSource, BlockTag) -> (Result_1);
eth_getLogs : (CandidRpcSource, GetLogsArgs) -> (Result_2);
eth_feeHistory : (CandidRpcSource, FeeHistoryArgs) -> (MultiRpcResult);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: from an interface perspective the name CandidRpcSource seems weird to me because the name of the language specifying the interface (Candid) shouldn't IMO appear anywhere in the interface iteself. Maybe rename it to RpcSource?

Copy link
Collaborator Author

@rvanasa rvanasa Dec 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original intent was to distinguish from what is currently called Source for JSON-RPC methods. I switched these around based on your suggestion (RpcSource for Candid and JsonRpcSource for JSON).

match services {
Some(services) => {
if services.is_empty() {
Err(ProviderError::ProviderNotFound)?;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

understanding question: why do we need to distinguish between no provider being specified and an empty vector? Why not in both cases, use the default providers?

If distinguishing both cases is really necessary, this seems a bit a misuse of ProviderError::ProviderNotFound, maybe a dedicate error would be better suited, something like ProviderError::MissingRequiredProvider?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not in both cases, use the default providers?

I went back and forth on this and came to the conclusion that passing an empty vector would be unintentional in almost all situations. Updated the error based on the above suggestion.

&self,
block: candid_types::BlockTag,
) -> MultiRpcResult<Block> {
multi_result(self.client.eth_get_block_by_number(block.into()).await)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

understanding question: interesting, it was not clear to me until now that this calls the corresponding function eth_get_block_by_number from your own branch of cketh. Do you plan on copying over the code into that repo so that ckETH can delete it when we will integeration with the EVM RPC canister or is that the part that's meant as a separate library?

Copy link
Collaborator Author

@rvanasa rvanasa Dec 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's the plan, although this is going to be a huge lift due to the large number of types shared between the rest of ckETH and the Ethereum RPC logic. This corresponds to the "create shared library with ckETH" part of the roadmap, which was deferred until after completing the MVP.

It's safe to remove the RPC logic on the ckETH end prior to this refactor.

Comment on lines +367 to +379
pub fn consistent(self) -> Option<RpcResult<T>> {
match self {
MultiRpcResult::Consistent(result) => Some(result),
MultiRpcResult::Inconsistent(_) => None,
}
}

pub fn inconsistent(self) -> Option<Vec<(RpcService, RpcResult<T>)>> {
match self {
MultiRpcResult::Consistent(_) => None,
MultiRpcResult::Inconsistent(results) => Some(results),
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice! I like the API!

}

impl<T> MultiRpcResult<T> {
pub fn map<R>(self, f: impl Fn(T) -> R) -> MultiRpcResult<R> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I would add unit tests to ensure the that function is executed on all ok variants.

),
})
}

fn wrap_result<T>(result: Result<T, MultiCallError<T>>) -> RpcResult<T> {
fn multi_result<T>(result: Result<T, MultiCallError<T>>) -> MultiRpcResult<T> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would add some unit tests for the result mapping

@ghost
Copy link

ghost commented Dec 19, 2023

approved. @rvanasa will address review comments in follow-up PRs

@rvanasa rvanasa merged commit 9a482c1 into main Dec 19, 2023
3 checks passed
@rvanasa rvanasa deleted the multi-candid-rpc branch December 19, 2023 19:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants