referrerpolicy=no-referrer-when-downgrade

sp_crypto_ec_utils/
ed_on_bls12_381_bandersnatch.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! *Ed-on-BLS12-381-Bandersnatch* types and host functions.
19//!
20//! Bandersnatch is an *incomplete* twisted Edwards curve: the HWCD
21//! add/double formulas can produce projective points with `z = 0` when fed
22//! cofactor-admixed (non-prime-order-subgroup) inputs. Such points have no
23//! affine representative, so the standard `(x || y)` FFI channel cannot
24//! carry them: arkworks' `From<Projective> for Affine` panics on
25//! `z.inverse().unwrap()`.
26//!
27//! The shared `utils::mul_te` / `utils::msm_te` helpers detect the
28//! degenerate case via `utils::IntoAffineSafe` and return
29//! `utils::Error::DegeneratePoint` across the FFI boundary instead of
30//! attempting to serialize an unrepresentable point. The runtime-side
31//! hooks defined in this module catch that error and substitute the
32//! all-zero projective point `(0, 0, 0, 0)`: not a valid curve point and
33//! with `z = 0` it has no affine representative, so any downstream
34//! validity or subgroup check on the result rejects it. The wire format
35//! stays byte-identical to `ArkScale<EdwardsAffine>`: no sentinel bit, no
36//! dedicated projective codec.
37
38use crate::utils::{
39	self, invalid_projective_fallback, Error, HostcallResult, IntoAffineSafe, FAIL_MSG,
40};
41use alloc::vec::Vec;
42use ark_ec::{AffineRepr, CurveConfig};
43use ark_ed_on_bls12_381_bandersnatch_ext::CurveHooks;
44use sp_runtime_interface::{
45	pass_by::{PassFatPointerAndRead, PassFatPointerAndWrite},
46	runtime_interface,
47};
48
49/// Group configuration.
50pub type BandersnatchConfig = ark_ed_on_bls12_381_bandersnatch_ext::BandersnatchConfig<HostHooks>;
51
52/// Group configuration for Twisted Edwards form (equal to [`BandersnatchConfig`]).
53pub type EdwardsConfig = ark_ed_on_bls12_381_bandersnatch_ext::EdwardsConfig<HostHooks>;
54/// Twisted Edwards form point affine representation.
55pub type EdwardsAffine = ark_ed_on_bls12_381_bandersnatch_ext::EdwardsAffine<HostHooks>;
56/// Twisted Edwards form point projective representation.
57pub type EdwardsProjective = ark_ed_on_bls12_381_bandersnatch_ext::EdwardsProjective<HostHooks>;
58
59/// Group configuration for Short Weierstrass form (equal to [`BandersnatchConfig`]).
60pub type SWConfig = ark_ed_on_bls12_381_bandersnatch_ext::SWConfig<HostHooks>;
61/// Short Weierstrass form point affine representation.
62pub type SWAffine = ark_ed_on_bls12_381_bandersnatch_ext::SWAffine<HostHooks>;
63/// Short Weierstrass form point projective representation.
64pub type SWProjective = ark_ed_on_bls12_381_bandersnatch_ext::SWProjective<HostHooks>;
65
66/// Group scalar field (Fr).
67pub type ScalarField = <BandersnatchConfig as CurveConfig>::ScalarField;
68
69/// Curve hooks jumping into [`host_calls`] host functions.
70#[derive(Copy, Clone)]
71pub struct HostHooks;
72
73impl CurveHooks for HostHooks {
74	fn msm_te(bases: &[EdwardsAffine], scalars: &[ScalarField]) -> EdwardsProjective {
75		let mut out = utils::buffer_for::<EdwardsAffine>();
76		match host_calls::ed_on_bls12_381_bandersnatch_msm(
77			&utils::encode(bases),
78			&utils::encode(scalars),
79			&mut out,
80		) {
81			Ok(()) => utils::decode::<EdwardsAffine>(&out).expect(FAIL_MSG).into_group(),
82			Err(Error::DegeneratePoint) => invalid_projective_fallback::<EdwardsConfig>(),
83			Err(_) => panic!("{FAIL_MSG}"),
84		}
85	}
86
87	fn mul_projective_te(base: &EdwardsProjective, scalar: &[u64]) -> EdwardsProjective {
88		// A `z = 0` projective cannot ride the affine FFI channel:
89		// `into_affine()` would panic. `into_affine_safe()` returns `None`
90		// in that case; we honor the same all-zero projective fallback the
91		// host applies on its side, locally. Honest subgroup-validated
92		// callers never produce such a projective.
93		let Some(base_aff) = base.into_affine_safe() else {
94			return invalid_projective_fallback::<EdwardsConfig>();
95		};
96		let mut out = utils::buffer_for::<EdwardsAffine>();
97		match host_calls::ed_on_bls12_381_bandersnatch_mul(
98			&utils::encode(base_aff),
99			&utils::encode(scalar),
100			&mut out,
101		) {
102			Ok(()) => utils::decode::<EdwardsAffine>(&out).expect(FAIL_MSG).into_group(),
103			Err(Error::DegeneratePoint) => invalid_projective_fallback::<EdwardsConfig>(),
104			Err(_) => panic!("{FAIL_MSG}"),
105		}
106	}
107}
108
109/// Interfaces for working with *Arkworks* *Ed-on-BLS12-381-Bandersnatch* elliptic curve related
110/// types from within the runtime.
111///
112/// All types are (de-)serialized through the wrapper types from `ark-scale`.
113///
114/// `ArkScale`'s `Usage` generic parameter is expected to be set to "not-validated"
115/// and "not-compressed".
116///
117/// When the projective result of a host call lands at `z = 0` (only reachable
118/// via non-subgroup inputs), the host returns `utils::Error::DegeneratePoint`
119/// instead of panicking, and the runtime-side `HostHooks` impl substitutes the
120/// all-zero projective point `(0, 0, 0, 0)`. See the module-level doc for the
121/// full contract.
122#[runtime_interface]
123pub trait HostCalls {
124	/// Twisted Edwards multi scalar multiplication for *Ed-on-BLS12-381-Bandersnatch*.
125	///
126	/// Receives encoded:
127	/// - `bases`: `Vec<EdwardsAffine>`.
128	/// - `scalars`: `Vec<ScalarField>`.
129	/// Writes encoded: `EdwardsAffine` to `out`.
130	fn ed_on_bls12_381_bandersnatch_msm(
131		bases: PassFatPointerAndRead<&[u8]>,
132		scalars: PassFatPointerAndRead<&[u8]>,
133		out: PassFatPointerAndWrite<&mut [u8]>,
134	) -> HostcallResult {
135		utils::msm_te::<ark_ed_on_bls12_381_bandersnatch::EdwardsConfig>(bases, scalars, out)
136	}
137
138	/// Twisted Edwards affine multiplication for *Ed-on-BLS12-381-Bandersnatch*.
139	///
140	/// Receives encoded:
141	/// - `base`: `EdwardsAffine`.
142	/// - `scalar`: `BigInteger`.
143	/// Writes encoded `EdwardsAffine` to `out`.
144	fn ed_on_bls12_381_bandersnatch_mul(
145		base: PassFatPointerAndRead<&[u8]>,
146		scalar: PassFatPointerAndRead<&[u8]>,
147		out: PassFatPointerAndWrite<&mut [u8]>,
148	) -> HostcallResult {
149		utils::mul_te::<ark_ed_on_bls12_381_bandersnatch::EdwardsConfig>(base, scalar, out)
150	}
151}
152
153#[cfg(test)]
154mod tests {
155	use super::*;
156	use crate::utils::testing::*;
157	use ark_ec::{
158		twisted_edwards::{Affine as TEAffine, Projective as TEProjective, TECurveConfig},
159		CurveGroup,
160	};
161	use ark_ed_on_bls12_381_bandersnatch::{EdwardsConfig as RawConfig, Fq, Fr};
162	use ark_ff::{AdditiveGroup, MontFp, PrimeField, Zero};
163
164	#[test]
165	fn mul_works() {
166		mul_te_test::<EdwardsAffine, ark_ed_on_bls12_381_bandersnatch::EdwardsAffine>();
167	}
168
169	#[test]
170	fn msm_works() {
171		msm_te_test::<EdwardsAffine, ark_ed_on_bls12_381_bandersnatch::EdwardsAffine>();
172	}
173
174	#[test]
175	fn mul_works_sw() {
176		mul_test::<SWAffine, ark_ed_on_bls12_381_bandersnatch::SWAffine>();
177	}
178
179	#[test]
180	fn msm_works_sw() {
181		msm_test::<SWAffine, ark_ed_on_bls12_381_bandersnatch::SWAffine>();
182	}
183
184	/// The cofactor-admixed `y = 2` non-subgroup point used as the
185	/// degenerate trigger throughout the tests below. Generic so the same
186	/// constructor serves both `RawConfig` (for raw-arithmetic precondition
187	/// checks) and `EdwardsConfig<HostHooks>` (the runtime-facing type).
188	fn y2_non_subgroup<P: TECurveConfig<BaseField = Fq>>() -> TEAffine<P> {
189		TEAffine::<P>::get_point_from_y_unchecked(Fq::from(2u64), false)
190			.expect("y=2 must yield a valid TEAffine point")
191	}
192
193	#[test]
194	fn host_mul_with_z_zero_result_returns_fallback() {
195		// Sanity: the raw operation does produce z = 0.
196		let proj: TEProjective<RawConfig> = y2_non_subgroup::<RawConfig>().into_group();
197		let raw_res = <RawConfig as TECurveConfig>::mul_projective(&proj, Fr::MODULUS.0.as_ref());
198		assert!(raw_res.z.is_zero(), "test precondition: y=2 * Fr::MODULUS must hit z=0");
199
200		// The raw host call surfaces the degenerate result as an error
201		// (the helper can't represent it on the affine FFI channel).
202		let scalar_bigint: Vec<u64> = Fr::MODULUS.0.to_vec();
203		let input_enc = utils::encode(y2_non_subgroup::<EdwardsConfig>());
204		let scalar_enc = utils::encode(scalar_bigint);
205		let mut out = utils::buffer_for::<EdwardsAffine>();
206		let err = host_calls::ed_on_bls12_381_bandersnatch_mul(&input_enc, &scalar_enc, &mut out)
207			.expect_err("z=0 result must surface as Err(DegeneratePoint)");
208		assert_eq!(err, Error::DegeneratePoint);
209
210		// The runtime-side hook catches that error and substitutes the
211		// all-zero invalid projective point.
212		let p_ext: EdwardsProjective = y2_non_subgroup::<EdwardsConfig>().into_group();
213		let r = <HostHooks as CurveHooks>::mul_projective_te(&p_ext, Fr::MODULUS.0.as_ref());
214		assert_eq!(
215			r,
216			invalid_projective_fallback::<EdwardsConfig>(),
217			"hook must return all-zero projective on degenerate"
218		);
219	}
220
221	#[test]
222	fn mul_projective_with_z_zero_input_returns_fallback() {
223		use ark_std::{test_rng, UniformRand};
224		let mut rng = test_rng();
225		let y = Fq::rand(&mut rng);
226		let t = Fq::rand(&mut rng);
227		let p = EdwardsProjective::new_unchecked(Fq::ZERO, y, t, Fq::ZERO);
228		let r = <HostHooks as CurveHooks>::mul_projective_te(&p, &[7u64, 0, 0, 0]);
229		assert_eq!(
230			r,
231			invalid_projective_fallback::<EdwardsConfig>(),
232			"z=0 input must yield all-zero coordinate projective"
233		);
234	}
235
236	#[test]
237	fn fallback_is_invalid_projective_point() {
238		// (1) The fallback is the all-zero projective point.
239		let fallback = invalid_projective_fallback::<EdwardsConfig>();
240		assert!(fallback.x.is_zero(), "fallback x must be zero");
241		assert!(fallback.y.is_zero(), "fallback y must be zero");
242		assert!(fallback.t.is_zero(), "fallback t must be zero");
243		assert!(fallback.z.is_zero(), "fallback z must be zero");
244
245		// (2) `z = 0` means `into_affine()` would hit `z.inverse().unwrap()`
246		// and panic, but only if arkworks' `is_zero()` doesn't short-circuit
247		// first. `is_zero()` for TE projective requires `!y.is_zero()`, so
248		// with `y = 0` the check returns `false` and the panic path is
249		// reached. This means `into_affine_safe()` returns `None` and
250		// `into_affine()` panics — both correctly rejecting this sentinel.
251		assert!(!fallback.is_zero(), "all-zero projective must NOT be considered identity");
252		assert!(
253			fallback.into_affine_safe().is_none(),
254			"all-zero projective must map to None via IntoAffineSafe",
255		);
256
257		// (3) Another degenerate z=0 shape: (X=0, Y!=0, Z=0).
258		// arkworks' `into_affine()` panics here because `is_zero()`
259		// requires `!y.is_zero()` AND `y == z`, so with Y!=0 and Z=0
260		// it returns false, falling through to `z.inverse().unwrap()`
261		// which panics. `into_affine_safe` returns `None` instead.
262		// Note: `msm_te` / `mul_te` can produce either this shape or
263		// the all-zero `(0,0,0,0)` shape — both have z=0 and both are
264		// caught by `into_affine_safe`.
265		let degenerate = TEProjective::<RawConfig>::new_unchecked(
266			Fq::ZERO,        // X = 0
267			Fq::from(7u64),  // Y != 0 (F-exception)
268			Fq::from(11u64), // arbitrary T
269			Fq::ZERO,        // Z = 0
270		);
271		assert!(
272			degenerate.into_affine_safe().is_none(),
273			"z=0 projective must map to None via IntoAffineSafe",
274		);
275	}
276
277	/// F_q-rational pair found via Sage brute-force search. They satisfy
278	/// d * x_A * x_B * y_A * y_B = 1, which forces HWCD's Z_3 = F*G = 0
279	/// even though both inputs are valid affine curve points.
280	fn exceptional_pair() -> (EdwardsAffine, EdwardsAffine, EdwardsAffine) {
281		let xa: Fq = MontFp!(
282			"12611587488970178020234800979835231446181428428390492190317266241455236381927"
283		);
284		let ya: Fq =
285			MontFp!("8625363597705895091270672088731506059935752500467284843225771956507605756711");
286		let xb: Fq =
287			MontFp!("5253339395048946693631279295832797565125937378490576959411837397991361739535");
288		let yb: Fq = MontFp!(
289			"24752777243643877000069062635360441442644758493268974317933177186378585499408"
290		);
291		// True A+B as computed via SW form in Sage (the group-law answer).
292		let x_sum: Fq = MontFp!(
293			"30239213723729448420307207485613680945165091785466061697591732383921178212543"
294		);
295		let y_sum: Fq = MontFp!(
296			"48407687168732128978323921344344221491641898681064657528705691267288289221251"
297		);
298		(
299			EdwardsAffine::new_unchecked(xa, ya),
300			EdwardsAffine::new_unchecked(xb, yb),
301			EdwardsAffine::new_unchecked(x_sum, y_sum),
302		)
303	}
304
305	#[test]
306	fn hwcd_exceptional_pair_produces_all_zero_projective() {
307		let (a, b, _expected_sum) = exceptional_pair();
308		assert!(a.is_on_curve(), "point A must be on curve");
309		assert!(b.is_on_curve(), "point B must be on curve");
310
311		// HWCD addition of this exceptional pair produces (0, 0, 0, 0).
312		let a_proj: EdwardsProjective = a.into_group();
313		let b_proj: EdwardsProjective = b.into_group();
314		let sum = a_proj + b_proj;
315		assert!(sum.x.is_zero(), "exceptional sum x must be zero");
316		assert!(sum.y.is_zero(), "exceptional sum y must be zero");
317		assert!(sum.t.is_zero(), "exceptional sum t must be zero");
318		assert!(sum.z.is_zero(), "exceptional sum z must be zero");
319	}
320
321	#[test]
322	fn hwcd_exceptional_pair_recovers_via_sage_sum() {
323		let (a, b, sage_sum) = exceptional_pair();
324		let a_proj: EdwardsProjective = a.into_group();
325		let b_proj: EdwardsProjective = b.into_group();
326		let sage_sum_proj: EdwardsProjective = sage_sum.into_group();
327
328		// A + (A+B from arkworks) produces all-zero: the HWCD exception
329		// propagates through the (0,0,0,0) intermediate.
330		let ark_sum = a_proj + b_proj;
331		let a_plus_ark_sum = a_proj + ark_sum;
332		assert_eq!(
333			a_plus_ark_sum,
334			invalid_projective_fallback::<EdwardsConfig>(),
335			"A + (A+B from arkworks) must produce all-zero projective"
336		);
337
338		// A + (A+B from Sage) gives the correct 2*A + B because A and
339		// the Sage sum are not an exceptional pair.
340		let two_a = a_proj + a_proj;
341		let two_a_plus_b = two_a + b_proj;
342		let a_plus_sage_sum = a_proj + sage_sum_proj;
343		assert_eq!(
344			a_plus_sage_sum.into_affine(),
345			two_a_plus_b.into_affine(),
346			"A + (A+B from Sage) must equal 2*A + B"
347		);
348	}
349
350	#[test]
351	fn hwcd_exceptional_pair_msm_produces_all_zero() {
352		use ark_ec::VariableBaseMSM;
353
354		let (a, b, _expected_sum) = exceptional_pair();
355
356		// msm([A, B], [2, 1]) = 2*A + B, but internally pippenger will
357		// compute A+B which hits the exceptional case.
358		let bases = vec![a, b];
359		let scalars = vec![Fr::from(2u64), Fr::from(1u64)];
360		let result = EdwardsProjective::msm(&bases, &scalars).unwrap();
361
362		assert_eq!(
363			result,
364			invalid_projective_fallback::<EdwardsConfig>(),
365			"msm([A, B], [2, 1]) must produce invalid projective fallback"
366		);
367	}
368
369	#[test]
370	fn hwcd_exceptional_pair_msm_te_returns_invalid_projective() {
371		let (a, b, _expected_sum) = exceptional_pair();
372
373		// msm_te via the host call should detect the degenerate z=0 result
374		// and return the invalid projective point fallback.
375		let bases = vec![a, b];
376		let scalars = vec![Fr::from(2u64), Fr::from(1u64)];
377		let result = <HostHooks as CurveHooks>::msm_te(&bases, &scalars);
378
379		assert_eq!(
380			result,
381			invalid_projective_fallback::<EdwardsConfig>(),
382			"msm_te must return invalid projective fallback"
383		);
384	}
385
386	#[test]
387	fn y2_point_deserialize_checked_vs_unchecked() {
388		use ark_scale::ark_serialize::{
389			CanonicalDeserialize, CanonicalSerialize, Compress, Validate,
390		};
391
392		let p = y2_non_subgroup::<EdwardsConfig>();
393		assert!(p.is_on_curve(), "y=2 point must be on curve");
394		assert!(
395			!p.is_in_correct_subgroup_assuming_on_curve(),
396			"y=2 point must NOT be in the prime-order subgroup",
397		);
398
399		let mut bytes = Vec::new();
400		p.serialize_with_mode(&mut bytes, Compress::No).unwrap();
401
402		// `Validate::No` accepts the non-subgroup point.
403		let decoded =
404			EdwardsAffine::deserialize_with_mode(&bytes[..], Compress::No, Validate::No).unwrap();
405		assert_eq!(decoded, p);
406
407		// `Validate::Yes` over the same bytes rejects it at decode time.
408		assert!(
409			EdwardsAffine::deserialize_with_mode(&bytes[..], Compress::No, Validate::Yes).is_err(),
410			"Validate::Yes must reject non-subgroup point",
411		);
412	}
413}