├── .dockerignore ├── .env.example ├── .github └── workflows │ └── build-aws-image.yaml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── contracts ├── Cargo.toml ├── abi │ ├── ERC20.json │ ├── IWETH9.json │ ├── LLMOracleCoordinator.json │ └── LLMOracleRegistry.json └── src │ ├── addresses.rs │ ├── balance.rs │ ├── errors.rs │ ├── interfaces.rs │ └── lib.rs ├── core ├── Cargo.toml ├── src │ ├── cli │ │ ├── commands │ │ │ ├── coordinator │ │ │ │ ├── mod.rs │ │ │ │ ├── request.rs │ │ │ │ ├── serve.rs │ │ │ │ └── view.rs │ │ │ ├── mod.rs │ │ │ ├── registry.rs │ │ │ └── token.rs │ │ ├── mod.rs │ │ └── parsers.rs │ ├── compute │ │ ├── execute.rs │ │ ├── generation │ │ │ ├── execute.rs │ │ │ ├── handler.rs │ │ │ ├── mod.rs │ │ │ ├── postprocess │ │ │ │ ├── identity.rs │ │ │ │ ├── mod.rs │ │ │ │ └── swan.rs │ │ │ ├── request.rs │ │ │ └── workflow.rs │ │ ├── handler.rs │ │ ├── mod.rs │ │ ├── nonce.rs │ │ ├── utils.rs │ │ └── validation │ │ │ ├── execute.rs │ │ │ ├── handler.rs │ │ │ ├── mod.rs │ │ │ └── workflow.rs │ ├── configurations │ │ └── mod.rs │ ├── lib.rs │ ├── main.rs │ └── node │ │ ├── anvil.rs │ │ ├── coordinator.rs │ │ ├── core.rs │ │ ├── mod.rs │ │ ├── registry.rs │ │ ├── token.rs │ │ └── types.rs └── tests │ ├── abi_encode_test.rs │ ├── contracts │ ├── TestNonce.sol │ └── TestSolidityPacked.sol │ ├── oracle_test.rs │ ├── registry_test.rs │ ├── request_test.rs │ ├── swan_test.rs │ └── weth_test.rs ├── misc └── arweave.js ├── secrets └── .gitignore └── storage ├── Cargo.toml └── src ├── arweave.rs ├── lib.rs └── traits.rs /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !src 3 | !Cargo.toml 4 | !Cargo.lock 5 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # RPC URL to connect with blockchain (required) 2 | RPC_URL=your-rpc-url 3 | 4 | # Logging level 5 | RUST_LOG=none,dria_oracle=info 6 | 7 | # Your Ethereum wallet for Oracle operations (required) 8 | # 32-byte private key, as a hexadecimal string without 0x prefix 9 | # example: ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 10 | SECRET_KEY=your-secret-key 11 | 12 | # Coordinator address (optional) 13 | COORDINATOR_ADDRESS= 14 | 15 | ## Arweave configurations 16 | # path to wallet, only required if your BYTE_LIMIT is enough that 17 | # you may do an Arweave upload to store a large value on-chain 18 | ARWEAVE_WALLET_PATH=./path/to/wallet.json 19 | # Base URIL for Arweave, may be kept as is 20 | ARWEAVE_BASE_URL=https://node1.bundlr.network 21 | # Bytesize threshold, if a value is larger than this it will be stored 22 | # on Arweave and the transaction id itself will be returned 23 | ARWEAVE_BYTE_LIMIT=1024 24 | 25 | ## Ollama (if used, optional) ## 26 | OLLAMA_HOST=http://127.0.0.1 27 | OLLAMA_PORT=11434 28 | # if "true", automatically pull models from Ollama 29 | OLLAMA_AUTO_PULL=true 30 | 31 | ## Open AI (if used, required) ## 32 | OPENAI_API_KEY= 33 | 34 | ## Gemini (if used, required) ## 35 | GEMINI_API_KEY= 36 | 37 | ## OpenRouter (if used, required) ## 38 | OPENROUTER_API_KEY= 39 | 40 | ## Additional Services (optional) 41 | SERPER_API_KEY= 42 | JINA_API_KEY= 43 | -------------------------------------------------------------------------------- /.github/workflows/build-aws-image.yaml: -------------------------------------------------------------------------------- 1 | name: Dria Oracle Image on AWS 2 | 3 | on: 4 | push: 5 | branches: ["master"] 6 | 7 | permissions: 8 | id-token: write 9 | contents: read 10 | 11 | jobs: 12 | build-and-push: 13 | name: Build and Push GPT Dev Image 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v3 19 | 20 | - name: Set up QEMU 21 | uses: docker/setup-qemu-action@v3 22 | 23 | - name: Set up Docker Buildx 24 | uses: docker/setup-buildx-action@v3 25 | 26 | - name: Configure AWS credentials 27 | uses: aws-actions/configure-aws-credentials@v4 28 | with: 29 | role-to-assume: ${{ secrets.AWS_ROLE_ARN }} 30 | aws-region: us-east-1 31 | 32 | - name: Login to ECR 33 | uses: docker/login-action@v3 34 | with: 35 | registry: 974464413599.dkr.ecr.us-east-1.amazonaws.com 36 | 37 | - name: Get Unix Time 38 | id: timestamp 39 | run: echo "timestamp=$(date +%s)" >> $GITHUB_OUTPUT 40 | 41 | - name: Get SHA 42 | id: sha 43 | run: echo "sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT 44 | 45 | - name: Get Branch Name 46 | id: branch 47 | run: echo "branch=$(echo ${GITHUB_REF#refs/heads/})" >> $GITHUB_OUTPUT 48 | 49 | - name: Set Image Tag 50 | id: itag 51 | run: echo "itag=${{ steps.branch.outputs.branch }}-${{ steps.sha.outputs.sha }}-${{ steps.timestamp.outputs.timestamp }}" >> $GITHUB_OUTPUT 52 | 53 | - name: Build and push 54 | uses: docker/build-push-action@v6 55 | env: 56 | IMAGE_TAG: ${{ steps.itag.outputs.itag }} 57 | with: 58 | target: gpt 59 | platforms: linux/amd64 60 | push: true 61 | tags: | 62 | 974464413599.dkr.ecr.us-east-1.amazonaws.com/dkn-l2-oracle-gpt:dev 63 | 974464413599.dkr.ecr.us-east-1.amazonaws.com/dkn-l2-oracle-gpt:${{ env.IMAGE_TAG }} 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .env* 3 | !.env.example 4 | .vscode 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = ["core", "contracts", "storage"] 4 | default-members = ["core"] 5 | 6 | [workspace.package] 7 | edition = "2021" 8 | version = "0.2.38" 9 | license = "Apache-2.0" 10 | readme = "README.md" 11 | authors = ["erhant"] 12 | 13 | [workspace.dependencies] 14 | # core 15 | alloy = { version = "0.8.0", features = ["full"] } 16 | alloy-chains = "0.1.24" 17 | tokio = { version = "1.39.2", features = [ 18 | "macros", 19 | "rt-multi-thread", 20 | "signal", 21 | ] } 22 | tokio-util = "0.7.13" 23 | 24 | # workflows 25 | dkn-workflows = { git = "https://github.com/firstbatchxyz/dkn-compute-node" } 26 | 27 | # errors & logging & env 28 | env_logger = "0.11.5" 29 | eyre = "0.6.12" 30 | log = "0.4.22" 31 | dotenvy = "0.15.7" 32 | 33 | # utils 34 | async-trait = "0.1.81" 35 | reqwest = "0.12.5" 36 | 37 | # serde 38 | serde = "1.0.204" 39 | serde_json = "1.0.122" 40 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=$BUILDPLATFORM rust:1.81.0 as builder 2 | 3 | # https://docs.docker.com/engine/reference/builder/#automatic-platform-args-in-the-global-scope 4 | # 5 | # We use distroless, which allow the following platforms: 6 | # linux/amd64 7 | # linux/arm64 8 | # linux/arm 9 | # 10 | # To build an image & push them to Docker hub for this Dockerfile: 11 | # 12 | # docker buildx build --platform=linux/amd64,linux/arm64,linux/arm . -t firstbatch/dria-compute-node:latest --builder=dria-builder --push 13 | ARG BUILDPLATFORM 14 | ARG TARGETPLATFORM 15 | RUN echo "Build platform: $BUILDPLATFORM" 16 | RUN echo "Target platform: $TARGETPLATFORM" 17 | 18 | # build release binary 19 | WORKDIR /usr/src/app 20 | COPY . . 21 | RUN cargo build --release 22 | 23 | # copy release binary to distroless 24 | FROM --platform=$BUILDPLATFORM gcr.io/distroless/cc AS gpt 25 | COPY --from=builder /usr/src/app/target/release/dria-oracle / 26 | 27 | ENTRYPOINT ["./dria-oracle"] 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright Copyright 2023 FirstBatch 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # load .env 2 | ifneq (,$(wildcard ./.env)) 3 | include .env 4 | export 5 | endif 6 | 7 | ############################################################################### 8 | .PHONY: launch # | Run with INFO logging & release mode 9 | launch: 10 | cargo run --release serve 11 | 12 | .PHONY: run # | Run with INFO logging 13 | run: 14 | cargo run serve 15 | 16 | .PHONY: debug # | Run with crate-level DEBUG logging & info-level workflows 17 | debug: 18 | RUST_LOG=none,dria_oracle=debug,dkn_workflows=debug,ollama_workflows=info cargo run serve 19 | 20 | ############################################################################### 21 | .PHONY: install # | Install to path 22 | install: 23 | cargo install --path . 24 | 25 | .PHONY: build # | Build 26 | build: 27 | cargo build 28 | 29 | .PHONY: docs # | Generate & open documentation 30 | docs: 31 | cargo doc --open --no-deps --document-private-items 32 | 33 | .PHONY: lint # | Run clippy 34 | lint: 35 | cargo clippy 36 | 37 | .PHONY: format # | Run formatter 38 | format: 39 | cargo fmt -v 40 | 41 | .PHONY: version # | Print version 42 | version: 43 | @cargo pkgid | cut -d# -f2 44 | 45 | .PHONY: test # | Run tests 46 | test: 47 | RUST_LOG=none,dria_oracle=info cargo test --all-features 48 | 49 | ############################################################################### 50 | # abi source can be given from outside, and defaults as shown here 51 | ABI_SRC_PATH?=../dria-contracts/artifacts 52 | ABI_DEST_PATH=./src/contracts/abi 53 | 54 | .PHONY: abis # | Copy contract abis from a neighbor repo (ABI_SRC_PATH). 55 | abis: 56 | @echo "Copying contract ABIs from $(ABI_SRC_PATH) to $(ABI_DEST_PATH)" 57 | cp $(ABI_SRC_PATH)/@openzeppelin/contracts/token/ERC20/ERC20.sol/ERC20.json $(ABI_DEST_PATH)/ERC20.json 58 | cp $(ABI_SRC_PATH)/contracts/llm/LLMOracleCoordinator.sol/LLMOracleCoordinator.json $(ABI_DEST_PATH)/LLMOracleCoordinator.json 59 | cp $(ABI_SRC_PATH)/contracts/llm/LLMOracleRegistry.sol/LLMOracleRegistry.json $(ABI_DEST_PATH)/LLMOracleRegistry.json 60 | 61 | ############################################################################### 62 | # https://stackoverflow.com/a/45843594 63 | .PHONY: help # | List targets 64 | help: 65 | @grep '^.PHONY: .* #' Makefile | sed 's/\.PHONY: \(.*\) # \(.*\)/\1 \2/' | expand -t20 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | logo 3 |

4 | 5 |

6 |

7 | Dria Oracle Node 8 |

9 |

10 | Dria Oracle Node serves LLM workflow tasks directly from smart-contracts. 11 |

12 |

