Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
485cdba
Add benchmark for Ripemd160
timohanke Apr 23, 2026
0b1f4eb
First step optimization by Opus 4.7 with input from Sha2 package
timohanke Apr 23, 2026
a26063a
Use dot-notation
timohanke Apr 23, 2026
c718177
Format Ripemd160.mo
timohanke Apr 23, 2026
ada4398
Run formatter on everything
timohanke Apr 23, 2026
693fdec
Update CHANGELOG
timohanke Apr 23, 2026
b709b73
Make use of explodeNat32()
timohanke Apr 23, 2026
e0050c2
Run formatter
timohanke Apr 23, 2026
ffdb3ba
Fixes by CodeRabbit
timohanke Apr 23, 2026
ab54ad3
Update CHANGELOG with PR number
timohanke Apr 23, 2026
40a6f5c
Protect Ripemd160 Digest from accidental writes after sum()
timohanke Apr 28, 2026
95912c1
Protect Hmac class from accidental writes after sum()
timohanke Apr 28, 2026
3234636
Formatting
timohanke Apr 28, 2026
fbaf19f
Update the comment in Ripemd160 about sum() locking the Digest
timohanke Apr 28, 2026
a2fc7bb
Add comment in Hmac to explain it locks after calling sum()
timohanke Apr 28, 2026
2b1a96b
Update CHANGELOG about the new guards
timohanke Apr 28, 2026
d8853cd
Add Ripemd160 test with full block plus tail input
timohanke Apr 28, 2026
1d387f3
Use GLV
timohanke Apr 30, 2026
7b3d76a
Bump core and moc versions
timohanke Apr 30, 2026
83c6228
Merge branch 'bench' into GLV
timohanke Apr 30, 2026
a2c5da4
Merge branch 'main' into GLV
timohanke Jul 14, 2026
7f4bb3a
Merge remote-tracking branch 'origin/main' into GLV
timohanke Oct 19, 2026
0f4893c
Prepare 0.2.3: GLV scalar multiplication
timohanke Oct 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.2.3] - 2026-10-19

### Added

- Add the `Curves.Glv` type with the GLV endomorphism parameters of secp256k1.

### Changed

- Speed up `Jacobi.mul()` and `Jacobi.mulBase()` on secp256k1 with the GLV
endomorphism: about 34% fewer instructions for 256-bit scalars. ECDSA
verification and BIP32 derivation are 29–35% cheaper.
- **Breaking:** `Curves.Curve` has a new field `glv : ?Glv`. Code that
constructs its own `Curve` values must add it (`glv = null` keeps the
previous scalar multiplication).

## [0.2.2] - 2026-10-16

