-
Notifications
You must be signed in to change notification settings - Fork 129
chore(l2): implement ecadd with substrate for ZisK and SP1 #5535
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Lines of code reportTotal lines added: Detailed view |
Benchmark Results ComparisonNo significant difference was registered for any benchmark run. Detailed ResultsBenchmark Results: BubbleSort
Benchmark Results: ERC20Approval
Benchmark Results: ERC20Mint
Benchmark Results: ERC20Transfer
Benchmark Results: Factorial
Benchmark Results: FactorialRecursive
Benchmark Results: Fibonacci
Benchmark Results: FibonacciRecursive
Benchmark Results: ManyHashes
Benchmark Results: MstoreBench
Benchmark Results: Push
Benchmark Results: SstoreBench_no_opt
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR implements the ecadd precompile using substrate-bn for ZisK and SP1 zkVMs to improve performance. The benchmarks show a 0.13-0.48% reduction in steps across tested blocks.
- Refactors the existing
ecaddfunction to extract point addition logic into a newbn254_g1_addhelper function - Provides two feature-gated implementations of
bn254_g1_add: one usingarkworks(default) and one usingsubstrate-bn(for sp1/zisk features) - Updates coordinate validation to use helper functions (
parse_bn254_g1andvalidate_bn254_g1_coords)
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| #[cfg(any(feature = "sp1", feature = "zisk"))] | ||
| #[inline] | ||
| pub fn bn254_g1_add(first_point: G1, second_point: G1) -> Result<Bytes, VMError> { | ||
| use substrate_bn::{AffineG1, Fq, G1, Group}; |
Copilot
AI
Dec 9, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The import of substrate_bn::G1 shadows the custom G1 struct defined at line 946. While this works because the import is scoped to this function, it could be confusing for readers. Consider using a qualified import or type alias for clarity:
use substrate_bn::{AffineG1, Fq, Group};
use substrate_bn::G1 as SubstrateG1;Then use SubstrateG1 instead of G1 in the function body (lines 833, 837).
| if first_point.is_zero() { | ||
| let (g1_x, g1_y) = ( | ||
| Fq::from_slice(&second_point.0.to_big_endian()) | ||
| .map_err(|_| PrecompileError::ParsingInputError)?, | ||
| Fq::from_slice(&second_point.1.to_big_endian()) | ||
| .map_err(|_| PrecompileError::ParsingInputError)?, | ||
| ); | ||
| // Validate that the point is on the curve | ||
| AffineG1::new(g1_x, g1_y).map_err(|_| PrecompileError::InvalidPoint)?; | ||
|
|
||
| let mut x_bytes = [0u8; 32]; | ||
| let mut y_bytes = [0u8; 32]; | ||
| g1_x.to_big_endian(&mut x_bytes); | ||
| g1_y.to_big_endian(&mut y_bytes); | ||
| return Ok(Bytes::from([x_bytes, y_bytes].concat())); | ||
| } | ||
|
|
||
| if second_point.is_zero() { | ||
| let (g1_x, g1_y) = ( | ||
| Fq::from_slice(&first_point.0.to_big_endian()) | ||
| .map_err(|_| PrecompileError::ParsingInputError)?, | ||
| Fq::from_slice(&first_point.1.to_big_endian()) | ||
| .map_err(|_| PrecompileError::ParsingInputError)?, | ||
| ); | ||
| // Validate that the point is on the curve | ||
| AffineG1::new(g1_x, g1_y).map_err(|_| PrecompileError::InvalidPoint)?; | ||
|
|
||
| let mut x_bytes = [0u8; 32]; | ||
| let mut y_bytes = [0u8; 32]; | ||
| g1_x.to_big_endian(&mut x_bytes); | ||
| g1_y.to_big_endian(&mut y_bytes); | ||
| return Ok(Bytes::from([x_bytes, y_bytes].concat())); | ||
| } |
Copilot
AI
Dec 9, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic for handling first_point.is_zero() (lines 785-800) and second_point.is_zero() (lines 802-817) is duplicated. This could be extracted into a helper function that converts a G1 point to bytes while validating it's on the curve. This would reduce code duplication and improve maintainability.
Example:
fn g1_point_to_bytes(point: &G1) -> Result<Bytes, VMError> {
let (g1_x, g1_y) = (
Fq::from_slice(&point.0.to_big_endian())
.map_err(|_| PrecompileError::ParsingInputError)?,
Fq::from_slice(&point.1.to_big_endian())
.map_err(|_| PrecompileError::ParsingInputError)?,
);
// Validate that the point is on the curve
AffineG1::new(g1_x, g1_y).map_err(|_| PrecompileError::InvalidPoint)?;
let mut x_bytes = [0u8; 32];
let mut y_bytes = [0u8; 32];
g1_x.to_big_endian(&mut x_bytes);
g1_y.to_big_endian(&mut y_bytes);
Ok(Bytes::from([x_bytes, y_bytes].concat()))
}Then use:
if first_point.is_zero() {
return g1_point_to_bytes(&second_point);
}
if second_point.is_zero() {
return g1_point_to_bytes(&first_point);
}
To use the patched substrate-bn of both zkVMs.
Benchmarks for ZisK:
main (7722c65):
zisk_ecadd:
diff in steps: