Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
337 changes: 337 additions & 0 deletions bip-ec-ops/reference.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,337 @@
#!/usr/bin/env python3
"""Reference implementation for BIP-EC-OPS: Elliptic Curve Operations for Bitcoin Script"""

from typing import Tuple, Optional

# secp256k1 parameters
p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141

# Generator point
G = (0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798,
0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8)

Point = Tuple[int, int]

# Core elliptic curve operations
def is_infinite(P: Optional[Point]) -> bool:
"""Check if point is at infinity."""
return P is None

def point_add(P1: Optional[Point], P2: Optional[Point]) -> Optional[Point]:
"""Add two elliptic curve points."""
if P1 is None:
return P2
if P2 is None:
return P1
if P1[0] == P2[0]:
if P1[1] != P2[1]:
return None # Point at infinity
# Point doubling
lam = (3 * P1[0] * P1[0] * pow(2 * P1[1], p - 2, p)) % p
else:
# Point addition
lam = ((P2[1] - P1[1]) * pow(P2[0] - P1[0], p - 2, p)) % p
x3 = (lam * lam - P1[0] - P2[0]) % p
y3 = (lam * (P1[0] - x3) - P1[1]) % p
return (x3, y3)

def point_mul(P: Optional[Point], k: int) -> Optional[Point]:
"""Multiply point by scalar."""
if k == 0:
return None
R = None
for i in range(256):
if (k >> i) & 1:
R = point_add(R, P)
P = point_add(P, P)
return R

def point_negate(P: Optional[Point]) -> Optional[Point]:
"""Negate an elliptic curve point."""
if P is None:
return None
return (P[0], (p - P[1]) % p)

# Encoding/decoding functions
def decode_compressed_point(data: bytes) -> Optional[Point]:
"""Decode a 33-byte compressed point."""
if len(data) != 33:
raise ValueError(f"Invalid compressed point length: {len(data)}")

prefix = data[0]
if prefix not in [0x02, 0x03]:
raise ValueError(f"Invalid compression prefix: {prefix:02x}")

x = int.from_bytes(data[1:33], byteorder='big')
if x >= p:
raise ValueError(f"X coordinate >= field prime")

