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: 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.15.0'

Class Method Summary collapse

Class Method Details

.bytes_to_int(bytes) ⇒ Integer

Convert a big-endian binary string to an Integer.

Parameters:

  • bytes (String)

    binary string (ASCII-8BIT)

Returns:

  • (Integer)


53
54
55
56
# File 'lib/secp256k1.rb', line 53

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.



110
111
112
# File 'lib/secp256k1.rb', line 110

def fadd(a, b)
  fred(a + b)
end

.finv(a) ⇒ Integer

Modular multiplicative inverse in the field (Fermat’s little theorem).

Parameters:

  • a (Integer)

    value to invert (must be non-zero mod P)

Returns:

  • (Integer)

    a^(P-2) mod P

Raises:

  • (ArgumentError)

    if a is zero mod P



129
130
131
132
133
# File 'lib/secp256k1.rb', line 129

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.



100
101
102
# File 'lib/secp256k1.rb', line 100

def fmul(a, b)
  fred(a * b)
end

.fneg(a) ⇒ Object

Modular negation in the field.



120
121
122
# File 'lib/secp256k1.rb', line 120

def fneg(a)
  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.

Parameters:

  • x (Integer)

    non-negative integer

Returns:

  • (Integer)

    x mod P, in range [0, P)



86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/secp256k1.rb', line 86

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.



105
106
107
# File 'lib/secp256k1.rb', line 105

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.

Parameters:

  • a (Integer)

Returns:

  • (Integer, nil)

    the square root, or nil if none exists



142
143
144
145
# File 'lib/secp256k1.rb', line 142

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.



115
116
117
# File 'lib/secp256k1.rb', line 115

def fsub(a, b)
  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.

Parameters:

  • n (Integer)

    the integer to convert

  • length (Integer) (defaults to: 32)

    desired byte length (default 32)

Returns:

  • (String)

    binary string (ASCII-8BIT)

Raises:

  • (ArgumentError)


63
64
65
66
67
68
69
70
71
72
73
# File 'lib/secp256k1.rb', line 63

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.

Parameters:

  • p (Array)

    first Jacobian point

  • q (Array)

    second Jacobian point

Returns:

  • (Array)

    resulting Jacobian point



212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
# File 'lib/secp256k1.rb', line 212

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).

Parameters:

  • p (Array(Integer, Integer, Integer))

    Jacobian point [X, Y, Z]

Returns:

  • (Array(Integer, Integer, Integer))


194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/secp256k1.rb', line 194

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.



390
391
392
393
394
# File 'lib/secp256k1.rb', line 390

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.

Parameters:

  • jp (Array(Integer, Integer, Integer))

Returns:

  • (Array(Integer, Integer))

    affine [x, y], or nil for infinity



246
247
248
249
250
251
252
253
254
255
# File 'lib/secp256k1.rb', line 246

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

.scalar_add(a, b) ⇒ Object

Scalar addition mod N.



173
174
175
# File 'lib/secp256k1.rb', line 173

def scalar_add(a, b)
  (a + b) % N
end

.scalar_inv(a) ⇒ Object

Scalar multiplicative inverse (Fermat).

Raises:

  • (ArgumentError)

    if a is zero mod N



161
162
163
164
165
# File 'lib/secp256k1.rb', line 161

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.



152
153
154
155
156
# File 'lib/secp256k1.rb', line 152

def scalar_mod(a)
  r = a % N
  r += N if r.negative?
  r
end

.scalar_mul(a, b) ⇒ Object

Scalar multiplication mod N.



168
169
170
# File 'lib/secp256k1.rb', line 168

def scalar_mul(a, b)
  (a * b) % N
end