Module: Secp256k1
- Defined in:
- lib/secp256k1.rb,
lib/secp256k1/version.rb
Overview
Pure Ruby secp256k1 elliptic curve implementation.
Provides field arithmetic, point operations with Jacobian coordinates, and windowed-NAF scalar multiplication. Ported from the BSV TypeScript SDK reference implementation.
All field operations work on plain Ruby Integer values (arbitrary
precision, C-backed in MRI). No external gems required.
Defined Under Namespace
Classes: InsecureOperationError, Point
Constant Summary collapse
- P =
The secp256k1 field prime: p = 2^256 - 2^32 - 977
0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F- N =
The curve order (number of points on the curve).
0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141- HALF_N =
Half the curve order, used for low-S normalisation (BIP-62).
N >> 1
- GX =
Generator point x-coordinate.
0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798- GY =
Generator point y-coordinate.
0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8- P_PLUS1_DIV4 =
(P + 1) / 4 — used for modular square root since P ≡ 3 (mod 4).
(P + 1) >> 2
- MASK_256 =
256-bit mask for fast reduction.
(1 << 256) - 1
- VERSION =
'0.18.0'
Class Method Summary collapse
-
.allow_pure_ruby_ct! ⇒ Object
Explicitly allow constant-time operations in pure-Ruby mode.
-
.bytes_to_int(bytes) ⇒ Integer
Convert a big-endian binary string to an Integer.
-
.fadd(a, b) ⇒ Object
Modular addition in the field.
-
.finv(a) ⇒ Integer
Modular multiplicative inverse in the field (Fermat's little theorem).
-
.fmul(a, b) ⇒ Object
Modular multiplication in the field.
-
.fneg(a) ⇒ Object
Modular negation in the field.
-
.fred(x) ⇒ Integer
Fast reduction modulo the secp256k1 prime.
-
.fsqr(a) ⇒ Object
Modular squaring in the field.
-
.fsqrt(a) ⇒ Integer?
Modular square root in the field.
-
.fsub(a, b) ⇒ Object
Modular subtraction in the field.
-
.int_to_bytes(n, length = 32) ⇒ String
Convert an Integer to a fixed-length big-endian binary string.
-
.jp_add(p, q) ⇒ Array
Add two Jacobian points.
-
.jp_double(p) ⇒ Array(Integer, Integer, Integer)
Double a Jacobian point.
-
.jp_neg(p) ⇒ Object
Negate a Jacobian point.
-
.jp_to_affine(jp) ⇒ Array(Integer, Integer)
Convert a Jacobian point to affine coordinates.
-
.native? ⇒ Boolean
Whether the native C extension is loaded and active.
- .pure_ruby_ct_allowed? ⇒ Boolean private
-
.scalar_add(a, b) ⇒ Object
Scalar addition mod N.
-
.scalar_inv(a) ⇒ Object
Scalar multiplicative inverse (Fermat).
-
.scalar_mod(a) ⇒ Object
Reduce modulo the curve order.
-
.scalar_mul(a, b) ⇒ Object
Scalar multiplication mod N.
Class Method Details
.allow_pure_ruby_ct! ⇒ Object
Explicitly allow constant-time operations in pure-Ruby mode. Call this only after evaluating the risks documented in docs/risks.md.
36 37 38 |
# File 'lib/secp256k1.rb', line 36 def self.allow_pure_ruby_ct! @allow_pure_ruby_ct = true end |
.bytes_to_int(bytes) ⇒ Integer
Convert a big-endian binary string to an Integer.
79 80 81 82 |
# File 'lib/secp256k1.rb', line 79 def bytes_to_int(bytes) # C-backed hex route is the fastest pure-Ruby byte→integer path (10x faster than inject). bytes.unpack1('H*').to_i(16) end |
.fadd(a, b) ⇒ Object
Modular addition in the field.
136 137 138 |
# File 'lib/secp256k1.rb', line 136 def fadd(a, b) fred(a + b) end |
.finv(a) ⇒ Integer
Modular multiplicative inverse in the field (Fermat's little theorem).
171 172 173 174 175 |
# File 'lib/secp256k1.rb', line 171 def finv(a) raise ArgumentError, 'field inverse is undefined for zero' if (a % P).zero? a.pow(P - 2, P) end |
.fmul(a, b) ⇒ Object
Modular multiplication in the field.
126 127 128 |
# File 'lib/secp256k1.rb', line 126 def fmul(a, b) fred(a * b) end |
.fneg(a) ⇒ Object
Modular negation in the field.
Canonicalises the operand so the result matches the C wrapper for any non-negative 256-bit input — see #fsub for the negative-input note.
161 162 163 164 |
# File 'lib/secp256k1.rb', line 161 def fneg(a) a %= P a.zero? ? 0 : P - a end |
.fred(x) ⇒ Integer
Fast reduction modulo the secp256k1 prime.
Exploits the structure P = 2^256 - 2^32 - 977 to avoid generic modular division. Two folding passes plus a conditional subtraction.
112 113 114 115 116 117 118 119 120 121 122 123 |
# File 'lib/secp256k1.rb', line 112 def fred(x) # First fold hi = x >> 256 x = (x & MASK_256) + (hi << 32) + (hi * 977) # Second fold (hi <= 2^32 + 977, so one more pass suffices) hi = x >> 256 x = (x & MASK_256) + (hi << 32) + (hi * 977) # Final conditional subtraction x >= P ? x - P : x end |
.fsqr(a) ⇒ Object
Modular squaring in the field.
131 132 133 |
# File 'lib/secp256k1.rb', line 131 def fsqr(a) fred(a * a) end |
.fsqrt(a) ⇒ Integer?
Modular square root in the field.
Uses the identity sqrt(a) = a^((P+1)/4) mod P, valid because
P ≡ 3 (mod 4). Returns nil if a is not a quadratic residue.
184 185 186 187 |
# File 'lib/secp256k1.rb', line 184 def fsqrt(a) r = a.pow(P_PLUS1_DIV4, P) fsqr(r) == (a % P) ? r : nil end |
.fsub(a, b) ⇒ Object
Modular subtraction in the field.
Canonicalises both operands so the result matches the C wrapper for any non-negative 256-bit input — load-bearing for the dfuzz differential, where pure-Ruby serves as the oracle. The dfuzz harness only feeds non-negative inputs (xorshift output, plus structured P-band vectors), so the differential never observes the negative case.
Note: pure-Ruby accepts negative inputs (Ruby % canonicalises them);
the C wrapper rejects negatives via rb_to_uint256. Backend parity
holds for all >= 0 inputs; intentional divergence on negatives.
151 152 153 154 155 |
# File 'lib/secp256k1.rb', line 151 def fsub(a, b) a %= P b %= P a >= b ? a - b : P - (b - a) end |
.int_to_bytes(n, length = 32) ⇒ String
Convert an Integer to a fixed-length big-endian binary string.
89 90 91 92 93 94 95 96 97 98 99 |
# File 'lib/secp256k1.rb', line 89 def int_to_bytes(n, length = 32) raise ArgumentError, 'negative integer' if n.negative? # C-backed hex route is the fastest pure-Ruby integer→byte path. Module deliberately avoids OpenSSL. hex = n.to_s(16) hex = "0#{hex}" if hex.length.odd? raise ArgumentError, "integer too large for #{length} bytes" if hex.length > length * 2 hex = hex.rjust(length * 2, '0') [hex].pack('H*') end |
.jp_add(p, q) ⇒ Array
Add two Jacobian points.
254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 |
# File 'lib/secp256k1.rb', line 254 def jp_add(p, q) _px, _py, pz = p _qx, _qy, qz = q return q if pz.zero? return p if qz.zero? z1z1 = fsqr(pz) z2z2 = fsqr(qz) u1 = fmul(p[0], z2z2) u2 = fmul(q[0], z1z1) s1 = fmul(p[1], fmul(z2z2, qz)) s2 = fmul(q[1], fmul(z1z1, pz)) h = fsub(u2, u1) r = fsub(s2, s1) if h.zero? return r.zero? ? jp_double(p) : JP_INFINITY end hh = fsqr(h) hhh = fmul(h, hh) v = fmul(u1, hh) x3 = fsub(fsub(fsqr(r), hhh), fmul(2, v)) y3 = fsub(fmul(r, fsub(v, x3)), fmul(s1, hhh)) z3 = fmul(h, fmul(pz, qz)) [x3, y3, z3] end |
.jp_double(p) ⇒ Array(Integer, Integer, Integer)
Double a Jacobian point.
Formula from hyperelliptic.org/EFD/g1p/auto-shortw-jacobian-0.html (a=0 for secp256k1).
236 237 238 239 240 241 242 243 244 245 246 247 |
# File 'lib/secp256k1.rb', line 236 def jp_double(p) x1, y1, z1 = p return JP_INFINITY if y1.zero? y1sq = fsqr(y1) s = fmul(4, fmul(x1, y1sq)) m = fmul(3, fsqr(x1)) # a=0 for secp256k1 x3 = fsub(fsqr(m), fmul(2, s)) y3 = fsub(fmul(m, fsub(s, x3)), fmul(8, fsqr(y1sq))) z3 = fmul(2, fmul(y1, z1)) [x3, y3, z3] end |
.jp_neg(p) ⇒ Object
Negate a Jacobian point.
435 436 437 438 439 |
# File 'lib/secp256k1.rb', line 435 def jp_neg(p) return p if p[2].zero? [p[0], fneg(p[1]), p[2]] end |
.jp_to_affine(jp) ⇒ Array(Integer, Integer)
Convert a Jacobian point to affine coordinates.
288 289 290 291 292 293 294 295 296 297 |
# File 'lib/secp256k1.rb', line 288 def jp_to_affine(jp) _x, _y, z = jp return nil if z.zero? zinv = finv(z) zinv2 = fsqr(zinv) x = fmul(jp[0], zinv2) y = fmul(jp[1], fmul(zinv2, zinv)) [x, y] end |
.native? ⇒ Boolean
Whether the native C extension is loaded and active.
30 31 32 |
# File 'lib/secp256k1.rb', line 30 def self.native? @native == true end |
.pure_ruby_ct_allowed? ⇒ Boolean
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
41 42 43 |
# File 'lib/secp256k1.rb', line 41 def self.pure_ruby_ct_allowed? @allow_pure_ruby_ct || ENV.key?('SECP256K1_ALLOW_PURE_RUBY_CT') end |
.scalar_add(a, b) ⇒ Object
Scalar addition mod N.
215 216 217 |
# File 'lib/secp256k1.rb', line 215 def scalar_add(a, b) (a + b) % N end |
.scalar_inv(a) ⇒ Object
Scalar multiplicative inverse (Fermat).
203 204 205 206 207 |
# File 'lib/secp256k1.rb', line 203 def scalar_inv(a) raise ArgumentError, 'scalar inverse is undefined for zero' if (a % N).zero? a.pow(N - 2, N) end |
.scalar_mod(a) ⇒ Object
Reduce modulo the curve order.
194 195 196 197 198 |
# File 'lib/secp256k1.rb', line 194 def scalar_mod(a) r = a % N r += N if r.negative? r end |
.scalar_mul(a, b) ⇒ Object
Scalar multiplication mod N.
210 211 212 |
# File 'lib/secp256k1.rb', line 210 def scalar_mul(a, b) (a * b) % N end |