Skip to content

Commit b718f2e

Browse files
committed
bip-0376: vendor test deps and add PSBTv2 vectors
1 parent 58a43d4 commit b718f2e

18 files changed

Lines changed: 1597 additions & 91 deletions

File tree

bip-0376/deps/bitcoin_test/messages.py

Lines changed: 449 additions & 0 deletions
Large diffs are not rendered by default.

bip-0376/deps/bitcoin_test/psbt.py

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2022-present The Bitcoin Core developers
3+
# Distributed under the MIT software license, see the accompanying
4+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5+
6+
########################################################################
7+
# Adapted from Bitcoin Core test framework psbt.py
8+
# for BIP-375 PSBT validation tests.
9+
########################################################################
10+
11+
import base64
12+
import struct
13+
14+
from io import BytesIO
15+
16+
from .messages import (
17+
CTransaction,
18+
deser_string,
19+
deser_compact_size,
20+
from_binary,
21+
ser_compact_size,
22+
)
23+
24+
25+
# global types
26+
PSBT_GLOBAL_UNSIGNED_TX = 0x00
27+
PSBT_GLOBAL_XPUB = 0x01
28+
PSBT_GLOBAL_TX_VERSION = 0x02
29+
PSBT_GLOBAL_FALLBACK_LOCKTIME = 0x03
30+
PSBT_GLOBAL_INPUT_COUNT = 0x04
31+
PSBT_GLOBAL_OUTPUT_COUNT = 0x05
32+
PSBT_GLOBAL_TX_MODIFIABLE = 0x06
33+
PSBT_GLOBAL_VERSION = 0xfb
34+
PSBT_GLOBAL_PROPRIETARY = 0xfc
35+
36+
# per-input types
37+
PSBT_IN_NON_WITNESS_UTXO = 0x00
38+
PSBT_IN_WITNESS_UTXO = 0x01
39+
PSBT_IN_PARTIAL_SIG = 0x02
40+
PSBT_IN_SIGHASH_TYPE = 0x03
41+
PSBT_IN_REDEEM_SCRIPT = 0x04
42+
PSBT_IN_WITNESS_SCRIPT = 0x05
43+
PSBT_IN_BIP32_DERIVATION = 0x06
44+
PSBT_IN_FINAL_SCRIPTSIG = 0x07
45+
PSBT_IN_FINAL_SCRIPTWITNESS = 0x08
46+
PSBT_IN_POR_COMMITMENT = 0x09
47+
PSBT_IN_RIPEMD160 = 0x0a
48+
PSBT_IN_SHA256 = 0x0b
49+
PSBT_IN_HASH160 = 0x0c
50+
PSBT_IN_HASH256 = 0x0d
51+
PSBT_IN_PREVIOUS_TXID = 0x0e
52+
PSBT_IN_OUTPUT_INDEX = 0x0f
53+
PSBT_IN_SEQUENCE = 0x10
54+
PSBT_IN_REQUIRED_TIME_LOCKTIME = 0x11
55+
PSBT_IN_REQUIRED_HEIGHT_LOCKTIME = 0x12
56+
PSBT_IN_TAP_KEY_SIG = 0x13
57+
PSBT_IN_TAP_SCRIPT_SIG = 0x14
58+
PSBT_IN_TAP_LEAF_SCRIPT = 0x15
59+
PSBT_IN_TAP_BIP32_DERIVATION = 0x16
60+
PSBT_IN_TAP_INTERNAL_KEY = 0x17
61+
PSBT_IN_TAP_MERKLE_ROOT = 0x18
62+
PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS = 0x1a
63+
PSBT_IN_MUSIG2_PUB_NONCE = 0x1b
64+
PSBT_IN_MUSIG2_PARTIAL_SIG = 0x1c
65+
PSBT_IN_PROPRIETARY = 0xfc
66+
67+
# per-output types
68+
PSBT_OUT_REDEEM_SCRIPT = 0x00
69+
PSBT_OUT_WITNESS_SCRIPT = 0x01
70+
PSBT_OUT_BIP32_DERIVATION = 0x02
71+
PSBT_OUT_AMOUNT = 0x03
72+
PSBT_OUT_SCRIPT = 0x04
73+
PSBT_OUT_TAP_INTERNAL_KEY = 0x05
74+
PSBT_OUT_TAP_TREE = 0x06
75+
PSBT_OUT_TAP_BIP32_DERIVATION = 0x07
76+
PSBT_OUT_MUSIG2_PARTICIPANT_PUBKEYS = 0x08
77+
PSBT_OUT_PROPRIETARY = 0xfc
78+
79+
80+
class PSBTMap:
81+
"""Class for serializing and deserializing PSBT maps"""
82+
83+
def __init__(self, map=None):
84+
self.map = map if map is not None else {}
85+
86+
def deserialize(self, f):
87+
m = {}
88+
while True:
89+
k = deser_string(f)
90+
if len(k) == 0:
91+
break
92+
v = deser_string(f)
93+
if len(k) == 1:
94+
k = k[0]
95+
assert k not in m
96+
m[k] = v
97+
self.map = m
98+
99+
def serialize(self):
100+
m = b""
101+
for k,v in self.map.items():
102+
if isinstance(k, int) and 0 <= k and k <= 255:
103+
k = bytes([k])
104+
if isinstance(v, list):
105+
assert all(type(elem) is bytes for elem in v)
106+
v = b"".join(v) # simply concatenate the byte-strings w/o size prefixes
107+
m += ser_compact_size(len(k)) + k
108+
m += ser_compact_size(len(v)) + v
109+
m += b"\x00"
110+
return m
111+
112+
class PSBT:
113+
"""Class for serializing and deserializing PSBTs"""
114+
115+
def __init__(self, *, g=None, i=None, o=None):
116+
self.g = g if g is not None else PSBTMap()
117+
self.i = i if i is not None else []
118+
self.o = o if o is not None else []
119+
self.in_count = len(i) if i is not None else None
120+
self.out_count = len(o) if o is not None else None
121+
self.version = None
122+
123+
def deserialize(self, f):
124+
assert f.read(5) == b"psbt\xff"
125+
self.g = from_binary(PSBTMap, f)
126+
127+
self.version = 0
128+
if PSBT_GLOBAL_VERSION in self.g.map:
129+
assert PSBT_GLOBAL_INPUT_COUNT in self.g.map
130+
assert PSBT_GLOBAL_OUTPUT_COUNT in self.g.map
131+
self.version = struct.unpack("<I", self.g.map[PSBT_GLOBAL_VERSION])[0]
132+
assert self.version in [0, 2]
133+
if self.version == 2:
134+
self.in_count = deser_compact_size(BytesIO(self.g.map[PSBT_GLOBAL_INPUT_COUNT]))
135+
self.out_count = deser_compact_size(BytesIO(self.g.map[PSBT_GLOBAL_OUTPUT_COUNT]))
136+
else:
137+
assert PSBT_GLOBAL_UNSIGNED_TX in self.g.map
138+
tx = from_binary(CTransaction, self.g.map[PSBT_GLOBAL_UNSIGNED_TX])
139+
self.in_count = len(tx.vin)
140+
self.out_count = len(tx.vout)
141+
142+
self.i = [from_binary(PSBTMap, f) for _ in range(self.in_count)]
143+
self.o = [from_binary(PSBTMap, f) for _ in range(self.out_count)]
144+
return self
145+
146+
def serialize(self):
147+
assert isinstance(self.g, PSBTMap)
148+
assert isinstance(self.i, list) and all(isinstance(x, PSBTMap) for x in self.i)
149+
assert isinstance(self.o, list) and all(isinstance(x, PSBTMap) for x in self.o)
150+
if self.version is not None and self.version == 2:
151+
self.g.map[PSBT_GLOBAL_INPUT_COUNT] = ser_compact_size(len(self.i))
152+
self.g.map[PSBT_GLOBAL_OUTPUT_COUNT] = ser_compact_size(len(self.o))
153+
154+
psbt = [x.serialize() for x in [self.g] + self.i + self.o]
155+
return b"psbt\xff" + b"".join(psbt)
156+
157+
def make_blank(self):
158+
"""
159+
Remove all fields except for required fields depending on version
160+
"""
161+
if self.version == 0:
162+
for m in self.i + self.o:
163+
m.map.clear()
164+
165+
self.g = PSBTMap(map={PSBT_GLOBAL_UNSIGNED_TX: self.g.map[PSBT_GLOBAL_UNSIGNED_TX]})
166+
elif self.version == 2:
167+
self.g = PSBTMap(map={
168+
PSBT_GLOBAL_TX_VERSION: self.g.map[PSBT_GLOBAL_TX_VERSION],
169+
PSBT_GLOBAL_INPUT_COUNT: self.g.map[PSBT_GLOBAL_INPUT_COUNT],
170+
PSBT_GLOBAL_OUTPUT_COUNT: self.g.map[PSBT_GLOBAL_OUTPUT_COUNT],
171+
PSBT_GLOBAL_VERSION: self.g.map[PSBT_GLOBAL_VERSION],
172+
})
173+
174+
new_i = []
175+
for m in self.i:
176+
new_i.append(PSBTMap(map={
177+
PSBT_IN_PREVIOUS_TXID: m.map[PSBT_IN_PREVIOUS_TXID],
178+
PSBT_IN_OUTPUT_INDEX: m.map[PSBT_IN_OUTPUT_INDEX],
179+
}))
180+
self.i = new_i
181+
182+
new_o = []
183+
for m in self.o:
184+
new_o.append(PSBTMap(map={
185+
PSBT_OUT_SCRIPT: m.map[PSBT_OUT_SCRIPT],
186+
PSBT_OUT_AMOUNT: m.map[PSBT_OUT_AMOUNT],
187+
}))
188+
self.o = new_o
189+
else:
190+
assert False
191+
192+
def to_base64(self):
193+
return base64.b64encode(self.serialize()).decode("utf8")
194+
195+
@classmethod
196+
def from_base64(cls, b64psbt):
197+
return from_binary(cls, base64.b64decode(b64psbt))
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
name: Tests
2+
on: [push, pull_request]
3+
jobs:
4+
ruff:
5+
runs-on: ubuntu-latest
6+
steps:
7+
- uses: actions/checkout@v4
8+
- name: Install the latest version of uv
9+
uses: astral-sh/setup-uv@v5
10+
- run: uvx ruff check .
11+
mypy:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v4
15+
- name: Install the latest version of uv
16+
uses: astral-sh/setup-uv@v5
17+
- run: uvx mypy .
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.9
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
## [1.0.0] - 2025-03-31
9+
10+
Initial release.

