Skip to content

Commit

Permalink
Merge pull request radixdlt#18 from hypr2771/create_epoch_duration_or…
Browse files Browse the repository at this point in the history
…acle

Create epoch duration oracle to get on-ledger epoch duration estimation
  • Loading branch information
klembee authored May 30, 2022
2 parents 66f0003 + 1e8edd5 commit e1ea008
Show file tree
Hide file tree
Showing 9 changed files with 986 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ Cargo.lock
**/*.rs.bk

# Flamegraph profiles
flamegraph.svg
flamegraph.svg
3 changes: 3 additions & 0 deletions 2-oracles/epoch_duration_oracle/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
build.sh
**/target/*
**.manifest
118 changes: 118 additions & 0 deletions 2-oracles/epoch_duration_oracle/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# epoch_duration_oracle

The goal of this oracle is to provide an on-ledger duration of all passed epoch since creation of the component as well as a way to compute time elapsed between an epoch and the current epoch.

This does not mean to be a precise clock ticking every seconds, but more of an on-ledger information on whether a time milestone was reached or not. This can not be used as a precise timer for short delays, but can show useful to request time between multiple epochs (the bigger the range, the less significant will be the potential error).

> All durations are currently in milliseconds.
## Use cases

An example of usage could be a timed auction. We would:

- create the auction with a duration
- during creation, mark the current epoch
- when doing various auction actions, we could call the oracle to know whether the time spent between current timestamp and creation timestamp is higher than auction duration, in which case we would end the auction

Instead of providing an "erratic" epoch duration, this would make that auction more precise in terms of human intelligible duration.

## Running

### Feed the oracle

To run and test the epoch oracle, you can run one of the following:

> - update the oracle only once per epoch (epochs are synchronized with PTE):
>
> ```console
> me@os:~$ (cd scrypto/epoch_duration_oracle && ./tick_on_epoch.sh)
> Current epoch 633, last epoch 633...
> Current epoch 633, last epoch 633...
> Current epoch set!
> Transaction Status: SUCCESS
> Execution Time: 16 ms
> Instructions:
> ├─ CallMethod { component_address: 020d3869346218a5e8deaaf2001216dc00fcacb79fb43e30ded79a, method: "create_proof_by_amount", args: [Decimal("1"), ResourceAddress("03f1820412ec5c07b54ff0407eb00bfc54f58d0784f3eabc2df9c7")] }
> └─ CallMethod { component_address: 028b858b202333e09f6bf24756b17e51cb6f6882d8bde08a47bdf1, method: "tick", args: [1035068u64] }
> Instruction Outputs:
> ├─ Proof(1024u32)
> └─ 634u64
> Logs: 0
> New Entities: 0
> Current epoch 634, last epoch 634...
> Current epoch 634, last epoch 634...
> ```
> - update the oracle each second (starts with epoch from PTE then increments epoch each seconds):
>
> ```console
> me@os:~$ (cd scrypto/epoch_duration_oracle && ./tick_asap.sh)
> Current epoch set!
> Transaction Status: SUCCESS
> Execution Time: 14 ms
> Instructions:
> ├─ CallMethod { component_address: 020d3869346218a5e8deaaf2001216dc00fcacb79fb43e30ded79a, method: "create_proof_by_amount", args: > [Decimal("1"), ResourceAddress("0384a3fa191c5506fe5df8a4622cb6eead59deab9d7fc48f64fc98")] }
> └─ CallMethod { component_address: 0244e8cb7761a9b1980e52d70999b4075f182839cec77bfffc1f6c, method: "tick", args: [781u64] }
> Instruction Outputs:
> ├─ Proof(1024u32)
> └─ 10u64
> Logs: 0
> New Entities: 0
> Current epoch set!
> Transaction Status: SUCCESS
> Execution Time: 17 ms
> Instructions:
> ├─ CallMethod { component_address: 020d3869346218a5e8deaaf2001216dc00fcacb79fb43e30ded79a, method: "create_proof_by_amount", args: > [Decimal("1"), ResourceAddress("0384a3fa191c5506fe5df8a4622cb6eead59deab9d7fc48f64fc98")] }
> └─ CallMethod { component_address: 0244e8cb7761a9b1980e52d70999b4075f182839cec77bfffc1f6c, method: "tick", args: [1063u64] }
> Instruction Outputs:
> ├─ Proof(1024u32)
> └─ 10u64
> Logs: 0
> New Entities: 0
> ```
### Query time since epoch
To query from the start of the oracle state, you can use:
```bash
resim run scrypto/epoch_duration_oracle/manifests/since_epoch_0.manifest
```
To see the difference, there is also a `since_epoch_{current_epoch_on_pte01 + 1}.manifest` to compute time elapsed from different epoch and a `since_epoch_0.manifest` to query elapsed time from the start of the oracle.

> *e.g.*:
> ```bash
> resim run scrypto/epoch_duration_oracle/manifests/since_epoch_9.manifest | grep 'Instruction Outputs' -A 1;
> resim run scrypto/epoch_duration_oracle/manifests/since_epoch_10.manifest | grep 'Instruction Outputs' -A 1;
> ```
## ABI
The available functions are:
- `EpochDurationOracle::new`: creates a new oracle starting from now and disregarding already elapsed time (does not consider elapsed time of the passed epochs).
- `EpochDurationOracle::new_with_bootstrap(last_epoch: u64, millis_in_last_epoch: u64)`: creates a new oracle starting from `last_epoch` which lasted `millis_in_last_epoch`. This is useful if you want to start the oracle at a given point in time (but after January 1st 1970).
The available method for oracle creator is:
- `EpochDurationOracle::tick(millis_since_last_tick: u64)`: ticks the internal clock by the provided milliseconds. This can:
- end the current epoch, categorize it with its duration and start counting down for a new epoch if previous epoch just ended
- add the tick amount to the epoch being currently counted down
- this always return the current ledger epoch
The available open methods are:
- `EpochDurationOracle::millis_since_epoch(epoch: u64)`: measure time passed between provided `epoch` and current epoch. This can give birth to few cases:
- the provided `epoch` is higher than on-ledger epoch: we will return an error
- the provided `epoch` is equal to the on-ledger epoch: we will return the time spent on the current epoch
- the provided `epoch` is lower than the on-ledger epoch: we will return the time spent between provided epoch and current epoch
- the provided `epoch` does not appear for oracle was not created yet: we will return the time spent from the closest lower bound epoch and current epoch (*e.g.*: if you request time spent from epoch `10` and oracle missed it, but we have epoch `11` and `12` and are in epoch `13`, we will compute time spent between epoch `13` and `11` and consider epoch `10` lasted `0`. This is acceptable since we are using timestamp, the actual duration will remain correct)
- `EpochDurationOracle::millis_in_epoch(epoch: u64)`: measure duration of provided `epoch`. This can give birth to few cases:
- the provided `epoch` is higher than on-ledger epoch: we return an error
- the provided `epoch` is equal to the on-ledger epoch: we will return the time spent on the current epoch
- the provided `epoch` is lower than the on-ledger epoch: we will return the time spent between provided epoch and current epoch
- the provided `epoch` is passed but not present on oracle: we will return 0 and suggest calling the `millis_since_epoch` method
> Note: we will be adding a method to get duration between two epochs provided.
8 changes: 8 additions & 0 deletions 2-oracles/epoch_duration_oracle/scrypto/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/bin/bash

set -x
set -e

cd "$(dirname "$0")"

(cd epoch_duration_oracle; scrypto build)
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
name = "epoch_duration_oracle"
version = "0.1.0"
edition = "2021"

[dependencies]
sbor = { git = "https://github.com/radixdlt/radixdlt-scrypto", branch = "release/0.4.0" }
scrypto = { git = "https://github.com/radixdlt/radixdlt-scrypto", branch = "release/0.4.0" }

[dev-dependencies]
radix-engine = { git = "https://github.com/radixdlt/radixdlt-scrypto", branch = "release/0.4.0" }

[profile.release]
opt-level = 's' # Optimize for size.
lto = true # Enable Link Time Optimization.
codegen-units = 1 # Reduce number of codegen units to increase optimizations.
panic = 'abort' # Abort on panic.
strip = "debuginfo" # Strip debug info.

[lib]
crate-type = ["cdylib", "lib"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
use scrypto::prelude::*;

blueprint! {
struct EpochDurationOracle {
epochs_duration_millis: HashMap<u64, u64>,
current_epoch: u64,
millis_in_current_epoch: u64,

// Owner
owner_badge_ref: ResourceAddress
}

impl EpochDurationOracle {
pub fn new() -> (ComponentAddress, Bucket) {
Self::new_with_bootstrap(0, 0)
}

pub fn new_with_bootstrap(current_epoch: u64, millis_in_current_epoch: u64) -> (ComponentAddress, Bucket) {

// Owner relative
let owner_badge: Bucket = ResourceBuilder::new_fungible()
.divisibility(DIVISIBILITY_NONE)
.metadata("name", format!("Owner of epoch duration oracle."))
.initial_supply(1);

let component = Self {
epochs_duration_millis: HashMap::new(),
current_epoch,
millis_in_current_epoch,
owner_badge_ref: owner_badge.resource_address()
}.instantiate();

// Access control
let access_rules = AccessRules::new()
.method("tick", rule!(require(owner_badge.resource_address())))
.default(AccessRule::AllowAll);

// Component with owner badge
(component.add_access_check(access_rules).globalize(), owner_badge)
}

pub fn tick(&mut self, millis_since_last_tick: u64) -> u64 {
if self.current_epoch >= Runtime::current_epoch() {
self.millis_in_current_epoch += millis_since_last_tick;
}
else {
self.epochs_duration_millis.insert(self.current_epoch, self.millis_in_current_epoch + millis_since_last_tick);
self.current_epoch = Runtime::current_epoch();
self.millis_in_current_epoch = 0;
}

return self.current_epoch
}

pub fn millis_since_epoch(&self, epoch: u64) -> u64 {

assert!(epoch <= self.current_epoch, "The requested epoch has not yet happened or was not yet registered on ledger.");

if epoch == self.current_epoch {
trace!("Requested elapsed millis since the current epoch");
return self.millis_in_current_epoch
}

trace!("Requested elapsed on a passed epoch");
let elapsed: u64 = self.epochs_duration_millis.iter()
.filter(|(k, _v)| k >= &&epoch)
.map(|(_k, v)| v)
.sum();

elapsed + self.millis_in_current_epoch
}

pub fn millis_in_epoch(&self, epoch: u64) -> u64 {

assert!(epoch <= self.current_epoch, "The requested epoch has not yet happened or was not yet registered on ledger.");

if epoch == self.current_epoch {
trace!("Requested elapsed millis on the current epoch");
return self.millis_in_current_epoch
}

trace!("Requested elapsed on a passed epoch");
let elapsed: &u64 = self.epochs_duration_millis.get(&epoch)
.unwrap_or_else(|| {
warn!("Epoch was not registered on the oracle, sorry for the inconvenience. We are returning 0 and suggest you call millis_since_epoch or contact an administrator if you absolutely need this epoch duration.");
&0u64
});

if self.epochs_duration_millis.contains_key(&epoch){
return elapsed + self.millis_in_current_epoch
}

*elapsed
}
}
}
Loading

0 comments on commit e1ea008

Please sign in to comment.