-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
6405c2e
commit 8a79181
Showing
6 changed files
with
318 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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... |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
Oops, something went wrong.