Using capsules for private storage-less open-oracles

Ideas on how to use capsules for implementing a private-land price feed using an open-oracle approach without having to rely on storage at all.

Searching for use cases that justify the existence of capsules in the Aztec Network, @rahul-kothari and @alexghr came up with the idea of oracles, so I figured I would front-run them and turn it into a post. Hopefully this will be the first of a series Capsules: What are they good for?.

seinfeld

A quick primer on capsules

Capsules are one of many @LHerskind brainchildren. A capsule is an arbitrary piece of data that a contract in private-land can request from the PXE via an oracle call at any time. Since data is provided directly by the user’s PXE, this data is fully unconstrained, meaning it needs to be somehow verified by the contract. Capsules allow a developer to inject data out-of-band into a contract when needed, though it requires the PXE or Dapp to understand what capsules need to be provided for a given execution run.

A good example use-case for capsules is AuthWits (which are currently implemented as a dedicated oracle call but could very well be built on top of capsules). An account contract may at any time request the PXE to provide a signature for a given action that is to be carried out on its behalf, such as a typical erc20 transferFrom, which then the account contract verifies against its own stored public keys. This pattern allows implementing a permit-like transfer in Aztec, while keeping the transfer interface simple, since the signed message is injected directly into the contract that needs it.

But @jaosef challenged us to find other valid use cases for capsules, so here we are.

What is an open-oracle?

The open-oracle is an oracle standard developed by Compound in which a trusted entity publishes data off-chain but signed with their Ethereum private key. Anyone can then grab that data and push it to an on-chain contract that validates the signature. This saves the oracle owner from having to pay the gas for every data update, and instead places the burden on whoever is interested in making use of the oracle. This standard was for instance implemented by Coinbase in early 2020.

How would an Aztec private open-oracle work?

The naive implementation would follow the same pattern: anyone can send a transaction to an oracle contract that creates a slow-update entry with the most recent value, and the oracle contract validates the signature of the payload before accepting it.

But we don’t need to store each data entry: we can just provide the data on-the-fly every time we query the oracle, validating it every time we use it. And since this happens in private-land, which happens entirely on the client side, there’s no extra gas cost for the signature validation.

However, this has a downside: it breaks the expected interface of an oracle, which should be as simple as a get(). Let’s say you wanted to deposit funds on an aggregator’s vault, which in turn called a defi protocol, which in turn called a handful of oracles to gather prices on the assets you’re working with. Having to add an extra set of arguments that needs to be passed from one function to the next is annoying to say the least.

But we can easily solve this with capsules: both the data and its signature can be injected directly via a capsule, so the oracle could work without the need for storage and without requiring a different interface.

Show me the (pseudo)code

In a few lines, the oracle would look something like this:

contract LazyOracle:
  immutable public_key

  def get_oracle_data():
    payload = get_capsule()
    signature = get_authwit_for(payload) # remember: get_authwit is just a specialized get_capsule!
    assert valid_signature(payload, signature, self.public_key)
    context.set_max_block_number(payload.valid_until)
    return payload.data

How else could we build this?

An alternative to using capsules, that allows for a simple interface and not using storage, would be to implement an equivalent to Ethereum’s TSTORE, where we store data that is only valid for the span of the transaction. The user could then make a multicall from their account contract, where they first seed all the oracles they need to, and then execute the action that depends on them. Personally, I found TSTORE more difficult to reason about and more error prone that capsules. But an oracle built on that would be more or less like:

contract LazyOracleWithTStore:
  immutable public_key
  transient data

  def set_oracle_data(data, valid_until, signature):
    assert valid_signature(payload, signature, self.public_key)
    context.set_max_block_number(payload.valid_until)
    self.data = data

  def get_oracle_data():
    return self.data

While the code is also relatively simple, the orchestration here feels more complex, since the caller needs to pre-seed the oracle (which takes more function calls, which increase proving time). And introducing TSTORE makes me nervous, but maybe that’s just me.


The information set out herein is for discussion purposes only and does not represent any binding indication or commitment by Aztec Labs and its employees to take any action whatsoever, including relating to the structure and/or any potential operation of the Aztec protocol or the protocol roadmap. In particular: (i) nothing in these posts is intended to create any contractual or other form of legal relationship with Aztec Labs or third parties who engage with such posts (including, without limitation, by submitting a proposal or responding to posts), (ii) by engaging with any post, the relevant persons are consenting to Aztec Labs’ use and publication of such engagement and related information on an open-source basis (and agree that Aztec Labs will not treat such engagement and related information as confidential), and (iii) Aztec Labs is not under any duty to consider any or all engagements, and that consideration of such engagements and any decision to award grants or other rewards for any such engagement is entirely at Aztec Labs’ sole discretion. Please do not rely on any information on this forum for any purpose - the development, release, and timing of any products, features or functionality remains subject to change and is currently entirely hypothetical. Nothing on this forum should be treated as an offer to sell any security or any other asset by Aztec Labs or its affiliates, and you should not rely on any forum posts or content for advice of any kind, including legal, investment, financial, tax or other professional advice.

34 Likes

Mach-34 team used capsules to feed turns into the game.

It seems like a very good idea to keep on allowing devs to implement arbitrary oracles without needing to modify PXE.

It might also make sense to have oracles ABI encoded given that they are just another input into a contract (just like function args). I would expect it to make developing new oracles easier.

23 Likes

Can you provide an example of how you prime the capsule? It seems like this could be error prone as well… And it feels like both approaches ostensibly do the same thing. Or could. The call to set_oracle_data could provide a typesafe and intuitive way to make the capsule data available to the get_oracle_data call?
So the signature validation etc would still happen in the get.
Maybe you don’t even need the set function, just the transient specifier on data type which then permits the user to set that data type before creating the tx.

20 Likes

That’d depend on what structure we use for capsules. I’ve been pushing for an address-scoped key-value approach similar to authwits, as opposed to a stack as we have today. If we did that, then we’d have something like this on the contract:

contract LazyOracle:
  immutable public_key

  def get_oracle_data(asset_name): # let's assume this oracle can handle multiple assets
    signed_payload = get_capsule(asset_name) # let's bundle the signature and payload into the same capsule for simplicity
    (payload, signature) = signed_payload
    assert valid_signature(payload, signature, self.public_key) # check the signature from the web2 authority
    assert payload.asset_name == asset_name # check that the payload we got back is for the same asset we requested
    context.set_max_block_number(payload.valid_until)
    return payload.data

And like this on the app:

const oracleSignedData = fetch('example.com/oracle-data?asset=FOO');
const fooVault = FooVault.at(FOO_VAULT_ADDRESS);
await fooVault.deposit(1e6, { 
  capsules: [{ address: FOO_ORACLE_ADDRESS, data: oracleSignedData }],
}).wait();

Yes for this use case. However, transient storage requires the private kernel to keep track of the data stored in it (more complexity to the kernel circuits), and involves an extra function call to prime the data.

15 Likes