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

chore: refactor foreign call executors #6659

Merged
merged 2 commits into from
Dec 2, 2024
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
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 @@ -69,6 +69,9 @@

#[derive(Debug, Error)]
pub enum ForeignCallError {
#[error("No handler could be found for foreign call `{0}`")]
NoHandler(String),

#[error("Foreign call inputs needed for execution are missing")]
MissingForeignCallInputs,

Expand All @@ -78,7 +81,7 @@
#[error("Failed calling external resolver. {0}")]
ExternalResolverError(#[from] jsonrpc::Error),

#[error("Assert message resolved after an unsatisified constrain. {0}")]

Check warning on line 84 in compiler/noirc_printable_type/src/lib.rs

View workflow job for this annotation

GitHub Actions / Code

Unknown word (unsatisified)
ResolvedAssertMessage(String),
}

Expand Down Expand Up @@ -230,12 +233,12 @@

(PrintableValue::Vec { array_elements, .. }, PrintableType::Tuple { types }) => {
output.push('(');
let mut elems = array_elements.iter().zip(types).peekable();

Check warning on line 236 in compiler/noirc_printable_type/src/lib.rs

View workflow job for this annotation

GitHub Actions / Code

Unknown word (elems)
while let Some((value, typ)) = elems.next() {

Check warning on line 237 in compiler/noirc_printable_type/src/lib.rs

View workflow job for this annotation

GitHub Actions / Code

Unknown word (elems)
output.push_str(
&PrintableValueDisplay::Plain(value.clone(), typ.clone()).to_string(),
);
if elems.peek().is_some() {

Check warning on line 241 in compiler/noirc_printable_type/src/lib.rs

View workflow job for this annotation

GitHub Actions / Code

Unknown word (elems)
output.push_str(", ");
}
}
Expand Down
2 changes: 1 addition & 1 deletion tooling/acvm_cli/src/cli/execute_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use clap::Args;

use crate::cli::fs::inputs::{read_bytecode_from_file, read_inputs_from_file};
use crate::errors::CliError;
use nargo::ops::{execute_program, DefaultForeignCallExecutor};
use nargo::{foreign_calls::DefaultForeignCallExecutor, ops::execute_program};

use super::fs::witness::{create_output_witness_string, save_witness_to_dir};

Expand Down
2 changes: 1 addition & 1 deletion tooling/debugger/src/foreign_calls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use acvm::{
pwg::ForeignCallWaitInfo,
AcirField, FieldElement,
};
use nargo::ops::{DefaultForeignCallExecutor, ForeignCallExecutor};
use nargo::foreign_calls::{DefaultForeignCallExecutor, ForeignCallExecutor};
use noirc_artifacts::debug::{DebugArtifact, DebugVars, StackFrame};
use noirc_errors::debug_info::{DebugFnId, DebugVarId};
use noirc_printable_type::ForeignCallError;
Expand Down
176 changes: 176 additions & 0 deletions tooling/nargo/src/foreign_calls/mocker.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
use acvm::{
acir::brillig::{ForeignCallParam, ForeignCallResult},
pwg::ForeignCallWaitInfo,
AcirField,
};
use noirc_printable_type::{decode_string_value, ForeignCallError};
use serde::{Deserialize, Serialize};

use super::{ForeignCall, ForeignCallExecutor};

/// This struct represents an oracle mock. It can be used for testing programs that use oracles.
#[derive(Debug, PartialEq, Eq, Clone)]
struct MockedCall<F> {
/// The id of the mock, used to update or remove it
id: usize,
/// The oracle it's mocking
name: String,
/// Optionally match the parameters
params: Option<Vec<ForeignCallParam<F>>>,
/// The parameters with which the mock was last called
last_called_params: Option<Vec<ForeignCallParam<F>>>,
/// The result to return when this mock is called
result: ForeignCallResult<F>,
/// How many times should this mock be called before it is removed
times_left: Option<u64>,
}

impl<F> MockedCall<F> {
fn new(id: usize, name: String) -> Self {
Self {
id,
name,
params: None,
last_called_params: None,
result: ForeignCallResult { values: vec![] },
times_left: None,
}
}
}

impl<F: PartialEq> MockedCall<F> {
fn matches(&self, name: &str, params: &[ForeignCallParam<F>]) -> bool {
self.name == name && (self.params.is_none() || self.params.as_deref() == Some(params))
}
}

#[derive(Debug, Default)]
pub(crate) struct MockForeignCallExecutor<F> {
/// Mocks have unique ids used to identify them in Noir, allowing to update or remove them.
last_mock_id: usize,
/// The registered mocks
mocked_responses: Vec<MockedCall<F>>,
}

impl<F: AcirField> MockForeignCallExecutor<F> {
fn extract_mock_id(
foreign_call_inputs: &[ForeignCallParam<F>],
) -> Result<(usize, &[ForeignCallParam<F>]), ForeignCallError> {
let (id, params) =
foreign_call_inputs.split_first().ok_or(ForeignCallError::MissingForeignCallInputs)?;
let id =
usize::try_from(id.unwrap_field().try_to_u64().expect("value does not fit into u64"))
.expect("value does not fit into usize");
Ok((id, params))
}

fn find_mock_by_id(&self, id: usize) -> Option<&MockedCall<F>> {
self.mocked_responses.iter().find(|response| response.id == id)
}

fn find_mock_by_id_mut(&mut self, id: usize) -> Option<&mut MockedCall<F>> {
self.mocked_responses.iter_mut().find(|response| response.id == id)
}

fn parse_string(param: &ForeignCallParam<F>) -> String {
let fields: Vec<_> = param.fields().to_vec();
decode_string_value(&fields)
}
}

impl<F: AcirField + Serialize + for<'a> Deserialize<'a>> ForeignCallExecutor<F>
for MockForeignCallExecutor<F>
{
fn execute(
&mut self,
foreign_call: &ForeignCallWaitInfo<F>,
) -> Result<ForeignCallResult<F>, ForeignCallError> {
let foreign_call_name = foreign_call.function.as_str();
match ForeignCall::lookup(foreign_call_name) {
Some(ForeignCall::CreateMock) => {
let mock_oracle_name = Self::parse_string(&foreign_call.inputs[0]);
assert!(ForeignCall::lookup(&mock_oracle_name).is_none());
let id = self.last_mock_id;
self.mocked_responses.push(MockedCall::new(id, mock_oracle_name));
self.last_mock_id += 1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not obvious to me why the mocks aren't stored in a BTreeMap indexed by ID. I looks like we won't have repeated IDs; I thought there might be multiple single-use responses lined up for the same ID, but not sure that's a use case.

Copy link
Member Author

@TomAFrench TomAFrench Dec 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our testing of mocked oracle calls is woefully incomplete atm so this may not be the ideal implementation. Let's handle this in a followup.


Ok(F::from(id).into())
}
Some(ForeignCall::SetMockParams) => {
let (id, params) = Self::extract_mock_id(&foreign_call.inputs)?;
self.find_mock_by_id_mut(id)
.unwrap_or_else(|| panic!("Unknown mock id {}", id))
.params = Some(params.to_vec());

Ok(ForeignCallResult::default())
}
Some(ForeignCall::GetMockLastParams) => {
let (id, _) = Self::extract_mock_id(&foreign_call.inputs)?;
let mock =
self.find_mock_by_id(id).unwrap_or_else(|| panic!("Unknown mock id {}", id));

let last_called_params = mock
.last_called_params
.clone()
.unwrap_or_else(|| panic!("Mock {} was never called", mock.name));

Ok(last_called_params.into())
}
Some(ForeignCall::SetMockReturns) => {
let (id, params) = Self::extract_mock_id(&foreign_call.inputs)?;
self.find_mock_by_id_mut(id)
.unwrap_or_else(|| panic!("Unknown mock id {}", id))
.result = ForeignCallResult { values: params.to_vec() };

Ok(ForeignCallResult::default())
}
Some(ForeignCall::SetMockTimes) => {
let (id, params) = Self::extract_mock_id(&foreign_call.inputs)?;
let times =
params[0].unwrap_field().try_to_u64().expect("Invalid bit size of times");

self.find_mock_by_id_mut(id)
.unwrap_or_else(|| panic!("Unknown mock id {}", id))
.times_left = Some(times);

Ok(ForeignCallResult::default())
}
Some(ForeignCall::ClearMock) => {
let (id, _) = Self::extract_mock_id(&foreign_call.inputs)?;
self.mocked_responses.retain(|response| response.id != id);
Ok(ForeignCallResult::default())
}
_ => {
let mock_response_position = self
.mocked_responses
.iter()
.position(|response| response.matches(foreign_call_name, &foreign_call.inputs));

if let Some(response_position) = mock_response_position {
// If the program has registered a mocked response to this oracle call then we prefer responding
// with that.

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

mock.last_called_params = Some(foreign_call.inputs.clone());

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(result.into())
} else {
Err(ForeignCallError::NoHandler(foreign_call_name.to_string()))
}
}
}
}
}
146 changes: 146 additions & 0 deletions tooling/nargo/src/foreign_calls/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
use std::path::PathBuf;

use acvm::{acir::brillig::ForeignCallResult, pwg::ForeignCallWaitInfo, AcirField};
use mocker::MockForeignCallExecutor;
use noirc_printable_type::ForeignCallError;
use print::PrintForeignCallExecutor;
use rand::Rng;
use rpc::RPCForeignCallExecutor;
use serde::{Deserialize, Serialize};

mod mocker;
mod print;
mod rpc;

pub trait ForeignCallExecutor<F> {
fn execute(
&mut self,
foreign_call: &ForeignCallWaitInfo<F>,
) -> Result<ForeignCallResult<F>, ForeignCallError>;
}

/// This enumeration represents the Brillig foreign calls that are natively supported by nargo.
/// After resolution of a foreign call, nargo will restart execution of the ACVM
pub enum ForeignCall {
Print,
CreateMock,
SetMockParams,
GetMockLastParams,
SetMockReturns,
SetMockTimes,
ClearMock,
}

impl std::fmt::Display for ForeignCall {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name())
}
}

