Skip to content
This repository has been archived by the owner on Apr 9, 2024. It is now read-only.

feat(brillig): implemented first blackbox functions #401

Merged
merged 7 commits into from
Jun 27, 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
10 changes: 10 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,13 @@ num-traits = "0.2"
thiserror = "1.0.21"

serde = { version = "1.0.136", features = ["derive"] }
blake2 = "0.10.6"
sha2 = "0.10.6"
sha3 = "0.10.6"
k256 = { version = "0.11.0", features = [
"ecdsa",
"ecdsa-core",
"sha256",
"digest",
"arithmetic",
] }
14 changes: 4 additions & 10 deletions acvm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,10 @@ thiserror.workspace = true
acir.workspace = true
stdlib.workspace = true

blake2 = "0.10.6"
sha2 = "0.10.6"
sha3 = "0.10.6"
k256 = { version = "0.11.0", features = [
"ecdsa",
"ecdsa-core",
"sha256",
"digest",
"arithmetic",
] }
blake2.workspace = true
sha2.workspace = true
sha3.workspace = true
k256.workspace = true
indexmap = "1.7.0"
async-trait = "0.1"

Expand Down
4 changes: 4 additions & 0 deletions brillig_vm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ repository.workspace = true
[dependencies]
acir_field.workspace = true
serde.workspace = true
blake2.workspace = true
sha2.workspace = true
sha3.workspace = true
k256.workspace = true

[features]
default = ["bn254"]
Expand Down
258 changes: 258 additions & 0 deletions brillig_vm/src/black_box.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
use crate::{memory::Memory, opcodes::HeapVector, HeapArray, RegisterIndex, Registers, Value};
use acir_field::FieldElement;
use blake2::digest::generic_array::GenericArray;
use blake2::{Blake2s256, Digest};
use k256::elliptic_curve::sec1::FromEncodedPoint;
use k256::elliptic_curve::PrimeField;
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use sha3::Keccak256;

use k256::{ecdsa::Signature, Scalar};
use k256::{
elliptic_curve::{
sec1::{Coordinates, ToEncodedPoint},
IsHigh,
},
AffinePoint, EncodedPoint, ProjectivePoint, PublicKey,
};

/// These opcodes provide an equivalent of ACIR blackbox functions.
/// They are implemented as native functions in the VM.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum BlackBoxOp {
/// Calculates the SHA256 hash of the inputs.
Sha256 { message: HeapVector, output: HeapArray },
/// Calculates the Blake2s hash of the inputs.
Blake2s { message: HeapVector, output: HeapArray },
/// Calculates the Keccak256 hash of the inputs.
Keccak256 { message: HeapVector, output: HeapArray },
/// Hashes a set of inputs and applies the field modulus to the result
/// to return a value which can be represented as a [`FieldElement`][acir_field::FieldElement]
///
/// This is implemented using the `Blake2s` hash function.
/// The "128" in the name specifies that this function should have 128 bits of security.
HashToField128Security { message: HeapVector, output: RegisterIndex },
/// Verifies a ECDSA signature over the secp256k1 curve.
EcdsaSecp256k1 {
hashed_msg: HeapVector,
public_key_x: HeapArray,
public_key_y: HeapArray,
signature: HeapArray,
result: RegisterIndex,
},
}

impl BlackBoxOp {
pub(crate) fn evaluate(&self, registers: &mut Registers, memory: &mut Memory) {
sirasistant marked this conversation as resolved.
Show resolved Hide resolved
match self {
BlackBoxOp::Sha256 { message, output } => {
generic_hash_256::<Sha256>(message, output, registers, memory);
}
BlackBoxOp::Blake2s { message, output } => {
generic_hash_256::<Blake2s256>(message, output, registers, memory);
}
BlackBoxOp::Keccak256 { message, output } => {
generic_hash_256::<Keccak256>(message, output, registers, memory);
}
BlackBoxOp::HashToField128Security { message, output } => {
generic_hash_to_field::<Blake2s256>(message, output, registers, memory);
}
BlackBoxOp::EcdsaSecp256k1 {
hashed_msg,
public_key_x,
public_key_y,
signature,
result: result_register,
} => {
let message_values = memory.read_slice(
registers.get(hashed_msg.pointer).to_usize(),
registers.get(hashed_msg.size).to_usize(),
);
let message_bytes = to_u8_vec(message_values);

let public_key_x_bytes: [u8; 32] =
to_u8_vec(memory.read_slice(
registers.get(public_key_x.pointer).to_usize(),
public_key_x.size,
))
.try_into()
.expect("Expected a 32-element public key x array");

let public_key_y_bytes: [u8; 32] =
to_u8_vec(memory.read_slice(
registers.get(public_key_y.pointer).to_usize(),
public_key_y.size,
))
.try_into()
.expect("Expected a 32-element public key y array");

let signature_bytes: [u8; 64] = to_u8_vec(
memory.read_slice(registers.get(signature.pointer).to_usize(), signature.size),
)
.try_into()
.expect("Expected a 64-element signature array");

let result = verify_secp256k1_ecdsa_signature(
&message_bytes,
&public_key_x_bytes,
&public_key_y_bytes,
&signature_bytes,
);

registers.set(*result_register, (result as u128).into())
}
}
}
}

