diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eb5a316 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +target diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..d341fd9 --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +scarb dev-2025-07-14 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0d49e12 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2025 Software Mansion + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..df2d532 --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +# Oracle + +This library provides type-safe interfaces for interacting with external oracles in Cairo applications. + +```toml +[dependencies] +oracle = "0.1.0-dev.1" +``` + +_Language support: requires Cairo 2.13+_ diff --git a/Scarb.lock b/Scarb.lock new file mode 100644 index 0000000..1575674 --- /dev/null +++ b/Scarb.lock @@ -0,0 +1,6 @@ +# Code generated by scarb DO NOT EDIT. +version = 1 + +[[package]] +name = "oracle" +version = "0.1.0-dev.1" diff --git a/Scarb.toml b/Scarb.toml new file mode 100644 index 0000000..1ffd6cf --- /dev/null +++ b/Scarb.toml @@ -0,0 +1,2 @@ +[workspace] +members = ["oracle"] diff --git a/oracle/Scarb.toml b/oracle/Scarb.toml new file mode 100644 index 0000000..8d3838a --- /dev/null +++ b/oracle/Scarb.toml @@ -0,0 +1,17 @@ +# TODO: Set cairo-version to 2.13 or whatever ships oracle cheatcode. + +[package] +name = "oracle" +version = "0.1.0-dev.1" +edition = "2024_07" +cairo-version = "2.11.4" +description = "Interfaces for calling external oracles" +authors = ["Software Mansion "] +homepage = "https://docs.swmansion.com/scarb" +license = "MIT" +repository = "https://github.com/software-mansion/cairo-oracle" + +[dependencies] + +[dev-dependencies] +cairo_test = "2.11.4" diff --git a/oracle/src/lib.cairo b/oracle/src/lib.cairo new file mode 100644 index 0000000..b8f6303 --- /dev/null +++ b/oracle/src/lib.cairo @@ -0,0 +1,167 @@ +//! This library provides the core functionality for interacting with **oracles** in Cairo. +//! Oracles are external, untrusted processes that can be called from Cairo code to fetch data or +//! perform computations not possible within the VM, like accessing web APIs or local files. +//! +//! ## Feature status +//! +//! As of the date when this package version has been released, oracle support in Scarb is +//! **experimental**. It must be enabled with `scarb execute --experimental-oracles` or by setting +//! the `SCARB_EXPERIMENTAL_ORACLES=1` environment variable. Both the API and protocol are +//! experimental and may change in future releases. +//! +//! ## What is an oracle? +//! +//! An oracle is an external process (like a script, binary, or web service) that exposes custom +//! logic or data to a Cairo program. You use it to perform tasks the Cairo VM cannot, such as +//! accessing real-world data or executing complex, non-provable computations. +//! +//! **IMPORTANT:** The execution of an oracle occurs **outside** of the Cairo VM. Consequently, its +//! operations are **not included** in the execution trace and are **not verified by the proof**. +//! The proof only validates that a call was made to an oracle and that your program correctly +//! handled the data it received. It provides no guarantee whatsoever that the data itself is +//! accurate or legitimate. +//! +//! ## How are oracles executed? +//! +//! Oracle execution is managed by the Cairo runtime (e.g., the `scarb execute`). The runtime is +//! responsible for interpreting the connection string and facilitating the communication between +//! the Cairo program and the external process. +//! +//! While the specific protocols are runtime-dependent, here are the common schemes: +//! - `stdio:./path/to/binary`: The runtime executes a local binary and pipes data between your +//! Cairo program and the process's standard input (stdin) and standard output (stdout). +//! - `stdio:python3 ./my_oracle.py`: The runtime executes a command with arguments, allowing for +//! more flexible process invocation. +//! - `stdio:npx -y my_oracle`: The runtime can execute package managers or other command-line +//! tools. +//! - `builtin:name`: The runtime may provide pre-compiled, optimized "builtin" oracles for common +//! tasks. For example, `builtin:fs` may refer to a runtime-provided oracle for filesystem +//! operations, which is more efficient and secure than invoking a generic script. +//! +//! Always consult your specific runtime's documentation for a complete list of supported protocols +//! and available built-in oracles. +//! +//! ## Never trust your oracle! +//! +//! This is the most important security principle in this library. Because oracle execution is not +//! proven, you must operate under the assumption that an oracle can be malicious or compromised. An +//! attacker can intercept or control the oracle to return arbitrary, invalid, or harmful data. +//! +//! Your Cairo code is the only line of defense. It is your responsibility to validate and verify +//! any data returned by an oracle before it is used in any state-changing logic. +//! +//! **Always treat oracle responses as untrusted input.** For example, if your program expects a +//! sorted list of values, it must immediately verify that the list is indeed sorted. Failure to do +//! so creates a critical security vulnerability. + +use core::fmt; +use core::result::Result as CoreResult; +use starknet::testing::cheatcode; + +/// Invokes an external oracle process and returns its result. +/// +/// Avoid calling this function directly in user code. Instead, write oracle interface modules, +/// which group all single oracle features together. +/// +/// To use an oracle, call `invoke` with: +/// 1. `connection_string`: A string describing how to connect to the oracle. The execution runtime +/// handles oracle process management transparently under the hood. Consult your runtime +/// documentation for details what protocols and options are supported. For stdio-based oracles, +/// this can be a path to an executable (e.g., `"stdio:./my_oracle"`), a command with arguments +/// (e.g., `"stdio:python3 ./my_oracle.py"`), or package manager invocations (e.g., `"stdio:npx +/// -y my_oracle"`). +/// 2. `selector`: The name or identifier of the method to invoke on the oracle (as short string). +/// It acts as a function name or command within the oracle process. +/// 3. `calldata`: The arguments to pass to the oracle method, as a serializable Cairo type. To pass +/// multiple arguments, use a tuple or struct that implements `Serde`. +/// +/// The function returns a `Result`, where `R` is the expected return type, or an +/// error if the invocation fails or the oracle returns an error. +/// +/// ```cairo +/// mod math_oracle { +/// pub type Result = oracle::Result; +/// +/// pub fn pow(x: u64, n: u32) -> Result { +/// oracle::invoke("stdio:python3 ./my_math_oracle.py", 'pow', (x, n)) +/// } +/// +/// pub fn sqrt(x: u64) -> Result { +/// oracle::invoke("stdio:python3 ./my_math_oracle.py", 'sqrt', x) +/// } +/// } +/// +/// mod fs_oracle { +/// pub type Result = oracle::Result; +/// +/// pub fn fs_read(path: ByteArray) -> Result { +/// oracle::invoke("builtin:fs", 'read', path) +/// } +/// +/// pub fn fs_exists(path: ByteArray) -> Result { +/// oracle::invoke("builtin:fs", 'exists', path) +/// } +/// } +/// ``` +pub fn invoke, +Drop, +Serde, R, +Serde>( + connection_string: ByteArray, selector: felt252, calldata: T, +) -> Result { + let mut input: Array = array![]; + connection_string.serialize(ref input); + selector.serialize(ref input); + calldata.serialize(ref input); + + let mut output = cheatcode::<'oracle_invoke'>(input.span()); + + // `unwrap_or_else` requires +Drop, which we do not ask for: + // https://github.com/software-mansion/cairo-lint/issues/387 + #[allow(manual_unwrap_or)] + match Serde::>::deserialize(ref output) { + Option::Some(result) => result, + Option::None => Err(deserialization_error()), + } +} + +/// `Result` +pub type Result = CoreResult; + +/// An error type that can be raised when invoking oracles. +/// +/// The internal structure of this type is opaque, but it can be displayed and (de)serialized. +#[derive(Drop, Clone, PartialEq, Serde)] +pub struct Error { + message: ByteArray, +} + +fn deserialization_error() -> Error { + Error { message: "failed to deserialize oracle response" } +} + +impl DisplayError of fmt::Display { + fn fmt(self: @Error, ref f: fmt::Formatter) -> CoreResult<(), fmt::Error> { + fmt::Display::fmt(self.message, ref f) + } +} + +impl DebugError of fmt::Debug { + fn fmt(self: @Error, ref f: fmt::Formatter) -> CoreResult<(), fmt::Error> { + write!(f, "oracle::Error({:?})", self.message) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_result_error_serde() { + let mut serialized: Array = array![]; + let original_error: Result<()> = Result::Err(Error { message: "abcdef" }); + original_error.serialize(ref serialized); + assert_eq!(serialized, array![1, 0, 107075202213222, 6]); + + let mut span = serialized.span(); + let deserialized = Serde::>::deserialize(ref span).unwrap(); + assert_eq!(deserialized, original_error); + } +}