Skip to content

Commit

Permalink
feat: FIP-0079: syscall for aggregated bls verification (#2003)
Browse files Browse the repository at this point in the history
Co-authored-by: DrPeterVanNostrand
  • Loading branch information
hanabi1224 authored May 8, 2024
1 parent 863f6ef commit 31118cf
Show file tree
Hide file tree
Showing 20 changed files with 729 additions and 97 deletions.
7 changes: 6 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
name: [build, check-m2-native, check-clippy, test-fvm, test, integration, conformance, calibration]
name: [build, check-m2-native, check-clippy, check-clippy-verify-signature, test-fvm, test, integration, conformance, calibration]
include:
- name: build
key: v3
Expand All @@ -47,6 +47,11 @@ jobs:
command: clippy
# we disable default features because rust will otherwise unify them and turn on opencl in CI.
args: --all --all-targets --no-default-features
- name: check-clippy-verify-signature
key: v3
command: clippy
# we disable default features because rust will otherwise unify them and turn on opencl in CI.
args: --all --all-targets --no-default-features --features verify-signature
- name: test-fvm
key: v3-cov
push: true
Expand Down
6 changes: 5 additions & 1 deletion fvm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ fvm = { path = ".", features = ["testing"], default-features = false }
coverage-helper = { workspace = true }

[features]
default = ["opencl"]
default = ["opencl", "verify-signature"]
opencl = ["filecoin-proofs-api/opencl"]
cuda = ["filecoin-proofs-api/cuda"]
cuda-supraseal = ["filecoin-proofs-api/cuda-supraseal"]
Expand All @@ -58,3 +58,7 @@ m2-native = []
upgrade-actor = []
gas_calibration = []
nv23-dev = []
# Use this feature to keep `verify_signature` syscall that is supposed to be removed by FIP-0079,
# The current implementation keeps it by default for backward compatibility reason.
# See <https://github.com/filecoin-project/ref-fvm/issues/2001>
verify-signature = []
34 changes: 34 additions & 0 deletions fvm/src/gas/price_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use std::ops::Mul;

use anyhow::Context;
use fvm_shared::clock::ChainEpoch;
#[cfg(feature = "verify-signature")]
use fvm_shared::crypto::signature::SignatureType;
use fvm_shared::piece::PieceInfo;
use fvm_shared::sector::{
Expand Down Expand Up @@ -102,6 +103,7 @@ lazy_static! {
address_lookup: Gas::new(1_050_000),
address_assignment: Gas::new(1_000_000),

#[cfg(feature = "verify-signature")]
sig_cost: total_enum_map!{
SignatureType {
Secp256k1 => ScalingCost {
Expand All @@ -115,6 +117,11 @@ lazy_static! {
}
},
secp256k1_recover_cost: Gas::new(1637292),
bls_pairing_cost: Gas::new(8299302),
bls_hashing_cost: ScalingCost {
flat: Gas::zero(),
scale: Gas::new(7),
},
hashing_cost: total_enum_map! {
SupportedHashes {
Sha2_256 => ScalingCost {
Expand Down Expand Up @@ -399,11 +406,15 @@ pub struct PriceList {
pub(crate) actor_create_storage: Gas,

/// Gas cost for verifying a cryptographic signature.
#[cfg(feature = "verify-signature")]
pub(crate) sig_cost: HashMap<SignatureType, ScalingCost>,

/// Gas cost for recovering secp256k1 signer public key
pub(crate) secp256k1_recover_cost: Gas,

pub(crate) bls_pairing_cost: Gas,
pub(crate) bls_hashing_cost: ScalingCost,

pub(crate) hashing_cost: HashMap<SupportedHashes, ScalingCost>,

/// Gas cost for walking up the chain.
Expand Down Expand Up @@ -591,13 +602,36 @@ impl PriceList {
}

/// Returns gas required for signature verification.
#[cfg(feature = "verify-signature")]
#[inline]
pub fn on_verify_signature(&self, sig_type: SignatureType, data_len: usize) -> GasCharge {
let cost = self.sig_cost[&sig_type];
let gas = cost.apply(data_len);
GasCharge::new("OnVerifySignature", gas, Zero::zero())
}

/// Returns gas required for BLS aggregate signature verification.
#[inline]
pub fn on_verify_aggregate_signature(&self, num_sigs: usize, data_len: usize) -> GasCharge {
// When `num_sigs` BLS signatures are aggregated into a single signature, the aggregate
// signature verifier must perform `num_sigs + 1` expensive pairing operations (one
// pairing on the aggregate signature, and one pairing for each signed plaintext's digest).
//
// Note that `bls_signatures` rearranges the textbook verifier equation (containing
// `num_sigs + 1` full pairings) into a more efficient equation containing `num_sigs + 1`
// Miller loops and one final exponentiation.
let num_pairings = num_sigs as u64 + 1;

let gas_pairings = self.bls_pairing_cost * num_pairings;
let gas_hashing = self.bls_hashing_cost.apply(data_len);

GasCharge::new(
"OnVerifyBlsAggregateSignature",
gas_pairings + gas_hashing,
Zero::zero(),
)
}

/// Returns gas required for recovering signer pubkey from signature
#[inline]
pub fn on_recover_secp_public_key(&self) -> GasCharge {
Expand Down
82 changes: 70 additions & 12 deletions fvm/src/kernel/default.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
// Copyright 2021-2023 Protocol Labs
// SPDX-License-Identifier: Apache-2.0, MIT
use std::convert::{TryFrom, TryInto};
use std::panic::{self, UnwindSafe};
use std::path::PathBuf;

use anyhow::{anyhow, Context as _};
use cid::Cid;
use fvm_ipld_blockstore::Blockstore;
use fvm_ipld_encoding::{CBOR, IPLD_RAW};
use fvm_shared::address::Payload;
use fvm_shared::crypto::signature;
use fvm_shared::error::ErrorNumber;
use fvm_shared::event::{ActorEvent, Entry, Flags};
Expand Down Expand Up @@ -575,13 +573,33 @@ impl<C> CryptoOps for DefaultKernel<C>
where
C: CallManager,
{
#[cfg(feature = "verify-signature")]
fn verify_signature(
&self,
sig_type: SignatureType,
signature: &[u8],
signer: &Address,
plaintext: &[u8],
) -> Result<bool> {
use fvm_shared::address::Payload;
use std::panic::{self, UnwindSafe};

fn catch_and_log_panic<F: FnOnce() -> Result<R> + UnwindSafe, R>(
context: &str,
f: F,
) -> Result<R> {
match panic::catch_unwind(f) {
Ok(v) => v,
Err(e) => {
log::error!("caught panic when {}: {:?}", context, e);
Err(
syscall_error!(IllegalArgument; "caught panic when {}: {:?}", context, e)
.into(),
)
}
}
}

let t = self.call_manager.charge_gas(
self.call_manager
.price_list()
Expand All @@ -605,6 +623,56 @@ where
}))
}

fn verify_bls_aggregate(
&self,
aggregate_sig: &[u8; signature::BLS_SIG_LEN],
pub_keys: &[[u8; signature::BLS_PUB_LEN]],
plaintexts_concat: &[u8],
plaintext_lens: &[u32],
) -> Result<bool> {
let num_signers = pub_keys.len();

if num_signers != plaintext_lens.len() {
return Err(syscall_error!(
IllegalArgument;
"unequal numbers of bls public keys and plaintexts"
)
.into());
}

let t = self.call_manager.charge_gas(
self.call_manager
.price_list()
.on_verify_aggregate_signature(num_signers, plaintexts_concat.len()),
)?;

let mut offset: usize = 0;
let plaintexts = plaintext_lens
.iter()
.map(|&len| {
let start = offset;
offset = start
.checked_add(len as usize)
.context("invalid bls plaintext length")
.or_illegal_argument()?;
plaintexts_concat
.get(start..offset)
.context("bls signature plaintext out of bounds")
.or_illegal_argument()
})
.collect::<Result<Vec<_>>>()?;
if offset != plaintexts_concat.len() {
return Err(
syscall_error!(IllegalArgument; "plaintexts buffer length doesn't match").into(),
);
}

t.record(
signature::ops::verify_bls_aggregate(aggregate_sig, pub_keys, &plaintexts)
.or(Ok(false)),
)
}

fn recover_secp_public_key(
&self,
hash: &[u8; SECP_SIG_MESSAGE_HASH_SIZE],
Expand Down Expand Up @@ -1080,13 +1148,3 @@ where
Ok(())
}
}

fn catch_and_log_panic<F: FnOnce() -> Result<R> + UnwindSafe, R>(context: &str, f: F) -> Result<R> {
match panic::catch_unwind(f) {
Ok(v) => v,
Err(e) => {
log::error!("caught panic when {}: {:?}", context, e);
Err(syscall_error!(IllegalArgument; "caught panic when {}: {:?}", context, e).into())
}
}
}
51 changes: 51 additions & 0 deletions fvm/src/kernel/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ pub trait ActorOps {
fn balance_of(&self, actor_id: ActorID) -> Result<TokenAmount>;
}

#[cfg(feature = "verify-signature")]
/// Cryptographic primitives provided by the kernel.
#[delegatable_trait]
pub trait CryptoOps {
Expand All @@ -232,6 +233,56 @@ pub trait CryptoOps {
plaintext: &[u8],
) -> Result<bool>;

/// Verifies a BLS aggregate signature. In the case where there is one signer/signed plaintext,
/// this is equivalent to verifying a non-aggregated BLS signature.
///
/// Returns:
/// - `Ok(true)` on a valid signature.
/// - `Ok(false)` on an invalid signature or if the signature or public keys' bytes represent an
/// invalid curve point.
/// - `Err(IllegalArgument)` if `pub_keys.len() != plaintexts.len()`.
fn verify_bls_aggregate(
&self,
aggregate_sig: &[u8; fvm_shared::crypto::signature::BLS_SIG_LEN],
pub_keys: &[[u8; fvm_shared::crypto::signature::BLS_PUB_LEN]],
plaintexts_concat: &[u8],
plaintext_lens: &[u32],
) -> Result<bool>;

/// Given a message hash and its signature, recovers the public key of the signer.
fn recover_secp_public_key(
&self,
hash: &[u8; SECP_SIG_MESSAGE_HASH_SIZE],
signature: &[u8; SECP_SIG_LEN],
) -> Result<[u8; SECP_PUB_LEN]>;

/// Hashes input `data_in` using with the specified hash function, writing the output to
/// `digest_out`, returning the size of the digest written to `digest_out`. If `digest_out` is
/// to small to fit the entire digest, it will be truncated. If too large, the leftover space
/// will not be overwritten.
fn hash(&self, code: u64, data: &[u8]) -> Result<Multihash>;
}

#[cfg(not(feature = "verify-signature"))]
/// Cryptographic primitives provided by the kernel.
#[delegatable_trait]
pub trait CryptoOps {
/// Verifies a BLS aggregate signature. In the case where there is one signer/signed plaintext,
/// this is equivalent to verifying a non-aggregated BLS signature.
///
/// Returns:
/// - `Ok(true)` on a valid signature.
/// - `Ok(false)` on an invalid signature or if the signature or public keys' bytes represent an
/// invalid curve point.
/// - `Err(IllegalArgument)` if `pub_keys.len() != plaintexts.len()`.
fn verify_bls_aggregate(
&self,
aggregate_sig: &[u8; fvm_shared::crypto::signature::BLS_SIG_LEN],
pub_keys: &[[u8; fvm_shared::crypto::signature::BLS_PUB_LEN]],
plaintexts_concat: &[u8],
plaintext_lens: &[u32],
) -> Result<bool>;

/// Given a message hash and its signature, recovers the public key of the signer.
fn recover_secp_public_key(
&self,
Expand Down
23 changes: 23 additions & 0 deletions fvm/src/syscalls/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,29 @@ impl Memory {
.or_error(ErrorNumber::IllegalArgument)
}

/// Return a slice of byte arrays into the actor's memory.
///
/// This slice of byte arrays is valid for the lifetime of the syscall, borrowing the actors memory without
/// copying.
pub fn try_chunks<const S: usize>(&self, offset: u32, len: u32) -> Result<&[[u8; S]]> {
let num_chunks = {
let len = len as usize;
if len % S != 0 {
return Err(syscall_error!(
IllegalArgument;
"buffer length {len} is not divisible by chunk len {S}"
)
.into());
}
len / S
};

self.try_slice(offset, len).map(|bytes| {
let arr_ptr = bytes.as_ptr() as *const [u8; S];
unsafe { std::slice::from_raw_parts(arr_ptr, num_chunks) }
})
}

/// Read a CID from actor memory starting at the given offset.
///
/// On failure, this method returns an [`ErrorNumber::IllegalArgument`] error.
Expand Down
Loading

0 comments on commit 31118cf

Please sign in to comment.