Skip to content

feat: FIP-0079: syscall for aggregated bls verification #2003

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

Merged
merged 5 commits into from
May 8, 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
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")]
Copy link
Member

Choose a reason for hiding this comment

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

Can we not put this on the verify_signature operation itself?

Copy link
Member

Choose a reason for hiding this comment

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

It appears that the answer is no. Oh well...

/// 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
Loading