impl ForeignCall {
pub(crate) fn name(&self) -> &'static str {
match self {
ForeignCall::Print => "print",
ForeignCall::CreateMock => "create_mock",
ForeignCall::SetMockParams => "set_mock_params",
ForeignCall::GetMockLastParams => "get_mock_last_params",
ForeignCall::SetMockReturns => "set_mock_returns",
ForeignCall::SetMockTimes => "set_mock_times",
ForeignCall::ClearMock => "clear_mock",
}
}

pub(crate) fn lookup(op_name: &str) -> Option<ForeignCall> {
match op_name {
"print" => Some(ForeignCall::Print),
"create_mock" => Some(ForeignCall::CreateMock),
"set_mock_params" => Some(ForeignCall::SetMockParams),
"get_mock_last_params" => Some(ForeignCall::GetMockLastParams),
"set_mock_returns" => Some(ForeignCall::SetMockReturns),
"set_mock_times" => Some(ForeignCall::SetMockTimes),
"clear_mock" => Some(ForeignCall::ClearMock),
_ => None,
}
}
}

#[derive(Debug, Default)]
pub struct DefaultForeignCallExecutor<F> {
/// The executor for any [`ForeignCall::Print`] calls.
printer: Option<PrintForeignCallExecutor>,
mocker: MockForeignCallExecutor<F>,
external: Option<RPCForeignCallExecutor>,
}