bip-0376/deps/secp256k1lab/COPYING

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2009-2024 The Bitcoin Core developers
4+
Copyright (c) 2009-2024 Bitcoin Developers
5+
Copyright (c) 2025- The secp256k1lab Developers
6+
7+
Permission is hereby granted, free of charge, to any person obtaining a copy
8+
of this software and associated documentation files (the "Software"), to deal
9+
in the Software without restriction, including without limitation the rights
10+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
copies of the Software, and to permit persons to whom the Software is
12+
furnished to do so, subject to the following conditions:
13+
14+
The above copyright notice and this permission notice shall be included in
15+
all copies or substantial portions of the Software.
16+
17+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23+
THE SOFTWARE.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
secp256k1lab
2+
============
3+
4+
![Dependencies: None](https://img.shields.io/badge/dependencies-none-success)
5+
6+
An INSECURE implementation of the secp256k1 elliptic curve and related cryptographic schemes written in Python, intended for prototyping, experimentation and education.
7+
8+
Features:
9+
* Low-level secp256k1 field and group arithmetic.
10+
* Schnorr signing/verification and key generation according to [BIP-340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki).
11+
* ECDH key exchange.
12+
13+
WARNING: The code in this library is slow and trivially vulnerable to side channel attacks.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
[project]
2+
name = "secp256k1lab"
3+
version = "1.0.0"
4+
description = "An INSECURE implementation of the secp256k1 elliptic curve and related cryptographic schemes, intended for prototyping, experimentation and education"
5+
readme = "README.md"
6+
authors = [
7+
{ name = "Pieter Wuille", email = "pieter@wuille.net" },
8+
{ name = "Tim Ruffing", email = "me@real-or-random.org" },
9+
{ name = "Jonas Nick", email = "jonasd.nick@gmail.com" },
10+
{ name = "Sebastian Falbesoner", email = "sebastian.falbesoner@gmail.com" }
11+
]
12+
maintainers = [
13+
{ name = "Tim Ruffing", email = "me@real-or-random.org" },
14+
{ name = "Jonas Nick", email = "jonasd.nick@gmail.com" },
15+
{ name = "Sebastian Falbesoner", email = "sebastian.falbesoner@gmail.com" }
16+
]
17+
requires-python = ">=3.9"
18+
license = "MIT"
19+
license-files = ["COPYING"]
20+
keywords = ["secp256k1", "elliptic curves", "cryptography", "Bitcoin"]
21+
classifiers = [
22+
"Development Status :: 5 - Production/Stable",
23+
"Intended Audience :: Developers",
24+
"Intended Audience :: Education",
25+
"Intended Audience :: Science/Research",
26+
"License :: OSI Approved :: MIT License",
27+
"Programming Language :: Python",
28+
"Topic :: Security :: Cryptography",
29+
]
30+
dependencies = []
31+
32+
[build-system]
33+
requires = ["hatchling"]
34+
build-backend = "hatchling.build"

bip-0376/deps/secp256k1lab/src/secp256k1lab/__init__.py

Whitespace-only changes.
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# The following functions are based on the BIP 340 reference implementation:
2+
# https://github.com/bitcoin/bips/blob/master/bip-0340/reference.py
3+
4+
from .secp256k1 import FE, GE, G
5+
from .util import int_from_bytes, bytes_from_int, xor_bytes, tagged_hash
6+
7+
8+
def pubkey_gen(seckey: bytes) -> bytes:
9+
d0 = int_from_bytes(seckey)
10+
if not (1 <= d0 <= GE.ORDER - 1):
11+
raise ValueError("The secret key must be an integer in the range 1..n-1.")
12+
P = d0 * G
13+
assert not P.infinity
14+
return P.to_bytes_xonly()
15+
16+
17+
def schnorr_sign(
18+
msg: bytes, seckey: bytes, aux_rand: bytes, tag_prefix: str = "BIP0340"
19+
) -> bytes:
20+
d0 = int_from_bytes(seckey)
21+
if not (1 <= d0 <= GE.ORDER - 1):
22+
raise ValueError("The secret key must be an integer in the range 1..n-1.")
23+
if len(aux_rand) != 32:
24+
raise ValueError("aux_rand must be 32 bytes instead of %i." % len(aux_rand))
25+
P = d0 * G
26+
assert not P.infinity
27+
d = d0 if P.has_even_y() else GE.ORDER - d0
28+
t = xor_bytes(bytes_from_int(d), tagged_hash(tag_prefix + "/aux", aux_rand))
29+
k0 = (
30+
int_from_bytes(tagged_hash(tag_prefix + "/nonce", t + P.to_bytes_xonly() + msg))
31+
% GE.ORDER
32+
)
33+
if k0 == 0:
34+
raise RuntimeError("Failure. This happens only with negligible probability.")
35+
R = k0 * G
36+
assert not R.infinity
37+
k = k0 if R.has_even_y() else GE.ORDER - k0
38+
e = (
39+
int_from_bytes(
40+
tagged_hash(
41+
tag_prefix + "/challenge", R.to_bytes_xonly() + P.to_bytes_xonly() + msg
42+
)
43+
)
44+
% GE.ORDER
45+
)
46+
sig = R.to_bytes_xonly() + bytes_from_int((k + e * d) % GE.ORDER)
47+
assert schnorr_verify(msg, P.to_bytes_xonly(), sig, tag_prefix=tag_prefix)
48+
return sig
49+
50+
51+
def schnorr_verify(
52+
msg: bytes, pubkey: bytes, sig: bytes, tag_prefix: str = "BIP0340"
53+
) -> bool:
54+
if len(pubkey) != 32:
55+
raise ValueError("The public key must be a 32-byte array.")
56+
if len(sig) != 64:
57+
raise ValueError("The signature must be a 64-byte array.")
58+
try:
59+
P = GE.from_bytes_xonly(pubkey)
60+
except ValueError:
61+
return False
62+
r = int_from_bytes(sig[0:32])
63+
s = int_from_bytes(sig[32:64])
64+
if (r >= FE.SIZE) or (s >= GE.ORDER):
65+
return False
66+
e = (
67+
int_from_bytes(tagged_hash(tag_prefix + "/challenge", sig[0:32] + pubkey + msg))
68+
% GE.ORDER
69+
)
70+
R = s * G - e * P
71+
if R.infinity or (not R.has_even_y()) or (R.x != r):
72+
return False
73+
return True

0 commit comments

Comments
 (0)