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

Use service to query ethereum node in Ethereum Tracker example #3144

Open
wants to merge 9 commits into
base: main
Choose a base branch
from

Conversation

jvff
Copy link
Contributor

@jvff jvff commented Jan 17, 2025

Motivation

While working on PR #2631, the end-to-end test for the Ethereum Tracker example started failing. That happened because the updated HTTP request API also included the response headers in the oracle recording, and a "Date" header made the response non-deterministic.

Proposal

Use the service to perform the HTTP request, and only send back to the contract the actual deterministic information that matters.

In order to do so, the EthereumClient had to be split in two, so that it could use either the contract system APIs or the service system APIs.

Test Plan

The end-to-end test passes locally in my repository when this fix is applied to the changes in PR #2631.

Release Plan

  • Nothing to do / These changes follow the usual release cycle, as it only changes the example and the SDK.

Links

jvff added 9 commits January 17, 2025 18:09
Prepare to add custom queries beyond the ones derived from
`SimpleObject`, in a way that has access to the `EthereumTrackerService`
type.
Read the Ethereum event and return a processed response.
Have one to use in contracts and one to use in services.
Make it easier to convert to the custom type.
Avoid non-deterministic HTTP responses.
Read the Ethereum events and return a processed response.
Avoid non-deterministic HTTP responses.
It's no longer used by the contract because now it's the service that
communicates with the Ethereum node.
Improve the interface when it becomes possible.
@jvff jvff added the enhancement New feature or request label Jan 17, 2025
@jvff jvff self-assigned this Jan 17, 2025
let async_graphql::Value::Object(data_object) = response.data else {
panic!("Unexpected response from `readTransferEvents`: {response:#?}");
};
let async_graphql::Value::List(ref events) = data_object["readTransferEvents"] else {
Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't the "ref variable" something that we want to avoid?

Comment on lines +116 to +118
let request = async_graphql::Request::new(format!(
r#"query {{ readTransferEvents(endBlock: {end_block}) }}"#
));
Copy link
Contributor

Choose a reason for hiding this comment

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

The code in main is reading events from a start_block to a to_block (renamed end_block here).
In the new code, there isn't anymore a start_code. How do we avoid repeating the operations?

Copy link
Contributor

Choose a reason for hiding this comment

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

The service has access to the state, too, and read_transfer_events uses the start_block field. No need to pass it in here.

Comment on lines +145 to +146
let value = U256::from_str_radix(&value_string[2..], 16)
.expect("Balance could not be parsed as a hex string");
Copy link
Contributor

Choose a reason for hiding this comment

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

It is a little unfortunate to have this rather low level conversion occurring in this part of the code.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'd imagine that FromStr would work on the whole string here, and interpret it as hexadecimal because it starts with 0x? I.e. maybe we can just use value_string.parse::<U256>()?

Comment on lines +106 to +114
async fn read_transfer_events(&self, end_block: u64) -> Vec<TransferEvent> {
let start_block = *self.0.state.start_block.get();
let events = self
.read_events(
"Transfer(address indexed,address indexed,uint256)",
start_block,
end_block,
)
.await;
Copy link
Contributor

Choose a reason for hiding this comment

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

What I find a little odd is that now the code has two ways to access the events.
Here, we have the read_transfer_events and in fn update a different code.

Copy link
Contributor

Choose a reason for hiding this comment

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

But update is using read_transfer_events. How else could we move this functionality into the service and use it from the contract?

Comment on lines +53 to +56
self.state
.save()
.await
.expect("Failed to write updated storage");
Copy link
Contributor

Choose a reason for hiding this comment

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

What is exactly the justification for adding this self.state.save() here? This is the only smart contract where a self.state.save() is added to the fn instantiate function. Why is that so?
For example, the crowd-funding and fungible are doing meaningful operations in their fn instantiate functions. Why haven't they some self.state.save().
Side question 1: When is the fn store function used?
Side question 2: Now, the fn save function is doing the writing to the batch and if the batch is empty, then nothing is done. So, could the save could be added by the linera-rpc?

let application_id = self.runtime.application_id();
let response = self.runtime.query_service(application_id, request);

let async_graphql::Value::Object(data_object) = response.data else {
Copy link
Contributor

Choose a reason for hiding this comment

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

We could avoid all those async_graphql::Value::Variant matches by using response.data.into_json() instead. The JSON Value type has a few more convenient methods like as_str etc. (And we'd rarely need as_object because e.g. json_data["readInitialEvent"]["address"] works, too.)

(Anyway, maybe that's outside the scope of this PR.)

Comment on lines +145 to +146
let value = U256::from_str_radix(&value_string[2..], 16)
.expect("Balance could not be parsed as a hex string");
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd imagine that FromStr would work on the whole string here, and interpret it as hexadecimal because it starts with 0x? I.e. maybe we can just use value_string.parse::<U256>()?

Comment on lines +106 to +114
async fn read_transfer_events(&self, end_block: u64) -> Vec<TransferEvent> {
let start_block = *self.0.state.start_block.get();
let events = self
.read_events(
"Transfer(address indexed,address indexed,uint256)",
start_block,
end_block,
)
.await;
Copy link
Contributor

Choose a reason for hiding this comment

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

But update is using read_transfer_events. How else could we move this functionality into the service and use it from the contract?

Comment on lines +116 to +118
let request = async_graphql::Request::new(format!(
r#"query {{ readTransferEvents(endBlock: {end_block}) }}"#
));
Copy link
Contributor

Choose a reason for hiding this comment

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

The service has access to the state, too, and read_transfer_events uses the start_block field. No need to pass it in here.

events
.into_iter()
.map(|event| {
let EthereumDataType::Address(source) = event.values[0].clone() else {
Copy link
Contributor

Choose a reason for hiding this comment

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

We aren't using event anymore after this, so we might as well use event.values.into_iter() instead of cloning the values.

@@ -5,7 +5,7 @@ use ethereum_tracker::U256Cont;
use linera_sdk::views::{linera_views, MapView, RegisterView, RootView, ViewStorageContext};

/// The application state.
#[derive(RootView, async_graphql::SimpleObject)]
#[derive(RootView)]
Copy link
Contributor

Choose a reason for hiding this comment

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

Alternatively, we could use ComplexObject and remove the manual implementations of the four getters.

use crate::contract::wit::contract_system_api;
use crate::{contract::wit::contract_system_api, service::wit::service_system_api};

// TODO(#3143): Unify the two types into a single `EthereumClient` type.
Copy link
Contributor

Choose a reason for hiding this comment

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

Does the Contract one still make sense at all? With the planned change for the HTTP oracle, I imagine it wouldn't work anymore?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
Status: No status
Development

Successfully merging this pull request may close these issues.

4 participants