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

Multi oracles with varying nb digits #186

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 108 additions & 1 deletion MultiOracle.md
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,113 @@ of `t` oracles, or else they can be set as constant parameters across all groupi

TODO: Decide how this is done and communicated.

### Multi Oracles with varying number of digits

The above described algorithms all assume that all oracles use the same number of digits to represent numerical event outcomes.
However that might not always be the case and dealing with oracles with varying number of digits require further care.

When a contract is created with multiple oracles with varying number of digits, the contract should only define payouts for outcomes up to the maximum value that can be attested by the oracle with the minimum number of digits (thereafter referred as `min_nb_digits`).
Copy link
Contributor

Choose a reason for hiding this comment

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

Probably worth a note that 2^min_nb_digits - 1 represents that value or greater (as usual)

Copy link
Member Author

Choose a reason for hiding this comment

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

Added a note under this (it was also referred already in the "out of bound outcomes" section but I guess it doesn't hurt repeating).

Recall that if this oracle signs the value `2^min_nb_digits - 1`, by convention it represents either the outcome value `2^min_nb_digits - 1` or a greater outcome.

#### Equal outcomes

When no difference is allowed between the oracles, one simply needs to "pre-pad" the outcome prefixes of oracles with larger number of digits with `0`s.
For example, for the prefix `1010`, if an oracle uses `min_nb_digits + 2` digits, the additional prefix `00` should be prepended to yield `001010`.
Dealing with outcomes that are above the maximum value that can be represented using `min_nb_digits` is [discussed later](#out-of-bound-outcomes).

#### Bounded error

If differences are allowed between oracles, the [same algorithm](#Algorithm) defined for oracles with equal number of digits should be used, but it should be ran with `min_nb_digits + 1`, and setting the primary oracle to be the first one (in order of preference) in the group with `min_nb_digits`.

Choose a reason for hiding this comment

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

Couldn't this algorithm be ran with min_nb_digits if the resulting digit-prefixes are padded with zeros when needed in the case of secondary oracles with higher number of digits? Why run it with min_nb_digits + 1 ?

Copy link
Member Author

Choose a reason for hiding this comment

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

Same reason as above

For example, if the list of oracles is `[A, B, C, D]` with respective number of digits `[12, 10, 11, 10]`, the algorithm should be run with the order `[B, A, C, D]`.
The list of prefixes should then be transformed so that:
- the prefixes for oracles with more than `min_nb_digits + 1` should be "pre-padded" with zeros to reach the proper length,

Choose a reason for hiding this comment

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

Why is it min_nb_digits + 1 here instead of min_nb_digits?

Copy link
Member Author

Choose a reason for hiding this comment

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

Because the point is that you want to allow the contract to close when e.g. oracle B and C are at their maximum value, while C and D are slightly above (within the specified limit). So to enable that you need to have one more digit than min_nb_digits.

Choose a reason for hiding this comment

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

Makes sense, thanks!

- the first `0` should be dropped for the prefixes for oracles with `min_nb_digits`
- any group of prefix that contains a prefix representing an outcome that cannot be attested to by the corresponding oracle should be discarded (e.g. in the previous example, if an outcome consists of oracle D attesting to a value greater than `2^min_nb_digits - 1` then that outcome should be discarded).

#### Out of bound outcomes

In the case of oracles with equal number of digits, if the event outcome is greater than what oracles can attest to, the contract can still be closed if all oracles attest to the maximum value (which is [the convention](./Oracle.md#Digit-Decomposition)).
To enable contracts to be closed if the event outcome is greater than what can be represented using `min_nb_digits` in the case of oracles with varying number of digits, all possible outcomes above are assigned to the maximum contract payout.
To cover all the possible outcomes, all the possible combinations of prefixes that cover the interval `[base^min_nb_digits; base^max_nb_digits - 1]` are generated, where `max_nb_digits` if the number of digits used by the oracle(s) with greatest `nb_digits`.
Using the previous example with the order `[A, B, C, D]` and respective number of digits `[12, 10, 11, 10]`, the list of prefixes to cover this interval would be:
```
1, 1111111111, 1, 1111111111
01, 1111111111, 1, 1111111111
```

Choose a reason for hiding this comment

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

In the example above we have: [base^min_nb_digits; base^max_nb_digits - 1] == [2^10; 2^12 - 1] == [10000000000; 111111111111]

Is it correct to say that this interval can be represented the following 2 digit prefixes (where _ stands for any digit): _1_ _ _ _ _ _ _ _ _ _

 and 1_ _ _ _ _ _ _ _ _ _ _ ?

Sorry I don't understand the notation above, is it equivalent to this?

Copy link
Member Author

Choose a reason for hiding this comment

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

It's not exactly equivalent because in what your wrote the prefixes would have overlapping intervals. It would be more like 1 _ _ _ _ _ _ _ _ _ _ _ and 0 1 _ _ _ _ _ _ _ _ _ _. Does that make sense?

Choose a reason for hiding this comment

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

Yes that's right, thanks!

Copy link

@maxpolizzo maxpolizzo Jun 22, 2023

Choose a reason for hiding this comment

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

Looking at how to interpret the notation


[
  [1, 1111111111, 1, 1111111111]
  [01, 1111111111, 1, 1111111111]
]

when it comes to the describing the list of digit prefixes that cover the interval [2^10; 2^12 - 1] == [10000000000; 111111111111]:

It seems like any of those sequences of digits at index i should be interpreted as representing the first digits of a digit prefix of length nb_digits[i] (where, in this case, nb_digits == [12, 10, 11, 10])

Padding those with _ on the right (_ representing any digit) to end up with a digit prefix of length nb_digits[i], then padding with 0 on the left so that all digit prefixes have the max length (12 in this case), and removing duplicates, we end up with:

1 _ _ _ _ _ _ _ _ _ _ _, 01_ _ _ _ _ _ _ _ _ _, 001111111111

Does it make sense to have 001111111111 here since 001111111111 does not belong to the [2^10; 2^12 - 1] == [10000000000; 111111111111] interval?

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not sure I understand what you're saying, but the point here is that oracles B and D can only represent 10 digits numbers, so their maximum value is 2^10 - 1. If the event outcome is above that, and oracles A and C attest to values greater than that, you still want to be able to close the contract, so this algorithm is covering the interval 2^10; 2^12 - 1 for oracle A and 2^10; 2^11 - 1 for oracle C, but B and D keep their max value (since anyway they can't attest to value greater than that). Does that make sense?

Copy link

@maxpolizzo maxpolizzo Jun 23, 2023

Choose a reason for hiding this comment

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

Yes that part is clear thanks!

And it is also clear to me now why the list of prefixes to cover this interval [2^10; 2^12 - 1] does include 1111111111 which is outside the interval, as 1111111111 == 2^min_nb_digits - 1 represents that value or greater for oracles B and D.


The following algorithm can be used to generate these prefixes:

```rust
fn get_max_covering_paths(nb_digits: &[usize], min_nb_digits: usize) -> Vec<Vec<Vec<usize>>> {
let mut paths: Vec<Vec<Vec<usize>>> = Vec::new();
if nb_digits.iter().all(|x| x == min_nb_digits) {
// No need to generate extra coverage.
return Vec::new();
}

// The extra digits that each oracle has compared to `min_nb_digits`.
let max = nb_digits
.iter()
.map(|x| *x - min_nb_digits)
.collect::<Vec<_>>();
// Counter for the extra length of prefixes of each oracle. 0 if the oracle
// has `min_nb_digits`, starts at 1 for others since at the minimum we want to generate
// a prefix with `min_nb_digits + 1`.
let mut counters = nb_digits
.iter()
.map(|x| if x == min_nb_digits { 0 } else { 1 })
.collect::<Vec<_>>();
let mut i = 0;
loop {
let path = counters
.iter()
.map(|x| {
// For oracles with `min_nb_digits` we just generate the max value.
let p = if *x == 0 {
std::iter::repeat(1_usize).take(*min_nb_digits).collect()
} else {
// For others we generate the prefix based on their current
// counter value. We insert `counter - 1` zero and then a 1.
let mut p = Vec::with_capacity(*x);
p.resize(x - 1, 0);
p.push(1);
p
};
p
})
.collect::<Vec<_>>();
paths.push(path);

// If all counters have reached their max prefix size value, we're done.
if counters.iter().zip(max.iter()).all(|(x, y)| x == y) {
break;
}

// We reset the counters of oracles that had reached their max length
// prefixes, until we reach one that had not yet. We increment the counter
// for that one.
while counters[i] == max[i] {
if &nb_digits[i] != min_nb_digits {
counters[i] = 1;
}
i += 1;
}
counters[i] += 1;
i = 0;
}
paths
}
```

Note that when using a threshold, the above algorithm should be ran for each group of `threshold` oracles.
Assuming a 2 out of 4 threshold in the previous example, the algorithm should be ran with the following inputs:
1. nb_digits = [12, 10], min_nb_digits = 10
1. nb_digits = [12, 11], min_nb_digits = 10
1. nb_digits = [12, 10], min_nb_digits = 10
1. nb_digits = [10, 11], min_nb_digits = 10
1. nb_digits = [10, 10], min_nb_digits = 10
1. nb_digits = [11, 10], min_nb_digits = 10

## Reference Implementations

* [bitcoin-s](https://github.com/nkohen/bitcoin-s-core/tree/multi-oracle)
Expand All @@ -459,4 +566,4 @@ Nadav Kohen <[email protected]>

![Creative Commons License](https://i.creativecommons.org/l/by/4.0/88x31.png "License CC-BY")
<br>
This work is licensed under a [Creative Commons Attribution 4.0 International License](http://creativecommons.org/licenses/by/4.0/).
This work is licensed under a [Creative Commons Attribution 4.0 International License](http://creativecommons.org/licenses/by/4.0/).
Loading