Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Resolve oracle calls via JSON-RPC #3902

Merged
merged 8 commits into from
Dec 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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