# Compute y from x
y_sq = (pow(x, 3, p) + 7) % p
y = pow(y_sq, (p + 1) // 4, p)

if pow(y, 2, p) != y_sq:
raise ValueError(f"Invalid point: not on curve")

# Choose correct y based on prefix
if (y & 1) != (prefix & 1):
y = p - y

return (x, y)

def encode_compressed_point(P: Point) -> bytes:
"""Encode point as 33-byte compressed format."""
prefix = 0x03 if P[1] & 1 else 0x02
return bytes([prefix]) + P[0].to_bytes(32, byteorder='big')

def extract_x_coordinate(P: Point) -> bytes:
"""Extract x-coordinate as 32 bytes."""
return P[0].to_bytes(32, byteorder='big')

# Bitcoin Script opcode implementations
def op_ec_point_add(stack: list) -> None:
"""
OP_EC_POINT_ADD implementation
Stack: [point2] [point1] -> [point1 + point2]
"""
if len(stack) < 2:
raise ValueError("OP_EC_POINT_ADD requires 2 stack elements")

# Pop elements (top first)
point2_bytes = stack.pop()
point1_bytes = stack.pop()

# Validate and decode points
if len(point1_bytes) != 33:
raise ValueError(f"Invalid point1 length: {len(point1_bytes)}")
if len(point2_bytes) != 33:
raise ValueError(f"Invalid point2 length: {len(point2_bytes)}")

P1 = decode_compressed_point(point1_bytes)
P2 = decode_compressed_point(point2_bytes)

# Perform addition
result = point_add(P1, P2)

# Push result
if result is None:
stack.append(b'') # Point at infinity
else:
stack.append(encode_compressed_point(result))

def op_ec_point_mul(stack: list) -> None:
"""
OP_EC_POINT_MUL implementation
Stack: [scalar] [point] -> [scalar * point]
"""
if len(stack) < 2:
raise ValueError("OP_EC_POINT_MUL requires 2 stack elements")

# Pop elements (top first)
scalar_bytes = stack.pop()
point_bytes = stack.pop()

# Validate scalar
if len(scalar_bytes) != 32:
raise ValueError(f"Invalid scalar length: {len(scalar_bytes)}")

scalar = int.from_bytes(scalar_bytes, byteorder='big')
# Reduce modulo n if needed (matches implementation behavior)
scalar = scalar % n

# Handle point
if len(point_bytes) == 0:
# Empty vector means generator point G
point = G
elif len(point_bytes) == 33:
point = decode_compressed_point(point_bytes)
else:
raise ValueError(f"Invalid point length: {len(point_bytes)}")

# Perform multiplication
result = point_mul(point, scalar)

# Push result
if result is None:
stack.append(b'') # Point at infinity
else:
stack.append(encode_compressed_point(result))

def op_ec_point_negate(stack: list) -> None:
"""
OP_EC_POINT_NEGATE implementation
Stack: [point] -> [-point]
"""
if len(stack) < 1:
raise ValueError("OP_EC_POINT_NEGATE requires 1 stack element")

# Pop element
point_bytes = stack.pop()

# Handle empty vector (infinity)
if len(point_bytes) == 0:
stack.append(b'') # -infinity = infinity
return

# Validate and decode point
if len(point_bytes) != 33:
raise ValueError(f"Invalid point length: {len(point_bytes)}")

point = decode_compressed_point(point_bytes)

# Perform negation
result = point_negate(point)

# Push result
if result is None:
stack.append(b'')
else:
stack.append(encode_compressed_point(result))

def op_ec_point_x_coord(stack: list) -> None:
"""
OP_EC_POINT_X_COORD implementation
Stack: [point] -> [x_coordinate]
"""
if len(stack) < 1:
raise ValueError("OP_EC_POINT_X_COORD requires 1 stack element")

# Pop element
point_bytes = stack.pop()

# Cannot extract x from infinity
if len(point_bytes) == 0:
raise ValueError("Cannot extract x-coordinate from point at infinity")

# Validate and decode point
if len(point_bytes) != 33:
raise ValueError(f"Invalid point length: {len(point_bytes)}")

point = decode_compressed_point(point_bytes)

# Extract and push x-coordinate
stack.append(extract_x_coordinate(point))

# Example: Computing a taproot tweak
def compute_taproot_tweak_example():
"""Example: Computing P + tweak*G using stack-based execution."""
# Sample internal key (33-byte compressed)
internal_key = bytes.fromhex("02" + "79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798")

# Sample tweak (32 bytes)
tweak = bytes.fromhex("0000000000000000000000000000000000000000000000000000000000000001")

# Simulate script execution: <tweak> <empty_vector> OP_EC_POINT_MUL <P> OP_EC_POINT_ADD OP_EC_POINT_X_COORD
stack = []

# Push tweak and empty vector (for G)
stack.append(b'') # empty vector for G
stack.append(tweak)

# OP_EC_POINT_MUL: compute tweak*G
op_ec_point_mul(stack)
print(f"After OP_EC_POINT_MUL: stack has {len(stack)} element(s)")

# Push internal key
stack.append(internal_key)

# OP_EC_POINT_ADD: compute P + tweak*G
op_ec_point_add(stack)
print(f"After OP_EC_POINT_ADD: stack has {len(stack)} element(s)")

# OP_EC_POINT_X_COORD: extract x-coordinate for taproot
op_ec_point_x_coord(stack)
print(f"After OP_EC_POINT_X_COORD: stack has {len(stack)} element(s)")

# Result is 32-byte x-coordinate on top of stack
return stack[0]

# Test vectors
def run_test_vectors():
"""Run basic test vectors for all opcodes using stack-based execution."""

print("Testing OP_EC_POINT_ADD...")
# Test: Add G + G = 2*G
G_compressed = encode_compressed_point(G)
stack = [G_compressed, G_compressed]
op_ec_point_add(stack)
# Result should be 2*G
expected = encode_compressed_point(point_mul(G, 2))
assert stack[0] == expected, f"Expected {expected.hex()}, got {stack[0].hex()}"
print(" ✓ Point addition test passed")

print("\nTesting OP_EC_POINT_MUL...")
# Test: Scalar multiplication with G
stack = [
b'', # Empty vector for G
bytes.fromhex("0000000000000000000000000000000000000000000000000000000000000002")
]
op_ec_point_mul(stack)
# Result should be 2*G
expected = encode_compressed_point(point_mul(G, 2))
assert stack[0] == expected
print(" ✓ Scalar multiplication test passed")

print("\nTesting OP_EC_POINT_NEGATE...")
# Test: Point negation
stack = [bytes.fromhex("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9")]
op_ec_point_negate(stack)
# Negated point should have opposite y parity
assert stack[0][0] == 0x03 # Changed from 0x02 to 0x03
print(" ✓ Point negation test passed")

print("\nTesting OP_EC_POINT_X_COORD...")
# Test: Extract x-coordinate
stack = [bytes.fromhex("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9")]
op_ec_point_x_coord(stack)
expected = bytes.fromhex("f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9")
assert stack[0] == expected
print(" ✓ X-coordinate extraction test passed")

print("\nTesting point at infinity...")
# Test: Adding point to its negation gives infinity
P = bytes.fromhex("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9")
stack = [P]
op_ec_point_negate(stack)
neg_P = stack[0]

stack = [P, neg_P]
op_ec_point_add(stack)
assert stack[0] == b'', "P + (-P) should equal infinity"
print(" ✓ Point at infinity test passed")

print("\nTesting error conditions...")
# Test: Insufficient stack elements
try:
stack = [bytes.fromhex("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9")]
op_ec_point_add(stack)
assert False, "Should have raised error for insufficient stack"
except ValueError as e:
assert "requires 2 stack elements" in str(e)
print(" ✓ Stack underflow check passed")

# Test: Invalid scalar length
try:
stack = [
b'', # G
bytes.fromhex("00000000000000000000000000000000000000000000000000000000000000") # 31 bytes
]
op_ec_point_mul(stack)
assert False, "Should have raised error for invalid scalar"
except ValueError as e:
assert "Invalid scalar length" in str(e)
print(" ✓ Invalid scalar length check passed")

print("\n✅ All test vectors passed!")

if __name__ == "__main__":
# Run test vectors
run_test_vectors()

# Example usage
print("\n" + "="*50)
print("Example: Computing taproot tweak")
print("="*50)
result = compute_taproot_tweak_example()
print(f"Taproot output key (x-only): {result.hex()}")
16 changes: 16 additions & 0 deletions bip-ec-ops/test-vectors/00dbf0c2d9398a1f_invalid.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"tx": "02000000000101000000000000000000000000000000000000000000000000000000000000000000000000000000000001a086010000000000000201bb21c1a328913bce52f1ed4a17cf00680f6c0f066a4a5c9f576db183ec422aa6a14f9900000000",
"prevouts": [
"a0860100000000002251205d11cca3ec3eab91d709ea18b2de550b82e3240ae0daad6c13c17037c6a3d09f"
],
"index": 0,
"flags": "P2SH,WITNESS,TAPROOT,EC_OPS",
"comment": "insufficient stack items",
"failure": {
"scriptSig": "",
"witness": [
"bb",
"c1a328913bce52f1ed4a17cf00680f6c0f066a4a5c9f576db183ec422aa6a14f99"
]
}
},
16 changes: 16 additions & 0 deletions bip-ec-ops/test-vectors/052b5efd9f5e22e4_valid.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"tx": "02000000000101000000000000000000000000000000000000000000000000000000000000000000000000000000000001a08601000000000000024620000000000000000000000000000000000000000000000000000000000000000200bc2102c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee58721c04e5d0761dac697652bc9e660749f9a56cbc8dc6eb7f5fcf4efef44ee960b976200000000",
"prevouts": [
"a086010000000000225120c534c653b0c46394875c8a377537a7fdd258d818ee1331e9ed804163d1dd8920"
],
"index": 0,
"flags": "P2SH,WITNESS,TAPROOT,EC_OPS",
"comment": "multiply generator by scalar (empty point)",
"success": {
"scriptSig": "",
"witness": [
"20000000000000000000000000000000000000000000000000000000000000000200bc2102c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee587",
"c04e5d0761dac697652bc9e660749f9a56cbc8dc6eb7f5fcf4efef44ee960b9762"
]
}
},
16 changes: 16 additions & 0 deletions bip-ec-ops/test-vectors/0cc2d06c510116a6_valid.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"tx": "02000000000101000000000000000000000000000000000000000000000000000000000000000000000000000000000001a0860100000000000002682103d5a5c6797a56d30378dba0484493302b5d8dc02dff2f550568641036796da61221038d1eadc80f1d0bbf345f3c5202946a0b72e2c217242f5d8c3c8bc5d5467ff0acbb210284df99cc50d1ec93e9bc32c666325a389dd69a7f42777b8f1670ad66d2e622c98721c1f1dd3079589438fb556253fa5b1d685518fe0dcda0cfd1dd28b11608b0651b6500000000",
"prevouts": [
"a086010000000000225120bdccf1fb4466b26d66d764c61d1cabe8fcbb6598748b62fd64deb9260dde7f04"
],
"index": 0,
"flags": "P2SH,WITNESS,TAPROOT,EC_OPS",
"comment": "add two 33-byte points",
"success": {
"scriptSig": "",
"witness": [
"2103d5a5c6797a56d30378dba0484493302b5d8dc02dff2f550568641036796da61221038d1eadc80f1d0bbf345f3c5202946a0b72e2c217242f5d8c3c8bc5d5467ff0acbb210284df99cc50d1ec93e9bc32c666325a389dd69a7f42777b8f1670ad66d2e622c987",
"c1f1dd3079589438fb556253fa5b1d685518fe0dcda0cfd1dd28b11608b0651b65"
]
}
},
Loading