### Changed
Expand Down
2 changes: 1 addition & 1 deletion mops.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "bitcoin"
version = "0.2.2"
version = "0.2.3"
description = "Algorithms for bitcoin integration"
repository = "https://github.com/caffeinelabs/motoko-bitcoin"
keywords = [ "bitcoin" ]
Expand Down
5 changes: 5 additions & 0 deletions src/Hmac.mo
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,12 @@ module {
/// instance: `closed` is set, and any subsequent call to `sum()` or
/// `writeArray()` will trap. To compute another HMAC, construct a new Hmac.
public type Hmac = {
// Append `data` to the message being authenticated. Traps if the Hmac
// has already been finalized via `sum()`.
writeArray : ([Nat8]) -> ();
// Finalize the HMAC and return the tag. Consumes the Hmac: the instance
// becomes closed, and any further call to `sum()` or `writeArray()`
// will trap. Callers must create a new Hmac for another computation.
sum : () -> Blob;
};

Expand Down
23 changes: 23 additions & 0 deletions src/ec/Curves.mo
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@
import Fp "Fp";

module {
/// Parameters for the GLV endomorphism `phi(x, y) = (beta * x, y)`, which
/// satisfies `phi(P) = lambda * P` on the curve. The basis `(a1, b1), (a2, b2)`
/// is used to decompose a scalar `k mod r` into a pair `(k1, k2)` of roughly
/// half the bit-length such that `k = k1 + k2 * lambda mod r`.
public type Glv = {
beta : Nat;
lambda : Nat;
a1 : Int;
b1 : Int;
a2 : Int;
b2 : Int;
};

/// Prime-field short Weierstrass curve parameters.
///
/// Defines a curve `y^2 = x^3 + a*x + b` over `F_p` with subgroup
Expand All @@ -23,6 +36,8 @@ module {
gx : Nat;
gy : Nat;
Fp : (Nat) -> Fp.Fp;
// Optional GLV endomorphism parameters for fast scalar multiplication.
glv : ?Glv;
};

/// secp256k1 curve definition.
Expand All @@ -36,6 +51,14 @@ module {
Fp = func(value : Nat) : Fp.Fp {
Fp.Fp(value, secp256k1.p);
};
glv = ?{
beta = 0x7ae96a2b657c07106e64479eac3434e99cf0497512f58995c1396c28719501ee;
lambda = 0x5363ad4cc05c30e0a5261c028812645a122e22ea20816678df02967c1b23bd72;
a1 = 0x3086d221a7d46bcde86c90e49284eb15;
b1 = -0xe4437ed6010e88286f547fa90abfe4c3;
a2 = 0x114ca50f7a8e2f3f657c1108d9d44cfd8;
b2 = 0x3086d221a7d46bcde86c90e49284eb15;
};
};

/// Compares two curves by core domain parameters.
Expand Down
75 changes: 75 additions & 0 deletions src/ec/Jacobi.mo
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,10 @@ module {
switch (scale(point)) {
case (#infinity(curve)) #infinity(curve);
case (#point(x2, y2, _, curve)) {
switch (curve.glv) {
case (?glv) return mulGlv(x2, y2, other, glv, curve);
case null {};
};
var p : (Int, Int, Int) = (0, 0, 1);
let naf = Numbers.toNaf(other);
let y2neg = y2.neg().value;
Expand Down Expand Up @@ -195,6 +199,77 @@ module {
};
};

/// Scalar multiplication using the GLV endomorphism. Decomposes the scalar
/// `k mod r` into `(k1, k2)` such that `k1 + lambda * k2 == k (mod r)` and
/// both halves are roughly half the bit-length of `r`. Then computes
/// `k1 * P + k2 * phi(P)` with a Straus/Shamir-style interleaved-NAF loop,
/// where `phi(X, Y, Z) = (beta * X, Y, Z)` in Jacobian coordinates.
/// Caller guarantees `(x, y)` is the affine representation (Z=1) of P and
/// that `scalar != 0`.
func mulGlv(
x : Fp,
y : Fp,
scalar : Nat,
glv : Curves.Glv,
curve : Curves.Curve,
) : Point {
let n : Int = curve.r;
let k : Int = scalar % curve.r;
if (k == 0) {
return #infinity(curve);
};
let halfN : Int = n / 2;

// Both `b2` and `-b1` are positive for valid GLV bases, so the numerators
// are non-negative and Motoko's truncating Int division yields rounding
// to nearest (with halves rounded away from zero, which is acceptable).
let c1 : Int = (glv.b2 * k + halfN) / n;
let c2 : Int = (-glv.b1 * k + halfN) / n;
let k1 : Int = k - c1 * glv.a1 - c2 * glv.a2;
let k2 : Int = -c1 * glv.b1 - c2 * glv.b2;

let absK1 = Int.abs(k1);
let absK2 = Int.abs(k2);

let p1x = x.value;
let yPos = y.value;
let yNeg = y.neg().value;
let p1y = if (k1 < 0) yNeg else yPos;
let p1yAlt = if (k1 < 0) yPos else yNeg;
let p2x = x.mul(curve.Fp(glv.beta)).value;
let p2y = if (k2 < 0) yNeg else yPos;
let p2yAlt = if (k2 < 0) yPos else yNeg;

let naf1 = Numbers.toNaf(absK1);
let naf2 = Numbers.toNaf(absK2);
let len = if (naf1.size() > naf2.size()) naf1.size() else naf2.size();

var p : (Int, Int, Int) = (0, 0, 1);
for (i in Nat.rangeByInclusive(len - 1, 0, -1)) {
p := doDouble(p.0, p.1, p.2, curve.a, curve.p);
if (i < naf1.size()) {
let d : Int = naf1[i];
if (d > 0) {
p := _add(p.0, p.1, p.2, p1x, p1y, 1, curve.a, curve.p);
} else if (d < 0) {
p := _add(p.0, p.1, p.2, p1x, p1yAlt, 1, curve.a, curve.p);
};
};
if (i < naf2.size()) {
let d : Int = naf2[i];
if (d > 0) {
p := _add(p.0, p.1, p.2, p2x, p2y, 1, curve.a, curve.p);
} else if (d < 0) {
p := _add(p.0, p.1, p.2, p2x, p2yAlt, 1, curve.a, curve.p);
};
};
};
if (p.1 == 0 or p.2 == 0) {
return #infinity(curve);
};
#point(fpFromInt(p.0, curve), fpFromInt(p.1, curve), fpFromInt(p.2, curve), curve);
};

/// Multiplies the curve generator by scalar `other`.
public func mulBase(other : Nat, curve : Curves.Curve) : Point {
mul(base(curve), other);
Expand Down
Loading