Class: Secp256k1::Point
- Inherits:
-
Object
- Object
- Secp256k1::Point
- Defined in:
- lib/secp256k1.rb
Overview
An elliptic curve point on secp256k1.
Stores affine coordinates (x, y) or represents the point at infinity. Scalar multiplication uses Jacobian coordinates internally with windowed-NAF for performance.
Instance Attribute Summary collapse
-
#x ⇒ Integer?
readonly
X-coordinate (nil for infinity).
-
#y ⇒ Integer?
readonly
Y-coordinate (nil for infinity).
Class Method Summary collapse
-
.from_bytes(bytes) ⇒ Point
Deserialise a point from compressed (33 bytes) or uncompressed (65 bytes) SEC1 encoding.
-
.from_coordinates(x, y) ⇒ Point
Construct a Point from raw (x, y) coordinates with curve-membership validation.
-
.generator ⇒ Point
The generator point G.
-
.infinity ⇒ Point
The point at infinity (additive identity).
Instance Method Summary collapse
-
#==(other) ⇒ Boolean
(also: #eql?)
Equality comparison.
-
#add(other) ⇒ Point
Point addition: self + other.
- #hash ⇒ Object
-
#infinity? ⇒ Boolean
Whether this is the point at infinity.
-
#initialize(x, y) ⇒ Point
constructor
A new instance of Point.
-
#mul(scalar) ⇒ Point
(also: #mul_ct)
Scalar multiplication: self * scalar (constant-time, Montgomery ladder).
-
#mul_vt(scalar) ⇒ Point
Variable-time scalar multiplication: self * scalar (wNAF).
-
#negate ⇒ Point
Point negation: -self.
-
#on_curve? ⇒ Boolean
Whether this point lies on the secp256k1 curve (y² = x³ + 7).
-
#to_octet_string(format = :compressed) ⇒ String
Serialise the point in SEC1 format.
Constructor Details
#initialize(x, y) ⇒ Point
Returns a new instance of Point.
461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 |
# File 'lib/secp256k1.rb', line 461 def initialize(x, y) # I-3 mitigation, hardened: only two valid shapes are accepted — # the point at infinity (nil, nil), or a finite point with both # coordinates canonical in [0, P). Catches Point.new(1, P-of-range), # Point.new(-1, 5), Point.new(nil, 5), and similar half-states at # construction so no downstream path (negate, to_octet_string, # on_curve?) has to second-guess the invariant. if x.nil? && y.nil? # point at infinity — both coordinates absent elsif x.is_a?(Integer) && y.is_a?(Integer) && x >= 0 && x < P && y >= 0 && y < P # finite point with canonical coordinates else raise ArgumentError, 'Point requires (nil, nil) for infinity or two Integers in [0, P)' end @x = x @y = y end |
Instance Attribute Details
#x ⇒ Integer? (readonly)
Returns x-coordinate (nil for infinity).
452 453 454 |
# File 'lib/secp256k1.rb', line 452 def x @x end |
#y ⇒ Integer? (readonly)
Returns y-coordinate (nil for infinity).
455 456 457 |
# File 'lib/secp256k1.rb', line 455 def y @y end |
Class Method Details
.from_bytes(bytes) ⇒ Point
Deserialise a point from compressed (33 bytes) or uncompressed (65 bytes) SEC1 encoding.
533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 |
# File 'lib/secp256k1.rb', line 533 def self.from_bytes(bytes) # I-4: reject non-String / empty input up front with a clean # ArgumentError. Without this, nil / Float / Integer raise # NoMethodError (on `.encoding`), and an empty String raises # NoMethodError (on `nil.to_s` in the else-branch error formatting). # All fail closed either way, but the error type is wrong. unless bytes.is_a?(String) && !bytes.empty? raise ArgumentError, 'bytes must be a non-empty String' end bytes = bytes.b if bytes.encoding != Encoding::BINARY prefix = bytes.getbyte(0) case prefix when 0x04 # Uncompressed raise ArgumentError, 'invalid uncompressed point length' unless bytes.length == 65 x = Secp256k1.bytes_to_int(bytes[1, 32]) y = Secp256k1.bytes_to_int(bytes[33, 32]) raise ArgumentError, 'x coordinate out of field range' if x >= P raise ArgumentError, 'y coordinate out of field range' if y >= P pt = new(x, y) raise ArgumentError, 'point is not on the curve' unless pt.on_curve? pt when 0x02, 0x03 # Compressed raise ArgumentError, 'invalid compressed point length' unless bytes.length == 33 x = Secp256k1.bytes_to_int(bytes[1, 32]) raise ArgumentError, 'x coordinate out of field range' if x >= P y_squared = Secp256k1.fadd(Secp256k1.fmul(Secp256k1.fsqr(x), x), 7) y = Secp256k1.fsqrt(y_squared) raise ArgumentError, 'invalid point: x not on curve' if y.nil? # Ensure y-parity matches prefix y = Secp256k1.fneg(y) if (y.odd? ? 0x03 : 0x02) != prefix new(x, y) else raise ArgumentError, "unknown point prefix: 0x#{prefix.to_s(16).rjust(2, '0')}" end end |
.from_coordinates(x, y) ⇒ Point
Construct a Point from raw (x, y) coordinates with curve-membership validation. This is the required entry point for caller-supplied coordinates (e.g. from an external protocol or user input).
Point.new is intended for always-on-curve intermediates produced by
mul / mul_vt / add / negate; it validates only the range of
the coordinates, not that they satisfy y² = x³ + 7 (mod P). Calling
mul on a Point constructed via Point.new with off-curve
coordinates is an invalid-curve precondition that this method
exists to close (L-5).
505 506 507 508 509 510 511 512 513 514 515 516 517 |
# File 'lib/secp256k1.rb', line 505 def self.from_coordinates(x, y) # Reject the (nil, nil) infinity shape that Point.new accepts. This # method's contract is "raw (x, y) Integers"; callers wanting infinity # should use Point.infinity (or Point.new(nil, nil) on the internal path). # Without this check, on_curve? returns true for infinity and we would # silently return it. raise ArgumentError, 'x and y must be Integers' if x.nil? || y.nil? pt = new(x, y) raise ArgumentError, 'point is not on the secp256k1 curve' unless pt.on_curve? pt end |
.generator ⇒ Point
The generator point G.
522 523 524 |
# File 'lib/secp256k1.rb', line 522 def self.generator @generator ||= new(GX, GY) end |
.infinity ⇒ Point
The point at infinity (additive identity).
484 485 486 |
# File 'lib/secp256k1.rb', line 484 def self.infinity new(nil, nil) end |
Instance Method Details
#==(other) ⇒ Boolean Also known as: eql?
Equality comparison.
721 722 723 724 725 726 727 728 729 730 731 |
# File 'lib/secp256k1.rb', line 721 def ==(other) return false unless other.is_a?(Point) if infinity? && other.infinity? true elsif infinity? || other.infinity? false else @x == other.x && @y == other.y end end |
#add(other) ⇒ Point
Point addition: self + other.
695 696 697 698 699 700 701 702 703 704 705 706 |
# File 'lib/secp256k1.rb', line 695 def add(other) return other if infinity? return self if other.infinity? jp1 = [@x, @y, 1] jp2 = [other.x, other.y, 1] jp_result = Secp256k1.jp_add(jp1, jp2) affine = Secp256k1.jp_to_affine(jp_result) return self.class.infinity if affine.nil? self.class.new(affine[0], affine[1]) end |
#hash ⇒ Object
734 735 736 |
# File 'lib/secp256k1.rb', line 734 def hash infinity? ? 0 : [@x, @y].hash end |
#infinity? ⇒ Boolean
Whether this is the point at infinity.
581 582 583 |
# File 'lib/secp256k1.rb', line 581 def infinity? @x.nil? end |
#mul(scalar) ⇒ Point Also known as: mul_ct
Scalar multiplication: self * scalar (constant-time, Montgomery ladder).
Processes all 256 bits unconditionally so execution time does not depend on the scalar value. Safe for both secret and public scalars. This is the default because the safe path should be the easy path.
For performance-critical public-scalar paths (e.g. batch verification) where constant-time is unnecessary, use #mul_vt.
Raises InsecureOperationError if the native C extension is not loaded,
unless explicitly allowed via Secp256k1.allow_pure_ruby_ct! or the
SECP256K1_ALLOW_PURE_RUBY_CT environment variable.
630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 |
# File 'lib/secp256k1.rb', line 630 def mul(scalar) unless Secp256k1.native? || Secp256k1.pure_ruby_ct_allowed? raise Secp256k1::InsecureOperationError, 'mul requires the native C extension for constant-time guarantees. ' \ 'Set SECP256K1_ALLOW_PURE_RUBY_CT=1 or call Secp256k1.allow_pure_ruby_ct! to override.' end scalar = normalise_scalar(scalar) return self.class.infinity if scalar.nil? jp = Secp256k1.scalar_multiply_ct(scalar, @x, @y) affine = Secp256k1.jp_to_affine(jp) return self.class.infinity if affine.nil? self.class.new(affine[0], affine[1]) end |
#mul_vt(scalar) ⇒ Point
Variable-time scalar multiplication: self * scalar (wNAF).
Faster than #mul but leaks timing information about the scalar. Use only when the scalar is public (e.g. signature verification, computing known generator multiples). Never use with secret scalars.
658 659 660 661 662 663 664 665 666 667 |
# File 'lib/secp256k1.rb', line 658 def mul_vt(scalar) scalar = normalise_scalar(scalar) return self.class.infinity if scalar.nil? jp = Secp256k1.scalar_multiply_wnaf(scalar, @x, @y) affine = Secp256k1.jp_to_affine(jp) return self.class.infinity if affine.nil? self.class.new(affine[0], affine[1]) end |
#negate ⇒ Point
Point negation: -self.
711 712 713 714 715 |
# File 'lib/secp256k1.rb', line 711 def negate return self if infinity? self.class.new(@x, Secp256k1.fneg(@y)) end |
#on_curve? ⇒ Boolean
Whether this point lies on the secp256k1 curve (y² = x³ + 7).
588 589 590 591 592 593 594 |
# File 'lib/secp256k1.rb', line 588 def on_curve? return true if infinity? lhs = Secp256k1.fsqr(@y) rhs = Secp256k1.fadd(Secp256k1.fmul(Secp256k1.fsqr(@x), @x), 7) lhs == rhs end |
#to_octet_string(format = :compressed) ⇒ String
Serialise the point in SEC1 format.
601 602 603 604 605 606 607 608 609 610 611 612 613 |
# File 'lib/secp256k1.rb', line 601 def to_octet_string(format = :compressed) raise 'cannot serialise point at infinity' if infinity? case format when :compressed prefix = @y.odd? ? "\x03".b : "\x02".b prefix + Secp256k1.int_to_bytes(@x, 32) when :uncompressed "\x04".b + Secp256k1.int_to_bytes(@x, 32) + Secp256k1.int_to_bytes(@y, 32) else raise ArgumentError, "unknown format: #{format}" end end |