From b07b143447cabd762cb46e3a1c897fcf221ce60c Mon Sep 17 00:00:00 2001 From: dovgopoly Date: Tue, 28 Jan 2025 15:18:38 +0200 Subject: [PATCH] [move][natives] added batch bulletproof natives --- .../src/gas_schedule/aptos_framework.rs | 25 +- .../src/components/feature_flags.rs | 3 + .../ristretto255_bulletproofs.move | 344 +++++++++++++++++- .../move-stdlib/sources/configs/features.move | 10 + .../src/natives/cryptography/bulletproofs.rs | 296 +++++++++++++-- .../src/natives/cryptography/ristretto255.rs | 14 +- crates/aptos-crypto/benches/bulletproofs.rs | 35 +- .../update_bulletproofs_gas_params.py | 42 ++- types/src/on_chain_config/aptos_features.rs | 1 + 9 files changed, 695 insertions(+), 75 deletions(-) diff --git a/aptos-move/aptos-gas-schedule/src/gas_schedule/aptos_framework.rs b/aptos-move/aptos-gas-schedule/src/gas_schedule/aptos_framework.rs index 72526a5404d6ef..aa1347d22c8ee3 100644 --- a/aptos-move/aptos-gas-schedule/src/gas_schedule/aptos_framework.rs +++ b/aptos-move/aptos-gas-schedule/src/gas_schedule/aptos_framework.rs @@ -236,10 +236,27 @@ crate::gas_schedule::macros::define_gas_parameters!( [hash_keccak256_per_byte: InternalGasPerByte, { 1.. => "hash.keccak256.per_byte" }, 165], // Bulletproofs gas parameters begin. - // Generated at time 1683148919.0628748 by `scripts/algebra-gas/update_bulletproofs_gas_params.py` with gas_per_ns=10.0. - [bulletproofs_base: InternalGas, { 11.. => "bulletproofs.base" }, 11794651], - [bulletproofs_per_bit_rangeproof_verify: InternalGasPerArg, { 11.. => "bulletproofs.per_bit_rangeproof_verify" }, 1004253], - [bulletproofs_per_byte_rangeproof_deserialize: InternalGasPerByte, { 11.. => "bulletproofs.per_byte_rangeproof_deserialize" }, 121], + // Generated at time 1738158053.7406907 by `scripts/algebra-gas/update_bulletproofs_gas_params.py` with gas_per_ns=10.0. + [bulletproofs_verify_base_1: InternalGas, { 11.. => "bulletproofs.verify.base_1" }, 1_966_039], + [bulletproofs_verify_base_2: InternalGas, { 11.. => "bulletproofs.verify.base_2" }, 2_992_994], + [bulletproofs_verify_base_4: InternalGas, { 11.. => "bulletproofs.verify.base_4" }, 6_362_208], + [bulletproofs_verify_base_8: InternalGas, { 11.. => "bulletproofs.verify.base_8" }, 8_565_140], + [bulletproofs_verify_base_16: InternalGas, { 11.. => "bulletproofs.verify.base_16" }, 12_538_560], + [bulletproofs_verify_per_bit_1: InternalGasPerByte, { 11.. => "bulletproofs.verify.per_bit_1" }, 130_096], + [bulletproofs_verify_per_bit_2: InternalGasPerByte, { 11.. => "bulletproofs.verify.per_bit_2" }, 228_691], + [bulletproofs_verify_per_bit_4: InternalGasPerByte, { 11.. => "bulletproofs.verify.per_bit_4" }, 359_323], + [bulletproofs_verify_per_bit_8: InternalGasPerByte, { 11.. => "bulletproofs.verify.per_bit_8" }, 658_527], + [bulletproofs_verify_per_bit_16: InternalGasPerByte, { 11.. => "bulletproofs.verify.per_bit_16" }, 1_212_581], + [bulletproofs_deserialize_base_1: InternalGas, { 11.. => "bulletproofs.deserialize.base_1" }, 3_253], + [bulletproofs_deserialize_base_2: InternalGas, { 11.. => "bulletproofs.deserialize.base_2" }, 3_226], + [bulletproofs_deserialize_base_4: InternalGas, { 11.. => "bulletproofs.deserialize.base_4" }, 3_155], + [bulletproofs_deserialize_base_8: InternalGas, { 11.. => "bulletproofs.deserialize.base_8" }, 3_647], + [bulletproofs_deserialize_base_16: InternalGas, { 11.. => "bulletproofs.deserialize.base_16" }, 2_496], + [bulletproofs_deserialize_per_byte_1: InternalGasPerByte, { 11.. => "bulletproofs.deserialize.per_byte_1" }, 34], + [bulletproofs_deserialize_per_byte_2: InternalGasPerByte, { 11.. => "bulletproofs.deserialize.per_byte_2" }, 44], + [bulletproofs_deserialize_per_byte_4: InternalGasPerByte, { 11.. => "bulletproofs.deserialize.per_byte_4" }, 39], + [bulletproofs_deserialize_per_byte_8: InternalGasPerByte, { 11.. => "bulletproofs.deserialize.per_byte_8" }, 28], + [bulletproofs_deserialize_per_byte_16: InternalGasPerByte, { 11.. => "bulletproofs.deserialize.per_byte_16" }, 59], // Bulletproofs gas parameters end. [type_info_type_of_base: InternalGas, "type_info.type_of.base", 1102], diff --git a/aptos-move/aptos-release-builder/src/components/feature_flags.rs b/aptos-move/aptos-release-builder/src/components/feature_flags.rs index 6a652d42e1c09f..d03a4e4f3ba0be 100644 --- a/aptos-move/aptos-release-builder/src/components/feature_flags.rs +++ b/aptos-move/aptos-release-builder/src/components/feature_flags.rs @@ -138,6 +138,7 @@ pub enum FeatureFlag { PermissionedSigner, AccountAbstraction, VMBinaryFormatV8, + BulletproofsBatchNatives, } fn generate_features_blob(writer: &CodeWriter, data: &[u64]) { @@ -367,6 +368,7 @@ impl From for AptosFeatureFlag { }, FeatureFlag::PermissionedSigner => AptosFeatureFlag::PERMISSIONED_SIGNER, FeatureFlag::AccountAbstraction => AptosFeatureFlag::ACCOUNT_ABSTRACTION, + FeatureFlag::BulletproofsBatchNatives => AptosFeatureFlag::BULLETPROOFS_BATCH_NATIVES, } } } @@ -523,6 +525,7 @@ impl From for FeatureFlag { }, AptosFeatureFlag::PERMISSIONED_SIGNER => FeatureFlag::PermissionedSigner, AptosFeatureFlag::ACCOUNT_ABSTRACTION => FeatureFlag::AccountAbstraction, + AptosFeatureFlag::BULLETPROOFS_BATCH_NATIVES => FeatureFlag::BulletproofsBatchNatives, } } } diff --git a/aptos-move/framework/aptos-stdlib/sources/cryptography/ristretto255_bulletproofs.move b/aptos-move/framework/aptos-stdlib/sources/cryptography/ristretto255_bulletproofs.move index 731ceabc74227a..0f3334016b3061 100644 --- a/aptos-move/framework/aptos-stdlib/sources/cryptography/ristretto255_bulletproofs.move +++ b/aptos-move/framework/aptos-stdlib/sources/cryptography/ristretto255_bulletproofs.move @@ -3,6 +3,10 @@ /// A Bulletproof-based zero-knowledge range proof is a proof that a Pedersen commitment /// $c = v G + r H$ commits to an $n$-bit value $v$ (i.e., $v \in [0, 2^n)$). Currently, this module only supports /// $n \in \{8, 16, 32, 64\}$ for the number of bits. +/// +/// The module also supports batch range proofs, allowing verification of multiple commitments in a single proof. +/// Each commitment in the batch must satisfy the same range constraint $v \in [0, 2^n)$, and the supported batch +/// sizes are limited to $\{1, 2, 4, 8, 16\}$. module aptos_std::ristretto255_bulletproofs { use std::error; use std::features; @@ -20,6 +24,7 @@ module aptos_std::ristretto255_bulletproofs { // Error codes // + #[deprecated] /// There was an error deserializing the range proof. const E_DESERIALIZE_RANGE_PROOF: u64 = 1; @@ -28,6 +33,12 @@ module aptos_std::ristretto255_bulletproofs { /// The range proof system only supports proving ranges of type $[0, 2^b)$ where $b \in \{8, 16, 32, 64\}$. const E_RANGE_NOT_SUPPORTED: u64 = 3; + + /// The range proof system only supports batch sizes of 1, 2, 4, 8, and 16. + const E_BATCH_SIZE_NOT_SUPPORTED: u64 = 4; + + /// The vector lengths of values and blinding factors do not match. + const E_VECTOR_LENGTHS_MISMATCH: u64 = 5; /// The native functions have not been rolled out yet. const E_NATIVE_FUN_NOT_AVAILABLE: u64 = 4; @@ -99,12 +110,69 @@ module aptos_std::ristretto255_bulletproofs { ) } + /// Verifies a zero-knowledge range proof for a batch of Pedersen commitments `comms`, ensuring that all values + /// `v` satisfy `v` in `[0, 2^num_bits)`. + /// Only works for `num_bits` in `{8, 16, 32, 64}` and batch size (length of `comms`) in `{1, 2, 4, 8, 16}`. + public fun verify_batch_range_proof_pedersen( + comms: &vector, proof: &RangeProof, + num_bits: u64, dst: vector): bool + { + assert!(features::bulletproofs_batch_enabled(), error::invalid_state(E_NATIVE_FUN_NOT_AVAILABLE)); + + let comms = std::vector::map_ref(comms, |com| ristretto255::point_to_bytes(&pedersen::commitment_as_compressed_point(com))); + + verify_batch_range_proof_internal( + comms, + &ristretto255::basepoint(), &ristretto255::hash_to_point_base(), + proof.bytes, num_bits, dst + ) + } + + // Verifies a zero-knowledge range proof for a batch of commitments `comms` (each of the form + /// `v * val_base + r * rand_base`), ensuring that all values `v` satisfy + /// `v` in `[0, 2^num_bits)`. Only works for `num_bits` in `{8, 16, 32, 64}` and batch size + /// (length of the `comms`) in `{1, 2, 4, 8, 16}`. + public fun verify_batch_range_proof( + comms: &vector, + val_base: &RistrettoPoint, rand_base: &RistrettoPoint, + proof: &RangeProof, num_bits: u64, dst: vector): bool + { + assert!(features::bulletproofs_batch_enabled(), error::invalid_state(E_NATIVE_FUN_NOT_AVAILABLE)); + + let comms = std::vector::map_ref(comms, |com| ristretto255::point_to_bytes(&pedersen::commitment_as_compressed_point(com))); + + verify_batch_range_proof_internal( + comms, + val_base, rand_base, + proof.bytes, num_bits, dst + ) + } + #[test_only] /// Computes a range proof for the Pedersen commitment to 'val' with randomness 'r', under the default Bulletproofs /// commitment key; see `pedersen::new_commitment_for_bulletproof`. Returns the said commitment too. /// Only works for `num_bits` in `{8, 16, 32, 64}`. public fun prove_range_pedersen(val: &Scalar, r: &Scalar, num_bits: u64, dst: vector): (RangeProof, pedersen::Commitment) { let (bytes, compressed_comm) = prove_range_internal(scalar_to_bytes(val), scalar_to_bytes(r), num_bits, dst, &ristretto255::basepoint(), &ristretto255::hash_to_point_base()); + let comm = pedersen::new_commitment_from_bytes(compressed_comm); + let comm = std::option::extract(&mut comm); + + ( + RangeProof { bytes }, + comm + ) + } + + #[test_only] + /// Computes a range proof for a commitment to `val` with randomness `r`. + /// The commitment is of the form `val * val_base + r * rand_base`. + /// Returns both the commitment and the corresponding range proof. Only works for `num_bits` in `{8, 16, 32, 64}`. + public fun prove_range( + val: &Scalar, r: &Scalar, + val_base: &RistrettoPoint, rand_base: &RistrettoPoint, + num_bits: u64, dst: vector): (RangeProof, pedersen::Commitment) + { + let (bytes, compressed_comm) = prove_range_internal(scalar_to_bytes(val), scalar_to_bytes(r), num_bits, dst, val_base, rand_base); let point = ristretto255::new_compressed_point_from_bytes(compressed_comm); let point = &std::option::extract(&mut point); @@ -114,12 +182,63 @@ module aptos_std::ristretto255_bulletproofs { ) } + #[test_only] + /// Computes a range proof for a batch of values `vals`, each committed with the corresponding randomness in `rs`, + /// under the default Bulletproofs commitment key; see `pedersen::new_commitment_for_bulletproof`. + /// Returns a tuple containing the batch range proof and a vector of said commitments. + /// Only works for `num_bits` in `{8, 16, 32, 64}` and batch sizes (length of `vals` and `rs`) in `{1, 2, 4, 8, 16}`. + public fun prove_batch_range_pedersen( + vals: &vector, rs: &vector, + num_bits: u64, dst: vector): (RangeProof, vector) + { + let vals = std::vector::map_ref(vals, |val| scalar_to_bytes(val)); + let rs = std::vector::map_ref(rs, |r| scalar_to_bytes(r)); + + let (bytes, compressed_comms) = prove_batch_range_internal( + vals, rs, + num_bits, dst, + &ristretto255::basepoint(), &ristretto255::hash_to_point_base() + ); + let comms = std::vector::map(compressed_comms, |compressed_comm| { + let comm = pedersen::new_commitment_from_bytes(compressed_comm); + std::option::extract(&mut comm) + }); + + ( + RangeProof { bytes }, + comms + ) + } + + #[test_only] + /// Computes a range proof for a batch of values `vals`, each committed with the corresponding randomness in `rs`, + /// using custom base points `val_base` and `rand_base`. Each commitment is of the form `val * val_base + r * rand_base`. + /// Returns a tuple containing the batch range proof and a vector of said commitments. + /// Only works for `num_bits` in `{8, 16, 32, 64}` and batch sizes (length of `vals` and `rs`) in `{1, 2, 4, 8, 16}`. + public fun prove_batch_range( + vals: &vector, rs: &vector, + val_base: &RistrettoPoint, rand_base: &RistrettoPoint, + num_bits: u64, dst: vector): (RangeProof, vector) + { + let vals = std::vector::map_ref(vals, |val| scalar_to_bytes(val)); + let rs = std::vector::map_ref(rs, |r| scalar_to_bytes(r)); + + let (bytes, compressed_comms) = prove_batch_range_internal(vals, rs, num_bits, dst, val_base, rand_base); + let comms = std::vector::map(compressed_comms, |compressed_comm| { + let comm = pedersen::new_commitment_from_bytes(compressed_comm); + std::option::extract(&mut comm) + }); + + ( + RangeProof { bytes }, + comms + ) + } + // // Native functions // - - /// Aborts with `error::invalid_argument(E_DESERIALIZE_RANGE_PROOF)` if `proof` is not a valid serialization of a - /// range proof. + /// Aborts with `error::invalid_argument(E_RANGE_NOT_SUPPORTED)` if an unsupported `num_bits` is provided. native fun verify_range_proof_internal( com: vector, @@ -128,6 +247,17 @@ module aptos_std::ristretto255_bulletproofs { proof: vector, num_bits: u64, dst: vector): bool; + + /// Aborts with `error::invalid_argument(E_RANGE_NOT_SUPPORTED)` if an unsupported `num_bits` is provided. + /// Aborts with `error::invalid_argument(E_BATCH_SIZE_NOT_SUPPORTED)` if an unsupported batch size is provided. + /// Aborts with `error::invalid_argument(E_VECTOR_LENGTHS_MISMATCH)` if the vector lengths of `comms` and `proof` do not match. + native fun verify_batch_range_proof_internal( + comms: vector>, + val_base: &RistrettoPoint, + rand_base: &RistrettoPoint, + proof: vector, + num_bits: u64, + dst: vector): bool; #[test_only] /// Returns a tuple consisting of (1) a range proof for 'val' committed with randomness 'r' under the default Bulletproofs @@ -143,34 +273,60 @@ module aptos_std::ristretto255_bulletproofs { val_base: &RistrettoPoint, rand_base: &RistrettoPoint): (vector, vector); + #[test_only] + /// Returns a tuple consisting of (1) a range proof for each value in `vals`, where each value is committed + /// with the corresponding randomness in `rs`, and (2) the corresponding commitments. + /// + /// Each commitment has the form `val * val_base + r * rand_base`, where `val` and `r` are the corresponding + /// elements from `vals` and `rs`, respectively. + /// + /// Aborts with `error::invalid_argument(E_RANGE_NOT_SUPPORTED)` if an unsupported `num_bits` is provided. + /// Aborts with `error::invalid_argument(E_VALUE_OUTSIDE_RANGE)` if `val_base` is not `num_bits` wide. + native fun prove_batch_range_internal( + vals: vector>, + rs: vector>, + num_bits: u64, + dst: vector, + val_base: &RistrettoPoint, + rand_base: &RistrettoPoint): (vector, vector>); + // // Testing // #[test_only] - use aptos_std::ristretto255::{Scalar, scalar_to_bytes, point_equals}; + use aptos_std::ristretto255::{Scalar, scalar_to_bytes}; + #[test_only] + use aptos_std::ristretto255_pedersen::commitment_equals; #[test_only] const A_DST: vector = b"AptosBulletproofs"; #[test_only] const A_VALUE: vector = x"870c2fa1b2e9ac45000000000000000000000000000000000000000000000000"; // i.e., 5020644638028926087u64 #[test_only] + const B_VALUE: vector = x"bb9d99fb7f9e572b000000000000000000000000000000000000000000000000"; // i.e., 3123139139123912123u64 + #[test_only] const A_BLINDER: vector = x"e7c7b42b75503bfc7b1932783786d227ebf88f79da752b68f6b865a9c179640c"; + #[test_only] + const B_BLINDER: vector = x"ce224fe5e1111a394fc254ee503aa2406706ef606efac6e2d0332711c7a7bc06"; // Pedersen commitment to A_VALUE with randomness A_BLINDER #[test_only] const A_COMM: vector = x"0a665260a4e42e575882c2cdcb3d0febd6cf168834f6de1e9e61e7b2e53dbf14"; + #[test_only] + const B_COMM: vector = x"748c244d880a1de3970a3d01670a04db6b74b9741bfec8732e512312384a6515"; // Range proof for A_COMM using domain-separation tag in A_DST, and MAX_RANGE_BITS #[test_only] const A_RANGE_PROOF_PEDERSEN: vector = x"d8d422d3fb9511d1942b78e3ec1a8c82fe1c01a0a690c55a4761e7e825633a753cca816667d2cbb716fe04a9c199cad748c2d4e59de4ed04fedf5f04f4341a74ae75b63c1997fd65d5fb3a8c03ad8771abe2c0a4f65d19496c11d948d6809503eac4d996f2c6be4e64ebe2df31102c96f106695bdf489dc9290c93b4d4b5411fb6298d0c33afa57e2e1948c38ef567268a661e7b1c099272e29591e717930a06a2c6e0e2d56aedea3078fd59334634f1a4543069865409eba074278f191039083102a9a0621791a9be09212a847e22061e083d7a712b05bca7274b25e4cb1201c679c4957f0842d7661fa1d3f5456a651e89112628b456026f8ad3a7abeaba3fec8031ec8b0392c0aa6c96205f7b21b0c2d6b5d064bd5bd1a1d91c41625d910688fa0dca35ec0f0e31a45792f8d6a330be970a22e1e0773111a083de893c89419ee7de97295978de90bcdf873a2826746809e64f9143417dbed09fa1c124e673febfed65c137cc45fabda963c96b64645802d1440cba5e58717e539f55f3321ab0c0f60410fba70070c5db500fee874265a343a2a59773fd150bcae09321a5166062e176e2e76bef0e3dd1a9250bcb7f4c971c10f0b24eb2a94e009b72c1fc21ee4267881e27b4edba8bed627ddf37e0c53cd425bc279d0c50d154d136503e54882e9541820d6394bd52ca2b438fd8c517f186fec0649c4846c4e43ce845d80e503dee157ce55392188039a7efc78719107ab989db8d9363b9dfc1946f01a84dbca5e742ed5f30b07ac61cf17ce2cf2c6a49d799ed3968a63a3ccb90d9a0e50960d959f17f202dd5cf0f2c375a8a702e063d339e48c0227e7cf710157f63f13136d8c3076c672ea2c1028fc1825366a145a4311de6c2cc46d3144ae3d2bc5808819b9817be3fce1664ecb60f74733e75e97ca8e567d1b81bdd4c56c7a340ba00"; + #[test_only] + const AB_BATCH_RANGE_PROOF_PEDERSEN: vector = x"103086c56ead10712514d2807c5605cb5f3a090566196549b5f03bedd7c1f450b4619bca9b00f87b2e039e844c24f9f2512901eea7f8f322f218f58c37186e1bd40ae74942f69b18f6806a536b2ab0793ab8e646eafc6e31d5219545dfcbb21334230c4e063e682d1f37fdfe7258d1735af1ba4764ca182803ef4566ddd386143550b83b8d686514988ee05bb7b4180f3b296a0a9711976365b678b537e2190c49cecded1d209ecec733e5cb85d5427f1f2ef1a44ebac41fdbf822692bd68b012515065faab0611aaabe87c1facbe68e648f2e2a0de6e5e81490dfa178546d0e1ec7a7c7ee6eb1e72f0e62b6a81abf23d4e4f946e5c5b28ca287d7ee30c72667ec1203ea9314a4ef182e3ed8a49700cb2452c3765fd29611e2abb5d8aa1970387452cd473383707a0b8e2eb46ba6826654e03ba5f73b56a0ae30012dc723576e76b280339600decef76eda350232ee9e53b373d745b958a19c8b4e7133f4b846727dab188441bb7d2484a73a9a83c1c94e7bea0ea0253418d3d5a751e63f940106e597772d169a01d93b495d10c08725c5d8cdef24306a164a2e1fa1b19eb0217239bbc661e0f1ead2bf3ecc3f178b6b49c61aa2c45f4832ba9ebc2744b79b413081e824b0978cab1934d29760f77751450e409da17941ff693b7dbc0b45d0659aeca05e1e92572fcd4c4d5846e7963e25cce6d54fc4a963da031747695a8e2000469e22e682e1b3f141891121d189504db63b4ab40e0d4c59f0b945b8188b79f0eb4916723a757bcfc787863ff28c5555c8ad93df81bba7b2ff9c164e180331a8b24cff4a9de0d2a8b71f73d24521781f0ced1a064698af138c00160c87eb7ffca5ab1d9a1bec5144c648c5f51a6093dbe8ed88a2fcaab4d5412c60ebb25827d8cab48787f705c5781e2ecd82939d3b3f864c21701fcecbc57b196db7c055273e86ac654a24016abd8ba7c6e87610a0e1b70ff57378992b2d5d45c963829b0aa9323b0dde3f02382e583cb3733c187b46903ed629820ec8043a8c18df42dc0a"; #[test(fx = @std)] #[expected_failure(abort_code = 0x010003, location = Self)] fun test_unsupported_ranges(fx: signer) { features::change_feature_flags_for_testing(&fx, vector[ features::get_bulletproofs_feature() ], vector[]); - let comm = ristretto255::new_point_from_bytes(A_COMM); + let comm = pedersen::new_commitment_from_bytes(A_COMM); let comm = std::option::extract(&mut comm); - let comm = pedersen::commitment_from_point(comm); assert!(verify_range_proof_pedersen( &comm, @@ -195,7 +351,63 @@ module aptos_std::ristretto255_bulletproofs { } #[test(fx = @std)] - #[expected_failure(abort_code = 0x010001, location = Self)] + fun test_batch_prover(fx: signer) { + features::change_feature_flags_for_testing(&fx, vector[ features::get_bulletproofs_batch_feature() ], vector[]); + + let vs = vector[ + ristretto255::new_scalar_from_u64(59), + ristretto255::new_scalar_from_u64(60), + ]; + let rs = vector[ + std::option::extract(&mut ristretto255::new_scalar_from_bytes(A_BLINDER)), + std::option::extract(&mut ristretto255::new_scalar_from_bytes(A_BLINDER)), + ]; + let num_bits = 8; + + let (proof, comms) = prove_batch_range_pedersen(&vs, &rs, num_bits, A_DST); + + assert!(verify_batch_range_proof_pedersen(&comms, &proof, 64, A_DST) == false, 1); + assert!(verify_batch_range_proof_pedersen(&comms, &proof, 32, A_DST) == false, 1); + assert!(verify_batch_range_proof_pedersen(&comms, &proof, 16, A_DST) == false, 1); + assert!(verify_batch_range_proof_pedersen(&comms, &proof, num_bits, A_DST), 1); + } + + #[test(fx = @std)] + #[expected_failure(abort_code = 0x030004, location = Self)] + fun test_bulletproof_feature_disabled(fx: signer) { + features::change_feature_flags_for_testing(&fx, vector[ ], vector[ features::get_bulletproofs_feature()]); + + let v = ristretto255::new_scalar_from_u64(59); + let r = ristretto255::new_scalar_from_bytes(A_BLINDER); + let r = std::option::extract(&mut r); + let num_bits = 8; + + let (proof, comm) = prove_range_pedersen(&v, &r, num_bits, A_DST); + + verify_range_proof_pedersen(&comm, &proof, num_bits, A_DST); + } + + #[test(fx = @std)] + #[expected_failure(abort_code = 0x030004, location = Self)] + fun test_bulletproof_batch_feature_disabled(fx: signer) { + features::change_feature_flags_for_testing(&fx, vector[ ], vector[ features::get_bulletproofs_batch_feature() ]); + + let vs = vector[ + ristretto255::new_scalar_from_u64(59), + ristretto255::new_scalar_from_u64(60), + ]; + let rs = vector[ + std::option::extract(&mut ristretto255::new_scalar_from_bytes(A_BLINDER)), + std::option::extract(&mut ristretto255::new_scalar_from_bytes(A_BLINDER)), + ]; + let num_bits = 8; + + let (proof, comms) = prove_batch_range_pedersen(&vs, &rs, num_bits, A_DST); + + verify_batch_range_proof_pedersen(&comms, &proof, num_bits, A_DST); + } + + #[test(fx = @std)] fun test_empty_range_proof(fx: signer) { features::change_feature_flags_for_testing(&fx, vector[ features::get_bulletproofs_feature() ], vector[]); @@ -207,13 +419,73 @@ module aptos_std::ristretto255_bulletproofs { ); // This will fail with error::invalid_argument(E_DESERIALIZE_RANGE_PROOF) - verify_range_proof_pedersen(&com, proof, num_bits, A_DST); + assert!(verify_range_proof_pedersen(&com, proof, num_bits, A_DST) == false, 1); } #[test(fx = @std)] - fun test_valid_range_proof_verifies_against_comm(fx: signer) { + fun test_empty_batch_range_proof(fx: signer) { + features::change_feature_flags_for_testing(&fx, vector[ features::get_bulletproofs_batch_feature() ], vector[]); + + let proof = &range_proof_from_bytes(vector[ ]); + let num_bits = 64; + let comms = vector[pedersen::new_commitment_for_bulletproof( + &ristretto255::scalar_one(), + &ristretto255::new_scalar_from_sha2_512(b"hello random world") + )]; + + // This will fail with error::invalid_argument(E_DESERIALIZE_RANGE_PROOF) + assert!(verify_batch_range_proof_pedersen(&comms, proof, num_bits, A_DST) == false, 1); + } + + #[test(fx = @std)] + #[expected_failure(abort_code = 0x010004, location = Self)] + fun test_invalid_batch_size_range_proof(fx: signer) { + features::change_feature_flags_for_testing(&fx, vector[ features::get_bulletproofs_batch_feature() ], vector[]); + + let value_a = ristretto255::new_scalar_from_bytes(A_VALUE); + let value_b = ristretto255::new_scalar_from_bytes(B_VALUE); + let value_c = ristretto255::new_scalar_from_u32(1); + + let blinder_a = ristretto255::new_scalar_from_bytes(A_BLINDER); + let blinder_b = ristretto255::new_scalar_from_bytes(B_BLINDER); + let blinder_c = ristretto255::new_scalar_from_u32(1); + + let values = vector[ + std::option::extract(&mut value_a), + std::option::extract(&mut value_b), + value_c, + ]; + let blinders = vector[ + std::option::extract(&mut blinder_a), + std::option::extract(&mut blinder_b), + blinder_c, + ]; + + // This will fail with error::invalid_argument(E_BATCH_SIZE_NOT_SUPPORTED) + prove_batch_range_pedersen(&values, &blinders, 64, A_DST); + } + + #[test(fx = @std)] + #[expected_failure(abort_code = 0x010005, location = Self)] + fun test_invalid_args_batch_range_proof(fx: signer) { features::change_feature_flags_for_testing(&fx, vector[ features::get_bulletproofs_feature() ], vector[]); + let value_a = ristretto255::new_scalar_from_bytes(A_VALUE); + let value_b = ristretto255::new_scalar_from_bytes(B_VALUE); + + let blinder_a = ristretto255::new_scalar_from_bytes(A_BLINDER); + + let values = vector[std::option::extract(&mut value_a), std::option::extract(&mut value_b)]; + let blinders = vector[std::option::extract(&mut blinder_a)]; + + // This will fail with error::invalid_argument(E_VECTOR_LENGTHS_MISMATCH) + prove_batch_range_pedersen(&values, &blinders, 64, A_DST); + } + + #[test(fx = @std)] + fun test_valid_range_proof_verifies_against_comm(fx: signer) { + features::change_feature_flags_for_testing(&fx, vector[ features::get_bulletproofs_batch_feature() ], vector[]); + let value = ristretto255::new_scalar_from_bytes(A_VALUE); let value = std::option::extract(&mut value); @@ -222,21 +494,45 @@ module aptos_std::ristretto255_bulletproofs { let comm = pedersen::new_commitment_for_bulletproof(&value, &blinder); - let expected_comm = std::option::extract(&mut ristretto255::new_point_from_bytes(A_COMM)); - assert!(point_equals(pedersen::commitment_as_point(&comm), &expected_comm), 1); + let expected_comm = std::option::extract(&mut pedersen::new_commitment_from_bytes(A_COMM)); + assert!(commitment_equals(&comm, &expected_comm), 1); assert!(verify_range_proof_pedersen( &comm, &range_proof_from_bytes(A_RANGE_PROOF_PEDERSEN), MAX_RANGE_BITS, A_DST), 1); } + #[test(fx = @std)] + fun test_valid_batch_range_proof_verifies_against_comm(fx: signer) { + features::change_feature_flags_for_testing(&fx, vector[ features::get_bulletproofs_batch_feature() ], vector[]); + + let value_a = ristretto255::new_scalar_from_bytes(A_VALUE); + let value_b = ristretto255::new_scalar_from_bytes(B_VALUE); + + let blinder_a = ristretto255::new_scalar_from_bytes(A_BLINDER); + let blinder_b = ristretto255::new_scalar_from_bytes(B_BLINDER); + + let values = vector[std::option::extract(&mut value_a), std::option::extract(&mut value_b)]; + let blinders = vector[std::option::extract(&mut blinder_a), std::option::extract(&mut blinder_b)]; + + let comms = std::vector::zip_map(values, blinders, |val, blinder| { + pedersen::new_commitment_for_bulletproof(&val, &blinder) + }); + + assert!(commitment_equals(std::vector::borrow(&comms, 0), &std::option::extract(&mut pedersen::new_commitment_from_bytes(A_COMM))), 1); + assert!(commitment_equals(std::vector::borrow(&comms, 1), &std::option::extract(&mut pedersen::new_commitment_from_bytes(B_COMM))), 1); + + assert!(verify_batch_range_proof_pedersen( + &comms, + &range_proof_from_bytes(AB_BATCH_RANGE_PROOF_PEDERSEN), MAX_RANGE_BITS, A_DST), 1); + } + #[test(fx = @std)] fun test_invalid_range_proof_fails_verification(fx: signer) { features::change_feature_flags_for_testing(&fx, vector[ features::get_bulletproofs_feature() ], vector[]); - let comm = ristretto255::new_point_from_bytes(A_COMM); + let comm = pedersen::new_commitment_from_bytes(A_COMM); let comm = std::option::extract(&mut comm); - let comm = pedersen::commitment_from_point(comm); // Take a valid proof... let range_proof_invalid = A_RANGE_PROOF_PEDERSEN; @@ -250,4 +546,26 @@ module aptos_std::ristretto255_bulletproofs { &comm, &range_proof_from_bytes(range_proof_invalid), MAX_RANGE_BITS, A_DST) == false, 1); } + + #[test(fx = @std)] + fun test_invalid_batch_range_proof_fails_verification(fx: signer) { + features::change_feature_flags_for_testing(&fx, vector[ features::get_bulletproofs_batch_feature() ], vector[]); + + let comm_a = pedersen::new_commitment_from_bytes(A_COMM); + let comm_b = pedersen::new_commitment_from_bytes(B_COMM); + + let comms = vector[std::option::extract(&mut comm_a), std::option::extract(&mut comm_b)]; + + // Take a valid proof... + let range_proof_invalid = AB_BATCH_RANGE_PROOF_PEDERSEN; + + // ...and modify a byte in the middle of the proof + let pos = std::vector::length(&range_proof_invalid) / 2; + let byte = std::vector::borrow_mut(&mut range_proof_invalid, pos); + *byte = *byte + 1; + + assert!(verify_batch_range_proof_pedersen( + &comms, + &range_proof_from_bytes(range_proof_invalid), MAX_RANGE_BITS, A_DST) == false, 1); + } } diff --git a/aptos-move/framework/move-stdlib/sources/configs/features.move b/aptos-move/framework/move-stdlib/sources/configs/features.move index 28de7017df8d9c..eb032b61f12ad0 100644 --- a/aptos-move/framework/move-stdlib/sources/configs/features.move +++ b/aptos-move/framework/move-stdlib/sources/configs/features.move @@ -634,6 +634,16 @@ module std::features { is_enabled(ACCOUNT_ABSTRACTION) } + /// Whether the batch Bulletproofs native functions are available. This is needed because of the introduction of a new native function. + /// Lifetime: transient + const BULLETPROOFS_BATCH_NATIVES: u64 = 87; + + public fun get_bulletproofs_batch_feature(): u64 { BULLETPROOFS_BATCH_NATIVES } + + public fun bulletproofs_batch_enabled(): bool acquires Features { + is_enabled(BULLETPROOFS_BATCH_NATIVES) + } + // ============================================================================================ // Feature Flag Implementation diff --git a/aptos-move/framework/src/natives/cryptography/bulletproofs.rs b/aptos-move/framework/src/natives/cryptography/bulletproofs.rs index 513da6fe504a96..37d5a48c3825b3 100644 --- a/aptos-move/framework/src/natives/cryptography/bulletproofs.rs +++ b/aptos-move/framework/src/natives/cryptography/bulletproofs.rs @@ -2,22 +2,22 @@ // SPDX-License-Identifier: Apache-2.0 #[cfg(feature = "testing")] -use crate::natives::cryptography::ristretto255::pop_scalar_from_bytes; +use crate::natives::cryptography::ristretto255::{pop_scalar_from_bytes, pop_scalars_from_bytes}; use crate::natives::cryptography::ristretto255_point::{ get_point_handle, NativeRistrettoPointContext, }; use aptos_crypto::bulletproofs::MAX_RANGE_BITS; use aptos_gas_schedule::gas_params::natives::aptos_framework::*; use aptos_native_interface::{ - safely_pop_arg, RawSafeNative, SafeNativeBuilder, SafeNativeContext, SafeNativeError, - SafeNativeResult, + safely_pop_arg, safely_pop_vec_arg, RawSafeNative, SafeNativeBuilder, SafeNativeContext, + SafeNativeError, SafeNativeResult, }; use bulletproofs::{BulletproofGens, PedersenGens}; #[cfg(feature = "testing")] use byteorder::{ByteOrder, LittleEndian}; use curve25519_dalek::ristretto::CompressedRistretto; use merlin::Transcript; -use move_core_types::gas_algebra::{NumArgs, NumBytes}; +use move_core_types::gas_algebra::NumBytes; use move_vm_runtime::native_functions::NativeFunction; use move_vm_types::{ loaded_data::runtime_types::Type, @@ -30,7 +30,7 @@ use std::collections::VecDeque; pub mod abort_codes { /// Abort code when deserialization fails (leading 0x01 == INVALID_ARGUMENT) /// NOTE: This must match the code in the Move implementation - pub const NFE_DESERIALIZE_RANGE_PROOF: u64 = 0x01_0001; + pub const _NFE_DESERIALIZE_RANGE_PROOF: u64 = 0x01_0001; /// Abort code when input value for a range proof is too large. /// NOTE: This must match the code in the Move implementation @@ -39,6 +39,14 @@ pub mod abort_codes { /// Abort code when the requested range is larger than the maximum supported one. /// NOTE: This must match the code in the Move implementation pub const NFE_RANGE_NOT_SUPPORTED: u64 = 0x01_0003; + + /// Abort code when the requested batch size is larger than the maximum supported one. + /// NOTE: This must match the code in the Move implementation + pub const NFE_BATCH_SIZE_NOT_SUPPORTED: u64 = 0x01_0004; + + /// Abort code when the vector lengths of values and blinding factors do not match. + /// NOTE: This must match the code in the Move implementation + pub const NFE_VECTOR_LENGTHS_MISMATCH: u64 = 0x01_0005; } /// The Bulletproofs library only seems to support proving [0, 2^{num_bits}) ranges where num_bits is @@ -47,9 +55,14 @@ fn is_supported_number_of_bits(num_bits: usize) -> bool { matches!(num_bits, 8 | 16 | 32 | 64) } +/// The Bulletproofs library only supports batch sizes of 1, 2, 4, 8, or 16. +fn is_supported_batch_size(batch_size: usize) -> bool { + matches!(batch_size, 1 | 2 | 4 | 8 | 16) +} + /// Public parameters of the Bulletproof range proof system static BULLETPROOF_GENERATORS: Lazy = - Lazy::new(|| BulletproofGens::new(MAX_RANGE_BITS, 1)); + Lazy::new(|| BulletproofGens::new(MAX_RANGE_BITS, 16)); fn native_verify_range_proof( context: &mut SafeNativeContext, @@ -91,6 +104,54 @@ fn native_verify_range_proof( verify_range_proof(context, &comm_point, &pg, &proof_bytes[..], num_bits, dst) } +fn native_verify_batch_range_proof( + context: &mut SafeNativeContext, + _ty_args: Vec, + mut args: VecDeque, +) -> SafeNativeResult> { + debug_assert!(_ty_args.is_empty()); + debug_assert!(args.len() == 6); + + let dst = safely_pop_arg!(args, Vec); + let num_bits = safely_pop_arg!(args, u64) as usize; + let proof_bytes = safely_pop_arg!(args, Vec); + let rand_base_handle = get_point_handle(&safely_pop_arg!(args, StructRef))?; + let val_base_handle = get_point_handle(&safely_pop_arg!(args, StructRef))?; + let comm_bytes = safely_pop_vec_arg!(args, Vec); + + let comm_points = comm_bytes + .iter() + .map(|comm_bytes| CompressedRistretto::from_slice(comm_bytes.as_slice())) + .collect::>(); + + if !is_supported_number_of_bits(num_bits) { + return Err(SafeNativeError::Abort { + abort_code: abort_codes::NFE_RANGE_NOT_SUPPORTED, + }); + } + if !is_supported_batch_size(comm_points.len()) { + return Err(SafeNativeError::Abort { + abort_code: abort_codes::NFE_BATCH_SIZE_NOT_SUPPORTED, + }); + } + + let pg = { + let point_context = context.extensions().get::(); + let point_data = point_context.point_data.borrow_mut(); + + let rand_base = point_data.get_point(&rand_base_handle); + let val_base = point_data.get_point(&val_base_handle); + + // TODO(Perf): Is there a way to avoid this unnecessary cloning here? + PedersenGens { + B: *val_base, + B_blinding: *rand_base, + } + }; + + verify_batch_range_proof(context, &comm_points, &pg, &proof_bytes[..], num_bits, dst) +} + #[cfg(feature = "testing")] /// This is a test-only native that charges zero gas. It is only exported in testing mode. fn native_test_only_prove_range( @@ -157,6 +218,93 @@ fn native_test_only_prove_range( ]) } +#[cfg(feature = "testing")] +/// This is a test-only native that charges zero gas. It is only exported in testing mode. +fn native_test_only_batch_prove_range( + context: &mut SafeNativeContext, + _ty_args: Vec, + mut args: VecDeque, +) -> SafeNativeResult> { + debug_assert!(_ty_args.is_empty()); + debug_assert!(args.len() == 6); + + let rand_base_handle = get_point_handle(&safely_pop_arg!(args, StructRef))?; + let val_base_handle = get_point_handle(&safely_pop_arg!(args, StructRef))?; + let dst = safely_pop_arg!(args, Vec); + let num_bits = safely_pop_arg!(args, u64) as usize; + let v_blindings = pop_scalars_from_bytes(&mut args)?; + let vs = pop_scalars_from_bytes(&mut args)?; + + if !is_supported_number_of_bits(num_bits) { + return Err(SafeNativeError::Abort { + abort_code: abort_codes::NFE_RANGE_NOT_SUPPORTED, + }); + } + if !is_supported_batch_size(vs.len()) { + return Err(SafeNativeError::Abort { + abort_code: abort_codes::NFE_BATCH_SIZE_NOT_SUPPORTED, + }); + } + if vs.len() != v_blindings.len() { + return Err(SafeNativeError::Abort { + abort_code: abort_codes::NFE_VECTOR_LENGTHS_MISMATCH, + }); + } + + // Make sure only the first 64 bits are set for each Scalar. + if !vs + .iter() + .all(|v| v.as_bytes()[8..].iter().all(|&byte| byte == 0u8)) + { + return Err(SafeNativeError::Abort { + abort_code: abort_codes::NFE_VALUE_OUTSIDE_RANGE, + }); + } + + // Convert each Scalar to u64. + let vs = vs + .iter() + .map(|v| LittleEndian::read_u64(v.as_bytes())) + .collect::>(); + + let mut t = Transcript::new(dst.as_slice()); + + let pg = { + let point_context = context.extensions().get::(); + let point_data = point_context.point_data.borrow_mut(); + + let rand_base = point_data.get_point(&rand_base_handle); + let val_base = point_data.get_point(&val_base_handle); + + // TODO(Perf): Is there a way to avoid this unnecessary cloning here? + PedersenGens { + B: *val_base, + B_blinding: *rand_base, + } + }; + + // Construct a range proof. + let (proof, commitments) = bulletproofs::RangeProof::prove_multiple( + &BULLETPROOF_GENERATORS, + &pg, + &mut t, + &vs, + &v_blindings, + num_bits, + ) + .expect("Bulletproofs prover failed unexpectedly"); + + Ok(smallvec![ + Value::vector_u8(proof.to_bytes()), + Value::vector_for_testing_only( + commitments + .iter() + .map(|commitment| Value::vector_u8(commitment.as_bytes().to_vec())) + .collect::>() + ) + ]) +} + /*************************************************************************************************** * module * @@ -171,23 +319,16 @@ fn verify_range_proof( bit_length: usize, dst: Vec, ) -> SafeNativeResult> { - context.charge( - BULLETPROOFS_BASE - + BULLETPROOFS_PER_BYTE_RANGEPROOF_DESERIALIZE - * NumBytes::new(proof_bytes.len() as u64), - )?; + // Batch size of 1 corresponds to the first element in the array. + charge_gas_for_deserialization(context, proof_bytes, 1)?; let range_proof = match bulletproofs::RangeProof::from_bytes(proof_bytes) { Ok(proof) => proof, - Err(_) => { - return Err(SafeNativeError::Abort { - abort_code: abort_codes::NFE_DESERIALIZE_RANGE_PROOF, - }) - }, + Err(_) => return Ok(smallvec![Value::bool(false)]), }; // The (Bullet)proof size is $\log_2(num_bits)$ and its verification time is $O(num_bits)$ - context.charge(BULLETPROOFS_PER_BIT_RANGEPROOF_VERIFY * NumArgs::new(bit_length as u64))?; + charge_gas_for_verification(context, bit_length, 1)?; let mut ver_trans = Transcript::new(dst.as_slice()); @@ -204,21 +345,124 @@ fn verify_range_proof( Ok(smallvec![Value::bool(success)]) } +/// Helper function to gas meter and verify a batch Bulletproof range proof for Pedersen +/// commitments with `pc_gens` as their commitment keys. +fn verify_batch_range_proof( + context: &mut SafeNativeContext, + comm_points: &[CompressedRistretto], + pc_gens: &PedersenGens, + proof_bytes: &[u8], + bit_length: usize, + dst: Vec, +) -> SafeNativeResult> { + charge_gas_for_deserialization(context, proof_bytes, comm_points.len())?; + + let range_proof = match bulletproofs::RangeProof::from_bytes(proof_bytes) { + Ok(proof) => proof, + Err(_) => return Ok(smallvec![Value::bool(false)]), + }; + + // The (Bullet)proof size is $\log_2(num_bits)$ and its verification time is $O(num_bits)$ + charge_gas_for_verification(context, bit_length, comm_points.len())?; + + let mut ver_trans = Transcript::new(dst.as_slice()); + + let success = range_proof + .verify_multiple( + &BULLETPROOF_GENERATORS, + pc_gens, + &mut ver_trans, + comm_points, + bit_length, + ) + .is_ok(); + + Ok(smallvec![Value::bool(success)]) +} + pub fn make_all( builder: &SafeNativeBuilder, ) -> impl Iterator + '_ { let mut natives = vec![]; #[cfg(feature = "testing")] - natives.extend([( - "prove_range_internal", - native_test_only_prove_range as RawSafeNative, - )]); - - natives.extend([( - "verify_range_proof_internal", - native_verify_range_proof as RawSafeNative, - )]); + natives.extend([ + ( + "prove_range_internal", + native_test_only_prove_range as RawSafeNative, + ), + ( + "prove_batch_range_internal", + native_test_only_batch_prove_range, + ), + ]); + + natives.extend([ + ( + "verify_range_proof_internal", + native_verify_range_proof as RawSafeNative, + ), + ( + "verify_batch_range_proof_internal", + native_verify_batch_range_proof, + ), + ]); builder.make_named_natives(natives) } + +/// Charges gas for deserializing a Bulletproof range proof. +fn charge_gas_for_deserialization( + context: &mut SafeNativeContext, + proof_bytes: &[u8], + batch_size: usize, +) -> SafeNativeResult<()> { + let proof_bytes_len = NumBytes::new(proof_bytes.len() as u64); + + match batch_size { + 1 => context.charge( + BULLETPROOFS_DESERIALIZE_BASE_1 + BULLETPROOFS_DESERIALIZE_PER_BYTE_1 * proof_bytes_len, + ), + 2 => context.charge( + BULLETPROOFS_DESERIALIZE_BASE_2 + BULLETPROOFS_DESERIALIZE_PER_BYTE_2 * proof_bytes_len, + ), + 4 => context.charge( + BULLETPROOFS_DESERIALIZE_BASE_4 + BULLETPROOFS_DESERIALIZE_PER_BYTE_4 * proof_bytes_len, + ), + 8 => context.charge( + BULLETPROOFS_DESERIALIZE_BASE_8 + BULLETPROOFS_DESERIALIZE_PER_BYTE_8 * proof_bytes_len, + ), + 16 => context.charge( + BULLETPROOFS_DESERIALIZE_BASE_16 + + BULLETPROOFS_DESERIALIZE_PER_BYTE_16 * proof_bytes_len, + ), + _ => unreachable!(), + } +} + +/// Charges gas for verifying a Bulletproof range proof. +fn charge_gas_for_verification( + context: &mut SafeNativeContext, + bit_length: usize, + batch_size: usize, +) -> SafeNativeResult<()> { + let bit_length = NumBytes::new(bit_length as u64); + + match batch_size { + 1 => { + context.charge(BULLETPROOFS_VERIFY_BASE_1 + BULLETPROOFS_VERIFY_PER_BIT_1 * bit_length) + }, + 2 => { + context.charge(BULLETPROOFS_VERIFY_BASE_2 + BULLETPROOFS_VERIFY_PER_BIT_2 * bit_length) + }, + 4 => { + context.charge(BULLETPROOFS_VERIFY_BASE_4 + BULLETPROOFS_VERIFY_PER_BIT_4 * bit_length) + }, + 8 => { + context.charge(BULLETPROOFS_VERIFY_BASE_8 + BULLETPROOFS_VERIFY_PER_BIT_8 * bit_length) + }, + 16 => context + .charge(BULLETPROOFS_VERIFY_BASE_16 + BULLETPROOFS_VERIFY_PER_BIT_16 * bit_length), + _ => unreachable!(), + } +} diff --git a/aptos-move/framework/src/natives/cryptography/ristretto255.rs b/aptos-move/framework/src/natives/cryptography/ristretto255.rs index 93d3d888843af5..88be28e2328671 100644 --- a/aptos-move/framework/src/natives/cryptography/ristretto255.rs +++ b/aptos-move/framework/src/natives/cryptography/ristretto255.rs @@ -5,8 +5,8 @@ use crate::natives::cryptography::{ristretto255_point, ristretto255_scalar}; use aptos_gas_algebra::GasExpression; use aptos_gas_schedule::{gas_params::natives::aptos_framework::*, NativeGasParameters}; use aptos_native_interface::{ - safely_assert_eq, safely_pop_arg, RawSafeNative, SafeNativeBuilder, SafeNativeError, - SafeNativeResult, + safely_assert_eq, safely_pop_arg, safely_pop_vec_arg, RawSafeNative, SafeNativeBuilder, + SafeNativeError, SafeNativeResult, }; use aptos_types::vm_status::StatusCode; use curve25519_dalek::scalar::Scalar; @@ -174,6 +174,16 @@ pub fn pop_scalar_from_bytes(arguments: &mut VecDeque) -> SafeNativeResul scalar_from_valid_bytes(bytes) } +/// Pops a Scalars off the argument stack when the argument was a `vector>`. +pub fn pop_scalars_from_bytes(arguments: &mut VecDeque) -> SafeNativeResult> { + let bytes = safely_pop_vec_arg!(arguments, Vec); + + bytes + .into_iter() + .map(scalar_from_valid_bytes) + .collect::>>() +} + /// The 'data' field inside a Move Scalar struct is at index 0. const DATA_FIELD_INDEX: usize = 0; diff --git a/crates/aptos-crypto/benches/bulletproofs.rs b/crates/aptos-crypto/benches/bulletproofs.rs index 95877aab593c24..9c123bb0814c47 100644 --- a/crates/aptos-crypto/benches/bulletproofs.rs +++ b/crates/aptos-crypto/benches/bulletproofs.rs @@ -4,6 +4,7 @@ #[macro_use] extern crate criterion; +use aptos_crypto::bulletproofs::MAX_RANGE_BITS; use bulletproofs::{BulletproofGens, PedersenGens, RangeProof}; use criterion::{measurement::Measurement, BenchmarkGroup, BenchmarkId, Criterion, Throughput}; use curve25519_dalek_ng::scalar::Scalar; @@ -28,12 +29,9 @@ fn get_values(num_bits: usize, batch_size: usize) -> (Vec, Vec) { fn bench_group(c: &mut Criterion) { let mut group = c.benchmark_group("bulletproofs"); - for num_bits in [32, 64] { - range_proof_deserialize(&mut group, num_bits); - } - - for batch_size in [1, 2] { + for batch_size in [1, 2, 4, 8, 16] { for num_bits in [32, 64] { + range_proof_deserialize(&mut group, num_bits, batch_size); range_prove(&mut group, num_bits, batch_size); range_verify(&mut group, num_bits, batch_size); } @@ -44,14 +42,11 @@ fn bench_group(c: &mut Criterion) { fn range_prove(g: &mut BenchmarkGroup, num_bits: usize, batch_size: usize) { let pg = PedersenGens::default(); - let bg = BulletproofGens::new(num_bits, batch_size); + let bg = BulletproofGens::new(MAX_RANGE_BITS, 16); g.throughput(Throughput::Elements(batch_size as u64)); g.bench_function( - BenchmarkId::new( - "range_prove", - format!("batch={}/bits={}", batch_size, num_bits), - ), + BenchmarkId::new(format!("range_prove_batch_{}", batch_size), num_bits), move |b| { b.iter_with_setup( || get_values(num_bits, batch_size), @@ -72,13 +67,20 @@ fn range_prove(g: &mut BenchmarkGroup, num_bits: usize, batch ); } -fn range_proof_deserialize(g: &mut BenchmarkGroup, num_bits: usize) { - let bp_gens = BulletproofGens::new(num_bits, 1); +fn range_proof_deserialize( + g: &mut BenchmarkGroup, + num_bits: usize, + batch_size: usize, +) { + let bp_gens = BulletproofGens::new(MAX_RANGE_BITS, 16); let pc_gens = PedersenGens::default(); g.throughput(Throughput::Elements(1)); g.bench_function( - BenchmarkId::new("range_proof_deserialize", num_bits), + BenchmarkId::new( + format!("range_proof_deserialize_batch_{}", batch_size), + num_bits, + ), move |b| { b.iter_with_setup( || { @@ -106,15 +108,12 @@ fn range_proof_deserialize(g: &mut BenchmarkGroup, num_bits: } fn range_verify(g: &mut BenchmarkGroup, num_bits: usize, batch_size: usize) { - let bp_gens = BulletproofGens::new(num_bits, batch_size); + let bp_gens = BulletproofGens::new(MAX_RANGE_BITS, 16); let pc_gens = PedersenGens::default(); g.throughput(Throughput::Elements(batch_size as u64)); g.bench_function( - BenchmarkId::new( - "range_verify", - format!("batch={}/bits={}", batch_size, num_bits), - ), + BenchmarkId::new(format!("range_verify_batch_{}", batch_size), num_bits), move |b| { b.iter_with_setup( || { diff --git a/scripts/algebra-gas/update_bulletproofs_gas_params.py b/scripts/algebra-gas/update_bulletproofs_gas_params.py index 67a553a3bfaeb3..ca778ccebee556 100755 --- a/scripts/algebra-gas/update_bulletproofs_gas_params.py +++ b/scripts/algebra-gas/update_bulletproofs_gas_params.py @@ -17,7 +17,7 @@ # Typically you are making a new version of gas schedule, # so this should be larger than `LATEST_GAS_FEATURE_VERSION` in `aptos-move/aptos-gas/src/gas_meter.rs`. -TARGET_GAS_VERSION = 9 +TARGET_GAS_VERSION = 11 def get_bench_ns_linear(bench_path): datapoints = load_bench_datapoints.main(bench_path) @@ -34,22 +34,40 @@ def prettify_number(x:int) -> str: def get_bulletproofs_lines(gas_per_ns): nanoseconds = {} - _,_,verify_slope,verify_base = get_bench_ns_linear('target/criterion/bulletproofs/range_proof_verify') - nanoseconds['per_bit_rangeproof_verify'] = verify_slope - #_,_,nanoseconds['per_bit_rangeproof_verify'],nanoseconds['rangeproof_verify_base'] = get_bench_ns_linear('target/criterion/bulletproofs/range_proof_verify') - _,_,deserialize_slope,deserialize_base = get_bench_ns_linear('target/criterion/bulletproofs/range_proof_deserialize') - nanoseconds['per_byte_rangeproof_deserialize'] = deserialize_slope - nanoseconds['base'] = verify_base + verify_slope + + for batch_size in [1, 2, 4, 8, 16]: + _,_,verify_slope,verify_base = get_bench_ns_linear(f'target/criterion/bulletproofs/range_verify_batch_{batch_size}') + _,_,deserialize_slope,deserialize_base = get_bench_ns_linear(f'target/criterion/bulletproofs/range_proof_deserialize_batch_{batch_size}') + + nanoseconds[f'bulletproofs_verify_base_{batch_size}'] = verify_base + nanoseconds[f'bulletproofs_verify_per_bit_{batch_size}'] = verify_slope + nanoseconds[f'bulletproofs_deserialize_base_{batch_size}'] = deserialize_base + nanoseconds[f'bulletproofs_deserialize_per_byte_{batch_size}'] = deserialize_slope + gas_units = {k:gas_per_ns*v for k,v in nanoseconds.items()} - lines = [f' [.bulletproofs.{k}, {{ {TARGET_GAS_VERSION}.. => "bulletproofs.{k}" }}, {prettify_number(v)} * MUL],' for k,v in sorted(gas_units.items())] + + lines = [] + + for batch_size in [1, 2, 4, 8, 16]: + lines.append(f' [bulletproofs_verify_base_{batch_size}: InternalGas, {{ {TARGET_GAS_VERSION}.. => "bulletproofs.verify.base_{batch_size}" }}, {prettify_number(gas_units[f"bulletproofs_verify_base_{batch_size}"])}],') + + for batch_size in [1, 2, 4, 8, 16]: + lines.append(f' [bulletproofs_verify_per_bit_{batch_size}: InternalGasPerByte, {{ {TARGET_GAS_VERSION}.. => "bulletproofs.verify.per_bit_{batch_size}" }}, {prettify_number(gas_units[f"bulletproofs_verify_per_bit_{batch_size}"])}],') + + for batch_size in [1, 2, 4, 8, 16]: + lines.append(f' [bulletproofs_deserialize_base_{batch_size}: InternalGas, {{ {TARGET_GAS_VERSION}.. => "bulletproofs.deserialize.base_{batch_size}" }}, {prettify_number(gas_units[f"bulletproofs_deserialize_base_{batch_size}"])}],') + + for batch_size in [1, 2, 4, 8, 16]: + lines.append(f' [bulletproofs_deserialize_per_byte_{batch_size}: InternalGasPerByte, {{ {TARGET_GAS_VERSION}.. => "bulletproofs.deserialize.per_byte_{batch_size}" }}, {prettify_number(gas_units[f"bulletproofs_deserialize_per_byte_{batch_size}"])}],') + return lines def main(gas_per_ns): - path = Path('aptos-move/aptos-gas/src/aptos_framework.rs') + path = Path('aptos-move/aptos-gas-schedule/src/gas_schedule/aptos_framework.rs') lines = path.read_text().split('\n') - line_id_begin = lines.index(' // Bulletproofs gas parameters begin.') - line_id_end = lines.index(' // Bulletproofs gas parameters end.') - generator_note_line = f' // Generated at time {time()} by `scripts/algebra-gas/update_bulletproofs_gas_params.py` with gas_per_ns={gas_per_ns}.' + line_id_begin = lines.index(' // Bulletproofs gas parameters begin.') + line_id_end = lines.index(' // Bulletproofs gas parameters end.') + generator_note_line = f' // Generated at time {time()} by `scripts/algebra-gas/update_bulletproofs_gas_params.py` with gas_per_ns={gas_per_ns}.' new_lines = lines[:line_id_begin+1] + [generator_note_line] + get_bulletproofs_lines(gas_per_ns) + lines[line_id_end:] path.write_text('\n'.join(new_lines)) diff --git a/types/src/on_chain_config/aptos_features.rs b/types/src/on_chain_config/aptos_features.rs index cb2668ee3e72f4..45101b50790536 100644 --- a/types/src/on_chain_config/aptos_features.rs +++ b/types/src/on_chain_config/aptos_features.rs @@ -120,6 +120,7 @@ pub enum FeatureFlag { ACCOUNT_ABSTRACTION = 85, /// Enables bytecode version v8 VM_BINARY_FORMAT_V8 = 86, + BULLETPROOFS_BATCH_NATIVES = 87, } impl FeatureFlag {