diff --git a/CHANGELOG.md b/CHANGELOG.md index 829c2180ce..91d6ada1ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [Unreleased] +### Added +- Implement `bn128` precompiles ‒ [2708](https://github.com/use-ink/ink/pull/2718) + ### Changed -- Refactor contract ref generation and add automatic re-exporting - [#2710](https://github.com/use-ink/ink/pull/2710) +- Refactor contract ref generation and add automatic re-exporting ‒ [#2710](https://github.com/use-ink/ink/pull/2710) - Implement and stabilize `terminate_contract` ‒ [2708](https://github.com/use-ink/ink/pull/2708) ## Version 6.0.0-beta @@ -2640,4 +2643,4 @@ impl Contract { This is useful if the `impl` block itself does not contain any ink! constructors or messages, but you still need to access some of the "magic" provided by ink!. In the example above, you would not have -access to `emit_event` without `#[ink(impl)]`. \ No newline at end of file +access to `emit_event` without `#[ink(impl)]`. diff --git a/crates/engine/src/ext.rs b/crates/engine/src/ext.rs index e9e69577ca..6c6261443d 100644 --- a/crates/engine/src/ext.rs +++ b/crates/engine/src/ext.rs @@ -353,6 +353,27 @@ impl Engine { } impl Engine { + /// Implements the `bn128_add` G1 addition precompile. + pub fn bn128_add(self, _x1: U256, _y1: U256, _x2: U256, _y2: U256) -> (U256, U256) { + unimplemented!( + "`bn128_add` is not implemented in the off-chain testing environment" + ); + } + + /// Implements the `bn128_mul` G1 scalar-mul precompile. + pub fn bn128_mul(self, _x1: U256, _y1: U256, _scalar: U256) -> (U256, U256) { + unimplemented!( + "`bn128_mul` is not implemented in the off-chain testing environment" + ); + } + + /// Implements the `bn128_pairing` precompile. + pub fn bn128_pairing(self, _input: &[u8]) -> bool { + unimplemented!( + "`bn128_pairing` is not implemented in the off-chain testing environment" + ); + } + /// Recovers the compressed ECDSA public key for given `signature` and `message_hash`, /// and stores the result in `output`. #[allow(clippy::arithmetic_side_effects)] // todo diff --git a/crates/env/src/api.rs b/crates/env/src/api.rs index ad3be06aab..6105dd8d26 100644 --- a/crates/env/src/api.rs +++ b/crates/env/src/api.rs @@ -617,6 +617,49 @@ where }) } +/// Calls the `bn128_add` G1 addition precompile. +/// +/// Inputs are affine G1 coordinates over Fq. +/// Returns the resulting affine point or (0, 0) if the result is ∞ / invalid. +/// +/// # Note +/// +/// The precompile address is `0x06`. You can find its implementation here: +/// +pub fn bn128_add(x1: U256, y1: U256, x2: U256, y2: U256) -> (U256, U256) { + ::on_instance(|instance| { + instance.bn128_add(x1, y1, x2, y2) + }) +} + +/// Calls the `bn128_mul` G1 scalar-mul precompile. +/// +/// Multiplies an affine G1 point (x1, y1) by a scalar ∈ Fr. +/// Returns the resulting affine point or (0, 0) if the result is ∞ / invalid. +/// +/// # Note +/// +/// The precompile address is `0x07`. You can find its implementation here: +/// +pub fn bn128_mul(x1: U256, y1: U256, scalar: U256) -> (U256, U256) { + ::on_instance(|instance| { + instance.bn128_mul(x1, y1, scalar) + }) +} + +/// Calls the `bn128_pairing` precompile. +/// +/// Input is the Solidity-ABI-encoded sequence of (G1, G2) pairs. +/// Returns `true` iff the product of pairings evaluates to the identity. +/// +/// # Note +/// +/// The precompile address is `0x08`. You can find its implementation here: +/// +pub fn bn128_pairing(input: &[u8]) -> bool { + ::on_instance(|instance| instance.bn128_pairing(input)) +} + /// Recovers the compressed ECDSA public key for given `signature` and `message_hash`, /// and stores the result in `output`. /// diff --git a/crates/env/src/backend.rs b/crates/env/src/backend.rs index ddb21119d7..f71a0ca063 100644 --- a/crates/env/src/backend.rs +++ b/crates/env/src/backend.rs @@ -173,6 +173,39 @@ pub trait EnvBackend { H: CryptoHash, T: scale::Encode; + /// Calls the `bn128_add` G1 addition precompile. + /// + /// Inputs are affine G1 coordinates over Fq. + /// Returns the resulting affine point or (0, 0) if the result is ∞ / invalid. + /// + /// # Note + /// + /// The precompile address is `0x06`. You can find its implementation here: + /// + fn bn128_add(&mut self, x1: U256, y1: U256, x2: U256, y2: U256) -> (U256, U256); + + /// Calls the `bn128_mul` G1 scalar-mul precompile. + /// + /// Multiplies an affine G1 point (x1, y1) by a scalar ∈ Fr. + /// Returns the resulting affine point or (0, 0) if the result is ∞ / invalid. + /// + /// # Note + /// + /// The precompile address is `0x07`. You can find its implementation here: + /// + fn bn128_mul(&mut self, x1: U256, y1: U256, scalar: U256) -> (U256, U256); + + /// Calls the `bn128_pairing` precompile. + /// + /// Input is the Solidity-ABI-encoded sequence of (G1, G2) pairs. + /// Returns `true` iff the product of pairings evaluates to the identity. + /// + /// # Note + /// + /// The precompile address is `0x08`. You can find its implementation here: + /// + fn bn128_pairing(&mut self, input: &[u8]) -> bool; + /// Recovers the compressed ECDSA public key for given `signature` and `message_hash`, /// and stores the result in `output`. fn ecdsa_recover( diff --git a/crates/env/src/engine/off_chain/impls.rs b/crates/env/src/engine/off_chain/impls.rs index 3938589559..6854b77c27 100644 --- a/crates/env/src/engine/off_chain/impls.rs +++ b/crates/env/src/engine/off_chain/impls.rs @@ -456,6 +456,24 @@ impl EnvBackend for EnvInstance { ::hash(enc_input, output) } + fn bn128_add(&mut self, _x1: U256, _y1: U256, _x2: U256, _y2: U256) -> (U256, U256) { + unimplemented!( + "`bn128_add` is not implemented in the off-chain testing environment" + ); + } + + fn bn128_mul(&mut self, _x1: U256, _y1: U256, _scalar: U256) -> (U256, U256) { + unimplemented!( + "`bn128_mul` is not implemented in the off-chain testing environment" + ); + } + + fn bn128_pairing(&mut self, _input: &[u8]) -> bool { + unimplemented!( + "`bn128_pairing` is not implemented in the off-chain testing environment" + ); + } + #[allow(clippy::arithmetic_side_effects)] // todo fn ecdsa_recover( &mut self, diff --git a/crates/env/src/engine/on_chain/pallet_revive.rs b/crates/env/src/engine/on_chain/pallet_revive.rs index 3b1acb8587..329968be10 100644 --- a/crates/env/src/engine/on_chain/pallet_revive.rs +++ b/crates/env/src/engine/on_chain/pallet_revive.rs @@ -868,6 +868,80 @@ impl EnvBackend for EnvInstance { ::hash(enc_input, output) } + fn bn128_add(&mut self, x1: U256, y1: U256, x2: U256, y2: U256) -> (U256, U256) { + const BN128_ADD: [u8; 20] = + hex_literal::hex!("0000000000000000000000000000000000000006"); + + let mut input = [0u8; 128]; + input[..32].copy_from_slice(&x1.to_big_endian()); + input[32..64].copy_from_slice(&y1.to_big_endian()); + input[64..96].copy_from_slice(&x2.to_big_endian()); + input[96..128].copy_from_slice(&y2.to_big_endian()); + + let mut output = [0u8; 64]; + let _ = ext::call( + CallFlags::empty(), + &BN128_ADD, + u64::MAX, // `ref_time` to devote for execution. `u64::MAX` = all + u64::MAX, // `proof_size` to devote for execution. `u64::MAX` = all + &[u8::MAX; 32], // No deposit limit. + &U256::zero().to_little_endian(), // Value transferred to the contract. + &input[..], + Some(&mut &mut output[..]), + ); + let x3 = U256::from_big_endian(&output[..32]); + let y3 = U256::from_big_endian(&output[32..64]); + (x3, y3) + } + + fn bn128_mul(&mut self, x1: U256, y1: U256, scalar: U256) -> (U256, U256) { + const BN128_ADD: [u8; 20] = + hex_literal::hex!("0000000000000000000000000000000000000007"); + + let mut input = [0u8; 128]; + input[..32].copy_from_slice(&x1.to_big_endian()); + input[32..64].copy_from_slice(&y1.to_big_endian()); + input[64..96].copy_from_slice(&scalar.to_big_endian()); + + let mut output = [0u8; 64]; + let _ = ext::call( + CallFlags::empty(), + &BN128_ADD, + u64::MAX, // `ref_time` to devote for execution. `u64::MAX` = all + u64::MAX, // `proof_size` to devote for execution. `u64::MAX` = all + &[u8::MAX; 32], // No deposit limit. + &U256::zero().to_little_endian(), // Value transferred to the contract. + &input[..], + Some(&mut &mut output[..]), + ); + let x2 = U256::from_big_endian(&output[..32]); + let y2 = U256::from_big_endian(&output[32..64]); + (x2, y2) + } + + fn bn128_pairing(&mut self, input: &[u8]) -> bool { + const BN128_ADD: [u8; 20] = + hex_literal::hex!("0000000000000000000000000000000000000008"); + + let mut output = [0u8; 32]; + let _ = ext::call( + CallFlags::empty(), + &BN128_ADD, + u64::MAX, // `ref_time` to devote for execution. `u64::MAX` = all + u64::MAX, // `proof_size` to devote for execution. `u64::MAX` = all + &[u8::MAX; 32], // No deposit limit. + &U256::zero().to_little_endian(), // Value transferred to the contract. + &input[..], + Some(&mut &mut output[..]), + ); + if output[31] == 1 { + debug_assert_eq!(&output[..31], [0u8; 31]); + return true; + } + debug_assert_eq!(&output[..32], [0u8; 32]); + false + } + fn ecdsa_recover( &mut self, signature: &[u8; 65], diff --git a/crates/ink/src/env_access.rs b/crates/ink/src/env_access.rs index 3652e2d763..2f726baca0 100644 --- a/crates/ink/src/env_access.rs +++ b/crates/ink/src/env_access.rs @@ -998,6 +998,42 @@ where output } + /// Calls the `bn128_add` G1 addition precompile. + /// + /// Inputs are affine G1 coordinates over Fq. + /// Returns the resulting affine point or (0, 0) if the result is ∞ / invalid. + /// + /// # Note + /// + /// For more details visit: [`ink_env::bn128_add`] + pub fn bn128_add(self, x1: U256, y1: U256, x2: U256, y2: U256) -> (U256, U256) { + ink_env::bn128_add(x1, y1, x2, y2) + } + + /// Calls the `bn128_mul` G1 scalar-mul precompile. + /// + /// Multiplies an affine G1 point (x1, y1) by a scalar ∈ Fr. + /// Returns the resulting affine point or (0, 0) if the result is ∞ / invalid. + /// + /// # Note + /// + /// For more details visit: [`ink_env::bn128_mul`] + pub fn bn128_mul(self, x1: U256, y1: U256, scalar: U256) -> (U256, U256) { + ink_env::bn128_mul(x1, y1, scalar) + } + + /// Calls the `bn128_pairing` precompile. + /// + /// Input is the Solidity-ABI-encoded sequence of (G1, G2) pairs. + /// Returns `true` iff the product of pairings evaluates to the identity. + /// + /// # Note + /// + /// For more details visit: [`ink_env::bn128_pairing`] + pub fn bn128_pairing(self, input: &[u8]) -> bool { + ink_env::bn128_pairing(input) + } + /// Recovers the compressed ECDSA public key for given `signature` and `message_hash`, /// and stores the result in `output`. /// diff --git a/integration-tests/internal/builtin-precompiles/Cargo.toml b/integration-tests/internal/builtin-precompiles/Cargo.toml new file mode 100755 index 0000000000..e5f6adb98b --- /dev/null +++ b/integration-tests/internal/builtin-precompiles/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "builtin_precompiles" +description = "E2E tests for various builtin precompiles" +version = "6.0.0-beta" +authors = ["Use Ink "] +edition = "2021" +publish = false + +[dependencies] +ink = { path = "../../../crates/ink", default-features = false } + +[dev-dependencies] +ink_e2e = { path = "../../../crates/e2e" } +hex-literal = "1" +impl-serde = { version = "0.5.0", default-features = false } + +[lib] +path = "lib.rs" + +[features] +default = ["std"] +std = [ + "ink/std", +] +ink-as-dependency = [] +e2e-tests = [] + +[package.metadata.ink-lang] +abi = "ink" diff --git a/integration-tests/internal/builtin-precompiles/lib.rs b/integration-tests/internal/builtin-precompiles/lib.rs new file mode 100755 index 0000000000..976fc250bb --- /dev/null +++ b/integration-tests/internal/builtin-precompiles/lib.rs @@ -0,0 +1,188 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] +#![allow(clippy::new_without_default)] + +#[ink::contract] +mod builtin_precompiles { + use ink::U256; + + #[ink(storage)] + pub struct BuiltinPrecompiles {} + + impl BuiltinPrecompiles { + #[ink(constructor)] + pub fn new() -> Self { + Self {} + } + + /// Tests the `bn128_add` functionality. + #[ink(message)] + pub fn bn128_add(&self) -> (U256, U256) { + self.env().bn128_add(1.into(), 2.into(), 1.into(), 2.into()) + } + + /// Tests the `bn128_mul` functionality. + #[ink(message)] + pub fn bn128_mul(&self) -> (U256, U256) { + self.env().bn128_mul(1.into(), 2.into(), 3.into()) + } + + /// Tests the `bn128_pairing` functionality. + #[ink(message)] + pub fn bn128_pairing(&self, input: ink::prelude::vec::Vec) -> bool { + self.env().bn128_pairing(input.as_ref()) + } + } + + #[cfg(all(test, feature = "e2e-tests"))] + mod e2e_tests { + use super::*; + use impl_serde::serialize as serde_hex; + use ink_e2e::ContractsBackend; + + type E2EResult = std::result::Result>; + + #[ink_e2e::test] + async fn bn128_add_works( + mut client: Client, + ) -> E2EResult<()> { + // given + let mut constructor = BuiltinPrecompilesRef::new(); + let contract = client + .instantiate("builtin-precompiles", &ink_e2e::eve(), &mut constructor) + .submit() + .await + .expect("instantiate failed"); + let call_builder = contract.call_builder::(); + + // when + let (x3, y3) = client + .call(&ink_e2e::eve(), &call_builder.bn128_add()) + .dry_run() + .await? + .return_value(); + + // then + // taken from https://github.com/polkadot-developers/polkavm-hardhat-examples/blob/v0.0.3/precompiles-hardhat/test/BN128Add.js + assert_eq!( + format!("{}", x3), + "1368015179489954701390400359078579693043519447331113978918064868415326638035" + ); + assert_eq!( + format!("{}", y3), + "9918110051302171585080402603319702774565515993150576347155970296011118125764" + ); + + Ok(()) + } + + #[ink_e2e::test] + async fn bn128_mul_works( + mut client: Client, + ) -> E2EResult<()> { + // given + let mut constructor = BuiltinPrecompilesRef::new(); + let contract = client + .instantiate("builtin-precompiles", &ink_e2e::eve(), &mut constructor) + .submit() + .await + .expect("instantiate failed"); + let call_builder = contract.call_builder::(); + + // when + let (x2, y2) = client + .call(&ink_e2e::eve(), &call_builder.bn128_mul()) + .dry_run() + .await? + .return_value(); + + // then + // taken from https://github.com/polkadot-developers/polkavm-hardhat-examples/blob/v0.0.3/precompiles-hardhat/test/BN128Mul.js + assert_eq!( + format!("{}", x2), + "3353031288059533942658390886683067124040920775575537747144343083137631628272" + ); + assert_eq!( + format!("{}", y2), + "19321533766552368860946552437480515441416830039777911637913418824951667761761" + ); + + Ok(()) + } + + #[ink_e2e::test] + async fn bn128_pairing_works( + mut client: Client, + ) -> E2EResult<()> { + // given + let mut constructor = BuiltinPrecompilesRef::new(); + let contract = client + .instantiate("builtin-precompiles", &ink_e2e::eve(), &mut constructor) + .submit() + .await + .expect("instantiate failed"); + let call_builder = contract.call_builder::(); + + // when + // Taken from https://github.com/polkadot-developers/polkavm-hardhat-examples/blob/v0.0.3/precompiles-hardhat/test/BN128Pairing.js + // + // Using the "two_point_match_2" test vector from your data + // This is a known valid pairing that should return true. + let input: [u8; 384] = hex_literal::hex!( + "00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c21800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c21800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed275dc4a288d1afb3cbb1ac09187524c7db36395df7be3b99e673b13a075a65ec1d9befcd05a5323e6da4d435f3b617cdb3af83285c2df711ef39c01571827f9d" + ); + let res = client + .call(&ink_e2e::eve(), &call_builder.bn128_pairing(input.to_vec())) + .dry_run() + .await? + .return_value(); + + // then + assert!(res); + + Ok(()) + } + + #[ink_e2e::test] + async fn bn128_pairing_zero_points_works( + mut client: Client, + ) -> E2EResult<()> { + // given + let mut constructor = BuiltinPrecompilesRef::new(); + let contract = client + .instantiate("builtin-precompiles", &ink_e2e::eve(), &mut constructor) + .submit() + .await + .expect("instantiate failed"); + let call_builder = contract.call_builder::(); + + // when + // Taken from https://github.com/polkadot-developers/polkavm-hardhat-examples/blob/v0.0.3/precompiles-hardhat/test/BN128Pairing.js + // + // Pairing with zero points: + // The zero point in G1 is (0, 0) and in G2 is ((0, 0), (0, 0)) + // Pairing of zero points should return true + let pairing = format!( + "{}{}{}{}{}{}", + // G1 zero point + "0000000000000000000000000000000000000000000000000000000000000000", /* G1.x = 0 */ + "0000000000000000000000000000000000000000000000000000000000000000", /* G1.y = 0 */ + // G2 zero point + "0000000000000000000000000000000000000000000000000000000000000000", /* G2.x imaginary = 0 */ + "0000000000000000000000000000000000000000000000000000000000000000", /* G2.x real = 0 */ + "0000000000000000000000000000000000000000000000000000000000000000", /* G2.y imaginary = 0 */ + "0000000000000000000000000000000000000000000000000000000000000000" + ); // G2.y real = 0 + let input = serde_hex::from_hex(&pairing).expect("parsing hex failed"); + let res = client + .call(&ink_e2e::eve(), &call_builder.bn128_pairing(input)) + .dry_run() + .await? + .return_value(); + + // then + assert!(res); + + Ok(()) + } + } +}