Skip to content

Commit

Permalink
feat: Resolve oracle calls via JSON-RPC (#3902)
Browse files Browse the repository at this point in the history
# Description

This adds a JSON-RPC client to the DefaultForeignCallExecutor. This
allows it to solve unknown oracle calls. The URL to the foreign call
executor is an optional argument to nargo execute, nargo test and nargo
prove.

## Problem\*

Resolves #1052

## Summary\*



## Additional Context
An example echo server in typescript

```typescript
import {
  JSONRPCServer,
  TypedJSONRPCServer,
} from "json-rpc-2.0";
import express from "express";
import bodyParser from "body-parser";

type Methods = {
  echo(params: ForeignCallParam[]): ForeignCallResult;
};

const server: TypedJSONRPCServer<Methods> = new JSONRPCServer(/* ... */);

interface Value {
  inner: string,
}

interface SingleForeignCallParam {
  Single: Value,
}

interface ArrayForeignCallParam {
  Array: Value[],
}

type ForeignCallParam = SingleForeignCallParam | ArrayForeignCallParam;

interface ForeignCallResult {
  values: ForeignCallParam[],
}

server.addMethod("echo", (params) => {
  return {values: params};
});


const app = express();
app.use(bodyParser.json());

app.post("/", (req, res) => {
  const jsonRPCRequest = req.body;
  console.log(jsonRPCRequest);
  // server.receive takes a JSON-RPC request and returns a promise of a JSON-RPC response.
  // It can also receive an array of requests, in which case it may return an array of responses.
  // Alternatively, you can use server.receiveJSON, which takes JSON string as is (in this case req.body).
  server.receive(jsonRPCRequest).then((jsonRPCResponse) => {
    if (jsonRPCResponse) {
      res.json(jsonRPCResponse);
    } else {
      // If response is absent, it was a JSON-RPC notification method.
      // Respond with no content status (204).
      res.sendStatus(204);
    }
  });
});

app.listen(5555);
```
And the corresponding main.nr
```rust
#[oracle(echo)]
fn echo_oracle(_x: Field) -> Field {}

unconstrained fn echo(x: Field) -> Field {
    echo_oracle(x)
}

fn main(x: Field, y: pub Field) {
    assert(echo(x) == y);
}
```

## Documentation\*

Check one:
- [ ] No documentation needed.
- [ ] Documentation included in this PR.
- [ ] **[Exceptional Case]** Documentation to be submitted in a separate
PR.

(We can document this when we evaluate if this solution is enough)

# PR Checklist\*

- [x] I have tested the changes locally.
- [x] I have formatted the changes with [Prettier](https://prettier.io/)
and/or `cargo fmt` on default settings.

---------

Co-authored-by: thunkar <[email protected]>
  • Loading branch information
sirasistant and Thunkar authored Dec 22, 2023
1 parent 2fcbef5 commit cc44613
Show file tree
Hide file tree
Showing 16 changed files with 687 additions and 118 deletions.
565 changes: 481 additions & 84 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ num-bigint = "0.4"
num-traits = "0.2"
similar-asserts = "1.5.0"
tempfile = "3.6.0"
jsonrpc = { version = "0.16.0", features = ["minreq_http"] }

tracing = "0.1.40"
tracing-web = "0.1.3"
Expand Down
5 changes: 3 additions & 2 deletions acvm-repo/acir_field/src/generic_ark.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use ark_ff::PrimeField;
use ark_ff::Zero;
use num_bigint::BigUint;
use serde::{Deserialize, Serialize};
use std::borrow::Cow;

// XXX: Switch out for a trait and proper implementations
// This implementation is in-efficient, can definitely remove hex usage and Iterator instances for trivial functionality
Expand Down Expand Up @@ -125,8 +126,8 @@ impl<'de, T: ark_ff::PrimeField> Deserialize<'de> for FieldElement<T> {
where
D: serde::Deserializer<'de>,
{
let s = <&str>::deserialize(deserializer)?;
match Self::from_hex(s) {
let s: Cow<'de, str> = Deserialize::deserialize(deserializer)?;
match Self::from_hex(&s) {
Some(value) => Ok(value),
None => Err(serde::de::Error::custom(format!("Invalid hex for FieldElement: {s}",))),
}
Expand Down
1 change: 1 addition & 0 deletions compiler/noirc_printable_type/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ regex = "1.9.1"
serde.workspace = true
serde_json.workspace = true
thiserror.workspace = true
jsonrpc.workspace = true

[dev-dependencies]
3 changes: 3 additions & 0 deletions compiler/noirc_printable_type/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ pub enum ForeignCallError {

#[error("Could not parse PrintableType argument. {0}")]
ParsingError(#[from] serde_json::Error),

#[error("Failed calling external resolver. {0}")]
ExternalResolverError(#[from] jsonrpc::Error),
}

impl TryFrom<&[ForeignCallParam]> for PrintableValueDisplay {
Expand Down
1 change: 1 addition & 0 deletions deny.toml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ exceptions = [
# so we prefer to not have dependencies using it
# https://tldrlegal.com/license/creative-commons-cc0-1.0-universal
{ allow = ["CC0-1.0"], name = "more-asserts" },
{ allow = ["CC0-1.0"], name = "jsonrpc" },
{ allow = ["MPL-2.0"], name = "sized-chunks" },
{ allow = ["MPL-2.0"], name = "webpki-roots" },

Expand Down
6 changes: 3 additions & 3 deletions tooling/debugger/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -529,7 +529,7 @@ mod tests {
circuit,
debug_artifact,
initial_witness,
Box::new(DefaultForeignCallExecutor::new(true)),
Box::new(DefaultForeignCallExecutor::new(true, None)),
);

assert_eq!(context.get_current_opcode_location(), Some(OpcodeLocation::Acir(0)));
Expand Down Expand Up @@ -623,7 +623,7 @@ mod tests {
circuit,
debug_artifact,
initial_witness,
Box::new(DefaultForeignCallExecutor::new(true)),
Box::new(DefaultForeignCallExecutor::new(true, None)),
);

// set breakpoint
Expand Down Expand Up @@ -673,7 +673,7 @@ mod tests {
&circuit,
&debug_artifact,
WitnessMap::new(),
Box::new(DefaultForeignCallExecutor::new(true)),
Box::new(DefaultForeignCallExecutor::new(true, None)),
);

assert_eq!(context.offset_opcode_location(&None, 0), (None, 0));
Expand Down
2 changes: 1 addition & 1 deletion tooling/debugger/src/dap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ impl<'a, R: Read, W: Write, B: BlackBoxFunctionSolver> DapSession<'a, R, W, B> {
circuit,
debug_artifact,
initial_witness,
Box::new(DefaultForeignCallExecutor::new(true)),
Box::new(DefaultForeignCallExecutor::new(true, None)),
);
Self {
server,
Expand Down
4 changes: 2 additions & 2 deletions tooling/debugger/src/repl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ impl<'a, B: BlackBoxFunctionSolver> ReplDebugger<'a, B> {
circuit,
debug_artifact,
initial_witness.clone(),
Box::new(DefaultForeignCallExecutor::new(true)),
Box::new(DefaultForeignCallExecutor::new(true, None)),
);
Self {
context,
Expand Down Expand Up @@ -278,7 +278,7 @@ impl<'a, B: BlackBoxFunctionSolver> ReplDebugger<'a, B> {
self.circuit,
self.debug_artifact,
self.initial_witness.clone(),
Box::new(DefaultForeignCallExecutor::new(true)),
Box::new(DefaultForeignCallExecutor::new(true, None)),
);
for opcode_location in breakpoints {
self.context.add_breakpoint(opcode_location);
Expand Down
10 changes: 8 additions & 2 deletions tooling/lsp/src/requests/test_run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,14 @@ fn on_test_run_request_inner(
)
})?;

let test_result =
run_test(&state.solver, &context, test_function, false, &CompileOptions::default());
let test_result = run_test(
&state.solver,
&context,
test_function,
false,
None,
&CompileOptions::default(),
);
let result = match test_result {
TestStatus::Pass => NargoTestRunResult {
id: params.id.clone(),
Expand Down
8 changes: 7 additions & 1 deletion tooling/nargo/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,14 @@ thiserror.workspace = true
codespan-reporting.workspace = true
tracing.workspace = true
rayon = "1.8.0"
jsonrpc.workspace = true

[dev-dependencies]
# TODO: This dependency is used to generate unit tests for `get_all_paths_in_dir`
# TODO: once that method is moved to nargo_cli, we can move this dependency to nargo_cli
tempfile.workspace = true
tempfile.workspace = true
jsonrpc-http-server = "18.0"
jsonrpc-core-client = "18.0"
jsonrpc-derive = "18.0"
jsonrpc-core = "18.0"
serial_test = "2.0"
153 changes: 137 additions & 16 deletions tooling/nargo/src/ops/foreign_calls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use acvm::{
acir::brillig::{ForeignCallParam, ForeignCallResult, Value},
pwg::ForeignCallWaitInfo,
};
use jsonrpc::{arg as build_json_rpc_arg, minreq_http::Builder, Client};
use noirc_printable_type::{decode_string_value, ForeignCallError, PrintableValueDisplay};

pub trait ForeignCallExecutor {
Expand Down Expand Up @@ -94,11 +95,22 @@ pub struct DefaultForeignCallExecutor {
mocked_responses: Vec<MockedCall>,
/// Whether to print [`ForeignCall::Print`] output.
show_output: bool,
/// JSON RPC client to resolve foreign calls
external_resolver: Option<Client>,
}

impl DefaultForeignCallExecutor {
pub fn new(show_output: bool) -> Self {
DefaultForeignCallExecutor { show_output, ..DefaultForeignCallExecutor::default() }
pub fn new(show_output: bool, resolver_url: Option<&str>) -> Self {
let oracle_resolver = resolver_url.map(|resolver_url| {
let transport_builder =
Builder::new().url(resolver_url).expect("Invalid oracle resolver URL");
Client::with_transport(transport_builder.build())
});
DefaultForeignCallExecutor {
show_output,
external_resolver: oracle_resolver,
..DefaultForeignCallExecutor::default()
}
}
}

Expand Down Expand Up @@ -190,27 +202,136 @@ impl ForeignCallExecutor for DefaultForeignCallExecutor {
Ok(ForeignCallResult { values: vec![] })
}
None => {
let response_position = self
let mock_response_position = self
.mocked_responses
.iter()
.position(|response| response.matches(foreign_call_name, &foreign_call.inputs))
.unwrap_or_else(|| panic!("Unknown foreign call {}", foreign_call_name));
.position(|response| response.matches(foreign_call_name, &foreign_call.inputs));

let mock = self
.mocked_responses
.get_mut(response_position)
.expect("Invalid position of mocked response");
let result = mock.result.values.clone();

if let Some(times_left) = &mut mock.times_left {
*times_left -= 1;
if *times_left == 0 {
self.mocked_responses.remove(response_position);
match (mock_response_position, &self.external_resolver) {
(Some(response_position), _) => {
let mock = self
.mocked_responses
.get_mut(response_position)
.expect("Invalid position of mocked response");
let result = mock.result.values.clone();

if let Some(times_left) = &mut mock.times_left {
*times_left -= 1;
if *times_left == 0 {
self.mocked_responses.remove(response_position);
}
}

Ok(ForeignCallResult { values: result })
}
(None, Some(external_resolver)) => {
let encoded_params: Vec<_> =
foreign_call.inputs.iter().map(build_json_rpc_arg).collect();

let req =
external_resolver.build_request(foreign_call_name, &encoded_params);

let response = external_resolver.send_request(req)?;

let parsed_response: ForeignCallResult = response.result()?;

Ok(parsed_response)
}
(None, None) => panic!("Unknown foreign call {}", foreign_call_name),
}
}
}
}
}

#[cfg(test)]
mod tests {
use acvm::{
acir::brillig::ForeignCallParam,
brillig_vm::brillig::{ForeignCallResult, Value},
pwg::ForeignCallWaitInfo,
FieldElement,
};
use jsonrpc_core::Result as RpcResult;
use jsonrpc_derive::rpc;
use jsonrpc_http_server::{Server, ServerBuilder};
use serial_test::serial;

use crate::ops::{DefaultForeignCallExecutor, ForeignCallExecutor};

Ok(ForeignCallResult { values: result })
#[allow(unreachable_pub)]
#[rpc]
pub trait OracleResolver {
#[rpc(name = "echo")]
fn echo(&self, param: ForeignCallParam) -> RpcResult<ForeignCallResult>;

#[rpc(name = "sum")]
fn sum(&self, array: ForeignCallParam) -> RpcResult<ForeignCallResult>;
}

struct OracleResolverImpl;

impl OracleResolver for OracleResolverImpl {
fn echo(&self, param: ForeignCallParam) -> RpcResult<ForeignCallResult> {
Ok(vec![param].into())
}

fn sum(&self, array: ForeignCallParam) -> RpcResult<ForeignCallResult> {
let mut res: FieldElement = 0_usize.into();

for value in array.values() {
res += value.to_field();
}

Ok(Value::from(res).into())
}
}

fn build_oracle_server() -> (Server, String) {
let mut io = jsonrpc_core::IoHandler::new();
io.extend_with(OracleResolverImpl.to_delegate());

let server = ServerBuilder::new(io)
.start_http(&"127.0.0.1:5555".parse().expect("Invalid address"))
.expect("Could not start server");

let url = format!("http://{}", server.address());
(server, url)
}

#[serial]
#[test]
fn test_oracle_resolver_echo() {
let (server, url) = build_oracle_server();

let mut executor = DefaultForeignCallExecutor::new(false, Some(&url));

let foreign_call = ForeignCallWaitInfo {
function: "echo".to_string(),
inputs: vec![ForeignCallParam::Single(1_u128.into())],
};

let result = executor.execute(&foreign_call);
assert_eq!(result.unwrap(), ForeignCallResult { values: foreign_call.inputs });

server.close();
}

#[serial]
#[test]
fn test_oracle_resolver_sum() {
let (server, url) = build_oracle_server();

let mut executor = DefaultForeignCallExecutor::new(false, Some(&url));

let foreign_call = ForeignCallWaitInfo {
function: "sum".to_string(),
inputs: vec![ForeignCallParam::Array(vec![1_usize.into(), 2_usize.into()])],
};

let result = executor.execute(&foreign_call);
assert_eq!(result.unwrap(), Value::from(3_usize).into());

server.close();
}
}
3 changes: 2 additions & 1 deletion tooling/nargo/src/ops/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pub fn run_test<B: BlackBoxFunctionSolver>(
context: &Context,
test_function: TestFunction,
show_output: bool,
foreign_call_resolver_url: Option<&str>,
config: &CompileOptions,
) -> TestStatus {
let program = compile_no_check(context, config, test_function.get_id(), None, false);
Expand All @@ -30,7 +31,7 @@ pub fn run_test<B: BlackBoxFunctionSolver>(
&program.circuit,
WitnessMap::new(),
blackbox_solver,
&mut DefaultForeignCallExecutor::new(show_output),
&mut DefaultForeignCallExecutor::new(show_output, foreign_call_resolver_url),
);
test_status_program_compile_pass(test_function, program.debug, circuit_execution)
}
Expand Down
18 changes: 14 additions & 4 deletions tooling/nargo_cli/src/cli/execute_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ pub(crate) struct ExecuteCommand {

#[clap(flatten)]
compile_options: CompileOptions,

/// JSON RPC url to solve oracle calls
#[clap(long)]
oracle_resolver: Option<String>,
}

pub(crate) fn run(
Expand Down Expand Up @@ -73,8 +77,12 @@ pub(crate) fn run(
expression_width,
)?;

let (return_value, solved_witness) =
execute_program_and_decode(compiled_program, package, &args.prover_name)?;
let (return_value, solved_witness) = execute_program_and_decode(
compiled_program,
package,
&args.prover_name,
args.oracle_resolver.as_deref(),
)?;

println!("[{}] Circuit witness successfully solved", package.name);
if let Some(return_value) = return_value {
Expand All @@ -93,11 +101,12 @@ fn execute_program_and_decode(
program: CompiledProgram,
package: &Package,
prover_name: &str,
foreign_call_resolver_url: Option<&str>,
) -> Result<(Option<InputValue>, WitnessMap), CliError> {
// Parse the initial witness values from Prover.toml
let (inputs_map, _) =
read_inputs_from_file(&package.root_dir, prover_name, Format::Toml, &program.abi)?;
let solved_witness = execute_program(&program, &inputs_map)?;
let solved_witness = execute_program(&program, &inputs_map, foreign_call_resolver_url)?;
let public_abi = program.abi.public_abi();
let (_, return_value) = public_abi.decode(&solved_witness)?;

Expand All @@ -107,6 +116,7 @@ fn execute_program_and_decode(
pub(crate) fn execute_program(
compiled_program: &CompiledProgram,
inputs_map: &InputMap,
foreign_call_resolver_url: Option<&str>,
) -> Result<WitnessMap, CliError> {
let blackbox_solver = Bn254BlackBoxSolver::new();

Expand All @@ -116,7 +126,7 @@ pub(crate) fn execute_program(
&compiled_program.circuit,
initial_witness,
&blackbox_solver,
&mut DefaultForeignCallExecutor::new(true),
&mut DefaultForeignCallExecutor::new(true, foreign_call_resolver_url),
);
match solved_witness_err {
Ok(solved_witness) => Ok(solved_witness),
Expand Down
Loading

0 comments on commit cc44613

Please sign in to comment.