13 | 14 | ## Installation 15 | 16 | Install Dria Oracle Node with: 17 | 18 | ```sh 19 | cargo install --git https://github.com/firstbatchxyz/dria-oracle-node 20 | ``` 21 | 22 | ## Setup 23 | 24 | Create an `.env` file by copying `.env.example`. You have to fill the following variables: 25 | 26 | - Get an RPC URL from a provider such as [Alchemy](https://www.alchemy.com/) or [Infura](https://www.infura.io/), and set it as `RPC_URL`. 27 | - Provide an Ethereum wallet secret key to `SECRET_KEY`, make sure it has funds to pay for gas and tokens. 28 | 29 | > [!NOTE] 30 | > 31 | > The contract addresses are determined with respect to the chain connected via RPC URL, but you can override it via `COORDINATOR_ADDRESS` environment variable. 32 | > In any case, you should not need to do this. 33 | 34 | > [!TIP] 35 | > 36 | > You can have multiple environment files, and specify them explicitly with the `-e` argument, e.g. 37 | > 38 | > ```sh 39 | > # base sepolia 40 | > dria-oracle -e ./.env.base-sepolia registrations 41 | > 42 | > # base mainnet 43 | > dria-oracle -e ./.env.base-mainnet registrations 44 | > ``` 45 | 46 | ### Arweave 47 | 48 | You can save gas costs using [Arweave](https://arweave.org/): 49 | 50 | - Provide an Arweave wallet via `ARWEAVE_WALLET_PATH` variable so that you can use Arweave for large results. You can create one [here](https://arweave.app/). 51 | - You can set `ARWEAVE_BYTE_LIMIT` to determine the byte length threshold, beyond which values are uploaded to Arweave. It defaults to 1024, so any data less than that many bytes will be written as-is. 52 | 53 | If you omit Arweave, it will only use the client for downloading things from Arweave, but will never upload. 54 | 55 | ### LLM Providers 56 | 57 | As for the LLM providers: 58 | 59 | - If you are using Ollama, make sure it is running and the host & port are correct. 60 | - If you are using OpenAI, provide the `OPENAI_API_KEY`. 61 | - If you are using Gemini, provide the `GEMINI_API_KEY`. 62 | - If you are using OpenRouter, provide the `OPENROUTER_API_KEY`. 63 | 64 | ## Usage 65 | 66 | After installatioon, a binary called `dria-oracle` will be created. You can see the available commands with: 67 | 68 | ```sh 69 | dria-oracle help 70 | ``` 71 | 72 | The CLI provides several methods to interact with the oracle contracts. 73 | 74 | - [Registration](#registration) 75 | - [Launching the Node](#launching-the-node) 76 | - [Viewing Tasks](#viewing-tasks) 77 | - [Balance & Rewards](#balance--rewards) 78 | 79 | > [!TIP] 80 | > 81 | > You can enable debug-level logs with the `-d` option. 82 | 83 | ### Registration 84 | 85 | To serve oracle requests, you **MUST** first register as your desired oracle type, i.e. `generator` or `validator`. These are handled by the registration commands `register` and `unregister` which accepts multiple arguments to register at once. You can then see your registrations with `registrations` command. 86 | 87 | Here is an example: 88 | 89 | ```sh 90 | # 1. Register as both generator and validator 91 | dria-oracle register generator validator 92 | 93 | # 2. See that you are registered 94 | dria-oracle registrations 95 | 96 | # 3. Unregister from validator 97 | dria-oracle unregister validator 98 | ``` 99 | 100 | You will need to have some tokens in your balance, which will be approved automatically if required by the register command. 101 | 102 | > [!TIP] 103 | > 104 | > Using WETH, we can do this quite easily via `cast`: 105 | > 106 | > ```sh 107 | > cast send 0x4200000000000000000000000000000000000006 "deposit()()" --rpc-url https://mainnet.base.org --private-key $SECRET_KEY --value $AMOUNT 108 | > ``` 109 | > 110 | > You can also convert back to ETH using: 111 | > 112 | > ```sh 113 | > cast send 0x4200000000000000000000000000000000000006 "withdraw(uint256)()" $AMOUNT --rpc-url https://mainnet.base.org --private-key $SECRET_KEY 114 | > ``` 115 | 116 | ### Launching the Node 117 | 118 | We launch our node using the `serve` command, followed by models of our choice and the oracle type that we would like to serve. 119 | If we provide no oracle types, it will default to the ones that we are registered to. 120 | 121 | ```sh 122 | # run as generator 123 | dria-oracle serve -m=gpt-4o-mini -m=llama3.1:latest generator 124 | 125 | # run as validator 126 | dria-oracle serve -m=gpt-4o validator 127 | 128 | # run as kinds that you are registered to 129 | dria-oracle serve -m=gpt-4o 130 | ``` 131 | 132 | We can start handling tasks from previous blocks until now, and then continue listening for more events: 133 | 134 | ```sh 135 | # start from 100th block until now, and subscribe to new events 136 | dria-oracle serve -m=gpt-4o --from=100 137 | ``` 138 | 139 | > [!TIP] 140 | > 141 | > You can terminate the application from the terminal as usual (e.g. CTRL+C) to quit the node. 142 | 143 | Or, we can handle tasks between specific blocks only, the application will exit upon finishing blocks unlike before: 144 | 145 | ```sh 146 | # handle tasks between blocks 100 and 500 147 | dria-oracle serve -m=gpt-4o --from=100 --to=500 148 | ``` 149 | 150 | Finally, we can handle an existing task specifically as well (if its unhandled for some reason): 151 | 152 | ```sh 153 | # note that if task id is given, `from` and `to` will be ignored 154 | dria-oracle serve -m=gpt-4o --task-id 155 | ``` 156 | 157 | > [!WARNING] 158 | > 159 | > Validators must use `gpt-4o` model. 160 | 161 | #### Using Arweave 162 | 163 | To save from gas fees, an Oracle node can upload its response to Arweave and then store the transaction id of that upload to the contract instead. This is differentiated by looking at the response, and see that it is exactly 64 hexadecimal characters. It is then decoded from hex and encoded to `base64url` format, which can then be used to access the data at `https//arweave.net/{txid-here}`. This **requires** an Arweave wallet. 164 | 165 | Following the same logic, the Oracle node can read task inputs from Arweave as well. This **does not require** an Arweave a wallet. 166 | 167 | ### Viewing Tasks 168 | 169 | You can `view` the details of a task by its task id: 170 | 171 | ```sh 172 | dria-oracle view --task-id 173 | ``` 174 | 175 | You can also view the task status updates between blocks with the same command, by providing `from` and `to` blocks, 176 | which defaults from `earliest` block to `latest` block if none is provided. 177 | 178 | ```sh 179 | dria-oracle view # earliest to latest 180 | dria-oracle view --from=100 # 100 to latest 181 | dria-oracle view --to=100 # earliest to 100 182 | dria-oracle view --from=100 --to=200 # 100 to 200 183 | ``` 184 | 185 | ### Balance & Rewards 186 | 187 | At any time, you can see your balance with: 188 | 189 | ```sh 190 | dria-oracle balance 191 | ``` 192 | 193 | As you respond to tasks, you will have rewards available to you. You can see & claim them using your node: 194 | 195 | ```sh 196 | # print rewards 197 | dria-oracle rewards 198 | 199 | # claim all rewards 200 | dria-oracle claim 201 | ``` 202 | 203 | ### Making a Request 204 | 205 | Although the oracle is only supposed to serve requests made from other parties, it is also able to make requests from the CLI. See usage with the help option: 206 | 207 | ```sh 208 | dria-oracle request -h 209 | ``` 210 | 211 | It mainly takes an input argument, followed by multiple model arguments: 212 | 213 | ```sh 214 | dria-oracle request "What is 2+2?" gpt-4o-mini phi3:3.8b 215 | ``` 216 | 217 | ## Development 218 | 219 | If you would like to contribute, please create an issue first! To start developing, clone the repository: 220 | 221 | ```sh 222 | git clone https://github.com/firstbatchxyz/dria-oracle-node.git 223 | ``` 224 | 225 | ### Testing 226 | 227 | Run tests with: 228 | 229 | ```sh 230 | make test 231 | ``` 232 | 233 | ### Documentation 234 | 235 | You can view the inline documentation with: 236 | 237 | ```sh 238 | make docs 239 | ``` 240 | 241 | ### Styling 242 | 243 | Lint and format with: 244 | 245 | ```sh 246 | make lint # clippy 247 | make format # rustfmt 248 | ``` 249 | 250 | ## License 251 | 252 | This project is licensed under the [Apache License 2.0](https://opensource.org/license/Apache-2.0). 253 | -------------------------------------------------------------------------------- /contracts/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dria-oracle-contracts" 3 | description = "Dria Oracle Contract Interfaces" 4 | version.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | authors.workspace = true 8 | 9 | [dependencies] 10 | alloy.workspace = true 11 | alloy-chains.workspace = true 12 | eyre.workspace = true 13 | -------------------------------------------------------------------------------- /contracts/abi/ERC20.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "address", 6 | "name": "spender", 7 | "type": "address" 8 | }, 9 | { 10 | "internalType": "uint256", 11 | "name": "allowance", 12 | "type": "uint256" 13 | }, 14 | { 15 | "internalType": "uint256", 16 | "name": "needed", 17 | "type": "uint256" 18 | } 19 | ], 20 | "name": "ERC20InsufficientAllowance", 21 | "type": "error" 22 | }, 23 | { 24 | "inputs": [ 25 | { 26 | "internalType": "address", 27 | "name": "sender", 28 | "type": "address" 29 | }, 30 | { 31 | "internalType": "uint256", 32 | "name": "balance", 33 | "type": "uint256" 34 | }, 35 | { 36 | "internalType": "uint256", 37 | "name": "needed", 38 | "type": "uint256" 39 | } 40 | ], 41 | "name": "ERC20InsufficientBalance", 42 | "type": "error" 43 | }, 44 | { 45 | "inputs": [ 46 | { 47 | "internalType": "address", 48 | "name": "approver", 49 | "type": "address" 50 | } 51 | ], 52 | "name": "ERC20InvalidApprover", 53 | "type": "error" 54 | }, 55 | { 56 | "inputs": [ 57 | { 58 | "internalType": "address", 59 | "name": "receiver", 60 | "type": "address" 61 | } 62 | ], 63 | "name": "ERC20InvalidReceiver", 64 | "type": "error" 65 | }, 66 | { 67 | "inputs": [ 68 | { 69 | "internalType": "address", 70 | "name": "sender", 71 | "type": "address" 72 | } 73 | ], 74 | "name": "ERC20InvalidSender", 75 | "type": "error" 76 | }, 77 | { 78 | "inputs": [ 79 | { 80 | "internalType": "address", 81 | "name": "spender", 82 | "type": "address" 83 | } 84 | ], 85 | "name": "ERC20InvalidSpender", 86 | "type": "error" 87 | }, 88 | { 89 | "anonymous": false, 90 | "inputs": [ 91 | { 92 | "indexed": true, 93 | "internalType": "address", 94 | "name": "owner", 95 | "type": "address" 96 | }, 97 | { 98 | "indexed": true, 99 | "internalType": "address", 100 | "name": "spender", 101 | "type": "address" 102 | }, 103 | { 104 | "indexed": false, 105 | "internalType": "uint256", 106 | "name": "value", 107 | "type": "uint256" 108 | } 109 | ], 110 | "name": "Approval", 111 | "type": "event" 112 | }, 113 | { 114 | "anonymous": false, 115 | "inputs": [ 116 | { 117 | "indexed": true, 118 | "internalType": "address", 119 | "name": "from", 120 | "type": "address" 121 | }, 122 | { 123 | "indexed": true, 124 | "internalType": "address", 125 | "name": "to", 126 | "type": "address" 127 | }, 128 | { 129 | "indexed": false, 130 | "internalType": "uint256", 131 | "name": "value", 132 | "type": "uint256" 133 | } 134 | ], 135 | "name": "Transfer", 136 | "type": "event" 137 | }, 138 | { 139 | "inputs": [ 140 | { 141 | "internalType": "address", 142 | "name": "owner", 143 | "type": "address" 144 | }, 145 | { 146 | "internalType": "address", 147 | "name": "spender", 148 | "type": "address" 149 | } 150 | ], 151 | "name": "allowance", 152 | "outputs": [ 153 | { 154 | "internalType": "uint256", 155 | "name": "", 156 | "type": "uint256" 157 | } 158 | ], 159 | "stateMutability": "view", 160 | "type": "function" 161 | }, 162 | { 163 | "inputs": [ 164 | { 165 | "internalType": "address", 166 | "name": "spender", 167 | "type": "address" 168 | }, 169 | { 170 | "internalType": "uint256", 171 | "name": "value", 172 | "type": "uint256" 173 | } 174 | ], 175 | "name": "approve", 176 | "outputs": [ 177 | { 178 | "internalType": "bool", 179 | "name": "", 180 | "type": "bool" 181 | } 182 | ], 183 | "stateMutability": "nonpayable", 184 | "type": "function" 185 | }, 186 | { 187 | "inputs": [ 188 | { 189 | "internalType": "address", 190 | "name": "account", 191 | "type": "address" 192 | } 193 | ], 194 | "name": "balanceOf", 195 | "outputs": [ 196 | { 197 | "internalType": "uint256", 198 | "name": "", 199 | "type": "uint256" 200 | } 201 | ], 202 | "stateMutability": "view", 203 | "type": "function" 204 | }, 205 | { 206 | "inputs": [], 207 | "name": "decimals", 208 | "outputs": [ 209 | { 210 | "internalType": "uint8", 211 | "name": "", 212 | "type": "uint8" 213 | } 214 | ], 215 | "stateMutability": "view", 216 | "type": "function" 217 | }, 218 | { 219 | "inputs": [], 220 | "name": "name", 221 | "outputs": [ 222 | { 223 | "internalType": "string", 224 | "name": "", 225 | "type": "string" 226 | } 227 | ], 228 | "stateMutability": "view", 229 | "type": "function" 230 | }, 231 | { 232 | "inputs": [], 233 | "name": "symbol", 234 | "outputs": [ 235 | { 236 | "internalType": "string", 237 | "name": "", 238 | "type": "string" 239 | } 240 | ], 241 | "stateMutability": "view", 242 | "type": "function" 243 | }, 244 | { 245 | "inputs": [], 246 | "name": "totalSupply", 247 | "outputs": [ 248 | { 249 | "internalType": "uint256", 250 | "name": "", 251 | "type": "uint256" 252 | } 253 | ], 254 | "stateMutability": "view", 255 | "type": "function" 256 | }, 257 | { 258 | "inputs": [ 259 | { 260 | "internalType": "address", 261 | "name": "to", 262 | "type": "address" 263 | }, 264 | { 265 | "internalType": "uint256", 266 | "name": "value", 267 | "type": "uint256" 268 | } 269 | ], 270 | "name": "transfer", 271 | "outputs": [ 272 | { 273 | "internalType": "bool", 274 | "name": "", 275 | "type": "bool" 276 | } 277 | ], 278 | "stateMutability": "nonpayable", 279 | "type": "function" 280 | }, 281 | { 282 | "inputs": [ 283 | { 284 | "internalType": "address", 285 | "name": "from", 286 | "type": "address" 287 | }, 288 | { 289 | "internalType": "address", 290 | "name": "to", 291 | "type": "address" 292 | }, 293 | { 294 | "internalType": "uint256", 295 | "name": "value", 296 | "type": "uint256" 297 | } 298 | ], 299 | "name": "transferFrom", 300 | "outputs": [ 301 | { 302 | "internalType": "bool", 303 | "name": "", 304 | "type": "bool" 305 | } 306 | ], 307 | "stateMutability": "nonpayable", 308 | "type": "function" 309 | } 310 | ] 311 | -------------------------------------------------------------------------------- /contracts/abi/IWETH9.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "anonymous": false, 4 | "inputs": [ 5 | { 6 | "indexed": true, 7 | "internalType": "address", 8 | "name": "src", 9 | "type": "address" 10 | }, 11 | { 12 | "indexed": true, 13 | "internalType": "address", 14 | "name": "guy", 15 | "type": "address" 16 | }, 17 | { 18 | "indexed": false, 19 | "internalType": "uint256", 20 | "name": "wad", 21 | "type": "uint256" 22 | } 23 | ], 24 | "name": "Approval", 25 | "type": "event" 26 | }, 27 | { 28 | "anonymous": false, 29 | "inputs": [ 30 | { 31 | "indexed": true, 32 | "internalType": "address", 33 | "name": "dst", 34 | "type": "address" 35 | }, 36 | { 37 | "indexed": false, 38 | "internalType": "uint256", 39 | "name": "wad", 40 | "type": "uint256" 41 | } 42 | ], 43 | "name": "Deposit", 44 | "type": "event" 45 | }, 46 | { 47 | "anonymous": false, 48 | "inputs": [ 49 | { 50 | "indexed": true, 51 | "internalType": "address", 52 | "name": "src", 53 | "type": "address" 54 | }, 55 | { 56 | "indexed": true, 57 | "internalType": "address", 58 | "name": "dst", 59 | "type": "address" 60 | }, 61 | { 62 | "indexed": false, 63 | "internalType": "uint256", 64 | "name": "wad", 65 | "type": "uint256" 66 | } 67 | ], 68 | "name": "Transfer", 69 | "type": "event" 70 | }, 71 | { 72 | "anonymous": false, 73 | "inputs": [ 74 | { 75 | "indexed": true, 76 | "internalType": "address", 77 | "name": "src", 78 | "type": "address" 79 | }, 80 | { 81 | "indexed": false, 82 | "internalType": "uint256", 83 | "name": "wad", 84 | "type": "uint256" 85 | } 86 | ], 87 | "name": "Withdrawal", 88 | "type": "event" 89 | }, 90 | { 91 | "payable": true, 92 | "stateMutability": "payable", 93 | "type": "fallback" 94 | }, 95 | { 96 | "constant": true, 97 | "inputs": [ 98 | { 99 | "internalType": "address", 100 | "name": "", 101 | "type": "address" 102 | }, 103 | { 104 | "internalType": "address", 105 | "name": "", 106 | "type": "address" 107 | } 108 | ], 109 | "name": "allowance", 110 | "outputs": [ 111 | { 112 | "internalType": "uint256", 113 | "name": "", 114 | "type": "uint256" 115 | } 116 | ], 117 | "payable": false, 118 | "stateMutability": "view", 119 | "type": "function" 120 | }, 121 | { 122 | "constant": false, 123 | "inputs": [ 124 | { 125 | "internalType": "address", 126 | "name": "guy", 127 | "type": "address" 128 | }, 129 | { 130 | "internalType": "uint256", 131 | "name": "wad", 132 | "type": "uint256" 133 | } 134 | ], 135 | "name": "approve", 136 | "outputs": [ 137 | { 138 | "internalType": "bool", 139 | "name": "", 140 | "type": "bool" 141 | } 142 | ], 143 | "payable": false, 144 | "stateMutability": "nonpayable", 145 | "type": "function" 146 | }, 147 | { 148 | "constant": true, 149 | "inputs": [ 150 | { 151 | "internalType": "address", 152 | "name": "", 153 | "type": "address" 154 | } 155 | ], 156 | "name": "balanceOf", 157 | "outputs": [ 158 | { 159 | "internalType": "uint256", 160 | "name": "", 161 | "type": "uint256" 162 | } 163 | ], 164 | "payable": false, 165 | "stateMutability": "view", 166 | "type": "function" 167 | }, 168 | { 169 | "constant": true, 170 | "inputs": [], 171 | "name": "decimals", 172 | "outputs": [ 173 | { 174 | "internalType": "uint8", 175 | "name": "", 176 | "type": "uint8" 177 | } 178 | ], 179 | "payable": false, 180 | "stateMutability": "view", 181 | "type": "function" 182 | }, 183 | { 184 | "constant": false, 185 | "inputs": [], 186 | "name": "deposit", 187 | "outputs": [], 188 | "payable": true, 189 | "stateMutability": "payable", 190 | "type": "function" 191 | }, 192 | { 193 | "constant": true, 194 | "inputs": [], 195 | "name": "name", 196 | "outputs": [ 197 | { 198 | "internalType": "string", 199 | "name": "", 200 | "type": "string" 201 | } 202 | ], 203 | "payable": false, 204 | "stateMutability": "view", 205 | "type": "function" 206 | }, 207 | { 208 | "constant": true, 209 | "inputs": [], 210 | "name": "symbol", 211 | "outputs": [ 212 | { 213 | "internalType": "string", 214 | "name": "", 215 | "type": "string" 216 | } 217 | ], 218 | "payable": false, 219 | "stateMutability": "view", 220 | "type": "function" 221 | }, 222 | { 223 | "constant": true, 224 | "inputs": [], 225 | "name": "totalSupply", 226 | "outputs": [ 227 | { 228 | "internalType": "uint256", 229 | "name": "", 230 | "type": "uint256" 231 | } 232 | ], 233 | "payable": false, 234 | "stateMutability": "view", 235 | "type": "function" 236 | }, 237 | { 238 | "constant": false, 239 | "inputs": [ 240 | { 241 | "internalType": "address", 242 | "name": "dst", 243 | "type": "address" 244 | }, 245 | { 246 | "internalType": "uint256", 247 | "name": "wad", 248 | "type": "uint256" 249 | } 250 | ], 251 | "name": "transfer", 252 | "outputs": [ 253 | { 254 | "internalType": "bool", 255 | "name": "", 256 | "type": "bool" 257 | } 258 | ], 259 | "payable": false, 260 | "stateMutability": "nonpayable", 261 | "type": "function" 262 | }, 263 | { 264 | "constant": false, 265 | "inputs": [ 266 | { 267 | "internalType": "address", 268 | "name": "src", 269 | "type": "address" 270 | }, 271 | { 272 | "internalType": "address", 273 | "name": "dst", 274 | "type": "address" 275 | }, 276 | { 277 | "internalType": "uint256", 278 | "name": "wad", 279 | "type": "uint256" 280 | } 281 | ], 282 | "name": "transferFrom", 283 | "outputs": [ 284 | { 285 | "internalType": "bool", 286 | "name": "", 287 | "type": "bool" 288 | } 289 | ], 290 | "payable": false, 291 | "stateMutability": "nonpayable", 292 | "type": "function" 293 | }, 294 | { 295 | "constant": false, 296 | "inputs": [ 297 | { 298 | "internalType": "uint256", 299 | "name": "wad", 300 | "type": "uint256" 301 | } 302 | ], 303 | "name": "withdraw", 304 | "outputs": [], 305 | "payable": false, 306 | "stateMutability": "nonpayable", 307 | "type": "function" 308 | } 309 | ] 310 | -------------------------------------------------------------------------------- /contracts/abi/LLMOracleRegistry.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "type": "constructor", "inputs": [], "stateMutability": "nonpayable" }, 3 | { 4 | "type": "function", 5 | "name": "UPGRADE_INTERFACE_VERSION", 6 | "inputs": [], 7 | "outputs": [{ "name": "", "type": "string", "internalType": "string" }], 8 | "stateMutability": "view" 9 | }, 10 | { 11 | "type": "function", 12 | "name": "addToWhitelist", 13 | "inputs": [ 14 | { "name": "accounts", "type": "address[]", "internalType": "address[]" } 15 | ], 16 | "outputs": [], 17 | "stateMutability": "nonpayable" 18 | }, 19 | { 20 | "type": "function", 21 | "name": "generatorStakeAmount", 22 | "inputs": [], 23 | "outputs": [{ "name": "", "type": "uint256", "internalType": "uint256" }], 24 | "stateMutability": "view" 25 | }, 26 | { 27 | "type": "function", 28 | "name": "getStakeAmount", 29 | "inputs": [ 30 | { 31 | "name": "kind", 32 | "type": "uint8", 33 | "internalType": "enum LLMOracleKind" 34 | } 35 | ], 36 | "outputs": [{ "name": "", "type": "uint256", "internalType": "uint256" }], 37 | "stateMutability": "view" 38 | }, 39 | { 40 | "type": "function", 41 | "name": "initialize", 42 | "inputs": [ 43 | { 44 | "name": "_generatorStakeAmount", 45 | "type": "uint256", 46 | "internalType": "uint256" 47 | }, 48 | { 49 | "name": "_validatorStakeAmount", 50 | "type": "uint256", 51 | "internalType": "uint256" 52 | }, 53 | { "name": "_token", "type": "address", "internalType": "address" }, 54 | { 55 | "name": "_minRegistrationTime", 56 | "type": "uint256", 57 | "internalType": "uint256" 58 | } 59 | ], 60 | "outputs": [], 61 | "stateMutability": "nonpayable" 62 | }, 63 | { 64 | "type": "function", 65 | "name": "isRegistered", 66 | "inputs": [ 67 | { "name": "user", "type": "address", "internalType": "address" }, 68 | { 69 | "name": "kind", 70 | "type": "uint8", 71 | "internalType": "enum LLMOracleKind" 72 | } 73 | ], 74 | "outputs": [{ "name": "", "type": "bool", "internalType": "bool" }], 75 | "stateMutability": "view" 76 | }, 77 | { 78 | "type": "function", 79 | "name": "isWhitelisted", 80 | "inputs": [{ "name": "", "type": "address", "internalType": "address" }], 81 | "outputs": [{ "name": "", "type": "bool", "internalType": "bool" }], 82 | "stateMutability": "view" 83 | }, 84 | { 85 | "type": "function", 86 | "name": "minRegistrationTime", 87 | "inputs": [], 88 | "outputs": [{ "name": "", "type": "uint256", "internalType": "uint256" }], 89 | "stateMutability": "view" 90 | }, 91 | { 92 | "type": "function", 93 | "name": "owner", 94 | "inputs": [], 95 | "outputs": [{ "name": "", "type": "address", "internalType": "address" }], 96 | "stateMutability": "view" 97 | }, 98 | { 99 | "type": "function", 100 | "name": "proxiableUUID", 101 | "inputs": [], 102 | "outputs": [{ "name": "", "type": "bytes32", "internalType": "bytes32" }], 103 | "stateMutability": "view" 104 | }, 105 | { 106 | "type": "function", 107 | "name": "register", 108 | "inputs": [ 109 | { 110 | "name": "kind", 111 | "type": "uint8", 112 | "internalType": "enum LLMOracleKind" 113 | } 114 | ], 115 | "outputs": [], 116 | "stateMutability": "nonpayable" 117 | }, 118 | { 119 | "type": "function", 120 | "name": "registrationTimes", 121 | "inputs": [ 122 | { "name": "oracle", "type": "address", "internalType": "address" }, 123 | { "name": "", "type": "uint8", "internalType": "enum LLMOracleKind" } 124 | ], 125 | "outputs": [ 126 | { 127 | "name": "registeredTime", 128 | "type": "uint256", 129 | "internalType": "uint256" 130 | } 131 | ], 132 | "stateMutability": "view" 133 | }, 134 | { 135 | "type": "function", 136 | "name": "registrations", 137 | "inputs": [ 138 | { "name": "oracle", "type": "address", "internalType": "address" }, 139 | { "name": "", "type": "uint8", "internalType": "enum LLMOracleKind" } 140 | ], 141 | "outputs": [ 142 | { "name": "amount", "type": "uint256", "internalType": "uint256" } 143 | ], 144 | "stateMutability": "view" 145 | }, 146 | { 147 | "type": "function", 148 | "name": "removeFromWhitelist", 149 | "inputs": [ 150 | { "name": "account", "type": "address", "internalType": "address" } 151 | ], 152 | "outputs": [], 153 | "stateMutability": "nonpayable" 154 | }, 155 | { 156 | "type": "function", 157 | "name": "renounceOwnership", 158 | "inputs": [], 159 | "outputs": [], 160 | "stateMutability": "nonpayable" 161 | }, 162 | { 163 | "type": "function", 164 | "name": "setStakeAmounts", 165 | "inputs": [ 166 | { 167 | "name": "_generatorStakeAmount", 168 | "type": "uint256", 169 | "internalType": "uint256" 170 | }, 171 | { 172 | "name": "_validatorStakeAmount", 173 | "type": "uint256", 174 | "internalType": "uint256" 175 | } 176 | ], 177 | "outputs": [], 178 | "stateMutability": "nonpayable" 179 | }, 180 | { 181 | "type": "function", 182 | "name": "token", 183 | "inputs": [], 184 | "outputs": [ 185 | { "name": "", "type": "address", "internalType": "contract ERC20" } 186 | ], 187 | "stateMutability": "view" 188 | }, 189 | { 190 | "type": "function", 191 | "name": "transferOwnership", 192 | "inputs": [ 193 | { "name": "newOwner", "type": "address", "internalType": "address" } 194 | ], 195 | "outputs": [], 196 | "stateMutability": "nonpayable" 197 | }, 198 | { 199 | "type": "function", 200 | "name": "unregister", 201 | "inputs": [ 202 | { 203 | "name": "kind", 204 | "type": "uint8", 205 | "internalType": "enum LLMOracleKind" 206 | } 207 | ], 208 | "outputs": [ 209 | { "name": "amount", "type": "uint256", "internalType": "uint256" } 210 | ], 211 | "stateMutability": "nonpayable" 212 | }, 213 | { 214 | "type": "function", 215 | "name": "upgradeToAndCall", 216 | "inputs": [ 217 | { 218 | "name": "newImplementation", 219 | "type": "address", 220 | "internalType": "address" 221 | }, 222 | { "name": "data", "type": "bytes", "internalType": "bytes" } 223 | ], 224 | "outputs": [], 225 | "stateMutability": "payable" 226 | }, 227 | { 228 | "type": "function", 229 | "name": "validatorStakeAmount", 230 | "inputs": [], 231 | "outputs": [{ "name": "", "type": "uint256", "internalType": "uint256" }], 232 | "stateMutability": "view" 233 | }, 234 | { 235 | "type": "event", 236 | "name": "AddedToWhitelist", 237 | "inputs": [ 238 | { 239 | "name": "account", 240 | "type": "address", 241 | "indexed": true, 242 | "internalType": "address" 243 | } 244 | ], 245 | "anonymous": false 246 | }, 247 | { 248 | "type": "event", 249 | "name": "Initialized", 250 | "inputs": [ 251 | { 252 | "name": "version", 253 | "type": "uint64", 254 | "indexed": false, 255 | "internalType": "uint64" 256 | } 257 | ], 258 | "anonymous": false 259 | }, 260 | { 261 | "type": "event", 262 | "name": "OwnershipTransferred", 263 | "inputs": [ 264 | { 265 | "name": "previousOwner", 266 | "type": "address", 267 | "indexed": true, 268 | "internalType": "address" 269 | }, 270 | { 271 | "name": "newOwner", 272 | "type": "address", 273 | "indexed": true, 274 | "internalType": "address" 275 | } 276 | ], 277 | "anonymous": false 278 | }, 279 | { 280 | "type": "event", 281 | "name": "Registered", 282 | "inputs": [ 283 | { 284 | "name": "", 285 | "type": "address", 286 | "indexed": true, 287 | "internalType": "address" 288 | }, 289 | { 290 | "name": "kind", 291 | "type": "uint8", 292 | "indexed": false, 293 | "internalType": "enum LLMOracleKind" 294 | } 295 | ], 296 | "anonymous": false 297 | }, 298 | { 299 | "type": "event", 300 | "name": "RemovedFromWhitelist", 301 | "inputs": [ 302 | { 303 | "name": "account", 304 | "type": "address", 305 | "indexed": true, 306 | "internalType": "address" 307 | } 308 | ], 309 | "anonymous": false 310 | }, 311 | { 312 | "type": "event", 313 | "name": "Unregistered", 314 | "inputs": [ 315 | { 316 | "name": "", 317 | "type": "address", 318 | "indexed": true, 319 | "internalType": "address" 320 | }, 321 | { 322 | "name": "kind", 323 | "type": "uint8", 324 | "indexed": false, 325 | "internalType": "enum LLMOracleKind" 326 | } 327 | ], 328 | "anonymous": false 329 | }, 330 | { 331 | "type": "event", 332 | "name": "Upgraded", 333 | "inputs": [ 334 | { 335 | "name": "implementation", 336 | "type": "address", 337 | "indexed": true, 338 | "internalType": "address" 339 | } 340 | ], 341 | "anonymous": false 342 | }, 343 | { 344 | "type": "error", 345 | "name": "AddressEmptyCode", 346 | "inputs": [ 347 | { "name": "target", "type": "address", "internalType": "address" } 348 | ] 349 | }, 350 | { 351 | "type": "error", 352 | "name": "AlreadyRegistered", 353 | "inputs": [{ "name": "", "type": "address", "internalType": "address" }] 354 | }, 355 | { 356 | "type": "error", 357 | "name": "ERC1967InvalidImplementation", 358 | "inputs": [ 359 | { 360 | "name": "implementation", 361 | "type": "address", 362 | "internalType": "address" 363 | } 364 | ] 365 | }, 366 | { "type": "error", "name": "ERC1967NonPayable", "inputs": [] }, 367 | { "type": "error", "name": "FailedCall", "inputs": [] }, 368 | { "type": "error", "name": "InsufficientFunds", "inputs": [] }, 369 | { "type": "error", "name": "InvalidInitialization", "inputs": [] }, 370 | { "type": "error", "name": "NotInitializing", "inputs": [] }, 371 | { 372 | "type": "error", 373 | "name": "NotRegistered", 374 | "inputs": [{ "name": "", "type": "address", "internalType": "address" }] 375 | }, 376 | { 377 | "type": "error", 378 | "name": "NotWhitelisted", 379 | "inputs": [ 380 | { "name": "validator", "type": "address", "internalType": "address" } 381 | ] 382 | }, 383 | { 384 | "type": "error", 385 | "name": "OwnableInvalidOwner", 386 | "inputs": [ 387 | { "name": "owner", "type": "address", "internalType": "address" } 388 | ] 389 | }, 390 | { 391 | "type": "error", 392 | "name": "OwnableUnauthorizedAccount", 393 | "inputs": [ 394 | { "name": "account", "type": "address", "internalType": "address" } 395 | ] 396 | }, 397 | { 398 | "type": "error", 399 | "name": "TooEarlyToUnregister", 400 | "inputs": [ 401 | { 402 | "name": "minTimeToWait", 403 | "type": "uint256", 404 | "internalType": "uint256" 405 | } 406 | ] 407 | }, 408 | { "type": "error", "name": "UUPSUnauthorizedCallContext", "inputs": [] }, 409 | { 410 | "type": "error", 411 | "name": "UUPSUnsupportedProxiableUUID", 412 | "inputs": [{ "name": "slot", "type": "bytes32", "internalType": "bytes32" }] 413 | } 414 | ] 415 | -------------------------------------------------------------------------------- /contracts/src/addresses.rs: -------------------------------------------------------------------------------- 1 | use alloy::primitives::{address, Address}; 2 | use alloy_chains::NamedChain; 3 | 4 | /// Contract addresses. 5 | /// 6 | /// All contracts can be derived from the `coordinator` contract. 7 | #[derive(Debug, Clone)] 8 | pub struct ContractAddresses { 9 | /// Token used within the registry and coordinator. 10 | pub token: Address, 11 | /// Oracle registry. 12 | pub registry: Address, 13 | /// Oracle coordinator. 14 | pub coordinator: Address, 15 | } 16 | 17 | impl std::fmt::Display for ContractAddresses { 18 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 19 | write!( 20 | f, 21 | "Contract Addresses:\n Token: {}\n Registry: {}\n Coordinator: {}", 22 | self.token, self.registry, self.coordinator 23 | ) 24 | } 25 | } 26 | 27 | /// Returns the coordinator contract address for a given chain. 28 | /// 29 | /// Will return an error if the chain is not supported, i.e. a coordinator address 30 | /// is not deployed there. 31 | pub fn get_coordinator_address(chain: NamedChain) -> eyre::Result
{ 32 | let addresses = match chain { 33 | NamedChain::AnvilHardhat => address!("9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0"), 34 | NamedChain::BaseSepolia => address!("13f977bde221b470d3ae055cde7e1f84debfe202"), 35 | NamedChain::Base => address!("17b6d1eddcd5f9ca19bb2ffed2f3deb6bd74bd20"), 36 | _ => return Err(eyre::eyre!("Chain {} is not supported", chain)), 37 | }; 38 | 39 | Ok(addresses) 40 | } 41 | -------------------------------------------------------------------------------- /contracts/src/balance.rs: -------------------------------------------------------------------------------- 1 | use alloy::primitives::{utils::format_ether, Address, U256}; 2 | use std::fmt::Display; 3 | 4 | /// A token balance contains amount, token symbol and the token address if its non-native token. 5 | #[derive(Debug)] 6 | pub struct TokenBalance { 7 | /// Amount of tokens as bigint. 8 | pub amount: U256, 9 | /// Token symbol, for display purposes. 10 | pub symbol: String, 11 | /// Token contract address, `None` if its ETH (native token). 12 | pub address: Option
, 13 | } 14 | 15 | impl TokenBalance { 16 | /// Create a new token result. 17 | pub fn new(amount: U256, symbol: impl ToString, address: Option
) -> Self { 18 | Self { 19 | amount, 20 | symbol: symbol.to_string(), 21 | address, 22 | } 23 | } 24 | } 25 | 26 | impl Display for TokenBalance { 27 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 28 | write!( 29 | f, 30 | "{} {} {}", 31 | format_ether(self.amount), 32 | self.symbol, 33 | self.address.map(|s| s.to_string()).unwrap_or_default() // empty-string if `None` 34 | ) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /contracts/src/errors.rs: -------------------------------------------------------------------------------- 1 | use alloy::contract::Error; 2 | use alloy::primitives::utils::format_ether; 3 | use eyre::{eyre, ErrReport}; 4 | 5 | use super::OracleCoordinator::OracleCoordinatorErrors; 6 | use super::OracleRegistry::OracleRegistryErrors; 7 | use super::ERC20::ERC20Errors; 8 | 9 | /// Generic contract error reporter, handles custom errors for known contracts such as ERC20, LLMOracleRegistry, and LLMOracleCoordinator. 10 | /// 11 | /// The given contract error is matched against known contract errors and a custom error message is returned 12 | pub fn contract_error_report(error: Error) -> ErrReport { 13 | match error { 14 | Error::UnknownFunction(function) => { 15 | eyre!("Unknown function: function {} does not exist", function) 16 | } 17 | Error::UnknownSelector(selector) => eyre!( 18 | "Unknown function: function with selector {} does not exist", 19 | selector 20 | ), 21 | Error::PendingTransactionError(tx) => { 22 | eyre!("Transaction is pending: {:?}", tx) 23 | } 24 | Error::NotADeploymentTransaction => { 25 | eyre!("Transaction is not a deployment transaction") 26 | } 27 | Error::ContractNotDeployed => eyre!("Contract is not deployed"), 28 | Error::AbiError(e) => eyre!("An error occurred ABI encoding or decoding: {}", e), 29 | Error::TransportError(error) => { 30 | const VALIDATE: bool = false; 31 | 32 | // here we try to parse the error w.r.t provided contract interfaces 33 | // or return a default one in the end if it was not parsed successfully 34 | if let Some(payload) = error.as_error_resp() { 35 | // an ERC20 error 36 | if let Some(erc_20_error) = payload.as_decoded_error::(VALIDATE) { 37 | erc_20_error.into() 38 | } else 39 | // an OracleRegistry error 40 | if let Some(registry_error) = 41 | payload.as_decoded_error::(VALIDATE) 42 | { 43 | registry_error.into() 44 | } else 45 | // an OracleCoordinator error 46 | if let Some(coordinator_error) = 47 | payload.as_decoded_error::(VALIDATE) 48 | { 49 | coordinator_error.into() 50 | } else { 51 | eyre!("Unhandled error response: {:#?}", payload) 52 | } 53 | } else { 54 | eyre!("Unknown transport error: {:#?}", error) 55 | } 56 | } 57 | } 58 | } 59 | 60 | impl From for ErrReport { 61 | fn from(value: ERC20Errors) -> Self { 62 | match value { 63 | ERC20Errors::ERC20InsufficientAllowance(e) => eyre!( 64 | "Insufficient allowance for {} (have {}, need {})", 65 | e.spender, 66 | format_ether(e.allowance), 67 | format_ether(e.needed) 68 | ), 69 | ERC20Errors::ERC20InsufficientBalance(e) => eyre!( 70 | "Insufficient balance for {} (have {}, need {})", 71 | e.sender, 72 | format_ether(e.balance), 73 | format_ether(e.needed) 74 | ), 75 | ERC20Errors::ERC20InvalidReceiver(e) => { 76 | eyre!("Invalid receiver: {}", e.receiver) 77 | } 78 | ERC20Errors::ERC20InvalidApprover(e) => { 79 | eyre!("Invalid approver: {}", e.approver) 80 | } 81 | ERC20Errors::ERC20InvalidSender(e) => eyre!("Invalid sender: {}", e.sender), 82 | ERC20Errors::ERC20InvalidSpender(e) => eyre!("Invalid spender: {}", e.spender), 83 | } 84 | } 85 | } 86 | 87 | impl From for ErrReport { 88 | fn from(value: OracleRegistryErrors) -> Self { 89 | match value { 90 | OracleRegistryErrors::AlreadyRegistered(e) => { 91 | eyre!("Already registered: {}", e._0) 92 | } 93 | OracleRegistryErrors::InsufficientFunds(_) => eyre!("Insufficient funds."), 94 | OracleRegistryErrors::NotRegistered(e) => eyre!("Not registered: {}", e._0), 95 | OracleRegistryErrors::OwnableInvalidOwner(e) => { 96 | eyre!("Invalid owner: {}", e.owner) 97 | } 98 | OracleRegistryErrors::OwnableUnauthorizedAccount(e) => { 99 | eyre!("Unauthorized account: {}", e.account) 100 | } // _ => eyre!("Unhandled Oracle registry error"), 101 | OracleRegistryErrors::TooEarlyToUnregister(e) => { 102 | eyre!( 103 | "Too early to unregister: {} secs remaining", 104 | e.minTimeToWait 105 | ) 106 | } 107 | OracleRegistryErrors::NotWhitelisted(e) => { 108 | eyre!("Validator {} is not whitelisted", e.validator) 109 | } 110 | // generic 111 | OracleRegistryErrors::FailedCall(_) => { 112 | eyre!("Failed call") 113 | } 114 | OracleRegistryErrors::ERC1967InvalidImplementation(e) => { 115 | eyre!("Invalid implementation: {}", e.implementation) 116 | } 117 | OracleRegistryErrors::UUPSUnauthorizedCallContext(_) => { 118 | eyre!("Unauthorized UUPS call context") 119 | } 120 | OracleRegistryErrors::UUPSUnsupportedProxiableUUID(e) => { 121 | eyre!("Unsupported UUPS proxiable UUID: {}", e.slot) 122 | } 123 | OracleRegistryErrors::ERC1967NonPayable(_) => { 124 | eyre!("ERC1967 Non-payable") 125 | } 126 | OracleRegistryErrors::InvalidInitialization(_) => { 127 | eyre!("Invalid initialization") 128 | } 129 | OracleRegistryErrors::AddressEmptyCode(e) => { 130 | eyre!("Address {} is empty", e.target) 131 | } 132 | OracleRegistryErrors::NotInitializing(_) => { 133 | eyre!("Not initializing") 134 | } 135 | } 136 | } 137 | } 138 | 139 | impl From for ErrReport { 140 | fn from(value: OracleCoordinatorErrors) -> Self { 141 | match value { 142 | OracleCoordinatorErrors::AlreadyResponded(e) => { 143 | eyre!("Already responded to task {}", e.taskId) 144 | } 145 | OracleCoordinatorErrors::InsufficientFees(e) => { 146 | eyre!("Insufficient fees (have: {}, want: {})", e.have, e.want) 147 | } 148 | OracleCoordinatorErrors::InvalidParameterRange(e) => { 149 | eyre!( 150 | "Invalid parameter range: {} <= {}* <= {}", 151 | e.min, 152 | e.have, 153 | e.max 154 | ) 155 | } 156 | OracleCoordinatorErrors::InvalidNonce(e) => { 157 | eyre!("Invalid nonce for task: {} (nonce: {})", e.taskId, e.nonce) 158 | } 159 | OracleCoordinatorErrors::InvalidTaskStatus(e) => eyre!( 160 | "Invalid status for task: {} (have: {}, want: {})", 161 | e.taskId, 162 | e.have, 163 | e.want 164 | ), 165 | OracleCoordinatorErrors::InvalidValidation(e) => { 166 | eyre!("Invalid validation for task: {}", e.taskId) 167 | } 168 | OracleCoordinatorErrors::NotRegistered(e) => { 169 | eyre!("Not registered: {}", e.oracle) 170 | } 171 | OracleCoordinatorErrors::OwnableInvalidOwner(e) => { 172 | eyre!("Invalid owner: {}", e.owner) 173 | } 174 | OracleCoordinatorErrors::OwnableUnauthorizedAccount(e) => { 175 | eyre!("Unauthorized account: {}", e.account) 176 | } 177 | // generic 178 | OracleCoordinatorErrors::FailedInnerCall(_) => { 179 | eyre!("Failed inner call") 180 | } 181 | OracleCoordinatorErrors::ERC1967InvalidImplementation(e) => { 182 | eyre!("Invalid implementation: {}", e.implementation) 183 | } 184 | OracleCoordinatorErrors::UUPSUnauthorizedCallContext(_) => { 185 | eyre!("Unauthorized UUPS call context") 186 | } 187 | OracleCoordinatorErrors::UUPSUnsupportedProxiableUUID(e) => { 188 | eyre!("Unsupported UUPS proxiable UUID: {}", e.slot) 189 | } 190 | OracleCoordinatorErrors::ERC1967NonPayable(_) => { 191 | eyre!("ERC1967 Non-payable") 192 | } 193 | OracleCoordinatorErrors::InvalidInitialization(_) => { 194 | eyre!("Invalid initialization") 195 | } 196 | OracleCoordinatorErrors::AddressEmptyCode(e) => { 197 | eyre!("Address {} is empty", e.target) 198 | } 199 | OracleCoordinatorErrors::NotInitializing(_) => { 200 | eyre!("Not initializing") 201 | } 202 | } 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /contracts/src/interfaces.rs: -------------------------------------------------------------------------------- 1 | use alloy::{ 2 | primitives::{Bytes, FixedBytes}, 3 | sol, 4 | }; 5 | use eyre::{Context, Result}; 6 | 7 | use self::OracleCoordinator::StatusUpdate; 8 | 9 | // OpenZepeplin ERC20 10 | sol!( 11 | #[allow(missing_docs)] 12 | #[sol(rpc)] 13 | ERC20, 14 | "./abi/ERC20.json" 15 | ); 16 | 17 | // Base WETH 18 | sol!( 19 | #[allow(missing_docs)] 20 | #[sol(rpc)] 21 | WETH, 22 | "./abi/IWETH9.json" 23 | ); 24 | 25 | sol!( 26 | #[allow(missing_docs)] 27 | #[sol(rpc)] 28 | OracleRegistry, 29 | "./abi/LLMOracleRegistry.json" 30 | ); 31 | 32 | sol!( 33 | #[allow(missing_docs)] 34 | #[sol(rpc)] 35 | OracleCoordinator, 36 | "./abi/LLMOracleCoordinator.json" 37 | ); 38 | 39 | /// `OracleKind` as it appears within the registry. 40 | #[derive(Debug, Clone, Copy, PartialEq)] 41 | pub enum OracleKind { 42 | Generator, 43 | Validator, 44 | } 45 | 46 | impl From for u8 { 47 | fn from(kind: OracleKind) -> u8 { 48 | kind as u8 49 | } 50 | } 51 | 52 | impl TryFrom for OracleKind { 53 | type Error = eyre::Error; 54 | 55 | fn try_from(value: u8) -> Result { 56 | match value { 57 | 0 => Ok(OracleKind::Generator), 58 | 1 => Ok(OracleKind::Validator), 59 | _ => Err(eyre::eyre!("Invalid OracleKind: {}", value)), 60 | } 61 | } 62 | } 63 | 64 | impl std::fmt::Display for OracleKind { 65 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 66 | match self { 67 | OracleKind::Generator => write!(f, "Generator"), 68 | OracleKind::Validator => write!(f, "Validator"), 69 | } 70 | } 71 | } 72 | 73 | /// `TaskStatus` as it appears within the coordinator. 74 | #[derive(Debug, Clone, Copy, Default)] 75 | pub enum TaskStatus { 76 | #[default] 77 | None, 78 | PendingGeneration, 79 | PendingValidation, 80 | Completed, 81 | } 82 | 83 | impl From for u8 { 84 | fn from(status: TaskStatus) -> u8 { 85 | status as u8 86 | } 87 | } 88 | 89 | impl TryFrom for TaskStatus { 90 | type Error = eyre::Error; 91 | 92 | fn try_from(value: u8) -> Result { 93 | match value { 94 | 0 => Ok(TaskStatus::None), 95 | 1 => Ok(TaskStatus::PendingGeneration), 96 | 2 => Ok(TaskStatus::PendingValidation), 97 | 3 => Ok(TaskStatus::Completed), 98 | _ => Err(eyre::eyre!("Invalid TaskStatus: {}", value)), 99 | } 100 | } 101 | } 102 | 103 | impl std::fmt::Display for TaskStatus { 104 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 105 | match self { 106 | Self::None => write!(f, "None"), 107 | Self::PendingGeneration => write!(f, "Pending Generation"), 108 | Self::PendingValidation => write!(f, "Pending Validation"), 109 | Self::Completed => write!(f, "Completed"), 110 | } 111 | } 112 | } 113 | 114 | impl std::fmt::Display for StatusUpdate { 115 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 116 | write!( 117 | f, 118 | "Task {}: {} -> {}", 119 | self.taskId, self.statusBefore, self.statusAfter 120 | ) 121 | } 122 | } 123 | 124 | /// Small utility to convert bytes to string. 125 | #[inline(always)] 126 | pub fn bytes_to_string(bytes: &Bytes) -> Result { 127 | String::from_utf8(bytes.to_vec()).wrap_err("could not convert bytes to string") 128 | } 129 | 130 | /// Small utility to convert string to bytes. 131 | #[inline(always)] 132 | pub fn string_to_bytes(input: String) -> Bytes { 133 | input.into() 134 | } 135 | 136 | /// Small utility to convert bytes32 to string. 137 | /// Simply reads bytes until the first null byte. 138 | pub fn bytes32_to_string(bytes: &FixedBytes<32>) -> Result { 139 | let mut bytes = bytes.to_vec(); 140 | if let Some(pos) = bytes.iter().position(|&b| b == 0) { 141 | bytes.truncate(pos); 142 | } else { 143 | return Err(eyre::eyre!("Could not find null terminator in bytes32")); 144 | } 145 | String::from_utf8(bytes).wrap_err("could not convert bytes to string") 146 | } 147 | 148 | /// Small utility to convert string to bytes32. 149 | /// Ensures that the string is at most 31 bytes long, and right-bads zeros for the null-terminator. 150 | pub fn string_to_bytes32(input: String) -> Result> { 151 | let mut bytes = input.into_bytes(); 152 | if bytes.len() > 31 { 153 | return Err(eyre::eyre!("String is too long for bytes32")); 154 | } 155 | 156 | // append bytes via resizing 157 | bytes.resize(32, 0); 158 | Ok(FixedBytes::from_slice(&bytes)) 159 | } 160 | 161 | #[cfg(test)] 162 | mod tests { 163 | use super::*; 164 | 165 | #[test] 166 | fn test_string_to_bytes() { 167 | let input = "hello".to_string(); 168 | let bytes = string_to_bytes(input.clone()); 169 | let string = bytes_to_string(&bytes).unwrap(); 170 | assert_eq!(input, string); 171 | } 172 | 173 | #[test] 174 | fn test_string_to_bytes32() { 175 | let input = "hello".to_string(); 176 | let bytes32 = string_to_bytes32(input.clone()).unwrap(); 177 | let string = bytes32_to_string(&bytes32).unwrap(); 178 | assert_eq!(input, string); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /contracts/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod balance; 2 | pub use balance::*; 3 | 4 | mod addresses; 5 | pub use addresses::*; 6 | 7 | mod interfaces; 8 | pub use interfaces::*; 9 | 10 | mod errors; 11 | pub use errors::*; 12 | -------------------------------------------------------------------------------- /core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dria-oracle" 3 | description = "Dria Knowledge Network Oracle Node" 4 | version.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | authors.workspace = true 8 | 9 | [features] 10 | anvil = ["alloy/node-bindings"] 11 | 12 | [dependencies] 13 | # core 14 | alloy.workspace = true 15 | alloy-chains.workspace = true 16 | tokio.workspace = true 17 | tokio-util.workspace = true 18 | 19 | # workflows 20 | dkn-workflows = { git = "https://github.com/firstbatchxyz/dkn-compute-node" } 21 | 22 | # errors & logging & env 23 | env_logger = "0.11.5" 24 | eyre = "0.6.12" 25 | log = "0.4.22" 26 | dotenvy = "0.15.7" 27 | 28 | # utils 29 | futures-util = "0.3.30" 30 | bytes = "1.7.1" 31 | rand = "0.8.5" 32 | reqwest.workspace = true 33 | 34 | # b64, hex, serde 35 | base64 = "0.22.1" 36 | hex = "0.4.3" 37 | hex-literal = "0.4.1" 38 | serde.workspace = true 39 | serde_json.workspace = true 40 | 41 | # cli 42 | clap = { version = "4.5.13", features = ["derive", "env"] } 43 | 44 | dria-oracle-storage = { path = "../storage" } 45 | dria-oracle-contracts = { path = "../contracts" } 46 | -------------------------------------------------------------------------------- /core/src/cli/commands/coordinator/mod.rs: -------------------------------------------------------------------------------- 1 | use alloy::eips::BlockNumberOrTag; 2 | use eyre::{Context, Result}; 3 | use futures_util::StreamExt; 4 | use std::time::Duration; 5 | use tokio_util::sync::CancellationToken; 6 | 7 | use crate::DriaOracle; 8 | 9 | mod request; 10 | mod serve; 11 | mod view; 12 | 13 | impl DriaOracle { 14 | /// Starts the oracle node. 15 | pub(in crate::cli) async fn serve( 16 | &self, 17 | from_block: Option, 18 | to_block: Option, 19 | cancellation: CancellationToken, 20 | ) -> Result<()> { 21 | log::info!( 22 | "Started oracle as {} using models: {}", 23 | self.kinds 24 | .iter() 25 | .map(|k| k.to_string()) 26 | .collect::>() 27 | .join(", "), 28 | self.workflows 29 | .models 30 | .iter() 31 | .map(|(_, m)| m.to_string()) 32 | .collect::>() 33 | .join(", ") 34 | ); 35 | 36 | // check previous tasks if `from_block` is given 37 | if let Some(from_block) = from_block { 38 | tokio::select! { 39 | _ = cancellation.cancelled() => { 40 | log::debug!("Cancellation signal received. Stopping..."); 41 | return Ok(()); 42 | } 43 | result = self.process_tasks_within_range(from_block, to_block.unwrap_or(BlockNumberOrTag::Latest)) => { 44 | if let Err(e) = result { 45 | log::error!("Could not handle previous tasks: {:?}", e); 46 | log::warn!("Continuing anyways..."); 47 | } 48 | } 49 | } 50 | } 51 | 52 | if to_block.is_some() { 53 | // if there was a `to_block` specified, we are done at this point 54 | return Ok(()); 55 | } 56 | 57 | // otherwise, we can continue with the event loop 58 | loop { 59 | // subscribe to new tasks 60 | log::info!("Subscribing to task events"); 61 | let mut event_stream = self 62 | .coordinator 63 | .StatusUpdate_filter() 64 | .watch() 65 | .await 66 | .wrap_err("could not subscribe to tasks")? 67 | .into_stream(); 68 | 69 | // start the event loop 70 | log::info!("Listening for events..."); 71 | loop { 72 | tokio::select! { 73 | _ = cancellation.cancelled() => { 74 | log::debug!("Cancellation signal received. Stopping..."); 75 | return Ok(()); 76 | } 77 | next = event_stream.next() => { 78 | match next { 79 | Some(Ok((event, log))) => { 80 | log::debug!( 81 | "Handling task {} (tx: {})", 82 | event.taskId, 83 | log.transaction_hash.unwrap_or_default() 84 | ); 85 | self.process_task_by_event(event).await 86 | } 87 | Some(Err(e)) => log::error!("Could not handle event: {}", e), 88 | None => { 89 | log::warn!("Stream ended, waiting a bit before restarting."); 90 | tokio::time::sleep(Duration::from_secs(5)).await; 91 | break 92 | }, 93 | } 94 | } 95 | } 96 | } 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /core/src/cli/commands/coordinator/request.rs: -------------------------------------------------------------------------------- 1 | use alloy::primitives::utils::format_ether; 2 | use dkn_workflows::Model; 3 | use dria_oracle_contracts::string_to_bytes; 4 | use eyre::Result; 5 | 6 | impl crate::DriaOracle { 7 | /// Requests a task with the given parameters. 8 | /// 9 | /// Oracle does not usually do this, but we still provide the capability for testing & playing around. 10 | pub async fn request_task( 11 | &self, 12 | input: &str, 13 | models: Vec, 14 | difficulty: u8, 15 | num_gens: u64, 16 | num_vals: u64, 17 | protocol: String, 18 | ) -> Result<()> { 19 | log::info!("Requesting a new task."); 20 | let input = string_to_bytes(input.to_string()); 21 | let models_str = models 22 | .iter() 23 | .map(|m| m.to_string()) 24 | .collect::>() 25 | .join(","); 26 | let models = string_to_bytes(models_str); 27 | 28 | // get total fee for the request 29 | log::debug!("Checking fee & allowance."); 30 | let total_fee = self 31 | .get_request_fee(difficulty, num_gens, num_vals) 32 | .await? 33 | .totalFee; 34 | // check balance 35 | let balance = self.get_token_balance(self.address()).await?.amount; 36 | if balance < total_fee { 37 | return Err(eyre::eyre!( 38 | "Insufficient balance. Please fund your wallet." 39 | )); 40 | } 41 | 42 | // check current allowance 43 | let allowance = self 44 | .allowance(self.address(), *self.coordinator.address()) 45 | .await? 46 | .amount; 47 | // make sure we have enough allowance 48 | if allowance < total_fee { 49 | let approval_amount = total_fee - allowance; 50 | log::info!( 51 | "Insufficient allowance. Approving the required amount: {}.", 52 | format_ether(approval_amount) 53 | ); 54 | 55 | self.approve(*self.coordinator.address(), approval_amount) 56 | .await?; 57 | log::info!("Token approval successful."); 58 | } 59 | 60 | // make the request 61 | let receipt = self 62 | .request(input, models, difficulty, num_gens, num_vals, protocol) 63 | .await?; 64 | log::info!( 65 | "Task requested successfully. tx: {}", 66 | receipt.transaction_hash 67 | ); 68 | 69 | Ok(()) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /core/src/cli/commands/coordinator/serve.rs: -------------------------------------------------------------------------------- 1 | use crate::{compute::handle_request, DriaOracle}; 2 | use alloy::{eips::BlockNumberOrTag, primitives::U256}; 3 | use dria_oracle_contracts::{ 4 | bytes32_to_string, bytes_to_string, OracleCoordinator::StatusUpdate, TaskStatus, 5 | }; 6 | use eyre::Result; 7 | 8 | impl DriaOracle { 9 | pub(in crate::cli) async fn process_task_by_id(&self, task_id: U256) -> Result<()> { 10 | log::info!("Processing task {}.", task_id); 11 | let request = self.coordinator.requests(task_id).call().await?; 12 | 13 | log::info!( 14 | "Request Information:\nRequester: {}\nStatus: {}\nInput: {}\nModels: {}\nProtocol: {}", 15 | request.requester, 16 | TaskStatus::try_from(request.status)?, 17 | bytes_to_string(&request.input)?, 18 | bytes_to_string(&request.models)?, 19 | bytes32_to_string(&request.protocol)? 20 | ); 21 | 22 | let status = TaskStatus::try_from(request.status)?; 23 | match handle_request(self, status, task_id, request.protocol).await { 24 | Ok(Some(_receipt)) => {} 25 | Ok(None) => { 26 | log::info!("Task {} ignored.", task_id) 27 | } 28 | // using `{:#}` here to get a single-line error message 29 | Err(e) => log::error!("Could not process task {}: {:#}", task_id, e), 30 | } 31 | 32 | Ok(()) 33 | } 34 | 35 | pub(in crate::cli) async fn process_task_by_event(&self, event: StatusUpdate) { 36 | let Ok(status) = TaskStatus::try_from(event.statusAfter) else { 37 | log::error!("Could not parse task status: {}", event.statusAfter); 38 | return; 39 | }; 40 | 41 | if let Err(err) = handle_request(self, status, event.taskId, event.protocol).await { 42 | log::error!("Could not process task {}: {:?}", event.taskId, err); 43 | } 44 | } 45 | 46 | pub(in crate::cli) async fn process_tasks_within_range( 47 | &self, 48 | from_block: BlockNumberOrTag, 49 | to_block: BlockNumberOrTag, 50 | ) -> Result<()> { 51 | log::info!( 52 | "Processing tasks between blocks: {} - {}", 53 | from_block 54 | .as_number() 55 | .map(|n| n.to_string()) 56 | .unwrap_or(from_block.to_string()), 57 | to_block 58 | .as_number() 59 | .map(|n| n.to_string()) 60 | .unwrap_or(to_block.to_string()) 61 | ); 62 | 63 | for (event, _) in self.get_tasks_in_range(from_block, to_block).await? { 64 | self.process_task_by_event(event).await; 65 | } 66 | 67 | Ok(()) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /core/src/cli/commands/coordinator/view.rs: -------------------------------------------------------------------------------- 1 | use alloy::{eips::BlockNumberOrTag, primitives::U256}; 2 | use dria_oracle_contracts::{bytes32_to_string, bytes_to_string, TaskStatus}; 3 | use eyre::Result; 4 | 5 | impl crate::DriaOracle { 6 | /// Views the task events between two blocks, logs everything on screen. 7 | pub(in crate::cli) async fn view_task_events( 8 | &self, 9 | from_block: BlockNumberOrTag, 10 | to_block: BlockNumberOrTag, 11 | ) -> Result<()> { 12 | log::info!( 13 | "Viewing task ids & statuses between blocks: {} - {}", 14 | from_block 15 | .as_number() 16 | .map(|n| n.to_string()) 17 | .unwrap_or(from_block.to_string()), 18 | to_block 19 | .as_number() 20 | .map(|n| n.to_string()) 21 | .unwrap_or(to_block.to_string()) 22 | ); 23 | 24 | let task_events = self.get_tasks_in_range(from_block, to_block).await?; 25 | for (event, log) in task_events { 26 | log::info!( 27 | "Task {} changed from {} to {} at block {}, tx: {}", 28 | event.taskId, 29 | TaskStatus::try_from(event.statusBefore).unwrap_or_default(), 30 | TaskStatus::try_from(event.statusAfter).unwrap_or_default(), 31 | log.block_number.unwrap_or_default(), 32 | log.transaction_hash.unwrap_or_default() 33 | ); 34 | } 35 | 36 | Ok(()) 37 | } 38 | 39 | /// Views the request, responses and validations of a single task, logs everything on screen. 40 | pub(in crate::cli) async fn view_task(&self, task_id: U256) -> Result<()> { 41 | log::info!("Viewing task {}.", task_id); 42 | let (request, responses, validations) = self.get_task(task_id).await?; 43 | 44 | log::info!( 45 | "Request Information:\nRequester: {}\nStatus: {}\nInput: {}\nModels: {}\nProtocol: {}", 46 | request.requester, 47 | TaskStatus::try_from(request.status)?, 48 | bytes_to_string(&request.input)?, 49 | bytes_to_string(&request.models)?, 50 | bytes32_to_string(&request.protocol)? 51 | ); 52 | 53 | log::info!("Responses:"); 54 | if responses._0.is_empty() { 55 | log::info!("There are no responses yet."); 56 | } else { 57 | for (idx, response) in responses._0.iter().enumerate() { 58 | log::info!( 59 | "Response #{}\nOutput: {}\nMetadata: {}\nGenerator: {}", 60 | idx, 61 | bytes_to_string(&response.output)?, 62 | bytes_to_string(&response.metadata)?, 63 | response.responder 64 | ); 65 | } 66 | } 67 | 68 | log::info!("Validations:"); 69 | if validations._0.is_empty() { 70 | log::info!("There are no validations yet."); 71 | } else { 72 | for (idx, validation) in validations._0.iter().enumerate() { 73 | log::info!( 74 | "Validation #{}\nScores: {:?}\nMetadata: {}\nValidator: {}", 75 | idx, 76 | validation.scores, 77 | bytes_to_string(&validation.metadata)?, 78 | validation.validator 79 | ); 80 | } 81 | } 82 | 83 | Ok(()) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /core/src/cli/commands/mod.rs: -------------------------------------------------------------------------------- 1 | use alloy::{eips::BlockNumberOrTag, primitives::U256}; 2 | use clap::Subcommand; 3 | use dkn_workflows::Model; 4 | use dria_oracle_contracts::OracleKind; 5 | 6 | use super::parsers::*; 7 | 8 | mod coordinator; 9 | mod registry; 10 | mod token; 11 | 12 | // https://docs.rs/clap/latest/clap/_derive/index.html#arg-attributes 13 | #[derive(Subcommand)] 14 | pub enum Commands { 15 | /// Register oracle as a specific oracle kind. 16 | Register { 17 | #[arg(help = "The oracle kinds to register as.", required = true, value_parser = parse_oracle_kind)] 18 | kinds: Vec, 19 | }, 20 | /// Unregister oracle as a specific oracle kind. 21 | Unregister { 22 | #[arg(help = "The oracle kinds to unregister as.", required = true, value_parser = parse_oracle_kind)] 23 | kinds: Vec, 24 | }, 25 | /// See all registrations. 26 | Registrations, 27 | /// See the current balance of the oracle node. 28 | Balance, 29 | /// See claimable rewards from the coordinator. 30 | Rewards, 31 | /// Claim rewards from the coordinator. 32 | Claim, 33 | /// Serve the oracle node. 34 | Serve { 35 | #[arg(help = "The oracle kinds to handle tasks as, if omitted will default to all registered kinds.", value_parser = parse_oracle_kind)] 36 | kinds: Vec, 37 | #[arg(short, long = "model", help = "The model(s) to serve.", required = true, value_parser = parse_model)] 38 | models: Vec, 39 | #[arg( 40 | long, 41 | help = "Block number to starting listening from, omit to start from latest block.", 42 | value_parser = parse_block_number_or_tag 43 | )] 44 | from: Option, 45 | #[arg( 46 | long, 47 | help = "Block number to stop listening at, omit to keep running the node indefinitely.", 48 | value_parser = parse_block_number_or_tag 49 | )] 50 | to: Option, 51 | #[arg( 52 | short, 53 | long, 54 | help = "Optional task id to serve specifically.", 55 | required = false 56 | )] 57 | task_id: Option, 58 | }, 59 | /// View tasks. 60 | View { 61 | #[arg(long, help = "Starting block number, defaults to 'earliest'.", value_parser = parse_block_number_or_tag)] 62 | from: Option, 63 | #[arg(long, help = "Ending block number, defaults to 'latest'.", value_parser = parse_block_number_or_tag)] 64 | to: Option, 65 | #[arg(short, long, help = "Task id to view.")] 66 | task_id: Option, 67 | }, 68 | /// Request a task. 69 | Request { 70 | #[arg(help = "The input to request a task with.", required = true)] 71 | input: String, 72 | #[arg(help = "The model(s) to accept.", required = true, value_parser=parse_model)] 73 | models: Vec, 74 | #[arg(long, help = "The difficulty of the task.", default_value_t = 2)] 75 | difficulty: u8, 76 | #[arg(long, help = "Protocol name for the request", default_value = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")))] 77 | protocol: String, 78 | #[arg( 79 | long, 80 | help = "The number of generations to request.", 81 | default_value_t = 1 82 | )] 83 | num_gens: u64, 84 | #[arg( 85 | long, 86 | help = "The number of validations to request.", 87 | default_value_t = 1 88 | )] 89 | num_vals: u64, 90 | }, 91 | } 92 | -------------------------------------------------------------------------------- /core/src/cli/commands/registry.rs: -------------------------------------------------------------------------------- 1 | use alloy::primitives::utils::format_ether; 2 | use dria_oracle_contracts::OracleKind; 3 | use eyre::Result; 4 | 5 | use crate::DriaOracle; 6 | 7 | impl DriaOracle { 8 | /// Registers the oracle node as an oracle for the given `kind`. 9 | /// 10 | /// - If the node is already registered, it will do nothing. 11 | /// - If the node is not registered, it will approve the required amount of tokens 12 | /// to the registry and then register the node. 13 | pub async fn register(&self, kind: OracleKind) -> Result<()> { 14 | log::info!("Registering as a {}.", kind); 15 | 16 | // check if registered already 17 | if self.is_registered(kind).await? { 18 | log::warn!("You are already registered as a {}.", kind); 19 | return Ok(()); 20 | } 21 | 22 | // calculate the required approval for registration 23 | let stake = self.get_registry_stake_amount(kind).await?; 24 | let allowance = self 25 | .allowance(self.address(), *self.registry.address()) 26 | .await?; 27 | 28 | // approve if necessary 29 | if allowance.amount < stake.amount { 30 | let difference = stake.amount - allowance.amount; 31 | log::info!( 32 | "Approving {} tokens for {} registration.", 33 | format_ether(difference), 34 | kind 35 | ); 36 | 37 | // check balance 38 | let balance = self.get_token_balance(self.address()).await?; 39 | if balance.amount < difference { 40 | return Err(eyre::eyre!( 41 | "Not enough balance to approve. (have: {}, required: {})", 42 | balance, 43 | difference 44 | )); 45 | } 46 | 47 | // approve the difference 48 | self.approve(*self.registry.address(), difference).await?; 49 | } else { 50 | log::info!("Already approved enough tokens."); 51 | } 52 | 53 | // register 54 | log::info!("Registering."); 55 | self.register_kind(kind).await?; 56 | 57 | Ok(()) 58 | } 59 | 60 | /// Unregisters the oracle node as an oracle for the given `kind`. 61 | /// 62 | /// - If the node is not registered, it will do nothing. 63 | /// - If the node is registered, it will unregister the node and transfer all allowance 64 | /// from the registry back to the oracle. 65 | pub async fn unregister(&self, kind: OracleKind) -> Result<()> { 66 | log::info!("Unregistering as {}.", kind); 67 | 68 | // check if not registered anyways 69 | if !self.is_registered(kind).await? { 70 | log::warn!("You are already not registered as a {}.", kind); 71 | return Ok(()); 72 | } 73 | 74 | self.unregister_kind(kind).await?; 75 | 76 | // transfer all allowance from registry back to oracle 77 | // to get back the registrations fee 78 | let allowance = self 79 | .allowance(*self.registry.address(), self.address()) 80 | .await?; 81 | log::info!( 82 | "Transferring all allowance ({}) back from registry.", 83 | allowance 84 | ); 85 | self.transfer_from(*self.registry.address(), self.address(), allowance.amount) 86 | .await?; 87 | 88 | Ok(()) 89 | } 90 | 91 | /// Displays the registration status of the oracle node for all oracle kinds. 92 | pub(in crate::cli) async fn display_registrations(&self) -> Result<()> { 93 | for kind in [OracleKind::Generator, OracleKind::Validator] { 94 | let is_registered = self.is_registered(kind).await?; 95 | log::info!("{}: {}", kind, is_registered); 96 | } 97 | 98 | Ok(()) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /core/src/cli/commands/token.rs: -------------------------------------------------------------------------------- 1 | use crate::DriaOracle; 2 | use eyre::Result; 3 | 4 | impl DriaOracle { 5 | /// Display token balances. 6 | pub(in crate::cli) async fn display_balance(&self) -> Result<()> { 7 | let eth_balance = self.get_native_balance(self.address()).await?; 8 | let token_balance = self.get_token_balance(self.address()).await?; 9 | 10 | log::info!( 11 | "Your balances:\n{}", 12 | [eth_balance, token_balance] 13 | .map(|b| b.to_string()) 14 | .join("\n") 15 | ); 16 | 17 | Ok(()) 18 | } 19 | 20 | /// Show the amount of claimable rewards. 21 | pub(in crate::cli) async fn display_rewards(&self) -> Result<()> { 22 | let allowance = self 23 | .allowance(*self.coordinator.address(), self.address()) 24 | .await?; 25 | 26 | log::info!("Claimable rewards:"); 27 | log::info!("{} ", allowance); 28 | if allowance.amount.is_zero() { 29 | log::warn!("You have no claimable rewards!"); 30 | } 31 | 32 | Ok(()) 33 | } 34 | 35 | /// Claim rewards 36 | pub(in crate::cli) async fn claim_rewards(&self) -> Result<()> { 37 | // get allowance 38 | let allowance = self 39 | .allowance(*self.coordinator.address(), self.address()) 40 | .await?; 41 | 42 | // check if there are rewards to claim 43 | if allowance.amount.is_zero() { 44 | log::warn!("No rewards to claim."); 45 | } else { 46 | // transfer rewards 47 | self.transfer_from( 48 | *self.coordinator.address(), 49 | self.address(), 50 | allowance.amount, 51 | ) 52 | .await?; 53 | log::info!("Rewards claimed: {}.", allowance); 54 | } 55 | 56 | Ok(()) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /core/src/cli/mod.rs: -------------------------------------------------------------------------------- 1 | // use alloy::eips::BlockNumberOrTag; 2 | use alloy::eips::BlockNumberOrTag; 3 | use eyre::Result; 4 | use std::{env, path::PathBuf}; 5 | use tokio_util::sync::CancellationToken; 6 | 7 | mod commands; 8 | use commands::Commands; 9 | 10 | mod parsers; 11 | use parsers::*; 12 | 13 | const DEFAULT_TX_TIMEOUT_SECS: u64 = 160; 14 | 15 | #[derive(clap::Parser)] 16 | #[command(version, about, long_about = None)] 17 | #[command(propagate_version = true)] 18 | pub struct Cli { 19 | #[command(subcommand)] 20 | pub command: Commands, 21 | 22 | /// Path to the .env file 23 | #[arg(short, long, default_value = "./.env")] 24 | pub env: PathBuf, 25 | 26 | /// Enable debug-level logs 27 | #[arg(short, long)] 28 | pub debug: bool, 29 | } 30 | 31 | impl Cli { 32 | pub fn read_secret_key() -> Result { 33 | let key = env::var("SECRET_KEY")?; 34 | parse_secret_key(&key) 35 | } 36 | 37 | pub fn read_rpc_url() -> Result { 38 | let url = env::var("RPC_URL")?; 39 | parse_url(&url) 40 | } 41 | 42 | pub fn read_tx_timeout() -> Result { 43 | let timeout = env::var("TX_TIMEOUT_SECS").unwrap_or(DEFAULT_TX_TIMEOUT_SECS.to_string()); 44 | timeout.parse().map_err(Into::into) 45 | } 46 | } 47 | 48 | /// Handles a given CLI command, using the provided node. 49 | pub async fn handle_command(command: Commands, mut node: crate::DriaOracle) -> Result<()> { 50 | match command { 51 | Commands::Balance => node.display_balance().await?, 52 | Commands::Claim => node.claim_rewards().await?, 53 | Commands::Rewards => node.display_rewards().await?, 54 | Commands::Serve { 55 | task_id, 56 | kinds, 57 | models, 58 | from, 59 | to, 60 | } => { 61 | let token = CancellationToken::new(); 62 | node.prepare_oracle(kinds, models).await?; 63 | 64 | if let Some(task_id) = task_id { 65 | node.process_task_by_id(task_id).await? 66 | } else { 67 | // create a signal handler 68 | let termination_token = token.clone(); 69 | let termination_handle = tokio::spawn(async move { 70 | wait_for_termination(termination_token).await.unwrap(); 71 | }); 72 | 73 | // launch node 74 | node.serve(from, to, token).await?; 75 | 76 | // wait for handle 77 | if let Err(e) = termination_handle.await { 78 | log::error!("Error in termination handler: {}", e); 79 | } 80 | } 81 | } 82 | Commands::View { task_id, from, to } => { 83 | if let Some(task_id) = task_id { 84 | node.view_task(task_id).await? 85 | } else { 86 | node.view_task_events( 87 | from.unwrap_or(BlockNumberOrTag::Earliest), 88 | to.unwrap_or(BlockNumberOrTag::Latest), 89 | ) 90 | .await? 91 | } 92 | } 93 | Commands::Register { kinds } => { 94 | for kind in kinds { 95 | node.register(kind).await? 96 | } 97 | } 98 | Commands::Unregister { kinds } => { 99 | for kind in kinds { 100 | node.unregister(kind).await?; 101 | } 102 | } 103 | Commands::Registrations => node.display_registrations().await?, 104 | Commands::Request { 105 | input, 106 | models, 107 | difficulty, 108 | num_gens, 109 | num_vals, 110 | protocol, 111 | } => { 112 | node.request_task(&input, models, difficulty, num_gens, num_vals, protocol) 113 | .await? 114 | } 115 | }; 116 | 117 | Ok(()) 118 | } 119 | 120 | /// Waits for various termination signals, and cancels the given token when the signal is received. 121 | async fn wait_for_termination(cancellation: CancellationToken) -> Result<()> { 122 | #[cfg(unix)] 123 | { 124 | use tokio::signal::unix::{signal, SignalKind}; 125 | let mut sigterm = signal(SignalKind::terminate())?; 126 | let mut sigint = signal(SignalKind::interrupt())?; 127 | tokio::select! { 128 | _ = sigterm.recv() => log::warn!("Recieved SIGTERM"), 129 | _ = sigint.recv() => log::warn!("Recieved SIGINT"), 130 | _ = cancellation.cancelled() => { 131 | // no need to wait if cancelled anyways 132 | // although this is not likely to happen 133 | return Ok(()); 134 | } 135 | }; 136 | 137 | cancellation.cancel(); 138 | } 139 | 140 | #[cfg(not(unix))] 141 | { 142 | log::error!("No signal handling for this platform: {}", env::consts::OS); 143 | cancellation.cancel(); 144 | } 145 | 146 | log::info!("Terminating the application..."); 147 | 148 | Ok(()) 149 | } 150 | -------------------------------------------------------------------------------- /core/src/cli/parsers.rs: -------------------------------------------------------------------------------- 1 | use alloy::{eips::BlockNumberOrTag, hex::FromHex, primitives::B256}; 2 | use dkn_workflows::Model; 3 | use dria_oracle_contracts::OracleKind; 4 | use eyre::{eyre, Result}; 5 | use reqwest::Url; 6 | use std::str::FromStr; 7 | 8 | /// `value_parser` to parse a `str` to `OracleKind`. 9 | #[inline] 10 | pub fn parse_model(value: &str) -> Result { 11 | Model::try_from(value.to_string()).map_err(|e| eyre!(e)) 12 | } 13 | 14 | /// `value_parser` to parse a `str` to `Url`. 15 | #[inline] 16 | pub fn parse_url(value: &str) -> Result { 17 | Url::parse(value).map_err(Into::into) 18 | } 19 | 20 | /// `value_parser` to parse a hexadecimal `str` to 256-bit type `B256`. 21 | #[inline] 22 | pub fn parse_secret_key(value: &str) -> Result { 23 | B256::from_hex(value).map_err(Into::into) 24 | } 25 | 26 | /// `value_parser` to parse a `str` to `OracleKind`. 27 | /// 28 | /// This could be done with `ValueEnum` as well, but we prefer this to not 29 | /// require `clap` in another crate for just that reason. 30 | #[inline] 31 | pub fn parse_oracle_kind(value: &str) -> Result { 32 | match value { 33 | "generator" => Ok(OracleKind::Generator), 34 | "validator" => Ok(OracleKind::Validator), 35 | _ => Err(eyre!("Invalid OracleKind: {}", value)), 36 | } 37 | } 38 | 39 | /// `value parser` to parse a `str` to `BlockNumberOrTag` 40 | /// where if it can be parsed as `u64`, we call `BlockNumberOrTag::from_u64` 41 | /// otherwise we call `BlockNumberOrTag::from_str`. 42 | #[inline] 43 | pub fn parse_block_number_or_tag(value: &str) -> Result { 44 | match value.parse::() { 45 | // parse block no from its decimal representation 46 | Ok(block_number) => Ok(BlockNumberOrTag::from(block_number)), 47 | // parse block no from hex, or parse its tag 48 | Err(_) => BlockNumberOrTag::from_str(value).map_err(Into::into), 49 | } 50 | } 51 | #[cfg(test)] 52 | mod tests { 53 | use super::*; 54 | 55 | #[test] 56 | fn test_parse_model() { 57 | let model_str = "llama3.1:latest"; 58 | let result = parse_model(model_str); 59 | assert!(result.is_ok()); 60 | let model = result.unwrap(); 61 | assert_eq!(model, Model::try_from(model_str.to_string()).unwrap()); 62 | } 63 | 64 | #[test] 65 | fn test_parse_url() { 66 | let url_str = "https://example.com"; 67 | let result = parse_url(url_str); 68 | assert!(result.is_ok()); 69 | let url = result.unwrap(); 70 | assert_eq!(url, Url::parse(url_str).unwrap()); 71 | } 72 | 73 | #[test] 74 | fn test_parse_secret_key() { 75 | let hex_str = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; 76 | let result = parse_secret_key(hex_str); 77 | assert!(result.is_ok()); 78 | 79 | let secret_key = result.unwrap(); 80 | assert_eq!(secret_key, B256::from_hex(hex_str).unwrap()); 81 | } 82 | 83 | #[test] 84 | fn test_parse_block_number_or_tag() { 85 | let block_number_str = "12345"; 86 | let result = parse_block_number_or_tag(block_number_str); 87 | assert!(result.is_ok()); 88 | let block_number_or_tag = result.unwrap(); 89 | assert_eq!(block_number_or_tag, BlockNumberOrTag::from(12345u64)); 90 | 91 | let block_tag_str = "latest"; 92 | let result = parse_block_number_or_tag(block_tag_str); 93 | assert!(result.is_ok()); 94 | let block_number_or_tag = result.unwrap(); 95 | assert_eq!(block_number_or_tag, BlockNumberOrTag::Latest); 96 | 97 | let block_hex_str = "0x3039"; 98 | let result = parse_block_number_or_tag(block_hex_str); 99 | assert!(result.is_ok()); 100 | let block_number_or_tag = result.unwrap(); 101 | assert_eq!( 102 | block_number_or_tag, 103 | BlockNumberOrTag::from_str(block_hex_str).unwrap() 104 | ); 105 | } 106 | 107 | #[test] 108 | fn test_parse_oracle_kind() { 109 | let kind_str = "generator"; 110 | let result = parse_oracle_kind(kind_str); 111 | assert!(result.is_ok()); 112 | let kind = result.unwrap(); 113 | assert_eq!(kind, OracleKind::Generator); 114 | 115 | let kind_str = "validator"; 116 | let result = parse_oracle_kind(kind_str); 117 | assert!(result.is_ok()); 118 | let kind = result.unwrap(); 119 | assert_eq!(kind, OracleKind::Validator); 120 | 121 | let kind_str = "invalid"; 122 | let result = parse_oracle_kind(kind_str); 123 | assert!(result.is_err()); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /core/src/compute/execute.rs: -------------------------------------------------------------------------------- 1 | use core::time::Duration; 2 | use dkn_workflows::{ExecutionError, Executor, Model, ProgramMemory, Workflow}; 3 | use eyre::Context; 4 | 5 | const NUM_RETRIES: usize = 4; 6 | 7 | /// A wrapper for executing a workflow with retries. 8 | /// 9 | /// - Creates an `Executor` with the given model. 10 | /// - Executes the given workflow with the executor over an empty memory. 11 | /// - If the execution fails due to timeout, retries up to 3 times with slightly increasing timeout durations. 12 | pub async fn execute_workflow_with_timedout_retries( 13 | workflow: &Workflow, 14 | model: Model, 15 | duration: Duration, 16 | ) -> eyre::Result { 17 | let executor = Executor::new(model); 18 | 19 | let mut retries = 0; 20 | while retries < NUM_RETRIES { 21 | let mut memory = ProgramMemory::new(); 22 | tokio::select! { 23 | result = executor.execute(None, workflow, &mut memory) => { 24 | if let Err(ExecutionError::WorkflowFailed(reason)) = result { 25 | // handle Workflow failed errors with retries 26 | log::warn!("Execution gave WorkflowFailed error with: {}", reason); 27 | if retries < NUM_RETRIES { 28 | retries += 1; 29 | log::warn!("Retrying {}/{}", retries, NUM_RETRIES); 30 | continue; 31 | } 32 | } else { 33 | return result.wrap_err("could not execute workflow"); 34 | } 35 | }, 36 | // normally the workflow has a timeout logic as well, but it doesnt work that well, and may get stuck 37 | _ = tokio::time::sleep(duration) => { 38 | // if we have retries left, log a warning and continue 39 | // note that other errors will be returned as is 40 | log::warn!("Execution timed out"); 41 | if retries < NUM_RETRIES { 42 | retries += 1; 43 | log::warn!("Retrying {}/{}", retries, NUM_RETRIES); 44 | continue; 45 | } 46 | } 47 | }; 48 | } 49 | 50 | // all retries failed 51 | Err(eyre::eyre!( 52 | "Execution failed after {} retries", 53 | NUM_RETRIES 54 | )) 55 | } 56 | -------------------------------------------------------------------------------- /core/src/compute/generation/execute.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use alloy::primitives::U256; 4 | use dkn_workflows::{MessageInput, Model}; 5 | use eyre::{eyre, Context, Result}; 6 | 7 | use super::request::GenerationRequest; 8 | use super::workflow::*; 9 | 10 | use crate::compute::execute_workflow_with_timedout_retries; 11 | use crate::compute::parse_downloadable; 12 | use crate::DriaOracle; 13 | 14 | /// Executes a request using the given model, and optionally a node. 15 | /// Returns the raw string output. 16 | pub async fn execute_generation( 17 | request: &GenerationRequest, 18 | model: Model, 19 | node: Option<&DriaOracle>, 20 | ) -> Result { 21 | log::debug!( 22 | "Executing {} generation request with: {}", 23 | request.request_type(), 24 | model 25 | ); 26 | 27 | match request { 28 | // workflows are executed directly without any prompts 29 | // as we expect their memory to be pre-filled 30 | GenerationRequest::Workflow(workflow) => { 31 | let duration = Duration::from_secs(workflow.get_config().max_time); 32 | execute_workflow_with_timedout_retries(workflow, model, duration).await 33 | } 34 | 35 | // string requests are used with the generation workflow with a given prompt 36 | GenerationRequest::String(input) => { 37 | let (workflow, duration) = make_generation_workflow(input.clone())?; 38 | execute_workflow_with_timedout_retries(&workflow, model, duration).await 39 | } 40 | 41 | // chat history requests are used with the chat workflow 42 | // and the existing history is fetched & parsed from previous requests 43 | GenerationRequest::ChatHistory(chat_request) => { 44 | let mut history = if chat_request.history_id == 0 { 45 | // if task id is zero, there is no prior history 46 | Vec::new() 47 | } else if let Some(node) = node { 48 | let history_id = U256::from(chat_request.history_id); 49 | // if task id is non-zero, we need the node to get the history 50 | // first make sure that next-task-id is larger than the history 51 | if history_id >= node.coordinator.nextTaskId().call().await?._0 { 52 | return Err(eyre!( 53 | "chat history cant exist as its larger than the latest task id" 54 | )); 55 | } 56 | 57 | let history_task = node 58 | .coordinator 59 | .getBestResponse(history_id) 60 | .call() 61 | .await 62 | .wrap_err("could not get chat history task from contract")? 63 | ._0; 64 | 65 | // parse it as chat history output 66 | let history_str = parse_downloadable(&history_task.output).await?; 67 | 68 | // if its a previous message array, we can parse it directly 69 | if let Ok(messages) = serde_json::from_str::>(&history_str) { 70 | messages 71 | } else { 72 | // otherwise, we can fallback to fetching input manually and creating a new history on-the-fly 73 | let request = node.coordinator.requests(history_id).call().await?; 74 | let input = parse_downloadable(&request.input).await?; 75 | 76 | // create a new history with the input 77 | vec![ 78 | MessageInput::new_user_message(input), 79 | MessageInput::new_assistant_message(history_str), 80 | ] 81 | } 82 | } else { 83 | return Err(eyre!("node is required for chat history")); 84 | }; 85 | 86 | // prepare the workflow with chat history 87 | let (workflow, duration) = 88 | make_chat_workflow(history.clone(), chat_request.content.clone(), None, None)?; 89 | let output = execute_workflow_with_timedout_retries(&workflow, model, duration).await?; 90 | 91 | // append user input to chat history 92 | history.push(MessageInput::new_assistant_message(output)); 93 | 94 | // return the stringified output 95 | serde_json::to_string(&history).wrap_err("could not serialize chat history") 96 | } 97 | } 98 | } 99 | 100 | #[cfg(test)] 101 | mod tests { 102 | use super::*; 103 | use crate::compute::generation::request::{ChatHistoryRequest, GenerationRequest}; 104 | #[tokio::test] 105 | #[ignore = "requires Ollama"] 106 | async fn test_ollama_generation() { 107 | dotenvy::dotenv().unwrap(); 108 | let request = GenerationRequest::String("What is the result of 2 + 2?".to_string()); 109 | let output = execute_generation(&request, Model::Llama3_1_8B, None) 110 | .await 111 | .unwrap(); 112 | 113 | println!("Output:\n{}", output); 114 | assert!(output.contains('4')); 115 | } 116 | 117 | #[tokio::test] 118 | #[ignore = "requires OpenAI API key"] 119 | async fn test_openai_generation() { 120 | dotenvy::dotenv().unwrap(); 121 | let request = GenerationRequest::String("What is the result of 2 + 2?".to_string()); 122 | let output = execute_generation(&request, Model::GPT4Turbo, None) 123 | .await 124 | .unwrap(); 125 | 126 | println!("Output:\n{}", output); 127 | assert!(output.contains('4')); 128 | } 129 | 130 | #[tokio::test] 131 | #[ignore = "requires OpenAI API key"] 132 | async fn test_openai_chat() { 133 | dotenvy::dotenv().unwrap(); 134 | let request = ChatHistoryRequest { 135 | history_id: 0, 136 | content: "What is 2+2?".to_string(), 137 | }; 138 | let request_bytes = serde_json::to_vec(&request).unwrap(); 139 | let request = GenerationRequest::try_parse_bytes(&request_bytes.into()) 140 | .await 141 | .unwrap(); 142 | let output = execute_generation(&request, Model::GPT4Turbo, None) 143 | .await 144 | .unwrap(); 145 | 146 | println!("Output:\n{}", output); 147 | assert!(output.contains('4')); 148 | } 149 | 150 | #[tokio::test] 151 | #[ignore = "requires OpenAI API key"] 152 | async fn test_workflow_on_arweave() { 153 | // cargo test --package dria-oracle --lib --all-features -- compute::generation::execute::tests::test_raw_workflow --exact --show-output --ignored 154 | dotenvy::dotenv().unwrap(); 155 | 156 | let contract_result = hex_literal::hex!("7b2261727765617665223a223658797a572d71666e7670756b787344535a444b2d4f514a6e715a686b62703044624e4e6649696c7a706f227d"); 157 | let request = GenerationRequest::try_parse_bytes(&contract_result.into()) 158 | .await 159 | .unwrap(); 160 | let output = execute_generation(&request, Model::GPT4o, None) 161 | .await 162 | .unwrap(); 163 | 164 | println!("{}", output); 165 | } 166 | 167 | #[tokio::test] 168 | #[ignore = "requires OpenRouter API key"] 169 | async fn test_workflow_fails_with_short_time() { 170 | dotenvy::dotenv().unwrap(); 171 | let _ = env_logger::builder() 172 | .filter_level(log::LevelFilter::Off) 173 | .filter_module("dria_oracle", log::LevelFilter::Info) 174 | .is_test(true) 175 | .try_init(); 176 | 177 | let (workflow, _) = 178 | make_chat_workflow(Vec::new(), "What is 2+2".into(), Some(1), None).unwrap(); 179 | let request = GenerationRequest::Workflow(workflow); 180 | let result = execute_generation(&request, Model::ORDeepSeek2_5, None).await; 181 | assert!(result.is_err()); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /core/src/compute/generation/handler.rs: -------------------------------------------------------------------------------- 1 | use crate::{compute::generation::execute::execute_generation, mine_nonce, DriaOracle}; 2 | use alloy::{ 3 | primitives::{FixedBytes, U256}, 4 | rpc::types::TransactionReceipt, 5 | }; 6 | use dria_oracle_contracts::{bytes32_to_string, bytes_to_string}; 7 | use dria_oracle_storage::ArweaveStorage; 8 | use eyre::Result; 9 | 10 | use super::postprocess::*; 11 | use super::request::GenerationRequest; 12 | 13 | /// Handles a generation request. 14 | /// 15 | /// 1. First, we check if we have already responded to the task. 16 | /// Contract will revert even if we dont do this check ourselves, but its better to provide the error here. 17 | /// 18 | /// 2. Then, we check if our models are compatible with the request. If not, we return an error. 19 | pub async fn handle_generation( 20 | node: &DriaOracle, 21 | task_id: U256, 22 | protocol: FixedBytes<32>, 23 | ) -> Result> { 24 | log::info!("Handling generation task {}", task_id); 25 | 26 | // check if we have responded to this generation already 27 | log::debug!("Checking existing generation responses"); 28 | let responses = node.coordinator.getResponses(task_id).call().await?._0; 29 | if responses.iter().any(|r| r.responder == node.address()) { 30 | log::debug!("Already responded to {} with generation", task_id); 31 | return Ok(None); 32 | } 33 | 34 | // fetch the request from contract 35 | log::debug!("Fetching the task request"); 36 | let request = node.coordinator.requests(task_id).call().await?; 37 | 38 | // choose model based on the request 39 | log::debug!("Choosing model to use"); 40 | let models_string = bytes_to_string(&request.models)?; 41 | let models_vec = models_string.split(',').map(|s| s.to_string()).collect(); 42 | let model = match node.workflows.get_any_matching_model(models_vec) { 43 | Ok((_, model)) => model, 44 | Err(e) => { 45 | log::error!( 46 | "No matching model found: {}, falling back to random model.", 47 | e 48 | ); 49 | 50 | node.workflows 51 | .get_matching_model("*".to_string()) 52 | .expect("should return at least one model") 53 | .1 54 | } 55 | }; 56 | log::debug!("Using model: {} from {}", model, models_string); 57 | 58 | // parse protocol string early, in case it cannot be parsed 59 | let protocol_string = bytes32_to_string(&protocol)?; 60 | 61 | // execute task 62 | log::debug!("Executing the workflow"); 63 | let input = GenerationRequest::try_parse_bytes(&request.input).await?; 64 | let output = execute_generation(&input, model, Some(node)).await?; 65 | log::debug!("Output: {}", output); 66 | 67 | // post-processing 68 | log::debug!( 69 | "Post-processing the output for protocol: {}", 70 | protocol_string 71 | ); 72 | let (output, metadata, use_storage) = 73 | match protocol_string.split('/').next().unwrap_or_default() { 74 | SwanPurchasePostProcessor::PROTOCOL => { 75 | SwanPurchasePostProcessor::new("", "").post_process(output) 76 | } 77 | _ => IdentityPostProcessor.post_process(output), 78 | }?; 79 | 80 | // uploading to storage 81 | let arweave = ArweaveStorage::new_from_env()?; 82 | let output = if use_storage { 83 | log::debug!("Uploading output to storage"); 84 | arweave.put_if_large(output).await? 85 | } else { 86 | log::debug!("Not uploading output to storage"); 87 | output 88 | }; 89 | log::debug!("Uploading metadata to storage"); 90 | let metadata = arweave.put_if_large(metadata).await?; 91 | 92 | // mine nonce 93 | log::debug!("Mining nonce for task"); 94 | let nonce = mine_nonce( 95 | request.parameters.difficulty, 96 | &request.requester, 97 | &node.address(), 98 | &request.input, 99 | &task_id, 100 | ) 101 | .nonce; 102 | 103 | // respond 104 | log::debug!("Responding with generation"); 105 | let tx_receipt = node 106 | .respond_generation(task_id, output, metadata, nonce) 107 | .await?; 108 | Ok(Some(tx_receipt)) 109 | } 110 | -------------------------------------------------------------------------------- /core/src/compute/generation/mod.rs: -------------------------------------------------------------------------------- 1 | mod execute; 2 | 3 | mod postprocess; 4 | 5 | mod workflow; 6 | 7 | mod handler; 8 | pub use handler::handle_generation; 9 | 10 | mod request; 11 | -------------------------------------------------------------------------------- /core/src/compute/generation/postprocess/identity.rs: -------------------------------------------------------------------------------- 1 | use alloy::primitives::Bytes; 2 | use eyre::Result; 3 | 4 | use super::PostProcess; 5 | 6 | /// An identity post-processor that does nothing. 7 | /// Input is directed to output and metadata is empty. 8 | #[derive(Default)] 9 | pub struct IdentityPostProcessor; 10 | 11 | impl PostProcess for IdentityPostProcessor { 12 | const PROTOCOL: &'static str = ""; 13 | 14 | fn post_process(&self, input: String) -> Result<(Bytes, Bytes, bool)> { 15 | Ok((input.into(), Default::default(), true)) 16 | } 17 | } 18 | 19 | #[cfg(test)] 20 | mod tests { 21 | use super::*; 22 | 23 | #[test] 24 | fn test_identity_post_processor() { 25 | let input = "hello".to_string(); 26 | let (output, metadata, _) = IdentityPostProcessor.post_process(input).unwrap(); 27 | assert_eq!(output, Bytes::from("hello")); 28 | assert_eq!(metadata, Bytes::default()); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /core/src/compute/generation/postprocess/mod.rs: -------------------------------------------------------------------------------- 1 | use alloy::primitives::Bytes; 2 | 3 | mod identity; 4 | pub use identity::*; 5 | 6 | mod swan; 7 | pub use swan::*; 8 | 9 | /// A `PostProcess` is a trait that defines a post-processing step for a workflow at application level. 10 | /// It's input is the raw output from the LLM, and it splits it into an output and metadata. 11 | /// The output is the main thing that is used within the contract, metadata is externally checked. 12 | pub trait PostProcess { 13 | /// Protocol string name, for instance if protocol is `foobar/1.0`, this should be `foobar`. 14 | const PROTOCOL: &'static str; 15 | 16 | /// A post-processing step that takes the raw output from the LLM and splits it into an output and metadata. 17 | /// 18 | /// Returns: 19 | /// - The output that is used within the contract. 20 | /// - The metadata that is externally checked. 21 | /// - A boolean indicating if the output should be uploaded to a storage if large enough. 22 | fn post_process(&self, input: String) -> eyre::Result<(Bytes, Bytes, bool)>; 23 | } 24 | -------------------------------------------------------------------------------- /core/src/compute/generation/request.rs: -------------------------------------------------------------------------------- 1 | use alloy::primitives::Bytes; 2 | use dkn_workflows::Workflow; 3 | use eyre::Result; 4 | 5 | use crate::compute::parse_downloadable; 6 | 7 | /// A request with chat history. 8 | #[derive(Debug, serde::Serialize, serde::Deserialize)] 9 | pub struct ChatHistoryRequest { 10 | /// Task Id of which the output will act like history. 11 | pub history_id: usize, 12 | /// Message content. 13 | pub content: String, 14 | } 15 | 16 | /// An oracle request. 17 | #[derive(Debug)] 18 | pub enum GenerationRequest { 19 | /// A chat-history request, usually indicates a previous response to be continued upon. 20 | ChatHistory(ChatHistoryRequest), 21 | /// A Workflow object, can be executed directly. 22 | Workflow(Workflow), 23 | /// A plain string request, can be executed with a generation workflow. 24 | String(String), 25 | } 26 | 27 | impl GenerationRequest { 28 | /// Returns hte name of the request type, mostly for diagnostics. 29 | pub fn request_type(&self) -> &str { 30 | match self { 31 | Self::ChatHistory(_) => "chat", 32 | Self::Workflow(_) => "workflow", 33 | Self::String(_) => "string", 34 | } 35 | } 36 | 37 | /// Given an input of byte-slice, parses it into a valid request type. 38 | pub async fn try_parse_bytes(input_bytes: &Bytes) -> Result { 39 | let input_string = parse_downloadable(input_bytes).await?; 40 | log::debug!("Parsing input string: {}", input_string); 41 | Ok(Self::try_parse_string(input_string).await) 42 | } 43 | 44 | /// Given an input of string, parses it into a valid request type. 45 | pub async fn try_parse_string(input_string: String) -> Self { 46 | if let Ok(chat_input) = serde_json::from_str::(&input_string) { 47 | GenerationRequest::ChatHistory(chat_input) 48 | } else if let Ok(workflow) = serde_json::from_str::(&input_string) { 49 | GenerationRequest::Workflow(workflow) 50 | } else { 51 | GenerationRequest::String(input_string) 52 | } 53 | } 54 | } 55 | 56 | #[cfg(test)] 57 | mod tests { 58 | use super::*; 59 | 60 | // only implemented for testing purposes 61 | // because `Workflow` does not implement `PartialEq` 62 | impl PartialEq for GenerationRequest { 63 | fn eq(&self, other: &Self) -> bool { 64 | match (self, other) { 65 | (Self::ChatHistory(a), Self::ChatHistory(b)) => { 66 | a.content == b.content && a.history_id == b.history_id 67 | } 68 | (Self::Workflow(_), Self::Workflow(_)) => true, // not implemented 69 | (Self::String(a), Self::String(b)) => a == b, 70 | _ => false, 71 | } 72 | } 73 | } 74 | 75 | #[tokio::test] 76 | async fn test_parse_request_string() { 77 | let request_str = "foobar"; 78 | let entry = GenerationRequest::try_parse_bytes(&request_str.as_bytes().into()).await; 79 | assert_eq!( 80 | entry.unwrap(), 81 | GenerationRequest::String(request_str.into()) 82 | ); 83 | } 84 | 85 | #[tokio::test] 86 | async fn test_parse_request_arweave() { 87 | // contains the string: "\"Hello, Arweave!\"" 88 | let arweave_key = serde_json::json!({ 89 | "arweave": "Zg6CZYfxXCWYnCuKEpnZCYfy7ghit1_v4-BCe53iWuA" 90 | }) 91 | .to_string(); 92 | let expected_str = "\"Hello, Arweave!\""; 93 | 94 | let entry = GenerationRequest::try_parse_bytes(&arweave_key.into()).await; 95 | assert_eq!( 96 | entry.unwrap(), 97 | GenerationRequest::String(expected_str.into()) 98 | ); 99 | } 100 | 101 | #[tokio::test] 102 | async fn test_parse_request_chat() { 103 | let request = ChatHistoryRequest { 104 | history_id: 0, 105 | content: "foobar".to_string(), 106 | }; 107 | let request_bytes = serde_json::to_vec(&request).unwrap(); 108 | let entry = GenerationRequest::try_parse_bytes(&request_bytes.into()).await; 109 | assert_eq!(entry.unwrap(), GenerationRequest::ChatHistory(request)); 110 | } 111 | 112 | #[tokio::test] 113 | async fn test_parse_request_workflow() { 114 | // contains a workflow 115 | let arweave_key = serde_json::json!({ 116 | "arweave": "ALRD6i-Xm7xSyl5hF-Tc9WRvsc5C71_TzV3fh1PVgkw" 117 | }) 118 | .to_string(); 119 | let workflow = GenerationRequest::try_parse_bytes(&arweave_key.into()) 120 | .await 121 | .unwrap(); 122 | if let GenerationRequest::Workflow(_) = workflow { 123 | /* do nothing */ 124 | } else { 125 | panic!("Expected workflow, got something else"); 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /core/src/compute/generation/workflow.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use dkn_workflows::{MessageInput, Workflow}; 4 | use serde_json::json; 5 | 6 | const DEFAULT_MAX_TIME: u64 = 50; 7 | const DEFAULT_MAX_STEPS: u64 = 10; 8 | 9 | /// Creates a generation workflow with the given input. 10 | /// 11 | /// It is an alias for `make_chat_workflow` with a single message alone. 12 | pub fn make_generation_workflow(input: String) -> Result<(Workflow, Duration), serde_json::Error> { 13 | make_chat_workflow(Vec::new(), input, None, None) 14 | } 15 | 16 | /// Creates a chat workflow with the given input. 17 | /// 18 | /// `messages` is the existing message history, which will be used as context for the `input` message. 19 | pub fn make_chat_workflow( 20 | mut messages: Vec, 21 | input: String, 22 | max_time_sec: Option, 23 | max_steps: Option, 24 | ) -> Result<(Workflow, Duration), serde_json::Error> { 25 | // add the new input to the message history as a user message 26 | messages.push(MessageInput::new_user_message(input)); 27 | 28 | // we do like this in-case a dynamic assign is needed 29 | let max_time_sec = max_time_sec.unwrap_or(DEFAULT_MAX_TIME); 30 | let max_steps = max_steps.unwrap_or(DEFAULT_MAX_STEPS); 31 | 32 | let workflow = json!({ 33 | "config": { 34 | "max_steps": max_steps, 35 | "max_time": max_time_sec, 36 | "tools": [""] 37 | }, 38 | "tasks": [ 39 | { 40 | "id": "A", 41 | "name": "Generate with history", 42 | "description": "Expects an array of messages for generation", 43 | "operator": "generation", 44 | "messages": messages, 45 | "outputs": [ 46 | { 47 | "type": "write", 48 | "key": "result", 49 | "value": "__result" 50 | } 51 | ] 52 | }, 53 | { 54 | "id": "__end", 55 | "operator": "end", 56 | "messages": [{ "role": "user", "content": "End of the task" }], 57 | } 58 | ], 59 | "steps": [ { "source": "A", "target": "__end" } ], 60 | "return_value": { 61 | "input": { 62 | "type": "read", 63 | "key": "result" 64 | } 65 | } 66 | }); 67 | 68 | let workflow = serde_json::from_value(workflow)?; 69 | 70 | Ok((workflow, Duration::from_secs(max_time_sec))) 71 | } 72 | -------------------------------------------------------------------------------- /core/src/compute/handler.rs: -------------------------------------------------------------------------------- 1 | use crate::DriaOracle; 2 | use dria_oracle_contracts::{OracleKind, TaskStatus}; 3 | 4 | use alloy::{ 5 | primitives::{FixedBytes, U256}, 6 | rpc::types::TransactionReceipt, 7 | }; 8 | use eyre::Result; 9 | 10 | use super::{handle_generation, handle_validation}; 11 | 12 | /// Handles a task request. 13 | /// 14 | /// - Generation tasks are forwarded to `handle_generation` 15 | /// - Validation tasks are forwarded to `handle_validation` 16 | pub async fn handle_request( 17 | node: &DriaOracle, 18 | status: TaskStatus, 19 | task_id: U256, 20 | protocol: FixedBytes<32>, 21 | ) -> Result> { 22 | log::debug!("Received event for task {} ()", task_id); 23 | 24 | // we check the `statusAfter` field of the event, which indicates the final status of the listened task 25 | let response_receipt = match status { 26 | TaskStatus::PendingGeneration => { 27 | if node.kinds.contains(&OracleKind::Generator) { 28 | handle_generation(node, task_id, protocol).await? 29 | } else { 30 | log::debug!( 31 | "Ignoring generation task {} as you are not generator.", 32 | task_id 33 | ); 34 | return Ok(None); 35 | } 36 | } 37 | TaskStatus::PendingValidation => { 38 | if node.kinds.contains(&OracleKind::Validator) { 39 | handle_validation(node, task_id).await? 40 | } else { 41 | log::debug!( 42 | "Ignoring generation task {} as you are not validator.", 43 | task_id 44 | ); 45 | return Ok(None); 46 | } 47 | } 48 | TaskStatus::Completed => { 49 | return Ok(None); 50 | } 51 | // this is unexpected to happen unless the contract has an eror 52 | // but we dont have to return an error just for this 53 | TaskStatus::None => { 54 | log::error!("None status received in an event: {}", task_id); 55 | return Ok(None); 56 | } 57 | }; 58 | 59 | if let Some(receipt) = &response_receipt { 60 | log::info!( 61 | "Task {} processed successfully. (tx: {})", 62 | task_id, 63 | receipt.transaction_hash 64 | ); 65 | } else { 66 | log::debug!("Task {} ignored.", task_id) 67 | } 68 | 69 | Ok(response_receipt) 70 | } 71 | -------------------------------------------------------------------------------- /core/src/compute/mod.rs: -------------------------------------------------------------------------------- 1 | mod handler; 2 | pub use handler::handle_request; 3 | 4 | mod nonce; 5 | pub use nonce::mine_nonce; 6 | 7 | mod generation; 8 | pub use generation::handle_generation; 9 | 10 | pub mod validation; 11 | pub use validation::handle_validation; 12 | 13 | mod utils; 14 | use utils::parse_downloadable; 15 | 16 | mod execute; 17 | use execute::execute_workflow_with_timedout_retries; 18 | -------------------------------------------------------------------------------- /core/src/compute/nonce.rs: -------------------------------------------------------------------------------- 1 | use alloy::primitives::{keccak256, Address, Bytes, U256}; 2 | use alloy::sol_types::SolValue; 3 | 4 | pub struct NonceResult { 5 | pub nonce: U256, 6 | pub candidate: U256, 7 | pub target: U256, 8 | } 9 | 10 | /// Mines a nonce for the oracle proof-of-work. 11 | /// 12 | /// Returns (nonce, candidate, target). 13 | pub fn mine_nonce( 14 | difficulty: u8, 15 | requester: &Address, 16 | responder: &Address, 17 | input: &Bytes, 18 | task_id: &U256, 19 | ) -> NonceResult { 20 | let big_one = U256::from(1); 21 | let mut nonce = U256::ZERO; 22 | 23 | // target is (2^256-1) / 2^difficulty 24 | let target = U256::MAX >> difficulty; 25 | 26 | loop { 27 | // encode packed 28 | let mut message = Vec::new(); 29 | task_id.abi_encode_packed_to(&mut message); 30 | input.abi_encode_packed_to(&mut message); 31 | requester.abi_encode_packed_to(&mut message); 32 | responder.abi_encode_packed_to(&mut message); 33 | nonce.abi_encode_packed_to(&mut message); 34 | 35 | // check hash 36 | let digest = keccak256(message.clone()); 37 | let candidate = U256::from_be_bytes(*digest); // big-endian! 38 | if candidate < target { 39 | return NonceResult { 40 | nonce, 41 | candidate, 42 | target, 43 | }; 44 | } 45 | 46 | nonce += big_one; 47 | } 48 | } 49 | 50 | #[cfg(test)] 51 | mod tests { 52 | use super::*; 53 | use crate::{DriaOracle, DriaOracleConfig}; 54 | use alloy::{ 55 | primitives::{address, Bytes, U256}, 56 | sol, 57 | }; 58 | use eyre::Result; 59 | 60 | sol! { 61 | #[allow(missing_docs)] 62 | // solc v0.8.26; solc tests/contracts/TestNonce.sol --via-ir --optimize --bin 63 | #[sol(rpc, bytecode="608080604052346015576101bc908161001a8239f35b5f80fdfe6080806040526004361015610012575f80fd5b5f3560e01c6359327f7c14610025575f80fd5b346101825760c0366003190112610182576024359067ffffffffffffffff8211610182573660238301121561018257816004013567ffffffffffffffff811161018257366024828501011161018257604435906001600160a01b038216820361018257606435906001600160a01b03821682036101825760a4359260ff841680940361018257604092828693602460208601996004358b5201868601378301916bffffffffffffffffffffffff199060601b16848301526bffffffffffffffffffffffff199060601b166054820152608435606882015203016028810183526067601f1991011682019282841067ffffffffffffffff85111761016e5760a0928492836040525f19901c908051832090608085525180938160808701528686015e5f84840186015281811115602085015260408401526060830152601f01601f19168101030190f35b634e487b7160e01b5f52604160045260245ffd5b5f80fdfea26469706673582212200bd4b20c7c71d8e44c60f255bf0a5e937f501f0e718e64687e36e0ddbc0d491864736f6c634300081a0033")] 64 | contract TestNonce { 65 | function assertValidNonce(uint256 taskId, bytes calldata input, address requester, address responder, uint256 nonce, uint8 difficulty) external 66 | view 67 | returns (bytes memory message, bool result, bytes32 candidate, uint256 target) 68 | { 69 | message = abi.encodePacked(taskId, input, requester, responder, nonce); 70 | target = type(uint256).max >> uint256(difficulty); 71 | candidate = keccak256(message); 72 | result = uint256(candidate) <= target; 73 | } 74 | } 75 | } 76 | 77 | #[tokio::test] 78 | async fn test_nonce_contract() -> Result<()> { 79 | dotenvy::dotenv().unwrap(); 80 | 81 | let config = DriaOracleConfig::new_from_env()?; 82 | let node = DriaOracle::new(config).await?; 83 | let contract = TestNonce::deploy(&node.provider).await?; 84 | 85 | // prepare parameters 86 | let difficulty = 2u8; 87 | let task_id = U256::from(1); 88 | let requester = address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266"); 89 | let responder = node.address(); 90 | let input = Bytes::from_iter("im some bytes yallllll".bytes()); 91 | 92 | // call contract 93 | let NonceResult { 94 | nonce, 95 | candidate, 96 | target, 97 | } = mine_nonce(difficulty, &requester, &responder, &input, &task_id); 98 | // println!("Nonce: {}", nonce); 99 | // println!("Target: {:x}", target); 100 | // println!("Candidate: {:x}", candidate); 101 | let contract_bytes = contract 102 | .assertValidNonce(task_id, input, requester, responder, nonce, difficulty) 103 | .call() 104 | .await?; 105 | 106 | // println!("\nResult: {}", contract_bytes.result); 107 | // println!("Target: {:x}", contract_bytes.target); 108 | // println!("Candidate: {:x}", contract_bytes.candidate); 109 | // println!("Message:\n{:x}", contract_bytes.message); 110 | assert_eq!(contract_bytes.target, target); 111 | assert_eq!(U256::from_be_bytes(contract_bytes.candidate.0), candidate); 112 | assert_eq!(contract_bytes.result, true); 113 | Ok(()) 114 | } 115 | 116 | #[test] 117 | fn test_nonce_local() { 118 | let requester = address!("0877022A137b8E8CE1C3020B9f047651dD02E37B"); 119 | let responder = address!("0877022A137b8E8CE1C3020B9f047651dD02E37B"); 120 | let input = vec![0x01, 0x02, 0x03].into(); 121 | let task_id = U256::from(0x1234); 122 | let difficulty = 2; 123 | 124 | let NonceResult { 125 | nonce, 126 | candidate, 127 | target, 128 | } = mine_nonce(difficulty, &requester, &responder, &input, &task_id); 129 | assert!(!nonce.is_zero()); 130 | 131 | println!("Nonce: {}", nonce); 132 | println!("Target: {:x}", target); 133 | println!("Candidate: {:x}", candidate); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /core/src/compute/utils.rs: -------------------------------------------------------------------------------- 1 | use alloy::primitives::Bytes; 2 | use dria_oracle_contracts::bytes_to_string; 3 | use dria_oracle_storage::{ArweaveStorage, IsExternalStorage}; 4 | use eyre::{Context, Result}; 5 | 6 | /// Parses a given bytes input to a string, 7 | /// and if it is a storage key identifier it automatically downloads the data from Arweave. 8 | pub async fn parse_downloadable(input_bytes: &Bytes) -> Result { 9 | // first, convert to string 10 | let mut input_string = bytes_to_string(input_bytes)?; 11 | 12 | // then, check storage 13 | if let Some(key) = ArweaveStorage::is_key(&input_string) { 14 | // if its a txid, we download the data and parse it again 15 | let input_bytes_from_arweave = ArweaveStorage::new_readonly() 16 | .get(key) 17 | .await 18 | .wrap_err("could not download from Arweave")?; 19 | 20 | // convert the input to string 21 | input_string = bytes_to_string(&input_bytes_from_arweave)?; 22 | } 23 | 24 | Ok(input_string) 25 | } 26 | -------------------------------------------------------------------------------- /core/src/compute/validation/execute.rs: -------------------------------------------------------------------------------- 1 | use alloy::primitives::U256; 2 | use dkn_workflows::Model; 3 | use eyre::{Context, Result}; 4 | 5 | use crate::compute::execute::execute_workflow_with_timedout_retries; 6 | 7 | use super::workflow::*; 8 | 9 | #[derive(Debug, serde::Deserialize, serde::Serialize)] 10 | pub struct ValidationResult { 11 | /// How helpful the response is. 12 | helpfulness: u8, 13 | /// How closely the response follows the instruction. 14 | instruction_following: u8, 15 | /// The final score of the response, which we write to the contract. 16 | final_score: u8, 17 | /// The truthfulness of the response. 18 | /// If the response is correct but it doesn't make sense w.r.t instruction, 19 | /// this may still be correct. 20 | truthfulness: u8, 21 | /// The rationale for the scores reported. 22 | rationale: String, 23 | } 24 | 25 | impl ValidationResult { 26 | /// Clamps the score to the range `[1, 5]` and scales it to the range `[1-255]`. 27 | pub fn final_score_as_solidity_type(&self) -> U256 { 28 | U256::from(match self.final_score.clamp(1, 5) { 29 | 1 => 51, 30 | 2 => 102, 31 | 3 => 153, 32 | 4 => 204, 33 | 5 => 255, 34 | _ => unreachable!(), 35 | }) 36 | } 37 | } 38 | 39 | /// Validates the given results. 40 | pub async fn execute_validations( 41 | instruction: String, 42 | generations: Vec, 43 | model: Model, 44 | ) -> Result> { 45 | let (workflow, duration) = make_validation_workflow(instruction, generations)?; 46 | 47 | log::debug!("Executing validation request with: {}", model); 48 | let result_str = execute_workflow_with_timedout_retries(&workflow, model, duration).await?; 49 | 50 | // first parse as vec of string 51 | // then parse each string as a ValidationResult 52 | // FIXME: this is a workflows bug, can return a single parseable string instead of array of parsable strings later 53 | let result: Vec = serde_json::from_str::>(&result_str) 54 | .wrap_err("could not parse validation results")? 55 | .into_iter() 56 | .map(|s| serde_json::from_str::(&s)) 57 | .collect::, _>>() 58 | .wrap_err("could not parse validation results")?; 59 | Ok(result) 60 | } 61 | 62 | #[cfg(test)] 63 | mod tests { 64 | use super::*; 65 | 66 | #[tokio::test] 67 | #[ignore = "requires OpenAI API key"] 68 | async fn test_validation_multiple() { 69 | dotenvy::dotenv().unwrap(); 70 | let _ = env_logger::builder() 71 | .filter_level(log::LevelFilter::Off) 72 | .filter_module("dria_oracle", log::LevelFilter::Debug) 73 | .is_test(true) 74 | .try_init(); 75 | 76 | let instruction = "What is 2 + 2".to_string(); 77 | let generations: Vec = [ 78 | "2 + 2 is 4.", // correct 79 | "2 + 2 is 889992.", // incorrect 80 | "Bonito applebum", // completely irrelevant 81 | "2 + 2 is 4 because apples are green.", // correct but irrational 82 | "2 + 2 is not 5.", // correct but irrelevant 83 | ] 84 | .iter() 85 | .map(|s| s.to_string()) 86 | .collect(); 87 | 88 | let model = Model::GPT4oMini; 89 | let results = execute_validations(instruction, generations.clone(), model) 90 | .await 91 | .unwrap(); 92 | 93 | assert_eq!( 94 | results.len(), 95 | generations.len(), 96 | "expected same number of results" 97 | ); 98 | assert!( 99 | results[0].final_score == 5, 100 | "expected top score from correct response" 101 | ); 102 | assert!( 103 | results[1].final_score < 3, 104 | "expected low score from wrong response" 105 | ); 106 | assert!( 107 | results[2].final_score < 3, 108 | "expected low score from irrelevant response" 109 | ); 110 | assert!( 111 | results[3].final_score < 3, 112 | "expected low score from correct but irrational response" 113 | ); 114 | assert!( 115 | results[4].final_score < 3, 116 | "expected low score from correct but irrelevant response" 117 | ); 118 | } 119 | 120 | #[tokio::test] 121 | #[ignore = "requires OpenAI API key"] 122 | async fn test_validation_single() { 123 | dotenvy::dotenv().unwrap(); 124 | let _ = env_logger::builder() 125 | .filter_level(log::LevelFilter::Off) 126 | .filter_module("dria_oracle", log::LevelFilter::Debug) 127 | .is_test(true) 128 | .try_init(); 129 | 130 | let instruction = "Can humans eat mango fruit?".to_string(); 131 | let generations: Vec = ["Yes they can."].iter().map(|s| s.to_string()).collect(); 132 | 133 | let model = Model::GPT4oMini; 134 | let results = execute_validations(instruction, generations.clone(), model) 135 | .await 136 | .unwrap(); 137 | 138 | assert!( 139 | results[0].final_score == 5, 140 | "expected top score from correct response" 141 | ); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /core/src/compute/validation/handler.rs: -------------------------------------------------------------------------------- 1 | use crate::{compute::parse_downloadable, mine_nonce, DriaOracle}; 2 | use alloy::{primitives::U256, rpc::types::TransactionReceipt}; 3 | use dkn_workflows::Model; 4 | use dria_oracle_storage::ArweaveStorage; 5 | use eyre::{eyre, Context, Result}; 6 | 7 | use super::execute::execute_validations; 8 | 9 | /// Handles a validation request. 10 | pub async fn handle_validation( 11 | node: &DriaOracle, 12 | task_id: U256, 13 | ) -> Result> { 14 | log::info!("Handling validation task {}", task_id); 15 | 16 | // check if already responded as generator, because we cant validate our own answer 17 | log::debug!("Checking if we are a generator for this task"); 18 | let responses = node.coordinator.getResponses(task_id).call().await?._0; 19 | if responses.iter().any(|r| r.responder == node.address()) { 20 | log::debug!( 21 | "Cant validate {} with your own generation response", 22 | task_id 23 | ); 24 | return Ok(None); 25 | } 26 | 27 | // check if we have validated anyways 28 | log::debug!("Checking if we have validated already"); 29 | let validations = node.coordinator.getValidations(task_id).call().await?._0; 30 | if validations.iter().any(|v| v.validator == node.address()) { 31 | return Err(eyre!("Already validated {}", task_id)); 32 | } 33 | 34 | // fetch the request from contract 35 | log::debug!("Fetching the task request"); 36 | let request = node.coordinator.requests(task_id).call().await?; 37 | 38 | // fetch each generation response & download its metadata 39 | log::debug!("Fetching response messages"); 40 | let responses = node.coordinator.getResponses(task_id).call().await?._0; 41 | let mut generations = Vec::new(); 42 | for response in responses { 43 | let metadata_str = parse_downloadable(&response.metadata).await?; 44 | generations.push(metadata_str); 45 | } 46 | let input = parse_downloadable(&request.input).await?; 47 | 48 | // validate each response 49 | log::debug!("Computing validation scores"); 50 | let model = Model::GPT4o; // all validations use Gpt 4o 51 | let validations = execute_validations(input, generations, model).await?; 52 | let scores = validations 53 | .iter() 54 | .map(|v| v.final_score_as_solidity_type()) 55 | .collect::>(); 56 | let metadata = 57 | serde_json::to_string(&validations).wrap_err("could not serialize validations")?; 58 | log::debug!("Validation metadata:\n{}", metadata); 59 | 60 | // uploading to storage 61 | log::debug!("Uploading metadata to storage"); 62 | let arweave = ArweaveStorage::new_from_env()?; 63 | let metadata = arweave.put_if_large(metadata.into()).await?; 64 | 65 | // mine nonce 66 | log::debug!("Mining nonce for task"); 67 | let nonce = mine_nonce( 68 | request.parameters.difficulty, 69 | &request.requester, 70 | &node.address(), 71 | &request.input, 72 | &task_id, 73 | ) 74 | .nonce; 75 | 76 | // respond 77 | log::debug!("Responding with validation"); 78 | let tx_receipt = node 79 | .respond_validation(task_id, scores, metadata, nonce) 80 | .await?; 81 | Ok(Some(tx_receipt)) 82 | } 83 | -------------------------------------------------------------------------------- /core/src/compute/validation/mod.rs: -------------------------------------------------------------------------------- 1 | mod execute; 2 | mod handler; 3 | mod workflow; 4 | 5 | pub use handler::handle_validation; 6 | -------------------------------------------------------------------------------- /core/src/compute/validation/workflow.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use dkn_workflows::Workflow; 4 | use serde_json::json; 5 | 6 | pub fn make_validation_workflow( 7 | instruction: String, 8 | mut generations: Vec, 9 | ) -> Result<(Workflow, Duration), serde_json::Error> { 10 | // workflow processes the array in reverse order, so we reverse the input outside 11 | // to get the correct order in results 12 | generations.reverse(); 13 | 14 | let max_time_sec = (generations.len() as u64) * 8 + 10; // we need at most few seconds per generation, plus some leeway here 15 | 16 | let workflow = json!({ 17 | "config": { 18 | "max_steps": generations.len() + 5, // we need one step per generation, plus some leeway here 19 | "max_time": max_time_sec, 20 | "tools": ["ALL"], 21 | "max_tokens": 1000 // max tokens do not need to change 22 | }, 23 | "external_memory": { 24 | "instruction": instruction, 25 | "generations": generations 26 | }, 27 | "tasks": [ 28 | { 29 | "id": "evaluate", 30 | "name": "Task", 31 | "description": "Task Description", 32 | "messages": [ 33 | { 34 | "role": "user", 35 | "content": "\n# Instruction Following Assessment\n\nEvaluate alignment between output and intent. Assess understanding of task goal and restrictions.\n\n**Instruction Components**: Task Goal (intended outcome), Restrictions (text styles, formats, or designated methods, etc).\n\n**Scoring**: Rate outputs 1 to 5:\n\n1. **Irrelevant**: No alignment.\n\n2. **Partial Focus**: Addresses one aspect poorly.\n\n3. **Partial Compliance**:\n\n- (1) Meets goal or restrictions, neglecting other.\n\n- (2) Acknowledges both but slight deviations.\n\n4. **Almost There**: Near alignment, minor deviations.\n\n5. **Comprehensive Compliance**: Fully aligns, meets all requirements.\n\n## Format:\n\n### Input\n\nInstruction: Write a compelling product description \n\nTexts:\n\n The sleek smartphone features a 6.1\" display\n\n Buy this phone it has good screen\n\n Experience crystal-clear visuals on the stunning 6.1\" OLED display\n\n### Output\n\n### Output for Text 1\n\n{\n \"score\": 3,\n \"rationale\": \"Basic description with specs but lacks compelling language.\"\n}\n\n### Output for Text 2\n\n{\n \"score\": 1,\n \"rationale\": \"Too informal and vague. Missing specific details.\"\n}\n\n### Output for Text 3\n\n{\n \"score\": 5,\n \"rationale\": \"Uses engaging language. Includes specific features. Creates desire.\"\n}\n\n---\n \n## Annotation\n\n### Input\n\nInstruction: {{instruction}}\n\nTexts:\n\n{{generations}}\n\n### Output" 36 | } 37 | ], 38 | "schema": "{\"properties\": {\"instruction_following\": {\"description\": \"The score for the instruction following ability.\", \"gte\": 1, \"lte\": 5, \"title\": \"Instruction Following\", \"type\": \"integer\"}, \"truthfulness\": {\"description\": \"The score for the truthfulness of the response.\", \"gte\": 1, \"lte\": 5, \"title\": \"Truthfulness\", \"type\": \"integer\"}, \"helpfulness\": {\"description\": \"The score for the helpfulness of the response.\", \"gte\": 1, \"lte\": 5, \"title\": \"Helpfulness\", \"type\": \"integer\"}, \"final_score\": {\"description\": \"The overall score of the evaluation.\", \"gte\": 1, \"lte\": 5, \"title\": \"Final Score\", \"type\": \"integer\"}, \"rationale\": {\"description\": \"The rationale of the evaluation.\", \"title\": \"Rationale\", \"type\": \"string\"}}, \"required\": [\"instruction_following\", \"truthfulness\", \"helpfulness\", \"final_score\", \"rationale\"], \"title\": \"Evaluation\", \"type\": \"object\", \"additionalProperties\": false}", 39 | "inputs": [ 40 | { 41 | "name": "instruction", 42 | "value": { 43 | "type": "read", 44 | "key": "instruction" 45 | }, 46 | "required": true 47 | }, 48 | { 49 | "name": "generations", 50 | "value": { 51 | "type": "pop", 52 | "key": "generations" 53 | }, 54 | "required": true 55 | } 56 | ], 57 | "operator": "generation", 58 | "outputs": [ 59 | { 60 | "type": "push", 61 | "key": "search_result", 62 | "value": "__result" 63 | } 64 | ] 65 | }, 66 | { 67 | "id": "_end", 68 | "name": "Task", 69 | "description": "Task Description", 70 | "messages": [ 71 | { 72 | "role": "user", 73 | "content": "" 74 | } 75 | ], 76 | "operator": "end" 77 | } 78 | ], 79 | "steps": [ 80 | { 81 | "source": "evaluate", 82 | "target": "_end", 83 | "condition": { 84 | "input": { 85 | "type": "size", 86 | "key": "generations" 87 | }, 88 | "expected": "0", 89 | "expression": "Equal", 90 | "target_if_not": "evaluate" 91 | } 92 | } 93 | ], 94 | "return_value": { 95 | "input": { 96 | "type": "get_all", 97 | "key": "search_result" 98 | }, 99 | "to_json": true 100 | } 101 | }); 102 | 103 | let workflow = serde_json::from_value(workflow)?; 104 | 105 | Ok((workflow, Duration::from_secs(max_time_sec))) 106 | } 107 | -------------------------------------------------------------------------------- /core/src/configurations/mod.rs: -------------------------------------------------------------------------------- 1 | use alloy::{ 2 | hex::FromHex, network::EthereumWallet, primitives::B256, signers::local::PrivateKeySigner, 3 | transports::http::reqwest::Url, 4 | }; 5 | 6 | use eyre::{Context, Result}; 7 | use std::env; 8 | use std::time::Duration; 9 | 10 | /// Configuration for the Dria Oracle. 11 | #[derive(Debug, Clone)] 12 | pub struct DriaOracleConfig { 13 | /// Wallet for the oracle. 14 | pub wallet: EthereumWallet, 15 | /// RPC URL for the oracle, decides the connected chain. 16 | pub rpc_url: Url, 17 | /// Optional transaction timeout, is useful to avoid getting stuck at `get_receipt()` when making a transaction. 18 | pub tx_timeout: Option, 19 | } 20 | 21 | impl DriaOracleConfig { 22 | pub fn new(private_key: &B256, rpc_url: Url) -> Result { 23 | let signer = 24 | PrivateKeySigner::from_bytes(private_key).wrap_err("could not parse private key")?; 25 | let wallet = EthereumWallet::from(signer); 26 | 27 | Ok(Self { 28 | wallet, 29 | rpc_url, 30 | tx_timeout: None, 31 | }) 32 | } 33 | 34 | /// Creates the config from the environment variables. 35 | /// 36 | /// Required environment variables: 37 | /// - `SECRET_KEY` 38 | /// - `RPC_URL` 39 | pub fn new_from_env() -> Result { 40 | // parse private key 41 | let private_key_hex = env::var("SECRET_KEY")?; 42 | let secret_key = B256::from_hex(private_key_hex).wrap_err("could not decode secret key")?; 43 | 44 | // parse rpc url 45 | let rpc_url_env = env::var("RPC_URL")?; 46 | let rpc_url = Url::parse(&rpc_url_env).wrap_err("could not parse RPC_URL")?; 47 | 48 | Self::new(&secret_key, rpc_url) 49 | } 50 | 51 | /// Change the transaction timeout. 52 | /// 53 | /// This will make transaction wait for the given duration before timing out, 54 | /// otherwise the node may get stuck waiting for a lost transaction. 55 | pub fn with_tx_timeout(mut self, tx_timeout: Duration) -> Self { 56 | self.tx_timeout = Some(tx_timeout); 57 | self 58 | } 59 | 60 | /// Change the RPC URL. 61 | pub fn with_rpc_url(mut self, rpc_url: Url) -> Self { 62 | self.rpc_url = rpc_url; 63 | self 64 | } 65 | 66 | /// Change the underlying wallet. 67 | pub fn with_wallet(mut self, wallet: EthereumWallet) -> Self { 68 | self.wallet = wallet; 69 | self 70 | } 71 | 72 | /// Change the signer with a new one with the given secret key. 73 | pub fn with_secret_key(&mut self, secret_key: &B256) -> Result<&mut Self> { 74 | let signer = 75 | PrivateKeySigner::from_bytes(secret_key).wrap_err("could not parse private key")?; 76 | self.wallet.register_default_signer(signer); 77 | Ok(self) 78 | } 79 | 80 | /// Change the signer with a new one. 81 | pub fn with_signer(&mut self, signer: PrivateKeySigner) -> &mut Self { 82 | self.wallet.register_default_signer(signer); 83 | self 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /core/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../../README.md")] 2 | 3 | mod cli; 4 | pub use cli::{handle_command, Cli}; 5 | 6 | mod node; 7 | pub use node::DriaOracle; 8 | 9 | /// Node configurations. 10 | mod configurations; 11 | pub use configurations::DriaOracleConfig; 12 | 13 | mod compute; 14 | pub use compute::{handle_generation, handle_request, handle_validation, mine_nonce}; 15 | -------------------------------------------------------------------------------- /core/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use clap::Parser; 4 | use dria_oracle::{Cli, DriaOracle, DriaOracleConfig}; 5 | 6 | #[tokio::main] 7 | async fn main() -> eyre::Result<()> { 8 | // default commands such as version and help exit at this point 9 | let cli = Cli::parse(); 10 | 11 | // read env w.r.t cli argument, defaults to `.env` 12 | let dotenv_result = dotenvy::from_path(&cli.env); 13 | 14 | // init env logger 15 | let log_level = match cli.debug { 16 | true => log::LevelFilter::Debug, 17 | false => log::LevelFilter::Info, 18 | }; 19 | env_logger::builder() 20 | .format_timestamp(Some(env_logger::TimestampPrecision::Millis)) 21 | .filter(None, log::LevelFilter::Off) 22 | .filter_module("dria_oracle", log_level) 23 | .filter_module("dkn_workflows", log_level) 24 | .filter_module("dria_oracle_contracts", log_level) 25 | .filter_module("dria_oracle_storage", log_level) 26 | .parse_default_env() 27 | .init(); 28 | 29 | // log about env usage after env logger init is executed 30 | match dotenv_result { 31 | Ok(_) => eprintln!("Loaded .env file at: {}", cli.env.display()), 32 | Err(e) => eprintln!("Could not load .env file: {}", e), 33 | } 34 | 35 | // read required env variables 36 | let secret_key = Cli::read_secret_key()?; 37 | let rpc_url = Cli::read_rpc_url()?; 38 | let tx_timeout = Cli::read_tx_timeout()?; 39 | 40 | // create config 41 | let config = DriaOracleConfig::new(&secret_key, rpc_url)? 42 | .with_tx_timeout(Duration::from_secs(tx_timeout)); 43 | 44 | // create node 45 | let node = DriaOracle::new(config).await?; 46 | log::info!("{}", node); 47 | 48 | // handle cli command 49 | dria_oracle::handle_command(cli.command, node).await?; 50 | 51 | log::info!("Bye!"); 52 | Ok(()) 53 | } 54 | -------------------------------------------------------------------------------- /core/src/node/anvil.rs: -------------------------------------------------------------------------------- 1 | //! Anvil-related utilities. 2 | //! 3 | //! This module is only available when the `anvil` feature is enabled, 4 | //! which is only expected to happen in tests. 5 | 6 | use super::DriaOracle; 7 | 8 | use alloy::network::{Ethereum, EthereumWallet}; 9 | use alloy::primitives::Address; 10 | use alloy::primitives::{utils::parse_ether, U256}; 11 | use alloy::providers::ext::AnvilApi; 12 | use alloy::providers::{PendingTransactionBuilder, Provider, ProviderBuilder}; 13 | use alloy::rpc::types::{TransactionReceipt, TransactionRequest}; 14 | use alloy::signers::local::PrivateKeySigner; 15 | use alloy::transports::http::Http; 16 | use eyre::Result; 17 | use reqwest::{Client, Url}; 18 | 19 | impl DriaOracle { 20 | /// We dedicate an unused port to Anvil. 21 | pub const ANVIL_PORT: u16 = 8545; 22 | /// Default ETH funding amount for generated wallets. 23 | pub const ANVIL_FUND_ETHER: &'static str = "10000"; 24 | 25 | /// Generates a random wallet, funded with the given `fund` amount. 26 | /// 27 | /// If `fund` is not provided, 10K ETH is used. 28 | pub async fn anvil_new_funded_wallet(&self, fund: Option) -> Result { 29 | let fund = fund.unwrap_or_else(|| parse_ether(Self::ANVIL_FUND_ETHER).unwrap()); 30 | let signer = PrivateKeySigner::random(); 31 | self.provider 32 | .anvil_set_balance(signer.address(), fund) 33 | .await?; 34 | let wallet = EthereumWallet::from(signer); 35 | Ok(wallet) 36 | } 37 | 38 | /// Whitelists a given address, impersonates the owner in doing so. 39 | pub async fn anvil_whitelist_registry(&self, address: Address) -> Result { 40 | let owner = self.registry.owner().call().await?._0; 41 | 42 | // increase owner balance 43 | self.anvil_increase_balance(owner, parse_ether("1").unwrap()) 44 | .await?; 45 | 46 | let tx = self 47 | .send_impersonated_transaction( 48 | self.registry 49 | .addToWhitelist(vec![address]) 50 | .into_transaction_request(), 51 | owner, 52 | ) 53 | .await?; 54 | let receipt = self.wait_for_tx(tx).await?; 55 | 56 | Ok(receipt) 57 | } 58 | 59 | /// Increases the balance of an account by the given amount. 60 | #[inline] 61 | pub async fn anvil_increase_balance(&self, address: Address, amount: U256) -> Result<()> { 62 | let balance = self.provider.get_balance(address).await?; 63 | self.provider 64 | .anvil_set_balance(address, balance + amount) 65 | .await?; 66 | Ok(()) 67 | } 68 | 69 | /// Assumes that an Anvil instance is running already at the given port. 70 | /// 71 | /// We use this due to the issue: https://github.com/alloy-rs/alloy/issues/1918 72 | #[inline] 73 | pub async fn send_impersonated_transaction( 74 | &self, 75 | tx: TransactionRequest, 76 | from: Address, 77 | ) -> Result, Ethereum>> { 78 | let anvil = ProviderBuilder::new().on_http(Self::anvil_url()); 79 | 80 | anvil.anvil_impersonate_account(from).await?; 81 | let pending_tx = anvil.send_transaction(tx.from(from)).await?; 82 | anvil.anvil_stop_impersonating_account(from).await?; 83 | 84 | Ok(pending_tx) 85 | } 86 | 87 | /// Returns the spawned Anvil URL, can be used with `ProviderBuilder::new().on_http(url)`. 88 | #[inline(always)] 89 | pub fn anvil_url() -> Url { 90 | Url::parse(&format!("http://localhost:{}", Self::ANVIL_PORT)).expect("could not parse URL") 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /core/src/node/coordinator.rs: -------------------------------------------------------------------------------- 1 | use super::DriaOracle; 2 | use alloy::eips::BlockNumberOrTag; 3 | use alloy::primitives::aliases::U40; 4 | use alloy::primitives::{Bytes, U256}; 5 | use alloy::rpc::types::{Log, TransactionReceipt}; 6 | use dria_oracle_contracts::string_to_bytes32; 7 | use eyre::{eyre, Result}; 8 | 9 | use dria_oracle_contracts::OracleCoordinator::{ 10 | getFeeReturn, getResponsesReturn, getValidationsReturn, requestsReturn, 11 | LLMOracleTaskParameters, StatusUpdate, 12 | }; 13 | 14 | impl DriaOracle { 15 | /// Request an oracle task. This is not done by the oracle normally, but we have it added for testing purposes. 16 | pub async fn request( 17 | &self, 18 | input: Bytes, 19 | models: Bytes, 20 | difficulty: u8, 21 | num_gens: u64, 22 | num_vals: u64, 23 | protocol: String, 24 | ) -> Result { 25 | let parameters = LLMOracleTaskParameters { 26 | difficulty, 27 | numGenerations: U40::from(num_gens), 28 | numValidations: U40::from(num_vals), 29 | }; 30 | 31 | let req = self 32 | .coordinator 33 | .request(string_to_bytes32(protocol)?, input, models, parameters); 34 | let tx = self.send_with_gas_hikes(req).await?; 35 | self.wait_for_tx(tx).await 36 | } 37 | 38 | /// Responds to a generation request with the response, metadata, and a valid nonce. 39 | pub async fn respond_generation( 40 | &self, 41 | task_id: U256, 42 | response: Bytes, 43 | metadata: Bytes, 44 | nonce: U256, 45 | ) -> Result { 46 | let req = self.coordinator.respond(task_id, nonce, response, metadata); 47 | let tx = self.send_with_gas_hikes(req).await?; 48 | self.wait_for_tx(tx).await 49 | } 50 | 51 | /// Responds to a validation request with the score, metadata, and a valid nonce. 52 | #[inline] 53 | pub async fn respond_validation( 54 | &self, 55 | task_id: U256, 56 | scores: Vec, 57 | metadata: Bytes, 58 | nonce: U256, 59 | ) -> Result { 60 | let req = self.coordinator.validate(task_id, nonce, scores, metadata); 61 | let tx = self.send_with_gas_hikes(req).await?; 62 | self.wait_for_tx(tx).await 63 | } 64 | 65 | /// Get previous tasks within the range of blocks. 66 | pub async fn get_tasks_in_range( 67 | &self, 68 | from_block: impl Into, 69 | to_block: impl Into, 70 | ) -> Result> { 71 | let tasks = self 72 | .coordinator 73 | .StatusUpdate_filter() 74 | .from_block(from_block) 75 | .to_block(to_block) 76 | .query() 77 | .await?; 78 | 79 | Ok(tasks) 80 | } 81 | 82 | /// Get task info for a given task id. 83 | pub async fn get_task( 84 | &self, 85 | task_id: U256, 86 | ) -> Result<(requestsReturn, getResponsesReturn, getValidationsReturn)> { 87 | // check if task id is valid 88 | if task_id.is_zero() { 89 | return Err(eyre!("Task ID must be non-zero.")); 90 | } else if task_id >= self.coordinator.nextTaskId().call().await?._0 { 91 | return Err(eyre!("Task with id {} has not been created yet.", task_id)); 92 | } 93 | 94 | // get task info 95 | let request = self.coordinator.requests(task_id).call().await?; 96 | let responses = self.coordinator.getResponses(task_id).call().await?; 97 | let validations = self.coordinator.getValidations(task_id).call().await?; 98 | 99 | Ok((request, responses, validations)) 100 | } 101 | 102 | /// Get fee details for a given request setting. 103 | pub async fn get_request_fee( 104 | &self, 105 | difficulty: u8, 106 | num_gens: u64, 107 | num_vals: u64, 108 | ) -> Result { 109 | let parameters = LLMOracleTaskParameters { 110 | difficulty, 111 | numGenerations: U40::from(num_gens), 112 | numValidations: U40::from(num_vals), 113 | }; 114 | 115 | let fees = self.coordinator.getFee(parameters).call().await?; 116 | 117 | Ok(fees) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /core/src/node/core.rs: -------------------------------------------------------------------------------- 1 | use alloy::contract::CallBuilder; 2 | use alloy::hex::FromHex; 3 | use alloy::providers::{PendingTransactionBuilder, WalletProvider}; 4 | use alloy::transports::RpcError; 5 | use alloy::{ 6 | network::EthereumWallet, 7 | primitives::Address, 8 | providers::{Provider, ProviderBuilder}, 9 | }; 10 | use alloy_chains::Chain; 11 | use dkn_workflows::{DriaWorkflowsConfig, Model, ModelProvider}; 12 | use dria_oracle_contracts::ERC20::ERC20Instance; 13 | use dria_oracle_contracts::{ 14 | contract_error_report, get_coordinator_address, OracleCoordinator, OracleKind, OracleRegistry, 15 | TokenBalance, ERC20, 16 | }; 17 | use eyre::{eyre, Context, Result}; 18 | use std::env; 19 | 20 | impl crate::DriaOracle { 21 | /// Creates a new Oracle node with the given private key and connected to the chain at the given RPC URL. 22 | /// 23 | /// If `anvil` feature is enabled, the node will connect to an Anvil fork of the chain. 24 | pub async fn new(config: crate::DriaOracleConfig) -> Result { 25 | #[cfg(not(feature = "anvil"))] 26 | let provider = ProviderBuilder::new() 27 | .with_recommended_fillers() 28 | .wallet(config.wallet.clone()) 29 | .on_http(config.rpc_url.clone()); 30 | 31 | #[cfg(feature = "anvil")] 32 | let provider = ProviderBuilder::new() 33 | .with_recommended_fillers() 34 | .wallet(config.wallet.clone()) 35 | .on_anvil_with_config(|anvil| { 36 | anvil.fork(config.rpc_url.clone()).port(Self::ANVIL_PORT) 37 | }); 38 | 39 | // fetch the chain id so that we can use the correct addresses 40 | let chain = Chain::from_id(provider.get_chain_id().await?) 41 | .named() 42 | .ok_or_else(|| eyre!("expected a named chain"))?; 43 | 44 | #[cfg(not(feature = "anvil"))] 45 | log::info!("Connected to {} network", chain); 46 | #[cfg(feature = "anvil")] 47 | log::info!("Connected to Anvil forked from {} network", chain); 48 | 49 | // get coordinator address from static list or the environment 50 | // (address within env can have 0x at the start, or not, does not matter) 51 | // and then create the coordinator instance 52 | let coordinator_address = if let Ok(addr) = env::var("COORDINATOR_ADDRESS") { 53 | Address::from_hex(addr).wrap_err("could not parse coordinator address in env")? 54 | } else { 55 | get_coordinator_address(chain)? 56 | }; 57 | let coordinator = OracleCoordinator::new(coordinator_address, provider.clone()); 58 | 59 | // get registry address from the coordinator & create instance 60 | let registry_address = coordinator 61 | .registry() 62 | .call() 63 | .await 64 | .wrap_err("could not get registry address from the coordinator")? 65 | ._0; 66 | let registry = OracleRegistry::new(registry_address, provider.clone()); 67 | 68 | // get token address from the coordinator & create instance 69 | let token_address = coordinator 70 | .feeToken() 71 | .call() 72 | .await 73 | .wrap_err("could not get token address from the coordinator")? 74 | ._0; 75 | let token = ERC20::new(token_address, provider.clone()); 76 | 77 | let node = Self { 78 | config, 79 | provider, 80 | token, 81 | coordinator, 82 | registry, 83 | kinds: Vec::default(), // TODO: take this from main config 84 | workflows: DriaWorkflowsConfig::default(), // TODO: take this from main config 85 | }; 86 | 87 | Ok(node) 88 | } 89 | 90 | /// Creates a new node that uses the given wallet as its signer. 91 | pub fn connect(&self, wallet: EthereumWallet) -> Self { 92 | // first, clone the provider and set the wallet 93 | let mut provider = self.provider.clone(); 94 | *provider.wallet_mut() = wallet.clone(); 95 | 96 | // then instantiate the contract instances with the new provider 97 | let token = ERC20Instance::new(*self.token.address(), provider.clone()); 98 | let coordinator = OracleCoordinator::new(*self.coordinator.address(), provider.clone()); 99 | let registry = OracleRegistry::new(*self.registry.address(), provider.clone()); 100 | 101 | Self { 102 | provider, 103 | config: self.config.clone().with_wallet(wallet), 104 | kinds: self.kinds.clone(), 105 | workflows: self.workflows.clone(), 106 | token, 107 | coordinator, 108 | registry, 109 | } 110 | } 111 | 112 | /// Given the kinds and models, prepares the configurations for the oracle. 113 | /// 114 | /// - If `kinds` is empty, it will check the registrations and use them as kinds. 115 | /// - If `models` is empty, gives an error. 116 | pub async fn prepare_oracle( 117 | &mut self, 118 | mut kinds: Vec, 119 | models: Vec, 120 | ) -> Result<()> { 121 | if kinds.is_empty() { 122 | // if kinds are not provided, use the registrations as kinds 123 | log::debug!("No kinds provided. Checking registrations."); 124 | for kind in [OracleKind::Generator, OracleKind::Validator] { 125 | if self.is_registered(kind).await? { 126 | kinds.push(kind); 127 | } 128 | } 129 | 130 | if kinds.is_empty() { 131 | return Err(eyre!("You are not registered as any type of oracle."))?; 132 | } 133 | } else { 134 | // otherwise, make sure we are registered to required kinds 135 | for kind in &kinds { 136 | if !self.is_registered(*kind).await? { 137 | return Err(eyre!("You need to register as {} first.", kind))?; 138 | } 139 | } 140 | } 141 | 142 | // prepare model config & check services 143 | let mut model_config = DriaWorkflowsConfig::new(models); 144 | let ollama_config = model_config.ollama.clone(); 145 | model_config = model_config.with_ollama_config( 146 | ollama_config 147 | .with_min_tps(5.0) 148 | .with_timeout(std::time::Duration::from_secs(150)), 149 | ); 150 | model_config.check_services().await?; 151 | if model_config.models.is_empty() { 152 | return Err(eyre!("No models provided."))?; 153 | } 154 | 155 | // validator-specific checks here 156 | if kinds.contains(&OracleKind::Validator) { 157 | // make sure we have GPT4o model 158 | if !model_config 159 | .models 160 | .contains(&(ModelProvider::OpenAI, Model::GPT4o)) 161 | { 162 | return Err(eyre!("Validator must have GPT4o model."))?; 163 | } 164 | 165 | // make sure node is whitelisted 166 | if !self.is_whitelisted(self.address()).await? { 167 | return Err(eyre!("You are not whitelisted in the registry."))?; 168 | } 169 | } 170 | 171 | self.workflows = model_config; 172 | self.kinds = kinds; 173 | 174 | Ok(()) 175 | } 176 | 177 | /// Returns the native token (ETH) balance of a given address. 178 | #[inline] 179 | pub async fn get_native_balance(&self, address: Address) -> Result { 180 | let balance = self.provider.get_balance(address).await?; 181 | Ok(TokenBalance::new(balance, "ETH", None)) 182 | } 183 | 184 | /// Returns the address of the configured wallet. 185 | #[inline(always)] 186 | pub fn address(&self) -> Address { 187 | self.config.wallet.default_signer().address() 188 | } 189 | 190 | /// Waits for a transaction to be mined, returning the receipt. 191 | #[inline] 192 | pub async fn wait_for_tx( 193 | &self, 194 | tx: PendingTransactionBuilder, 195 | ) -> Result 196 | where 197 | T: alloy::transports::Transport + Clone, 198 | N: alloy::network::Network, 199 | { 200 | let tx_hash = *tx.tx_hash(); 201 | log::info!("Waiting for tx: {:?}", tx_hash); 202 | let receipt = tx 203 | .with_timeout(self.config.tx_timeout) 204 | .get_receipt() 205 | .await 206 | .wrap_err(format!("tx {} failed", tx_hash))?; 207 | Ok(receipt) 208 | } 209 | 210 | /// Given a request, retries sending it with increasing gas prices to avoid 211 | /// the "tx underpriced" errors. 212 | #[inline] 213 | pub async fn send_with_gas_hikes( 214 | &self, 215 | req: CallBuilder, 216 | ) -> Result> 217 | where 218 | T: alloy::transports::Transport + Clone, 219 | P: alloy::providers::Provider + Clone, 220 | D: alloy::contract::CallDecoder + Clone, 221 | N: alloy::network::Network, 222 | { 223 | // gas price hikes to try in increasing order, first is 0 to simply use the 224 | // initial gas fee for the first attempt 225 | const GAS_PRICE_HIKES: [u128; 4] = [0, 12, 24, 36]; 226 | 227 | // try and send tx, with increasing gas prices for few attempts 228 | let initial_gas_price = self.provider.get_gas_price().await?; 229 | for (attempt_no, increase_percentage) in GAS_PRICE_HIKES.iter().enumerate() { 230 | // set gas price 231 | let gas_price = initial_gas_price + (initial_gas_price / 100) * increase_percentage; 232 | 233 | // try to send tx with gas price 234 | match req.clone().gas_price(gas_price).send().await { 235 | // if all is well, we can return the tx 236 | Ok(tx) => { 237 | return Ok(tx); 238 | } 239 | // if we get an RPC error; specifically, if the tx is underpriced, we try again with higher gas 240 | Err(alloy::contract::Error::TransportError(RpcError::ErrorResp(err))) => { 241 | // TODO: kind of a code-smell, can we do better check here? 242 | if err.message.contains("underpriced") { 243 | log::warn!( 244 | "{} with gas {} in attempt {}", 245 | err.message, 246 | gas_price, 247 | attempt_no + 1, 248 | ); 249 | 250 | // wait just a little bit 251 | tokio::time::sleep(std::time::Duration::from_millis(300)).await; 252 | 253 | continue; 254 | } else { 255 | // otherwise let it be handled by the error report 256 | return Err(contract_error_report( 257 | alloy::contract::Error::TransportError(RpcError::ErrorResp(err)), 258 | )); 259 | } 260 | } 261 | // if we get any other error, we report it 262 | Err(err) => return Err(contract_error_report(err)), 263 | }; 264 | } 265 | 266 | // all attempts failed 267 | Err(eyre!("Failed all attempts send tx due to underpriced gas.")) 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /core/src/node/mod.rs: -------------------------------------------------------------------------------- 1 | use alloy::network::Ethereum; 2 | use dkn_workflows::DriaWorkflowsConfig; 3 | use dria_oracle_contracts::OracleKind; 4 | use dria_oracle_contracts::{OracleCoordinator, OracleRegistry, ERC20}; 5 | 6 | mod coordinator; 7 | mod core; 8 | mod registry; 9 | mod token; 10 | 11 | mod types; 12 | use types::*; 13 | 14 | #[cfg(feature = "anvil")] 15 | mod anvil; 16 | 17 | use super::DriaOracleConfig; 18 | pub struct DriaOracle { 19 | pub config: DriaOracleConfig, 20 | /// Contract addresses for the oracle, respects the connected chain. 21 | // pub addresses: ContractAddresses, 22 | pub token: ERC20::ERC20Instance, 23 | pub coordinator: OracleCoordinator::OracleCoordinatorInstance< 24 | DriaOracleTransport, 25 | DriaOracleProvider, 26 | Ethereum, 27 | >, 28 | pub registry: 29 | OracleRegistry::OracleRegistryInstance, 30 | /// Underlying provider type. 31 | pub provider: DriaOracleProvider, 32 | /// Kinds of this oracle, i.e. `generator`, `validator`. 33 | pub kinds: Vec, 34 | /// Workflows config, defines the available models & services. 35 | pub workflows: DriaWorkflowsConfig, 36 | } 37 | 38 | impl std::fmt::Display for DriaOracle { 39 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 40 | write!( 41 | f, 42 | "Dria Oracle Node v{}\nOracle Address: {}\nRPC URL: {}\nCoordinator: {}\nTx timeout: {}s", 43 | env!("CARGO_PKG_VERSION"), 44 | self.address(), 45 | self.config.rpc_url, 46 | self.coordinator.address(), 47 | self.config.tx_timeout.map(|t| t.as_secs()).unwrap_or_default() 48 | ) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /core/src/node/registry.rs: -------------------------------------------------------------------------------- 1 | use alloy::{primitives::Address, rpc::types::TransactionReceipt}; 2 | use dria_oracle_contracts::{OracleKind, TokenBalance}; 3 | use eyre::Result; 4 | 5 | impl crate::DriaOracle { 6 | /// Register the oracle with the registry. 7 | #[inline] 8 | pub async fn register_kind(&self, kind: OracleKind) -> Result { 9 | let req = self.registry.register(kind.into()); 10 | let tx = self.send_with_gas_hikes(req).await?; 11 | 12 | self.wait_for_tx(tx).await 13 | } 14 | 15 | /// Unregister from the oracle registry. 16 | #[inline] 17 | pub async fn unregister_kind(&self, kind: OracleKind) -> Result { 18 | let req = self.registry.unregister(kind.into()); 19 | let tx = self.send_with_gas_hikes(req).await?; 20 | 21 | self.wait_for_tx(tx).await 22 | } 23 | 24 | /// Returns the amount of tokens to be staked to registry. 25 | pub async fn get_registry_stake_amount(&self, kind: OracleKind) -> Result { 26 | let stake_amount = self.registry.getStakeAmount(kind.into()).call().await?._0; 27 | 28 | // return the symbol as well 29 | let token_symbol = self.token.symbol().call().await?._0; 30 | 31 | Ok(TokenBalance::new( 32 | stake_amount, 33 | token_symbol, 34 | Some(*self.token.address()), 35 | )) 36 | } 37 | 38 | /// Returns whether the oracle is registered as a given kind. 39 | #[inline] 40 | pub async fn is_registered(&self, kind: OracleKind) -> Result { 41 | let is_registered = self 42 | .registry 43 | .isRegistered(self.address(), kind.into()) 44 | .call() 45 | .await?; 46 | Ok(is_registered._0) 47 | } 48 | 49 | /// Returns whether a given address is whitelisted or not. 50 | #[inline] 51 | pub async fn is_whitelisted(&self, address: Address) -> Result { 52 | let is_whitelisted = self.registry.isWhitelisted(address).call().await?; 53 | Ok(is_whitelisted._0) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /core/src/node/token.rs: -------------------------------------------------------------------------------- 1 | use super::DriaOracle; 2 | use alloy::primitives::{Address, U256}; 3 | use alloy::rpc::types::TransactionReceipt; 4 | use dria_oracle_contracts::TokenBalance; 5 | use eyre::Result; 6 | 7 | impl DriaOracle { 8 | /// Returns the token balance of a given address. 9 | pub async fn get_token_balance(&self, address: Address) -> Result { 10 | let token_balance = self.token.balanceOf(address).call().await?._0; 11 | let token_symbol = self.token.symbol().call().await?._0; 12 | 13 | Ok(TokenBalance::new( 14 | token_balance, 15 | token_symbol, 16 | Some(*self.token.address()), 17 | )) 18 | } 19 | 20 | /// Transfer tokens from one address to another, calls `transferFrom` of the ERC20 contract. 21 | /// 22 | /// Assumes that approvals are made priorly. 23 | pub async fn transfer_from( 24 | &self, 25 | from: Address, 26 | to: Address, 27 | amount: U256, 28 | ) -> Result { 29 | let req = self.token.transferFrom(from, to, amount); 30 | let tx = self.send_with_gas_hikes(req).await?; 31 | self.wait_for_tx(tx).await 32 | } 33 | 34 | /// Approves the `spender` to spend `amount` tokens on behalf of the caller. 35 | pub async fn approve(&self, spender: Address, amount: U256) -> Result { 36 | let req = self.token.approve(spender, amount); 37 | let tx = self.send_with_gas_hikes(req).await?; 38 | self.wait_for_tx(tx).await 39 | } 40 | 41 | /// Returns the allowance of a given `spender` address to spend tokens on behalf of `owner` address. 42 | pub async fn allowance(&self, owner: Address, spender: Address) -> Result { 43 | let token_symbol = self.token.symbol().call().await?._0; 44 | let allowance = self.token.allowance(owner, spender).call().await?._0; 45 | 46 | Ok(TokenBalance::new( 47 | allowance, 48 | token_symbol, 49 | Some(*self.token.address()), 50 | )) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /core/src/node/types.rs: -------------------------------------------------------------------------------- 1 | //! This module exports the transport and provider type based on the `anvil` feature. 2 | //! 3 | //! - If `anvil` is enabled, then Anvil-compatible provider type is created. 4 | //! - Otherwise, default provider is created for Ethereum-like networks. 5 | 6 | use alloy::providers::fillers::{ 7 | BlobGasFiller, ChainIdFiller, FillProvider, GasFiller, JoinFill, NonceFiller, WalletFiller, 8 | }; 9 | use alloy::{ 10 | network::{Ethereum, EthereumWallet}, 11 | providers::{Identity, RootProvider}, 12 | }; 13 | 14 | #[cfg(not(feature = "anvil"))] 15 | pub type DriaOracleTransport = alloy::transports::http::Http; 16 | 17 | #[cfg(feature = "anvil")] 18 | pub type DriaOracleTransport = alloy::transports::BoxTransport; 19 | 20 | #[cfg(not(feature = "anvil"))] 21 | pub type DriaOracleProvider = FillProvider< 22 | JoinFill< 23 | JoinFill< 24 | Identity, 25 | JoinFill>>, 26 | >, 27 | WalletFiller, 28 | >, 29 | RootProvider, 30 | DriaOracleTransport, 31 | Ethereum, 32 | >; 33 | 34 | #[cfg(feature = "anvil")] 35 | pub type DriaOracleProvider = FillProvider< 36 | JoinFill< 37 | JoinFill< 38 | Identity, 39 | JoinFill>>, 40 | >, 41 | WalletFiller, 42 | >, 43 | alloy::providers::layers::AnvilProvider, DriaOracleTransport>, 44 | DriaOracleTransport, 45 | Ethereum, 46 | >; 47 | -------------------------------------------------------------------------------- /core/tests/abi_encode_test.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "anvil")] 2 | 3 | use std::str::FromStr; 4 | 5 | use alloy::{ 6 | network::EthereumWallet, 7 | node_bindings::Anvil, 8 | primitives::{address, keccak256, Address, Bytes, U256}, 9 | providers::ProviderBuilder, 10 | signers::local::PrivateKeySigner, 11 | sol, 12 | sol_types::SolValue, 13 | }; 14 | use eyre::Result; 15 | 16 | sol! { 17 | #[allow(missing_docs)] 18 | // solc v0.8.26; solc tests/contracts/TestSolidityPacked.sol --via-ir --optimize --bin 19 | #[sol(rpc, bytecode="608080604052346015576102bd908161001a8239f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c8063026131d7146101115780639dac80891461007f5763aa1e84de1461003a575f80fd5b3461007b57602036600319011261007b5760043567ffffffffffffffff811161007b5761006d60209136906004016101a9565b818151910120604051908152f35b5f80fd5b3461007b5761010d60206100f96059610097366101ff565b916040979397519788956bffffffffffffffffffffffff199060601b1685870152603486015263ffffffff60e01b9060e01b166054850152151560f81b60588401528051918291018484015e81015f838201520301601f198101835282610173565b604051918291602083526020830190610263565b0390f35b3461007b5761010d6101696100f963ffffffff61012d366101ff565b604080516001600160a01b03909616602087015285019390935293166060830152911515608082015260a08082015292839160c0830190610263565b03601f1981018352825b90601f8019910116810190811067ffffffffffffffff82111761019557604052565b634e487b7160e01b5f52604160045260245ffd5b81601f8201121561007b5780359067ffffffffffffffff821161019557604051926101de601f8401601f191660200185610173565b8284526020838301011161007b57815f926020809301838601378301015290565b60a060031982011261007b576004356001600160a01b038116810361007b57916024359160443563ffffffff8116810361007b5791606435801515810361007b57916084359067ffffffffffffffff821161007b57610260916004016101a9565b90565b805180835260209291819084018484015e5f828201840152601f01601f191601019056fea2646970667358221220e3f817afaaaef1121ac298a855627f97dac246dd3de24d8585bef6c9fce3ab5664736f6c634300081a0033")] 20 | contract TestSolidityPacked { 21 | function encodePacked(address someAddress, uint256 someNumber, uint32 someShort, bool someBool, bytes memory someBytes) public pure returns (bytes memory) { 22 | return abi.encodePacked(someAddress, someNumber, someShort, someBool, someBytes); 23 | } 24 | 25 | function encode(address someAddress, uint256 someNumber, uint32 someShort, bool someBool, bytes memory someBytes) public pure returns (bytes memory) { 26 | return abi.encode(someAddress, someNumber, someShort, someBool, someBytes); 27 | } 28 | 29 | function hash(bytes memory data) public pure returns (bytes32) { 30 | return keccak256(data); 31 | } 32 | } 33 | } 34 | 35 | #[tokio::test] 36 | async fn test_encode_packed() -> Result<()> { 37 | let anvil = Anvil::new().try_spawn()?; 38 | let signer: PrivateKeySigner = anvil.keys()[0].clone().into(); 39 | let wallet = EthereumWallet::from(signer); 40 | let rpc_url = anvil.endpoint().parse()?; 41 | let provider = ProviderBuilder::new() 42 | .with_recommended_fillers() 43 | .wallet(wallet) 44 | .on_http(rpc_url); 45 | let contract = TestSolidityPacked::deploy(&provider).await?; 46 | 47 | // prepare parameters 48 | let some_address = address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266"); 49 | let some_number = U256::from(0x12345678); 50 | let some_short = 0x69u32; 51 | let some_bool = true; 52 | let some_bytes = Bytes::from_static(&[0x42u8; 42]); 53 | 54 | // call contract 55 | let contract_bytes = contract 56 | .encodePacked( 57 | some_address, 58 | some_number, 59 | some_short, 60 | some_bool, 61 | some_bytes.clone(), 62 | ) 63 | .call() 64 | .await? 65 | ._0; 66 | 67 | // encode with alloy 68 | type Data = (Address, U256, u32, bool, Bytes); 69 | let data = Data::from((some_address, some_number, some_short, some_bool, some_bytes)); 70 | assert_eq!("(address,uint256,uint32,bool,bytes)", data.sol_name()); 71 | let local_bytes = Bytes::from(data.abi_encode_packed()); 72 | 73 | assert_eq!(contract_bytes, local_bytes); 74 | 75 | Ok(()) 76 | } 77 | 78 | #[tokio::test] 79 | async fn test_encode() -> Result<()> { 80 | let anvil = Anvil::new().try_spawn()?; 81 | let signer: PrivateKeySigner = anvil.keys()[0].clone().into(); 82 | let wallet = EthereumWallet::from(signer); 83 | let rpc_url = anvil.endpoint().parse()?; 84 | let provider = ProviderBuilder::new() 85 | .with_recommended_fillers() 86 | .wallet(wallet) 87 | .on_http(rpc_url); 88 | let contract = TestSolidityPacked::deploy(&provider).await?; 89 | 90 | // prepare parameters 91 | let some_address = address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266"); 92 | let some_number = U256::from(0x12345678); 93 | let some_short = 0x69u32; 94 | let some_bool = true; 95 | let some_bytes = Bytes::from_static(&[0x42u8; 42]); 96 | 97 | // call contract 98 | let contract_bytes = contract 99 | .encode( 100 | some_address, 101 | some_number, 102 | some_short, 103 | some_bool, 104 | some_bytes.clone(), 105 | ) 106 | .call() 107 | .await? 108 | ._0; 109 | 110 | // encode with alloy 111 | type Data = (Address, U256, u32, bool, Bytes); 112 | let data = Data::from((some_address, some_number, some_short, some_bool, some_bytes)); 113 | assert_eq!("(address,uint256,uint32,bool,bytes)", data.sol_name()); 114 | let local_bytes = Bytes::from(data.abi_encode_sequence()); 115 | 116 | assert_eq!(contract_bytes, local_bytes); 117 | 118 | Ok(()) 119 | } 120 | 121 | #[tokio::test] 122 | async fn test_decode() -> Result<()> { 123 | let b = Bytes::from_str("000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000060000000000000000000000007befbfbd18777d12efbfbdefbfbdefbfbd79466b6fefbfbd6cefbfbd01efbfbd380000000000000000000000007c467217056444efbfbdd99ac6ab0231efbfbd55efbfbdefbfbd240f0000000000000000000000005defbfbd0b2b3d4eefbfbd3b45efbfbd42efbfbd0457efbfbdefbfbd1b64664f000000000000000000000000efbfbdefbfbd66efbfbdefbfbdefbfbd13efbfbd56efbfbd4d11efbfbdefbfbdefbfbd5befbfbd7869130000000000000000000000001b0cefbfbd01efbfbdefbfbdefbfbd2862efbfbd5130efbfbdefbfbdefbfbd03efbfbd2defbfbd6800000000000000000000000052efbfbd7a5b2f152133efbfbd773d2969efbfbd15efbfbdefbfbdefbfbdefbfbd")?; 124 | let addr = Vec::
::abi_decode(&b, false)?; 125 | println!("{:?}", addr); 126 | 127 | Ok(()) 128 | } 129 | #[tokio::test] 130 | async fn test_hash() -> Result<()> { 131 | let anvil = Anvil::new().try_spawn()?; 132 | let signer: PrivateKeySigner = anvil.keys()[0].clone().into(); 133 | let wallet = EthereumWallet::from(signer); 134 | let rpc_url = anvil.endpoint().parse()?; 135 | let provider = ProviderBuilder::new() 136 | .with_recommended_fillers() 137 | .wallet(wallet) 138 | .on_http(rpc_url); 139 | let contract = TestSolidityPacked::deploy(&provider).await?; 140 | 141 | let some_bytes = Bytes::from_static(&hex_literal::hex!("deadbeef123deadbeef123deadbeef")); 142 | 143 | let contract_bytes = contract.hash(some_bytes.clone()).call().await?._0; 144 | let local_bytes = keccak256(some_bytes); 145 | 146 | assert_eq!(contract_bytes, local_bytes); 147 | 148 | Ok(()) 149 | } 150 | -------------------------------------------------------------------------------- /core/tests/contracts/TestNonce.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.26; 3 | 4 | /// @dev solc v0.8.26; solc tests/contracts/TestNonce.sol --via-ir --optimize --bin 5 | /// @dev you may have to do: `solc-select install 0.8.26` 6 | contract TestNonce { 7 | function assertValidNonce(uint256 taskId, bytes calldata input, address requester, address responder, uint256 nonce, uint8 difficulty) 8 | external 9 | pure 10 | returns (bytes memory message, bool result, bytes32 candidate, uint256 target) 11 | { 12 | message = abi.encodePacked(taskId, input, requester, responder, nonce); 13 | target = type(uint256).max >> uint256(difficulty); 14 | candidate = keccak256(message); 15 | result = uint256(candidate) <= target; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /core/tests/contracts/TestSolidityPacked.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.26; 3 | 4 | /// @dev solc v0.8.26; solc tests/contracts/TestSolidityPacked.sol --via-ir --optimize --bin 5 | /// @dev you may have to do: `solc-select install 0.8.26` 6 | contract TestSolidityPacked { 7 | function encodePacked(address someAddress, uint256 someNumber, uint32 someShort, bool someBool, bytes memory someBytes) public pure returns (bytes memory) { 8 | return abi.encodePacked(someAddress, someNumber, someShort, someBool, someBytes); 9 | } 10 | 11 | function encode(address someAddress, uint256 someNumber, uint32 someShort, bool someBool, bytes memory someBytes) public pure returns (bytes memory) { 12 | return abi.encode(someAddress, someNumber, someShort, someBool, someBytes); 13 | } 14 | 15 | function hash(bytes memory data) public pure returns (bytes32) { 16 | return keccak256(data); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /core/tests/oracle_test.rs: -------------------------------------------------------------------------------- 1 | //! Test the oracle with a simple 2 + 2 question. 2 | //! 3 | //! ```sh 4 | //! cargo test --package dria-oracle --test oracle_test --all-features -- test_oracle_two_plus_two --exact --show-output 5 | //! ``` 6 | #![cfg(feature = "anvil")] 7 | 8 | use alloy::{eips::BlockNumberOrTag, primitives::utils::parse_ether}; 9 | use dkn_workflows::Model; 10 | use dria_oracle::{handle_request, DriaOracle, DriaOracleConfig}; 11 | use dria_oracle_contracts::{bytes_to_string, string_to_bytes, OracleKind, TaskStatus, WETH}; 12 | use eyre::Result; 13 | 14 | #[tokio::test] 15 | async fn test_oracle_two_plus_two() -> Result<()> { 16 | dotenvy::dotenv().unwrap(); 17 | let _ = env_logger::builder() 18 | .filter_level(log::LevelFilter::Off) 19 | .filter_module("dria_oracle", log::LevelFilter::Debug) 20 | .filter_module("oracle_test", log::LevelFilter::Debug) 21 | .is_test(true) 22 | .try_init(); 23 | 24 | // task setup 25 | let model = Model::GPT4o; 26 | let difficulty = 1; 27 | let models = string_to_bytes(model.to_string()); 28 | let protocol = format!("test/{}", env!("CARGO_PKG_VERSION")); 29 | let input = string_to_bytes("What is the result of 2 + 2?".to_string()); 30 | 31 | // node setup 32 | let config = DriaOracleConfig::new_from_env()?; 33 | let node = DriaOracle::new(config).await?; 34 | 35 | // setup accounts 36 | let requester = node.connect(node.anvil_new_funded_wallet(None).await?); 37 | let mut generator = node.connect(node.anvil_new_funded_wallet(None).await?); 38 | let mut validator = node.connect(node.anvil_new_funded_wallet(None).await?); 39 | 40 | // buy some WETH for all people 41 | let amount = parse_ether("100")?; 42 | for node in [&requester, &generator, &validator] { 43 | let token = WETH::new(*node.token.address(), &node.provider); 44 | let balance_before = node.get_token_balance(node.address()).await?; 45 | 46 | let call = token.deposit().value(amount); 47 | let _ = call.send().await?.get_receipt().await?; 48 | 49 | let balance_after = node.get_token_balance(node.address()).await?; 50 | assert!(balance_after.amount > balance_before.amount); 51 | } 52 | 53 | // whitelist validator with impersonation 54 | node.anvil_whitelist_registry(validator.address()).await?; 55 | assert!(node.is_whitelisted(validator.address()).await?); 56 | 57 | // register & prepare generator oracle 58 | generator.register(OracleKind::Generator).await?; 59 | generator 60 | .prepare_oracle(vec![OracleKind::Generator], vec![model.clone()]) 61 | .await?; 62 | assert!(generator.is_registered(OracleKind::Generator).await?); 63 | 64 | // register & prepare validator oracle 65 | validator.register(OracleKind::Validator).await?; 66 | validator 67 | .prepare_oracle(vec![OracleKind::Validator], vec![model]) 68 | .await?; 69 | assert!(validator.is_registered(OracleKind::Validator).await?); 70 | 71 | // approve some tokens for the coordinator from requester 72 | requester 73 | .approve(*node.coordinator.address(), amount) 74 | .await?; 75 | 76 | // make a request with just one generation and validation request 77 | let request_receipt = requester 78 | .request(input, models, difficulty, 1, 1, protocol) 79 | .await?; 80 | 81 | // handle generation by reading the latest event 82 | let tasks = node 83 | .get_tasks_in_range( 84 | request_receipt.block_number.unwrap(), 85 | BlockNumberOrTag::Latest, 86 | ) 87 | .await?; 88 | assert!(tasks.len() == 1); 89 | let event = tasks[0].0.clone(); 90 | let task_id = event.taskId; 91 | assert_eq!(event.statusBefore, TaskStatus::None as u8); 92 | assert_eq!(event.statusAfter, TaskStatus::PendingGeneration as u8); 93 | let generation_receipt = handle_request( 94 | &generator, 95 | TaskStatus::PendingGeneration, 96 | event.taskId, 97 | event.protocol, 98 | ) 99 | .await? 100 | .unwrap(); 101 | 102 | // handle validation by reading the latest event 103 | let tasks = node 104 | .get_tasks_in_range( 105 | generation_receipt.block_number.unwrap(), 106 | BlockNumberOrTag::Latest, 107 | ) 108 | .await?; 109 | assert!(tasks.len() == 1); 110 | let (event, _) = tasks.into_iter().next().unwrap(); 111 | assert_eq!(event.taskId, task_id); 112 | assert_eq!(event.statusBefore, TaskStatus::PendingGeneration as u8); 113 | assert_eq!(event.statusAfter, TaskStatus::PendingValidation as u8); 114 | let validation_receipt = handle_request( 115 | &validator, 116 | TaskStatus::PendingValidation, 117 | event.taskId, 118 | event.protocol, 119 | ) 120 | .await? 121 | .unwrap(); 122 | 123 | let tasks = node 124 | .get_tasks_in_range( 125 | validation_receipt.block_number.unwrap(), 126 | BlockNumberOrTag::Latest, 127 | ) 128 | .await?; 129 | assert!(tasks.len() == 1); 130 | let (event, _) = tasks.into_iter().next().unwrap(); 131 | assert_eq!(event.taskId, task_id); 132 | assert_eq!(event.statusBefore, TaskStatus::PendingValidation as u8); 133 | assert_eq!(event.statusAfter, TaskStatus::Completed as u8); 134 | 135 | // get responses 136 | let responses = node.coordinator.getResponses(task_id).call().await?._0; 137 | assert_eq!(responses.len(), 1); 138 | let response = responses.into_iter().next().unwrap(); 139 | let output_string = bytes_to_string(&response.output)?; 140 | assert!(output_string.contains("4"), "output must contain 4"); 141 | assert!(!response.score.is_zero(), "score must be non-zero"); 142 | log::debug!("Output: {}", output_string); 143 | 144 | Ok(()) 145 | } 146 | -------------------------------------------------------------------------------- /core/tests/registry_test.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "anvil")] 2 | 3 | use dria_oracle::{DriaOracle, DriaOracleConfig}; 4 | use dria_oracle_contracts::OracleKind; 5 | use eyre::Result; 6 | 7 | #[tokio::test] 8 | async fn test_registry() -> Result<()> { 9 | dotenvy::dotenv().unwrap(); 10 | let _ = env_logger::builder() 11 | .filter_level(log::LevelFilter::Off) 12 | .filter_module("dria_oracle", log::LevelFilter::Debug) 13 | .filter_module("registry_test", log::LevelFilter::Debug) 14 | .is_test(true) 15 | .try_init(); 16 | 17 | let config = DriaOracleConfig::new_from_env()?; 18 | let node = DriaOracle::new(config).await?; 19 | 20 | // tries to register if registered, or opposite, to trigger an error 21 | const KIND: OracleKind = OracleKind::Generator; 22 | let result = if node.is_registered(KIND).await? { 23 | log::info!("Oracle is registered, we will try to register again to get an error."); 24 | node.register_kind(KIND).await 25 | } else { 26 | log::info!("Oracle is not registered, we will try to unregister again to get an error."); 27 | node.unregister_kind(KIND).await 28 | }; 29 | assert!(result.is_err()); 30 | 31 | // both errors include the node address in their message, which we look for here: 32 | let err = result.unwrap_err(); 33 | err.to_string().contains(&node.address().to_string()); 34 | 35 | Ok(()) 36 | } 37 | -------------------------------------------------------------------------------- /core/tests/request_test.rs: -------------------------------------------------------------------------------- 1 | //! Tests the request command, resulting in a task being created in the coordinator contract. 2 | //! 3 | //! 1. Requester buys some WETH, and it is approved within the request command. 4 | //! 2. Requester requests a task with a given input, models, difficulty, num_gens, and num_vals. 5 | //! 3. The task is created in the coordinator contract. 6 | //! 7 | //! ```sh 8 | //! cargo test --package dria-oracle --test request_test --all-features -- test_request --exact --show-output 9 | //! ``` 10 | #![cfg(feature = "anvil")] 11 | 12 | use alloy::primitives::{aliases::U40, utils::parse_ether}; 13 | use dkn_workflows::Model; 14 | use dria_oracle::{DriaOracle, DriaOracleConfig}; 15 | use dria_oracle_contracts::{bytes_to_string, WETH}; 16 | use eyre::Result; 17 | 18 | #[tokio::test] 19 | async fn test_request() -> Result<()> { 20 | dotenvy::dotenv().unwrap(); 21 | let _ = env_logger::builder() 22 | .filter_level(log::LevelFilter::Off) 23 | .filter_module("dria_oracle", log::LevelFilter::Debug) 24 | .filter_module("request_test", log::LevelFilter::Debug) 25 | .is_test(true) 26 | .try_init(); 27 | 28 | // task setup 29 | let (difficulty, num_gens, num_vals) = (1, 1, 1); 30 | let protocol = format!("test/{}", env!("CARGO_PKG_VERSION")); 31 | let models = vec![Model::GPT4Turbo]; 32 | let input = "What is the result of 2 + 2?".to_string(); 33 | 34 | // node setup 35 | let config = DriaOracleConfig::new_from_env()?; 36 | let node = DriaOracle::new(config).await?; 37 | 38 | // setup account & buy some WETH 39 | let requester = node.connect(node.anvil_new_funded_wallet(None).await?); 40 | let token = WETH::new(*requester.token.address(), &requester.provider); 41 | let _ = token.deposit().value(parse_ether("100")?).send().await?; 42 | 43 | // request a task, and see it in the coordinator 44 | let task_id = node.coordinator.nextTaskId().call().await?._0; 45 | requester 46 | .request_task(&input, models, difficulty, num_gens, num_vals, protocol) 47 | .await?; 48 | 49 | // get the task info 50 | let (request, _, _) = node.get_task(task_id).await?; 51 | assert_eq!(input, bytes_to_string(&request.input).unwrap()); 52 | assert_eq!(difficulty, request.parameters.difficulty); 53 | assert_eq!(U40::from(num_gens), request.parameters.numGenerations); 54 | assert_eq!(U40::from(num_vals), request.parameters.numValidations); 55 | 56 | Ok(()) 57 | } 58 | -------------------------------------------------------------------------------- /core/tests/swan_test.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "anvil")] 2 | 3 | use alloy::{eips::BlockNumberOrTag, primitives::utils::parse_ether}; 4 | use dkn_workflows::Model; 5 | use dria_oracle::{handle_request, DriaOracle, DriaOracleConfig}; 6 | use dria_oracle_contracts::{string_to_bytes, OracleKind, TaskStatus, WETH}; 7 | use eyre::Result; 8 | 9 | #[tokio::test] 10 | async fn test_swan() -> Result<()> { 11 | dotenvy::dotenv().unwrap(); 12 | let _ = env_logger::builder() 13 | .filter_level(log::LevelFilter::Off) 14 | .filter_module("dria_oracle", log::LevelFilter::Debug) 15 | .filter_module("swan_test", log::LevelFilter::Debug) 16 | .is_test(true) 17 | .try_init(); 18 | 19 | // task setup 20 | let difficulty = 1; 21 | let models = string_to_bytes(Model::GPT4Turbo.to_string()); 22 | let protocol = format!("swan/0.0.1-test"); 23 | let input = string_to_bytes( 24 | r#" 25 | Print the exact text below, do not reply with anything else: 26 | 27 | 28 | 0x36f55f830D6E628a78Fcb70F73f9D005BaF88eE3 29 | 0x671527de058BaD60C6151cA29d501C87439bCF62 30 | 0x66FC9dC1De3db773891753CD257359A26e876305 31 | 32 | "# 33 | .to_string(), 34 | ); 35 | 36 | // node setup 37 | let config = DriaOracleConfig::new_from_env()?; 38 | let node = DriaOracle::new(config).await?; 39 | 40 | // setup accounts 41 | let requester = node.connect(node.anvil_new_funded_wallet(None).await?); 42 | let mut generator = node.connect(node.anvil_new_funded_wallet(None).await?); 43 | let mut validator = node.connect(node.anvil_new_funded_wallet(None).await?); 44 | 45 | // buy some WETH for all people 46 | log::info!("Buying WETH for all accounts"); 47 | let amount = parse_ether("100").unwrap(); 48 | for node in [&requester, &generator, &validator] { 49 | let balance_before = node.get_token_balance(node.address()).await?; 50 | 51 | let token = WETH::new(*node.token.address(), &node.provider); 52 | let call = token.deposit().value(amount); 53 | let _ = call.send().await?.get_receipt().await?; 54 | 55 | let balance_after = node.get_token_balance(node.address()).await?; 56 | assert!(balance_after.amount > balance_before.amount); 57 | } 58 | 59 | // whitelist validator with impersonation 60 | node.anvil_whitelist_registry(validator.address()).await?; 61 | assert!(node.is_whitelisted(validator.address()).await?); 62 | 63 | // register & prepare generator oracle 64 | generator.register(OracleKind::Generator).await?; 65 | generator 66 | .prepare_oracle(vec![OracleKind::Generator], vec![Model::GPT4Turbo]) 67 | .await?; 68 | assert!(generator.is_registered(OracleKind::Generator).await?); 69 | 70 | // register & prepare validator oracle 71 | validator.register(OracleKind::Validator).await?; 72 | validator 73 | .prepare_oracle(vec![OracleKind::Validator], vec![Model::GPT4o]) 74 | .await?; 75 | assert!(validator.is_registered(OracleKind::Validator).await?); 76 | 77 | // approve some tokens for the coordinator from requester 78 | requester 79 | .approve(*node.coordinator.address(), amount) 80 | .await?; 81 | 82 | // make a request with just one generation and validation request 83 | let request_receipt = requester 84 | .request(input, models, difficulty, 1, 1, protocol) 85 | .await?; 86 | 87 | // handle generation by reading the latest event 88 | let tasks = node 89 | .get_tasks_in_range( 90 | request_receipt.block_number.unwrap(), 91 | BlockNumberOrTag::Latest, 92 | ) 93 | .await?; 94 | assert!(tasks.len() == 1); 95 | let (event, _) = tasks.into_iter().next().unwrap(); 96 | let task_id = event.taskId; 97 | assert_eq!(event.statusBefore, TaskStatus::None as u8); 98 | assert_eq!(event.statusAfter, TaskStatus::PendingGeneration as u8); 99 | let generation_receipt = handle_request( 100 | &generator, 101 | TaskStatus::PendingGeneration, 102 | event.taskId, 103 | event.protocol, 104 | ) 105 | .await? 106 | .unwrap(); 107 | 108 | // handle validation by reading the latest event 109 | let tasks = node 110 | .get_tasks_in_range( 111 | generation_receipt.block_number.unwrap(), 112 | BlockNumberOrTag::Latest, 113 | ) 114 | .await?; 115 | assert!(tasks.len() == 1); 116 | let (event, _) = tasks.into_iter().next().unwrap(); 117 | assert_eq!(event.taskId, task_id); 118 | assert_eq!(event.statusBefore, TaskStatus::PendingGeneration as u8); 119 | assert_eq!(event.statusAfter, TaskStatus::PendingValidation as u8); 120 | let validation_receipt = handle_request( 121 | &validator, 122 | TaskStatus::PendingValidation, 123 | event.taskId, 124 | event.protocol, 125 | ) 126 | .await? 127 | .unwrap(); 128 | 129 | let tasks = node 130 | .get_tasks_in_range( 131 | validation_receipt.block_number.unwrap(), 132 | BlockNumberOrTag::Latest, 133 | ) 134 | .await?; 135 | assert!(tasks.len() == 1); 136 | let (event, _) = tasks.into_iter().next().unwrap(); 137 | assert_eq!(event.taskId, task_id); 138 | assert_eq!(event.statusBefore, TaskStatus::PendingValidation as u8); 139 | assert_eq!(event.statusAfter, TaskStatus::Completed as u8); 140 | 141 | // get responses 142 | let responses = node.coordinator.getResponses(task_id).call().await?._0; 143 | assert_eq!(responses.len(), 1); 144 | let response = responses.into_iter().next().unwrap(); 145 | println!("Output: {:?}", response.output); 146 | assert!(!response.score.is_zero(), "score must be non-zero"); 147 | 148 | Ok(()) 149 | } 150 | -------------------------------------------------------------------------------- /core/tests/weth_test.rs: -------------------------------------------------------------------------------- 1 | //! Using the forked blockchain, creates two accounts (alice, bob) and then, 2 | //! 3 | //! 1. Alice buys WETH 4 | //! 2. Alice approves Bob 5 | //! 3. Bob transfers WETH from Alice 6 | #![cfg(feature = "anvil")] 7 | 8 | use alloy::primitives::utils::parse_ether; 9 | use dria_oracle::{DriaOracle, DriaOracleConfig}; 10 | use dria_oracle_contracts::WETH; 11 | use eyre::Result; 12 | 13 | #[tokio::test] 14 | async fn test_weth_transfer() -> Result<()> { 15 | dotenvy::dotenv().unwrap(); 16 | 17 | // amount of WETH that will be transferred 18 | let amount = parse_ether("100")?; 19 | 20 | let config = DriaOracleConfig::new_from_env()?; 21 | let node = DriaOracle::new(config).await?; 22 | 23 | // setup alice 24 | let alice = node.connect(node.anvil_new_funded_wallet(None).await?); 25 | let alice_token = WETH::new(*node.token.address(), &alice.provider); 26 | 27 | // setup bob 28 | let bob = node.connect(node.anvil_new_funded_wallet(None).await?); 29 | let bob_token = WETH::new(*node.token.address(), &bob.provider); 30 | 31 | // record existing balances 32 | let alice_balance_before = node.get_token_balance(alice.address()).await?; 33 | let bob_balance_before = node.get_token_balance(bob.address()).await?; 34 | 35 | // alice buys WETH 36 | let call = alice_token.deposit().value(amount); 37 | let _ = call.send().await?.get_receipt().await?; 38 | let alice_balance_after = node.get_token_balance(alice.address()).await?; 39 | assert_eq!( 40 | alice_balance_after.amount - alice_balance_before.amount, 41 | amount 42 | ); 43 | 44 | // alice approves bob 45 | let call = alice_token.approve(bob.address(), amount); 46 | let _ = call.send().await?.get_receipt().await?; 47 | 48 | // bob transfers WETH from alice 49 | let call = bob_token.transferFrom(alice.address(), bob.address(), amount); 50 | let _ = call.send().await?.get_receipt().await?; 51 | 52 | // balances should be updated 53 | let alice_balance_after = node.get_token_balance(alice.address()).await?; 54 | let bob_balance_after = node.get_token_balance(bob.address()).await?; 55 | assert_eq!(alice_balance_after.amount, alice_balance_before.amount); 56 | assert_eq!(bob_balance_after.amount - bob_balance_before.amount, amount); 57 | 58 | Ok(()) 59 | } 60 | -------------------------------------------------------------------------------- /misc/arweave.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A helper script to print the content of an Arweave transaction. 3 | * 4 | * Usage: 5 | * 6 | * ```sh 7 | * # calldata as-is 8 | * bun run ./misc/arweave.js 0x7b2261727765617665223a224d49555775656361634b417a62755442335a6a57613463784e6461774d71435a704550694f71675a625a63227d 9 | * 10 | * # as an object (with escaped quotes) 11 | * bun run ./misc/arweave.js "{\"arweave\":\"MIUWuecacKAzbuTB3ZjWa4cxNdawMqCZpEPiOqgZbZc\"}" 12 | * 13 | * # base64 txid 14 | * bun run ./misc/arweave.js MIUWuecacKAzbuTB3ZjWa4cxNdawMqCZpEPiOqgZbZc 15 | * ``` 16 | * 17 | * Can be piped to `pbcopy` on macOS to copy the output to clipboard. 18 | */ 19 | 20 | async function main() { 21 | // parse input 22 | let input = process.argv[2]; 23 | if (!input) { 24 | console.error("No input provided."); 25 | return; 26 | } 27 | 28 | let arweaveTxId; 29 | if (input.startsWith("0x")) { 30 | // if it starts with 0x, we assume its all hex 31 | arweaveTxId = JSON.parse( 32 | Buffer.from(input.slice(2), "hex").toString() 33 | ).arweave; 34 | } else if (input.startsWith("{")) { 35 | // if it starts with {, we assume its a JSON string 36 | console.log("input", input); 37 | arweaveTxId = JSON.parse(input).arweave; 38 | } else { 39 | // otherwise, we assume its a base64 txid 40 | arweaveTxId = input; 41 | } 42 | 43 | // construct the URL 44 | // download the actual response from Arweave 45 | const url = `https://arweave.net/${arweaveTxId}`; 46 | const res = await fetch(url); 47 | console.log(await res.text()); 48 | } 49 | 50 | main().catch(console.error); 51 | -------------------------------------------------------------------------------- /secrets/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dria-oracle-storage" 3 | description = "Dria Oracle External Storages" 4 | version.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | authors.workspace = true 8 | 9 | [dependencies] 10 | # arweave uploader 11 | # NOTE: there are many unused stuff here, but everything breaks if you use the minimal set 12 | # because Bundlr SDK is not maintained at all 13 | bundlr-sdk = { version = "0.5.0" } 14 | 15 | alloy.workspace = true 16 | eyre.workspace = true 17 | log.workspace = true 18 | 19 | async-trait.workspace = true 20 | 21 | reqwest.workspace = true 22 | serde.workspace = true 23 | serde_json.workspace = true 24 | 25 | [dev-dependencies] 26 | env_logger.workspace = true 27 | dotenvy.workspace = true 28 | tokio.workspace = true 29 | -------------------------------------------------------------------------------- /storage/src/arweave.rs: -------------------------------------------------------------------------------- 1 | use alloy::primitives::Bytes; 2 | use async_trait::async_trait; 3 | use bundlr_sdk::{currency::arweave::ArweaveBuilder, tags::Tag, BundlrBuilder}; 4 | use eyre::{eyre, Context, Result}; 5 | use reqwest::{Client, Url}; 6 | use std::{env, path::PathBuf}; 7 | 8 | use super::IsExternalStorage; 9 | 10 | const DEFAULT_UPLOAD_BASE_URL: &str = "https://node1.bundlr.network"; 11 | const DEFAULT_DOWNLOAD_BASE_URL: &str = "https://arweave.net"; 12 | const DEFAULT_BYTE_LIMIT: usize = 1024; // 1KB 13 | 14 | #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] 15 | pub struct ArweaveKey { 16 | /// The base64url encoded key, can be used to download data directly. 17 | pub arweave: String, 18 | } 19 | 20 | /// External data storage for Arweave. 21 | /// 22 | /// - `put` corresponds to uploading (via Irys) 23 | /// - `get` corresponds to downloading 24 | pub struct ArweaveStorage { 25 | /// Path to Arweave keypair (usually JSON) 26 | wallet: Option, 27 | /// Base URL for uploading data on Arweave, e.g.: 28 | /// - https://gateway.irys.xyz 29 | /// - https://node1.bundlr.network 30 | upload_base_url: Url, 31 | /// Base URL for downloading data from Arweave, e.g.: 32 | /// - https://arweave.net 33 | download_base_url: Url, 34 | /// Reqwest client for downloads. 35 | client: Client, 36 | /// Byte limit for the data to be considered for Arweave. 37 | /// 38 | /// - If the data exceeds this limit, it will be uploaded to Arweave. 39 | /// - Otherwise, it will be stored as is. 40 | byte_limit: usize, 41 | } 42 | 43 | impl ArweaveStorage { 44 | /// Creates an Arweave storage client with the given wallet's path. 45 | pub fn new(wallet: &str) -> Result { 46 | let ar = Self::new_readonly(); 47 | Ok(ar.with_wallet(wallet)) 48 | } 49 | 50 | /// Creates an Arweave storage client without a wallet, used only for downloads. 51 | pub fn new_readonly() -> Self { 52 | Self { 53 | wallet: None, 54 | upload_base_url: Url::parse(DEFAULT_UPLOAD_BASE_URL).unwrap(), 55 | download_base_url: Url::parse(DEFAULT_DOWNLOAD_BASE_URL).unwrap(), 56 | byte_limit: DEFAULT_BYTE_LIMIT, 57 | client: Client::new(), 58 | } 59 | } 60 | 61 | /// Sets the wallet path for the Arweave storage. 62 | pub fn with_wallet(mut self, wallet: &str) -> Self { 63 | self.wallet = Some(PathBuf::from(wallet)); 64 | self 65 | } 66 | 67 | /// Sets the byte limit for the data to be considered for Arweave, default is 1024 bytes (1KB). 68 | /// 69 | /// - If the data exceeds this limit, it will be uploaded to Arweave. 70 | /// - Otherwise, it will be stored as is. 71 | /// 72 | /// If this is too large, you may spend quite a bit of gas fees. 73 | pub fn with_upload_byte_limit(mut self, limit: usize) -> Self { 74 | self.byte_limit = limit; 75 | self 76 | } 77 | 78 | /// Sets the download base URL for Arweave. 79 | /// 80 | /// We don't need to change this usually, as `http://arweave.net` is enough. 81 | pub fn with_download_base_url(mut self, url: &str) -> Result { 82 | self.download_base_url = Url::parse(url).wrap_err("could not parse download base URL")?; 83 | Ok(self) 84 | } 85 | 86 | /// Sets the upload base URL for Arweave. 87 | pub fn with_upload_base_url(mut self, url: &str) -> Result { 88 | self.upload_base_url = Url::parse(url).wrap_err("could not parse upload base URL")?; 89 | Ok(self) 90 | } 91 | 92 | /// Creates a new Arweave instance from the environment variables. 93 | /// 94 | /// - `ARWEAVE_WALLET_PATH` is required 95 | /// - `ARWEAVE_BASE_URL` is optional 96 | /// - `ARWEAVE_BYTE_LIMIT` is optional 97 | /// 98 | /// All these variables have defaults if they are missing. 99 | pub fn new_from_env() -> Result { 100 | // use wallet from env 101 | let wallet = 102 | env::var("ARWEAVE_WALLET_PATH").wrap_err("could not read wallet path from env")?; 103 | let mut ar = Self::new(&wallet)?; 104 | 105 | // get base url if it exists 106 | if let Ok(base_url) = env::var("ARWEAVE_BASE_URL") { 107 | ar = ar.with_upload_base_url(&base_url)?; 108 | } 109 | 110 | // update upload byte limit if needed 111 | if let Ok(byte_limit) = env::var("ARWEAVE_BYTE_LIMIT") { 112 | ar = ar.with_upload_byte_limit(byte_limit.parse().unwrap_or(DEFAULT_BYTE_LIMIT)); 113 | } 114 | 115 | Ok(ar) 116 | } 117 | 118 | /// Puts the value if it is larger than the byte limit. 119 | #[inline] 120 | pub async fn put_if_large(&self, value: Bytes) -> Result { 121 | let value_size = value.len(); 122 | if value_size > self.byte_limit { 123 | log::info!( 124 | "Uploading large ({}B > {}B) value to Arweave", 125 | value_size, 126 | self.byte_limit 127 | ); 128 | let key = self.put(value.clone()).await?; 129 | let key_str = serde_json::to_string(&key).wrap_err("could not serialize key")?; 130 | Ok(key_str.into()) 131 | } else { 132 | Ok(value) 133 | } 134 | } 135 | } 136 | 137 | #[async_trait(?Send)] 138 | impl IsExternalStorage for ArweaveStorage { 139 | type Key = ArweaveKey; 140 | type Value = Bytes; 141 | 142 | async fn get(&self, key: Self::Key) -> Result { 143 | let url = self.download_base_url.join(&key.arweave)?; 144 | 145 | log::debug!("Fetching from Arweave: {}", url); 146 | let response = self 147 | .client 148 | .get(url) 149 | .send() 150 | .await 151 | .wrap_err("failed to fetch from Arweave")?; 152 | 153 | if !response.status().is_success() { 154 | return Err(eyre!("Failed to fetch from Arweave: {}", response.status())); 155 | } 156 | 157 | let response_bytes = response.bytes().await?; 158 | Ok(response_bytes.into()) 159 | } 160 | 161 | async fn put(&self, value: Self::Value) -> Result { 162 | let wallet_path = self 163 | .wallet 164 | .as_ref() 165 | .ok_or_else(|| eyre!("Wallet path is not set"))?; 166 | 167 | #[derive(Debug, serde::Deserialize)] 168 | #[serde(rename_all = "camelCase")] 169 | #[allow(unused)] 170 | struct UploadResponse { 171 | block: u64, 172 | deadline_height: u64, 173 | id: String, 174 | public: String, 175 | signature: String, 176 | timestamp: u64, 177 | validator_signatures: Vec, 178 | version: String, 179 | } 180 | 181 | // ensure that wallet exists 182 | // NOTE: we do this here instead of `new` so that we can work without any wallet 183 | // in case we only want to download data. 184 | if !wallet_path.try_exists()? { 185 | return Err(eyre!("Wallet does not exist at {}.", wallet_path.display())); 186 | } 187 | 188 | // create tag 189 | let base_tag = Tag::new( 190 | "User-Agent", 191 | &format!("{}/{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")), 192 | ); 193 | 194 | // create Arweave currency instance 195 | let currency = ArweaveBuilder::new() 196 | .keypair_path(wallet_path.clone()) 197 | .build()?; 198 | 199 | // create the Bundlr instance 200 | let bundlr = BundlrBuilder::new() 201 | .url(self.upload_base_url.clone()) 202 | .currency(currency) 203 | .fetch_pub_info() 204 | .await? 205 | .build()?; 206 | 207 | // create & sign transaction 208 | let mut tx = bundlr.create_transaction(value.into(), vec![base_tag])?; 209 | bundlr.sign_transaction(&mut tx).await?; 210 | let response_body = bundlr.send_transaction(tx).await?; 211 | let res = serde_json::from_value::(response_body)?; 212 | 213 | log::debug!("Uploaded to Arweave: {:#?}", res); 214 | log::info!("Uploaded at {}", self.upload_base_url.join(&res.id)?); 215 | 216 | // the key is in base64 format, we want to convert that to hexadecimals 217 | Ok(ArweaveKey { arweave: res.id }) 218 | } 219 | 220 | /// Check if key is an Arweave key, which is a JSON object of type `{arweave: string}` 221 | /// where the `arweave` field contains the base64url encoded txid. 222 | /// 223 | /// For example: 224 | /// 225 | /// ```json 226 | /// { arweave: "Zg6CZYfxXCWYnCuKEpnZCYfy7ghit1_v4-BCe53iWuA" } 227 | /// ``` 228 | #[inline(always)] 229 | fn is_key(key: impl AsRef) -> Option { 230 | serde_json::from_str::(key.as_ref()).ok() 231 | } 232 | 233 | #[inline(always)] 234 | fn describe() -> &'static str { 235 | "Arweave" 236 | } 237 | } 238 | 239 | #[cfg(test)] 240 | mod tests { 241 | use super::*; 242 | 243 | #[tokio::test] 244 | #[ignore = "run manually"] 245 | async fn test_download_data() -> Result<()> { 246 | dotenvy::dotenv().unwrap(); 247 | 248 | // https://gateway.irys.xyz/Zg6CZYfxXCWYnCuKEpnZCYfy7ghit1_v4-BCe53iWuA 249 | let tx_id = "Zg6CZYfxXCWYnCuKEpnZCYfy7ghit1_v4-BCe53iWuA".to_string(); 250 | let key = ArweaveKey { arweave: tx_id }; 251 | let arweave = ArweaveStorage::new_from_env()?; 252 | 253 | let result = arweave.get(key).await?; 254 | let val = serde_json::from_slice::(&result)?; 255 | assert_eq!(val, "Hello, Arweave!"); 256 | 257 | Ok(()) 258 | } 259 | 260 | #[tokio::test] 261 | #[ignore = "run manually with Arweave wallet"] 262 | async fn test_upload_and_download_data() -> Result<()> { 263 | dotenvy::dotenv().unwrap(); 264 | 265 | let arweave = ArweaveStorage::new_from_env()?; 266 | let input = b"Hi there Im a test data".to_vec(); 267 | 268 | // put data 269 | let key = arweave.put(input.clone().into()).await?; 270 | println!("{:?}", key); 271 | 272 | // get it again 273 | let result = arweave.get(key).await?; 274 | assert_eq!(input, result); 275 | 276 | Ok(()) 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /storage/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod arweave; 2 | pub use arweave::ArweaveStorage; 3 | 4 | mod traits; 5 | pub use traits::IsExternalStorage; 6 | -------------------------------------------------------------------------------- /storage/src/traits.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use eyre::Result; 3 | 4 | /// A generalized external storage trait. 5 | /// 6 | /// Putting a value should return a unique key, even for the same value uploaded multiple times. 7 | /// Getting a value should be done with that returned key. 8 | /// 9 | /// Note that the `async_trait` has `?Send` specified, as by default it makes them `Send` but Arweave does not have it. 10 | #[async_trait(?Send)] 11 | pub trait IsExternalStorage { 12 | type Key: Clone; 13 | type Value: Clone + std::fmt::Debug; 14 | 15 | /// Returns the value (if exists) at the given key. 16 | /// Returns an error if the key is invalid or the value does not exist. 17 | async fn get(&self, key: Self::Key) -> Result; 18 | 19 | /// Puts the value and returns the generated key. 20 | async fn put(&self, value: Self::Value) -> Result; 21 | 22 | /// Checks if the given string constitutes a key, and returns it. 23 | fn is_key(key: impl AsRef) -> Option; 24 | 25 | /// Describes the implementation. 26 | fn describe() -> &'static str; 27 | } 28 | --------------------------------------------------------------------------------