#[cfg(test)]
mod test {
use crate::{
black_box::to_u8_vec, BlackBoxOp, HeapArray, HeapVector, Memory, Registers, Value,
};

fn to_value_vec(input: &[u8]) -> Vec<Value> {
input.iter().map(|x| Value::from(*x as usize)).collect()
}

#[test]
fn sha256() {
let message: Vec<u8> = b"hello world".to_vec();
let message_length = message.len();

let mut memory = Memory::from(vec![]);
let message_pointer = 0;
let result_pointer = message_pointer + message_length;
memory.write_slice(message_pointer, to_value_vec(&message).as_slice());

let mut registers = Registers {
inner: vec![
Value::from(message_pointer),
Value::from(message_length),
Value::from(result_pointer),
],
};

let op = BlackBoxOp::Sha256 {
message: HeapVector { pointer: 0.into(), size: 1.into() },
output: HeapArray { pointer: 2.into(), size: 32 },
};

op.evaluate(&mut registers, &mut memory);

let result = memory.read_slice(result_pointer, 32);

assert_eq!(
to_u8_vec(result),
vec![
185, 77, 39, 185, 147, 77, 62, 8, 165, 46, 82, 215, 218, 125, 171, 250, 196, 132,
239, 227, 122, 83, 128, 238, 144, 136, 247, 172, 226, 239, 205, 233
]
);
}
}

/// Extracts the last byte of every value
fn to_u8_vec(inputs: &[Value]) -> Vec<u8> {
let mut result = Vec::with_capacity(inputs.len());
for input in inputs {
let field_bytes = input.to_field().to_be_bytes();
let byte = field_bytes.last().unwrap();
result.push(*byte);
}
result
}

/// Does a generic hash of the inputs storing the resulting 32 bytes as items in the output array.
fn generic_hash_256<D: Digest>(
sirasistant marked this conversation as resolved.
Show resolved Hide resolved
message: &HeapVector,
output: &HeapArray,
registers: &Registers,
memory: &mut Memory,
) {
let message_values = memory.read_slice(
registers.get(message.pointer).to_usize(),
registers.get(message.size).to_usize(),
);
let message_bytes = to_u8_vec(message_values);

assert!(output.size == 32, "Expected a 32-element result array");

let output_bytes: [u8; 32] =
D::digest(message_bytes).as_slice().try_into().expect("digest should be 256 bits");
let output_values: Vec<Value> = output_bytes.iter().map(|b| (*b as u128).into()).collect();

memory.write_slice(registers.get(output.pointer).to_usize(), &output_values);
}

/// Does a generic hash of the entire inputs storing the resulting hash into a single output register.
fn generic_hash_to_field<D: Digest>(
message: &HeapVector,
output: &RegisterIndex,
registers: &mut Registers,
memory: &Memory,
) {
let message_values = memory.read_slice(
registers.get(message.pointer).to_usize(),
registers.get(message.size).to_usize(),
);
let mut message_bytes = Vec::new();

for value in message_values {
let field_bytes = value.to_field().to_be_bytes();
message_bytes.extend_from_slice(&field_bytes);
}

let output_bytes: [u8; 32] =
D::digest(message_bytes).as_slice().try_into().expect("digest should be 256 bits");

let reduced_res = FieldElement::from_be_bytes_reduce(&output_bytes);

registers.set(*output, reduced_res.into());
}

// TODO(https://github.com/noir-lang/acvm/issues/402): remove from here and use the one from acvm
fn verify_secp256k1_ecdsa_signature(
hashed_msg: &[u8],
public_key_x_bytes: &[u8; 32],
public_key_y_bytes: &[u8; 32],
signature: &[u8; 64],
) -> bool {
// Convert the inputs into k256 data structures

let signature = Signature::try_from(signature.as_slice()).unwrap();

let point = EncodedPoint::from_affine_coordinates(
public_key_x_bytes.into(),
public_key_y_bytes.into(),
true,
);
let pubkey = PublicKey::from_encoded_point(&point).unwrap();

let z = Scalar::from_repr(*GenericArray::from_slice(hashed_msg)).unwrap();

// Finished converting bytes into data structures

let r = signature.r();
let s = signature.s();

// Ensure signature is "low S" normalized ala BIP 0062
if s.is_high().into() {
return false;
}

let s_inv = s.invert().unwrap();
let u1 = z * s_inv;
let u2 = *r * s_inv;

#[allow(non_snake_case)]
let R: AffinePoint = ((ProjectivePoint::GENERATOR * u1)
+ (ProjectivePoint::from(*pubkey.as_affine()) * u2))
.to_affine();

match R.to_encoded_point(false).coordinates() {
Coordinates::Uncompressed { x, y: _ } => Scalar::from_repr(*x).unwrap().eq(&r),
_ => unreachable!("Point is uncompressed"),
}
}
Loading