Skip to content

Commit

Permalink
CDS + External calls beginning
Browse files Browse the repository at this point in the history
  • Loading branch information
andytudhope committed Mar 29, 2024
1 parent 6405c2e commit 8a79181
Show file tree
Hide file tree
Showing 6 changed files with 318 additions and 16 deletions.
2 changes: 2 additions & 0 deletions docs/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ module.exports = {
'tutorials/run-suave',
'tutorials/deploy-contracts',
'tutorials/onchain-offchain',
'tutorials/confidential-store',
'tutorials/external-call',
'tutorials/build-suapp-webapp',
'tutorials/confidential-compute-requests',
'tutorials/create-precompiles'
Expand Down
4 changes: 2 additions & 2 deletions docs/tutorials/build-suapp-webapp.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Building - Webapp
description: Build a webapp to interact with SUAPPs.
title: In the Browser
description: Build a web application to interact with SUAPPs.
keywords:
- application
- build
Expand Down
162 changes: 162 additions & 0 deletions docs/tutorials/confidential-store.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
---
title: Confidential Store
description: Understanding the key primitives required to build great SUAPPs
keywords:
- contract
- deploy
- suave
- solidity
- confidential
- private
- store
---

# Using the Confidential Store

If you've followed along from the [previous tutorial](./onchain-offchain), you'll have deployed a simple contract with `onchain` and `offchain` functions that use the [`SUAVE-STD` library](https://github.com/flashbots/suave-std) to emit logs onchain that come from offchain compute results.

Now it's time to look at another key primitive often required to build powerful Suapps: the confidential data store.

In particular, we'll consider how to store a private key confidentially, and then use that key to sign transactions intended for other chains. This pattern is useful when you want the results of your offchain computation to cause a transaction on another chain, and it finds application in everything from Uniswap v4 hooks to NFTs for concert tickets.

## Import SUAVE-STD

We're going to want to use more of the functionality offered by `SUAVE-STD` in order to both store our private key, and then retrieve it and use it to sign a transaction intended for another domain.

Create a new file called `ConfidentialStore.sol` and begin by importing the supporting contracts and libraries we need:

```solidity
// SPDX-License-Identifier: Unlicensed
pragma solidity ^0.8.8;
import "suave-std/Suapp.sol";
import "suave-std/Context.sol";
import "suave-std/Transactions.sol";
import "suave-std/suavelib/Suave.sol";
```

If you look through each of these imports, you'll see that:

1. `Suapp.sol` gives us the ability to easily emit logs from offchain computations onchain.
2. `Context.sol` allows any function to determine whether there are confidential inputs being passed along with the function call. We will use this to pass our private key to our Suapp without revealing what it is.
3. `Transactions.sol` helps decode/encode transactions from/to other domains.
4. `Suave.sol` contains all the precompiles which make up the MEVM that runs in each Kettle, along with the addresses they're deployed to.

## Store Keys Confidentially

Next, add the logic you'll need to store a private key confidentially with this Suapp:

```solidity
contract ConfidentialStore is Suapp {
Suave.DataId signingKeyRecord;
string public PRIVATE_KEY = "ETH_L1_SIGNER";
// onchain-offchain pattern to register the new private key in Confidential Store
function updateKeyRecordOnchain(Suave.DataId _signingKeyRecord) public {
signingKeyRecord = _signingKeyRecord;
}
function registerPrivateKeyOffchain() public returns (bytes memory) {
bytes memory keyData = Context.confidentialInputs();
address[] memory peekers = new address[](1);
peekers[0] = address(this);
Suave.DataRecord memory record = Suave.newDataRecord(0, peekers, peekers, "private_key");
Suave.confidentialStore(record.id, PRIVATE_KEY, keyData);
return abi.encodeWithSelector(this.updateKeyRecordOnchain.selector, record.id);
}
}
```

The confidential store is a key value store, and the convention is to store "data records" as the values that are keyed by "data IDs". The SUAVE library helps abstract this so you can just call `Suave.DataId` etc.

A `newDataRecord` expects four values:

1. The "decryption condition" - this is an artifact which will be removed in later versions of `suave-geth`.
1. Set to `0` for now.
2. The "allowed peekers" - this determines who can "get" data associated with the `DataId`.
3. The "allowed stores" - this determines who can "set" data associated with the `DataId`.
1. In this example we set the `allowedPeekers` == `allowedStores` == an array of 1 address, which is set to `address(this)`. That is, only this contract can get the private key we're storing, or set it to something else.
4. The "data type" - a string which specifies the type of data being stored.
1. In this case, it is set to `"private_key"`.

We are also following the same pattern as previous tutorials, with an offchain function that does the heavy lifting (creating the data record for the private key we want to store), which then returns a callback to an onchain function that (rather than emitting an event) updates the `DataId` by which the private key may be fetched by this specific contract.

Anyone can call this function (you may want to change that) and, if they pass in a private key in the `confidentialInputs` field of their Confidential Compute Request (CCR), then the Kettle which processes that CCR will set the private key in its store according to the logic above, all without revealing what that key is.

So, let's compile the contract, deploy it, and send the CCR that will store a private key!

```bash
forge build
```
```bash
suave-geth spell deploy ConfidentialStore.sol:ConfidentialStore
```

If you built `suave-geth` from source, you may need to specify the whole path:

```bash
./<path_to_suave-geth>/build/bin/suave-geth spell deploy ConfidentialStore.sol:ConfidentialStore
```

**TODO**: Now we can send the private key as a confidential input when we call `registerPrivateKeyOffchain`:

```bash
suave-geth spell conf-request <your_new_contract_address> 'registerPrivateKeyOffchain()'
```

You shouldn't see anything in the logs as you did in previous tutorials, as we're not emitting events from the `updateKeyRecordOnchain` function (thouh you can modify that yourself if you like).

## Sign a Tx with your Private Key

Let's now add the other important piece we need for this contract, which is using the private key we just stored to sign a transaction for another domain (in this Ethereum L1):

```solidity
event TxnSignature(bytes32 r, bytes32 s);
function onchain() public emitOffchainLogs {}
function offchain() public returns (bytes memory) {
bytes memory signingKey = Suave.confidentialRetrieve(signingKeyRecord, PRIVATE_KEY);
Transactions.EIP155Request memory txnWithToAddress = Transactions.EIP155Request({
to: address(0x00000000000000000000000000000000DeaDBeef),
gas: 1000000,
gasPrice: 500,
value: 1,
nonce: 1,
data: bytes(""),
chainId: 1
});
Transactions.EIP155 memory txn = Transactions.signTxn(txnWithToAddress, string(signingKey));
emit TxnSignature(txn.r, txn.s);
return abi.encodeWithSelector(this.onchain.selector);
}
```

By now, this onchain-offchain pattern should be becoming more familiar. Offchain, we do the heavy lifting of constructing the transaction object, which we then emit in the logs of an event onchain by returning a callback to the onchain function.

The theory here is that any searcher or other service could listen to logs from `TxnSignature` events in this contract on SUAVE and submit them as part of their bundles to block builders for Ethereum L1. However, you can also use the Gateway pattern discussed in the next tutorial to call your preferred RPC provider yourself, such that you need not rely on these events being detected.

Recompile and redeploy your contract, and call the offchain function to see this all in action:

```bash
forge build
```
```bash
suave-geth spell deploy ConfidentialStore.sol:ConfidentialStore
```

```bash
suave-geth spell conf-request <your_new_contract_address> 'offchain()'
```

You should see something like this printed to your terminal:

**TODO**

Congratulations! 💃 You've just begun to master the confidential store. The applications that can be built from this kind of foundation are extensive. We're excited to see what you build...
12 changes: 6 additions & 6 deletions docs/tutorials/deploy-contracts.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,15 @@ You can then open the resulting files in whichever text editor you prefer.

## Onchain offchain

We need to adapt the empty contract in the `src` directory to start understanding the key differences between onchain and offchain computation.
Let's adapt the empty contract in the `src` directory to see the key differences between onchain and offchain computation.

Begin by renaming `Contract.sol` to `MyFirstSuapp.sol` and deleting the `Contract.t.sol` file in the `test` directory.

Then, you can write two simple functions called `onchain()` and `offchain()`:

```solidity
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.13;
pragma solidity ^0.8.8;
contract MyFirstSuapp {
function onchain() public {}
Expand All @@ -65,9 +65,9 @@ contract MyFirstSuapp {
}
```

All offchain functions should return a function selector to another function inside your SUAPP, such that the relevant outputs of offchain computation can result in the appropriate onchain state transitions. The Kettle which executes a user's call to `offchain()` will take the results of any offchain computation and wrap it in a "SUAVE transaction", using the specified function selector. That SUAVE transaction is broadcast onchain, which eventually results in the onchain component being executed too.
All offchain functions should return a function selector to another function inside your SUAPP, such that the relevant outputs of offchain computation result in onchain state transitions. The Kettle which executes a user's call to `offchain()` will take the results of any offchain computation and wrap it in a "SUAVE transaction", using the specified function selector. That SUAVE transaction is broadcast onchain, which eventually results in the onchain component being executed too.

Let's take a practical example. In an orderflow auction, users might send their transactions from other domains to a Suapp as confidential inputs. The Suapp can emit specific information about those transactions (without revealing everything) such that searchers listening for events on SUAVE chain can construct backruns. They can then submit their backruns to the same Suapp, which can merge the original transactions and the best backruns offchain (it can take a lot of compute to do this), before emitting the resulting bundle onchain for a block builder on the original domain to pick up and use.
Let's take a practical example. In an [orderflow auction](https://github.com/flashbots/suapp-examples/tree/main/examples/app-ofa-private), users might send their transactions from other domains to a Suapp as confidential inputs. The Suapp can emit specific information about those transactions (without revealing everything) such that searchers listening for events on SUAVE chain can construct backruns. They can then submit their backruns to the same Suapp, which can merge the original transactions and the best backruns offchain (it can take many computational steps do this), before emitting the resulting bundle onchain for a block builder on the original domain to pick up and use.

There are many examples which use this onchain-offchain pattern to achieve results that are not otherwise possible in normal public blockchains. We'll be introducing them one by one through the course of these tutorials.

Expand Down Expand Up @@ -95,7 +95,7 @@ There is a helper facility called `spell` in `suave-geth` which facilitates depl
suave-geth spell deploy MyFirstSuapp.sol:MyFirstSuapp
```

If you built from source, run
If you built from source, run:

```bash
./<path_to_suave-geth>/build/bin/suave-geth spell deploy MyFirstSuapp.sol:MyFirstSuapp
Expand All @@ -113,4 +113,4 @@ INFO [03-26|09:59:54.515] Contract deployed address=0xFcd

Congratulations! You just deployed your first contract to a local SUAVE network! 💃

Take note of the address to which it has been deployed: you'll need it in the next tutorial, where we'll go over how to send confidential compute requests to your contracts, and how to extend the functions to emit information about offchain computations onchain.
Take note of the address to which it has been deployed: you'll need it in the next tutorial, where we'll send confidential compute requests to your contracts, and extend the functions to emit information about offchain computations onchain.
138 changes: 138 additions & 0 deletions docs/tutorials/external-call.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
---
title: External Calls
description: Understanding the key primitives required to build great SUAPPs
keywords:
- contract
- deploy
- suave
- solidity
- http
- external
- data
---

# Making external calls

If you've followed along from the [previous tutorial](./confidential-store), you'll have deployed a simple contract with `onchain` and `offchain` functions, and extended it so that you can store a private key from another domain and use it to sign transactions after executing some offchain, confidential computation using SUAVE.

Building unique and powerful Suapps often requires one more key primitive (in addition to understanding on and offchain computations and how to use the confidential store). **Suapps can make arbitrary http requests to other domains, fetch data, and use that in their offchain computation**.

Let's walk through how to do this, first fetching balance information about USDC on Ethereum L1, and then by making a request to Chat GPT's completion endpoint.

## Fetching Balances from Ethereum

Create a new contract in your `src` directory called `ExternalCall.sol`, and paste this into the file (making sure to update with your API key, or preferred RPC provider details):

```solidity
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.8;
import "suave-std/Suapp.sol";
import "suave-std/Context.sol";
import "suave-std/Gateway.sol";
interface ERC20 {
function balanceOf(address) external view returns (uint256);
}
contract ExternalCall is Suapp {
function onchain() external payable emitOffchainLogs {}
event Balance(uint256 balance);
function offchain() external returns (bytes memory) {
string memory jsonRpc = Context.confidentialInputs();
// targeting USDC contract on ETH L1
Gateway gateway = new Gateway(jsonRpc, 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
ERC20 token = ERC20(address(gateway));
// Fetching the balance for a Binance exchange account
uint256 balance = token.balanceOf(0xDFd5293D8e347dFe59E90eFd55b2956a1343963d);
emit Balance(balance);
return abi.encodeWithSelector(this.onchain.selector);
}
}
```

Follow the by-now familiar pattern of recompiling, deploying, and calling the offchain function using our `spell` tool:

```bash
forge build
```
```bash
suave-geth spell deploy ExternalCall.sol:ExternalCall
```
If you built `suave-geth` from source, you may need to specify the whole path:

```bash
./<path_to_suave-geth>/build/bin/suave-geth spell deploy ExternalCall.sol:ExternalCall
```
```bash
suave-geth spell conf-request <your_new_contract_address> 'offchain()' **TODO**
```

You should see the balance of the a Binance exchange account (because why not?) emitted as a log in your console:

**TODO**

Fetching blockchain data from domains beyond SUAVE is as easy as that! We're incredibly hyped to see what you build with this primitive. However, blockchain balances and other data is not the only kind of data you can fetch and use in your Suapps...

## Using Chat GPT in a smart contract on SUAVE

:::info

You will need your own ChatGPT API key for this section.

:::

Create a new contract in your `src` directory called `ChatContract.sol`, and paste this into the file:

```solidity
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.8;
import "suave-std/Suapp.sol";
import "suave-std/Context.sol";
import "suave-std/protocols/ChatGPT.sol";
contract ChatContract is Suapp {
function onchain() external payable emitOffchainLogs {}
event Response(string message);
function offchain() external returns (bytes memory) {
/* TODO: can Context.confidentialInputs() decode more than 1 value? If so, how? */
string memory apiKey = Context.confidentialInputs(0);
string[] memory messages = Context.confidentialInputs(1);
ChatGPT chatGPT = new ChatGPT(apiKey);
string memory response = chatGPT.complete(messages);
emit Response(response);
return abi.encodeWithSelector(this.onchain.selector);
}
}
```

If you look under the hood at the [`ChatGPT.sol`](https://github.com/flashbots/suave-std/blob/main/src/protocols/ChatGPT.sol) file you are importing, you will see it uses the general-purpose `Suave.doHttpRequest()` precompile to achieve the magic of querying an LLM from within an otherwise-ordinary smart contract.

Recompile, deploy, and call the offchain function using our `spell` tool:

```bash
forge build
```
```bash
suave-geth spell deploy ChatContract.sol:ChatContract
```
```bash
suave-geth spell conf-request <your_new_contract_address> 'offchain()' **TODO**
```

You should see the response from ChatGPT printed in your console:

**TODO**

Congratulations! 💃 You now have all the tools you need to build powerful and unique Suapps which simply are not possible to build with other public blockchains.

Feel free to post any Suapp you build on our [forum](https://collective.flashbots.net/c/suave/27) and we'll be happy to help you review interpolate, as well as invite you to our Developer Chat so you can see what others are building and share tips and best practices with the sharpest engineers we know.
Loading

0 comments on commit 8a79181

Please sign in to comment.