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

Commit

Permalink
feat(brillig): implemented first blackbox functions (#401)
Browse files Browse the repository at this point in the history
  • Loading branch information
sirasistant authored Jun 27, 2023
1 parent 73fbc95 commit 62d40f7
Show file tree
Hide file tree
Showing 6 changed files with 347 additions and 29 deletions.
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) {
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>(
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

0 comments on commit 62d40f7

Please sign in to comment.