impl<F: Default> DefaultForeignCallExecutor<F> {
pub fn new(
show_output: bool,
resolver_url: Option<&str>,
root_path: Option<PathBuf>,
package_name: Option<String>,
) -> Self {
let id = rand::thread_rng().gen();
let printer = if show_output { Some(PrintForeignCallExecutor) } else { None };
let external_resolver = resolver_url.map(|resolver_url| {
RPCForeignCallExecutor::new(resolver_url, id, root_path, package_name)
});
DefaultForeignCallExecutor {
printer,
mocker: MockForeignCallExecutor::default(),
external: external_resolver,
}
}
}

impl<F: AcirField + Serialize + for<'a> Deserialize<'a>> ForeignCallExecutor<F>
for DefaultForeignCallExecutor<F>
{
fn execute(
&mut self,
foreign_call: &ForeignCallWaitInfo<F>,
) -> Result<ForeignCallResult<F>, ForeignCallError> {
let foreign_call_name = foreign_call.function.as_str();
match ForeignCall::lookup(foreign_call_name) {
Some(ForeignCall::Print) => {
if let Some(printer) = &mut self.printer {
printer.execute(foreign_call)
} else {
Ok(ForeignCallResult::default())
}
}
Some(
ForeignCall::CreateMock
| ForeignCall::SetMockParams
| ForeignCall::GetMockLastParams
| ForeignCall::SetMockReturns
| ForeignCall::SetMockTimes
| ForeignCall::ClearMock,
) => self.mocker.execute(foreign_call),

None => {
// First check if there's any defined mock responses for this foreign call.
match self.mocker.execute(foreign_call) {
Err(ForeignCallError::NoHandler(_)) => (),
response_or_error => return response_or_error,
};

if let Some(external_resolver) = &mut self.external {
// If the user has registered an external resolver then we forward any remaining oracle calls there.
match external_resolver.execute(foreign_call) {
Err(ForeignCallError::NoHandler(_)) => (),
response_or_error => return response_or_error,
};
}

// If all executors have no handler for the given foreign call then we cannot
// return a correct response to the ACVM. The best we can do is to return an empty response,
// this allows us to ignore any foreign calls which exist solely to pass information from inside
// the circuit to the environment (e.g. custom logging) as the execution will still be able to progress.
//
// We optimistically return an empty response for all oracle calls as the ACVM will error
// should a response have been required.
Ok(ForeignCallResult::default())
}
}
}
}
36 changes: 36 additions & 0 deletions tooling/nargo/src/foreign_calls/print.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use acvm::{acir::brillig::ForeignCallResult, pwg::ForeignCallWaitInfo, AcirField};
use noirc_printable_type::{ForeignCallError, PrintableValueDisplay};

use super::{ForeignCall, ForeignCallExecutor};

#[derive(Debug, Default)]
pub(super) struct PrintForeignCallExecutor;

impl<F: AcirField> ForeignCallExecutor<F> for PrintForeignCallExecutor {
fn execute(
&mut self,
foreign_call: &ForeignCallWaitInfo<F>,
) -> Result<ForeignCallResult<F>, ForeignCallError> {
let foreign_call_name = foreign_call.function.as_str();
match ForeignCall::lookup(foreign_call_name) {
Some(ForeignCall::Print) => {
let skip_newline = foreign_call.inputs[0].unwrap_field().is_zero();

let foreign_call_inputs = foreign_call
.inputs
.split_first()
.ok_or(ForeignCallError::MissingForeignCallInputs)?
.1;

let display_values: PrintableValueDisplay<F> = foreign_call_inputs.try_into()?;
let display_string =
format!("{display_values}{}", if skip_newline { "" } else { "\n" });

print!("{display_string}");

Ok(ForeignCallResult::default())
}
_ => Err(ForeignCallError::NoHandler(foreign_call_name.to_string())),
}
}
}
Loading