Skip to content

Commit 31118cf

Browse files
authored
feat: FIP-0079: syscall for aggregated bls verification (#2003)
Co-authored-by: DrPeterVanNostrand
1 parent 863f6ef commit 31118cf

File tree

20 files changed

+729
-97
lines changed

20 files changed

+729
-97
lines changed

.github/workflows/ci.yml

+6-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ jobs:
2828
fail-fast: false
2929
matrix:
3030
os: [ubuntu-latest, macos-latest]
31-
name: [build, check-m2-native, check-clippy, test-fvm, test, integration, conformance, calibration]
31+
name: [build, check-m2-native, check-clippy, check-clippy-verify-signature, test-fvm, test, integration, conformance, calibration]
3232
include:
3333
- name: build
3434
key: v3
@@ -47,6 +47,11 @@ jobs:
4747
command: clippy
4848
# we disable default features because rust will otherwise unify them and turn on opencl in CI.
4949
args: --all --all-targets --no-default-features
50+
- name: check-clippy-verify-signature
51+
key: v3
52+
command: clippy
53+
# we disable default features because rust will otherwise unify them and turn on opencl in CI.
54+
args: --all --all-targets --no-default-features --features verify-signature
5055
- name: test-fvm
5156
key: v3-cov
5257
push: true

fvm/Cargo.toml

+5-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ fvm = { path = ".", features = ["testing"], default-features = false }
4848
coverage-helper = { workspace = true }
4949

5050
[features]
51-
default = ["opencl"]
51+
default = ["opencl", "verify-signature"]
5252
opencl = ["filecoin-proofs-api/opencl"]
5353
cuda = ["filecoin-proofs-api/cuda"]
5454
cuda-supraseal = ["filecoin-proofs-api/cuda-supraseal"]
@@ -58,3 +58,7 @@ m2-native = []
5858
upgrade-actor = []
5959
gas_calibration = []
6060
nv23-dev = []
61+
# Use this feature to keep `verify_signature` syscall that is supposed to be removed by FIP-0079,
62+
# The current implementation keeps it by default for backward compatibility reason.
63+
# See <https://github.com/filecoin-project/ref-fvm/issues/2001>
64+
verify-signature = []

fvm/src/gas/price_list.rs

+34
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use std::ops::Mul;
77

88
use anyhow::Context;
99
use fvm_shared::clock::ChainEpoch;
10+
#[cfg(feature = "verify-signature")]
1011
use fvm_shared::crypto::signature::SignatureType;
1112
use fvm_shared::piece::PieceInfo;
1213
use fvm_shared::sector::{
@@ -102,6 +103,7 @@ lazy_static! {
102103
address_lookup: Gas::new(1_050_000),
103104
address_assignment: Gas::new(1_000_000),
104105

106+
#[cfg(feature = "verify-signature")]
105107
sig_cost: total_enum_map!{
106108
SignatureType {
107109
Secp256k1 => ScalingCost {
@@ -115,6 +117,11 @@ lazy_static! {
115117
}
116118
},
117119
secp256k1_recover_cost: Gas::new(1637292),
120+
bls_pairing_cost: Gas::new(8299302),
121+
bls_hashing_cost: ScalingCost {
122+
flat: Gas::zero(),
123+
scale: Gas::new(7),
124+
},
118125
hashing_cost: total_enum_map! {
119126
SupportedHashes {
120127
Sha2_256 => ScalingCost {
@@ -399,11 +406,15 @@ pub struct PriceList {
399406
pub(crate) actor_create_storage: Gas,
400407

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

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

415+
pub(crate) bls_pairing_cost: Gas,
416+
pub(crate) bls_hashing_cost: ScalingCost,
417+
407418
pub(crate) hashing_cost: HashMap<SupportedHashes, ScalingCost>,
408419

409420
/// Gas cost for walking up the chain.
@@ -591,13 +602,36 @@ impl PriceList {
591602
}
592603

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

613+
/// Returns gas required for BLS aggregate signature verification.
614+
#[inline]
615+
pub fn on_verify_aggregate_signature(&self, num_sigs: usize, data_len: usize) -> GasCharge {
616+
// When `num_sigs` BLS signatures are aggregated into a single signature, the aggregate
617+
// signature verifier must perform `num_sigs + 1` expensive pairing operations (one
618+
// pairing on the aggregate signature, and one pairing for each signed plaintext's digest).
619+
//
620+
// Note that `bls_signatures` rearranges the textbook verifier equation (containing
621+
// `num_sigs + 1` full pairings) into a more efficient equation containing `num_sigs + 1`
622+
// Miller loops and one final exponentiation.
623+
let num_pairings = num_sigs as u64 + 1;
624+
625+
let gas_pairings = self.bls_pairing_cost * num_pairings;
626+
let gas_hashing = self.bls_hashing_cost.apply(data_len);
627+
628+
GasCharge::new(
629+
"OnVerifyBlsAggregateSignature",
630+
gas_pairings + gas_hashing,
631+
Zero::zero(),
632+
)
633+
}
634+
601635
/// Returns gas required for recovering signer pubkey from signature
602636
#[inline]
603637
pub fn on_recover_secp_public_key(&self) -> GasCharge {

fvm/src/kernel/default.rs

+70-12
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
// Copyright 2021-2023 Protocol Labs
22
// SPDX-License-Identifier: Apache-2.0, MIT
33
use std::convert::{TryFrom, TryInto};
4-
use std::panic::{self, UnwindSafe};
54
use std::path::PathBuf;
65

76
use anyhow::{anyhow, Context as _};
87
use cid::Cid;
98
use fvm_ipld_blockstore::Blockstore;
109
use fvm_ipld_encoding::{CBOR, IPLD_RAW};
11-
use fvm_shared::address::Payload;
1210
use fvm_shared::crypto::signature;
1311
use fvm_shared::error::ErrorNumber;
1412
use fvm_shared::event::{ActorEvent, Entry, Flags};
@@ -575,13 +573,33 @@ impl<C> CryptoOps for DefaultKernel<C>
575573
where
576574
C: CallManager,
577575
{
576+
#[cfg(feature = "verify-signature")]
578577
fn verify_signature(
579578
&self,
580579
sig_type: SignatureType,
581580
signature: &[u8],
582581
signer: &Address,
583582
plaintext: &[u8],
584583
) -> Result<bool> {
584+
use fvm_shared::address::Payload;
585+
use std::panic::{self, UnwindSafe};
586+
587+
fn catch_and_log_panic<F: FnOnce() -> Result<R> + UnwindSafe, R>(
588+
context: &str,
589+
f: F,
590+
) -> Result<R> {
591+
match panic::catch_unwind(f) {
592+
Ok(v) => v,
593+
Err(e) => {
594+
log::error!("caught panic when {}: {:?}", context, e);
595+
Err(
596+
syscall_error!(IllegalArgument; "caught panic when {}: {:?}", context, e)
597+
.into(),
598+
)
599+
}
600+
}
601+
}
602+
585603
let t = self.call_manager.charge_gas(
586604
self.call_manager
587605
.price_list()
@@ -605,6 +623,56 @@ where
605623
}))
606624
}
607625

626+
fn verify_bls_aggregate(
627+
&self,
628+
aggregate_sig: &[u8; signature::BLS_SIG_LEN],
629+
pub_keys: &[[u8; signature::BLS_PUB_LEN]],
630+
plaintexts_concat: &[u8],
631+
plaintext_lens: &[u32],
632+
) -> Result<bool> {
633+
let num_signers = pub_keys.len();
634+
635+
if num_signers != plaintext_lens.len() {
636+
return Err(syscall_error!(
637+
IllegalArgument;
638+
"unequal numbers of bls public keys and plaintexts"
639+
)
640+
.into());
641+
}
642+
643+
let t = self.call_manager.charge_gas(
644+
self.call_manager
645+
.price_list()
646+
.on_verify_aggregate_signature(num_signers, plaintexts_concat.len()),
647+
)?;
648+
649+
let mut offset: usize = 0;
650+
let plaintexts = plaintext_lens
651+
.iter()
652+
.map(|&len| {
653+
let start = offset;
654+
offset = start
655+
.checked_add(len as usize)
656+
.context("invalid bls plaintext length")
657+
.or_illegal_argument()?;
658+
plaintexts_concat
659+
.get(start..offset)
660+
.context("bls signature plaintext out of bounds")
661+
.or_illegal_argument()
662+
})
663+
.collect::<Result<Vec<_>>>()?;
664+
if offset != plaintexts_concat.len() {
665+
return Err(
666+
syscall_error!(IllegalArgument; "plaintexts buffer length doesn't match").into(),
667+
);
668+
}
669+
670+
t.record(
671+
signature::ops::verify_bls_aggregate(aggregate_sig, pub_keys, &plaintexts)
672+
.or(Ok(false)),
673+
)
674+
}
675+
608676
fn recover_secp_public_key(
609677
&self,
610678
hash: &[u8; SECP_SIG_MESSAGE_HASH_SIZE],
@@ -1080,13 +1148,3 @@ where
10801148
Ok(())
10811149
}
10821150
}
1083-
1084-
fn catch_and_log_panic<F: FnOnce() -> Result<R> + UnwindSafe, R>(context: &str, f: F) -> Result<R> {
1085-
match panic::catch_unwind(f) {
1086-
Ok(v) => v,
1087-
Err(e) => {
1088-
log::error!("caught panic when {}: {:?}", context, e);
1089-
Err(syscall_error!(IllegalArgument; "caught panic when {}: {:?}", context, e).into())
1090-
}
1091-
}
1092-
}

fvm/src/kernel/mod.rs

+51
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ pub trait ActorOps {
220220
fn balance_of(&self, actor_id: ActorID) -> Result<TokenAmount>;
221221
}
222222

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

236+
/// Verifies a BLS aggregate signature. In the case where there is one signer/signed plaintext,
237+
/// this is equivalent to verifying a non-aggregated BLS signature.
238+
///
239+
/// Returns:
240+
/// - `Ok(true)` on a valid signature.
241+
/// - `Ok(false)` on an invalid signature or if the signature or public keys' bytes represent an
242+
/// invalid curve point.
243+
/// - `Err(IllegalArgument)` if `pub_keys.len() != plaintexts.len()`.
244+
fn verify_bls_aggregate(
245+
&self,
246+
aggregate_sig: &[u8; fvm_shared::crypto::signature::BLS_SIG_LEN],
247+
pub_keys: &[[u8; fvm_shared::crypto::signature::BLS_PUB_LEN]],
248+
plaintexts_concat: &[u8],
249+
plaintext_lens: &[u32],
250+
) -> Result<bool>;
251+
252+
/// Given a message hash and its signature, recovers the public key of the signer.
253+
fn recover_secp_public_key(
254+
&self,
255+
hash: &[u8; SECP_SIG_MESSAGE_HASH_SIZE],
256+
signature: &[u8; SECP_SIG_LEN],
257+
) -> Result<[u8; SECP_PUB_LEN]>;
258+
259+
/// Hashes input `data_in` using with the specified hash function, writing the output to
260+
/// `digest_out`, returning the size of the digest written to `digest_out`. If `digest_out` is
261+
/// to small to fit the entire digest, it will be truncated. If too large, the leftover space
262+
/// will not be overwritten.
263+
fn hash(&self, code: u64, data: &[u8]) -> Result<Multihash>;
264+
}
265+
266+
#[cfg(not(feature = "verify-signature"))]
267+
/// Cryptographic primitives provided by the kernel.
268+
#[delegatable_trait]
269+
pub trait CryptoOps {
270+
/// Verifies a BLS aggregate signature. In the case where there is one signer/signed plaintext,
271+
/// this is equivalent to verifying a non-aggregated BLS signature.
272+
///
273+
/// Returns:
274+
/// - `Ok(true)` on a valid signature.
275+
/// - `Ok(false)` on an invalid signature or if the signature or public keys' bytes represent an
276+
/// invalid curve point.
277+
/// - `Err(IllegalArgument)` if `pub_keys.len() != plaintexts.len()`.
278+
fn verify_bls_aggregate(
279+
&self,
280+
aggregate_sig: &[u8; fvm_shared::crypto::signature::BLS_SIG_LEN],
281+
pub_keys: &[[u8; fvm_shared::crypto::signature::BLS_PUB_LEN]],
282+
plaintexts_concat: &[u8],
283+
plaintext_lens: &[u32],
284+
) -> Result<bool>;
285+
235286
/// Given a message hash and its signature, recovers the public key of the signer.
236287
fn recover_secp_public_key(
237288
&self,

fvm/src/syscalls/context.rs

+23
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,29 @@ impl Memory {
8787
.or_error(ErrorNumber::IllegalArgument)
8888
}
8989

90+
/// Return a slice of byte arrays into the actor's memory.
91+
///
92+
/// This slice of byte arrays is valid for the lifetime of the syscall, borrowing the actors memory without
93+
/// copying.
94+
pub fn try_chunks<const S: usize>(&self, offset: u32, len: u32) -> Result<&[[u8; S]]> {
95+
let num_chunks = {
96+
let len = len as usize;
97+
if len % S != 0 {
98+
return Err(syscall_error!(
99+
IllegalArgument;
100+
"buffer length {len} is not divisible by chunk len {S}"
101+
)
102+
.into());
103+
}
104+
len / S
105+
};
106+
107+
self.try_slice(offset, len).map(|bytes| {
108+
let arr_ptr = bytes.as_ptr() as *const [u8; S];
109+
unsafe { std::slice::from_raw_parts(arr_ptr, num_chunks) }
110+
})
111+
}
112+
90113
/// Read a CID from actor memory starting at the given offset.
91114
///
92115
/// On failure, this method returns an [`ErrorNumber::IllegalArgument`] error.

0 commit comments

Comments
 (0)