Skip to main content

It's gonna be Legen... wait for it...

· 5 min read
Guille
Docs Maintainer

dary! Legendary! NEAR protocol is getting updated with the ability to yield and resume computations

waiting on a loop

The problem of waiting

Currently, smart contracts have no way to wait for an external event to happen. This can be a problem when the contract relies on an external service to provide a result.

We encountered this issue while implementing Chain Signatures, which work by requiring an external service to provide a signature.

Until now, the only workaround has been to make the contract call itself in a loop, checking on each iteration if the result is ready. Each call delays the result by one block (~1 second), allowing the contract to wait almost a minute before running out of gas.

waiting on a loop Until now, contracts had to wait by calling themselves until a external service replies... more often than not the contract will run out of gas waiting

While this method works, it's far from ideal. It wastes a lot of gas on looping and - more often than not - runs out of gas, forcing the user to retry the transaction.

Yield and Resume

Starting from version 1.40 of the protocol, developers can delay the execution of a function until certain conditions are met (e.g. an external service provides a result).

This way, instead of the contract calling itself on a loop waiting, the contract can simply yield calling the function that gives the result. When an external response is provided, the contract will resume and return the result.

waiting on a yield Contracts can now yield the execution of a function until an external service signals that the result is ready

What is exactly being yielded?

It is important to notice that the contract is not halting or blocking its ability to execute, nor halting in the middle of a function to later resume it.

In the same way that a function can return a promise to call another contract, now a function can return a yield to call another function.

Indeed, the contract is not halting, but simply delaying the execution of a callback until an external agent signals that it is ok to resume.

If the contract does not trigger a resume after 200 blocks - around 4 minutes - the yielded function will resume receiving a "timeout error" as input.

State Changes

People can keep calling functions on the contract between a yield/resume, and the function that creates the yield can be called multiple times.

The state can change between the yield and the resume, since people can keep interacting with the contract.

Moreover, since the function used to signal is public, developers must make sure to guard it properly to avoid unwanted calls. This can be done by simply checking the caller of the function.

How does it change for the user?

Between the yield and resume the user will simply be waiting to receive the result. But, in contrast with waiting on a loop, the user will not pay GAS just for having the contract waiting!

How I can use yield/resume in my contract?

While we have not created any official yield/resume example, you can refer to Saketh Are's example, who has been working on the yield/resume implementation.

The basic idea is that the SDK now exposes two functions:

  • A yield(function_to_yield) that returns a yield_ID which identifies the yield
  • A resume(yield_ID) that signals which instance of function_to_yield can now execute

Simplified Example

// const DATA_ID_REGISTER: u64 = 0;

pub fn request_weather(&mut self, city: String) {
let index = self.next_available_request_index;
self.next_available_request_index += 1;

let yield_promise = env::promise_yield_create(
"callback_return_result",
&serde_json::to_vec(&(index,)).unwrap(),
SIGN_ON_FINISH_CALL_GAS,
GasWeight(0),
DATA_ID_REGISTER,
);

// Store the request, so an external service can easily fetch it
// This is optional, as an indexer could simply observe it in the receipts
let data_id: CryptoHash =
env::read_register(DATA_ID_REGISTER).expect("").try_into().expect("");
self.requests.insert(&index, WeatherRequest{&data_id, &city});

// The return will be the result of "callback_return_result" (defined below)
env::promise_return(yield_promise);
}

/// Called by external participants to submit a response
pub fn respond(&mut self, data_id: String, weather: String) {
let mut data_id_buf = [0u8; 32];
hex::decode_to_slice(data_id, &mut data_id_buf).expect("");
let data_id = data_id_buf;

// check that caller is allowed to respond, weather is valid, etc.
// ...

log!("submitting response {} for data id {:?}", &weather, &data_id);
env::promise_yield_resume(&data_id, &serde_json::to_vec(&weather).unwrap());
}

/// Callback receiving the external data (or a PromiseError in case of timeout)
pub fn callback_return_result(
&mut self,
request_index: u64,
#[callback_result] weather: Result<String, PromiseError>,
) -> String {
// Clean up the local state
self.requests.remove(&request_index);

match weather {
Ok(weather) => "weather received: ".to_owned() + &weather,
Err(_) => "request timed out".to_string(),
}
}

Conclusion

The ability to yield and resume computations is a big step forward for the NEAR protocol, as it enables developers to create contracts that rely on external services.

Currently, the feature is only available on testnet, and we are looking for feedback on how to improve it.

We expect to have a more user-friendly way to use yield and resume in the future, so stay tuned!