orangecheck 2.1.0__tar.gz → 2.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {orangecheck-2.1.0 → orangecheck-2.2.0}/CHANGELOG.md +23 -0
- {orangecheck-2.1.0 → orangecheck-2.2.0}/PKG-INFO +1 -1
- {orangecheck-2.1.0 → orangecheck-2.2.0}/orangecheck/__init__.py +3 -1
- {orangecheck-2.1.0 → orangecheck-2.2.0}/orangecheck/bond.py +98 -13
- {orangecheck-2.1.0 → orangecheck-2.2.0}/pyproject.toml +1 -1
- orangecheck-2.2.0/tests/test_bond_address.py +34 -0
- {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/test_conformance.py +2 -1
- {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/index.json +2 -2
- {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/tv24.json +2 -2
- {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/tv25.json +6 -6
- orangecheck-2.2.0/tests/vectors/tv26.json +36 -0
- orangecheck-2.2.0/tests/vectors/tv27.json +19 -0
- orangecheck-2.1.0/tests/vectors/tv26.json +0 -24
- orangecheck-2.1.0/tests/vectors/tv27.json +0 -18
- {orangecheck-2.1.0 → orangecheck-2.2.0}/.gitignore +0 -0
- {orangecheck-2.1.0 → orangecheck-2.2.0}/README.md +0 -0
- {orangecheck-2.1.0 → orangecheck-2.2.0}/orangecheck/__main__.py +0 -0
- {orangecheck-2.1.0 → orangecheck-2.2.0}/orangecheck/canonical.py +0 -0
- {orangecheck-2.1.0 → orangecheck-2.2.0}/orangecheck/client.py +0 -0
- {orangecheck-2.1.0 → orangecheck-2.2.0}/orangecheck/errors.py +0 -0
- {orangecheck-2.1.0 → orangecheck-2.2.0}/orangecheck/pledge/__init__.py +0 -0
- {orangecheck-2.1.0 → orangecheck-2.2.0}/orangecheck/pledge/bond.py +0 -0
- {orangecheck-2.1.0 → orangecheck-2.2.0}/orangecheck/pledge/canonical.py +0 -0
- {orangecheck-2.1.0 → orangecheck-2.2.0}/orangecheck/pledge/delegation.py +0 -0
- {orangecheck-2.1.0 → orangecheck-2.2.0}/orangecheck/pledge/envelopes.py +0 -0
- {orangecheck-2.1.0 → orangecheck-2.2.0}/orangecheck/pledge/resolution.py +0 -0
- {orangecheck-2.1.0 → orangecheck-2.2.0}/orangecheck/pledge/state.py +0 -0
- {orangecheck-2.1.0 → orangecheck-2.2.0}/orangecheck/py.typed +0 -0
- {orangecheck-2.1.0 → orangecheck-2.2.0}/orangecheck/top_level.py +0 -0
- {orangecheck-2.1.0 → orangecheck-2.2.0}/orangecheck/types.py +0 -0
- {orangecheck-2.1.0 → orangecheck-2.2.0}/orangecheck/verify_sig.py +0 -0
- {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/pledge/__init__.py +0 -0
- {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/pledge/test_authority.py +0 -0
- {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/pledge/test_delegation.py +0 -0
- {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/pledge/test_vectors.py +0 -0
- {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/pledge/test_wrap.py +0 -0
- {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/test_client.py +0 -0
- {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/test_client_failure_modes.py +0 -0
- {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/tv01.json +0 -0
- {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/tv02.json +0 -0
- {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/tv03.json +0 -0
- {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/tv04.json +0 -0
- {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/tv05.json +0 -0
- {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/tv06.json +0 -0
- {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/tv07.json +0 -0
- {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/tv08.json +0 -0
- {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/tv09.json +0 -0
- {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/tv10.json +0 -0
- {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/tv11.json +0 -0
- {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/tv12.json +0 -0
- {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/tv13.json +0 -0
- {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/tv14.json +0 -0
- {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/tv15.json +0 -0
- {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/tv16.json +0 -0
- {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/tv17.json +0 -0
- {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/tv18.json +0 -0
- {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/tv19.json +0 -0
- {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/tv20.json +0 -0
- {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/tv21.json +0 -0
- {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/tv22.json +0 -0
- {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/tv23.json +0 -0
- {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/tv28.json +0 -0
|
@@ -4,6 +4,29 @@ All notable changes to the **`orangecheck`** Python package. Follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and
|
|
5
5
|
[Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [2.2.0] — 2026-10-16
|
|
8
|
+
|
|
9
|
+
Matches `@orangecheck/sdk` 1.13.0 and the spec's regenerated `tv24`–`tv27`.
|
|
10
|
+
|
|
11
|
+
### Changed (security)
|
|
12
|
+
|
|
13
|
+
- `bond_nullifier(key, audience, outpoint)` takes the registry's secret key
|
|
14
|
+
(at least 32 bytes) and returns HMAC-SHA256. 2.1.0's unkeyed hash could be
|
|
15
|
+
inverted by hashing every unspent output, which found the coin.
|
|
16
|
+
- The signed acknowledgement reads "I back this subject with this coin. It
|
|
17
|
+
stays in my wallet."
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- `verify_bond_signature` refuses a signing window longer than an hour
|
|
22
|
+
(`bond_window_too_long`; `max_window_seconds`, `BOND_MAX_WINDOW_SECONDS`).
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- A bond's address is checked the way `@orangecheck/bip322` checks it,
|
|
27
|
+
checksum included, so a mistyped address is `bond_malformed` here as in
|
|
28
|
+
TypeScript rather than `sig_invalid`.
|
|
29
|
+
|
|
7
30
|
## [2.1.0] — 2026-10-16
|
|
8
31
|
|
|
9
32
|
### Added — bonds (oc-attest-protocol `BOND.md`)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: orangecheck
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: Python SDK for OrangeCheck — proof of Bitcoin stake + the OC Pledge primitive (commitments) for the open web.
|
|
5
5
|
Project-URL: Homepage, https://ochk.io
|
|
6
6
|
Project-URL: Documentation, https://ochk.io/docs
|
|
@@ -43,6 +43,7 @@ except ImportError: # pragma: no cover - optional-dep path
|
|
|
43
43
|
verify_bip322_signature = None # type: ignore[assignment]
|
|
44
44
|
from .bond import (
|
|
45
45
|
BOND_DAYS_FLOORS,
|
|
46
|
+
BOND_MAX_WINDOW_SECONDS,
|
|
46
47
|
BOND_SATS_FLOORS,
|
|
47
48
|
BondFields,
|
|
48
49
|
bond_floors,
|
|
@@ -97,6 +98,7 @@ __all__ = [
|
|
|
97
98
|
"verify_bond_signature",
|
|
98
99
|
"BOND_SATS_FLOORS",
|
|
99
100
|
"BOND_DAYS_FLOORS",
|
|
101
|
+
"BOND_MAX_WINDOW_SECONDS",
|
|
100
102
|
# Types
|
|
101
103
|
"CheckResult",
|
|
102
104
|
"VerifyOutcome",
|
|
@@ -111,7 +113,7 @@ __all__ = [
|
|
|
111
113
|
"VerificationError",
|
|
112
114
|
]
|
|
113
115
|
|
|
114
|
-
__version__ = "2.
|
|
116
|
+
__version__ = "2.2.0"
|
|
115
117
|
|
|
116
118
|
# OC Pledge submodule — sibling primitive for forward-looking commitments
|
|
117
119
|
# bound to a Bitcoin address. Imported here so consumers can write:
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Bonds (oc-attest-protocol BOND.md): one unspent coin
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
the coin.
|
|
2
|
+
Bonds (oc-attest-protocol BOND.md): one unspent coin backing one subject.
|
|
3
|
+
The coin stays in its holder's wallet; spending it ends the bond. An audience
|
|
4
|
+
sees floors and a keyed per-audience nullifier, never the coin.
|
|
6
5
|
|
|
7
6
|
The message, id, nullifier and floors are pure and need nothing installed.
|
|
8
7
|
``verify_bond_signature`` needs the ``[verify]`` extra for BIP-322. Checking
|
|
@@ -14,26 +13,102 @@ says.
|
|
|
14
13
|
from __future__ import annotations
|
|
15
14
|
|
|
16
15
|
import hashlib
|
|
16
|
+
import hmac
|
|
17
17
|
import re
|
|
18
18
|
from dataclasses import dataclass
|
|
19
19
|
from datetime import datetime, timezone
|
|
20
20
|
from typing import Optional
|
|
21
21
|
|
|
22
22
|
HEADER = "orangecheck-bond"
|
|
23
|
-
ACK = "I
|
|
23
|
+
ACK = "I back this subject with this coin. It stays in my wallet."
|
|
24
24
|
FIELDS = ("address", "outpoint", "subject", "nonce", "issued_at", "expires_at")
|
|
25
25
|
|
|
26
26
|
#: Value floors a bond claim reports, in sats (BOND.md §5).
|
|
27
27
|
BOND_SATS_FLOORS = (1_000, 10_000, 100_000, 1_000_000, 10_000_000)
|
|
28
28
|
#: Age floors a bond claim reports, in days of 144 blocks (BOND.md §5).
|
|
29
29
|
BOND_DAYS_FLOORS = (7, 30, 90, 365)
|
|
30
|
+
#: The longest signing window a verifier accepts by default, in seconds (BOND.md §2).
|
|
31
|
+
BOND_MAX_WINDOW_SECONDS = 3600
|
|
30
32
|
|
|
31
33
|
_OUTPOINT = re.compile(r"^[0-9a-f]{64}:(0|[1-9][0-9]{0,9})$")
|
|
32
34
|
_NONCE = re.compile(r"^[0-9a-f]{32}$")
|
|
33
35
|
_TIME = re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$")
|
|
34
36
|
_SUBJECT = re.compile(r"^[\x21-\x7e]{1,256}$")
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
|
|
38
|
+
_BECH32 = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
|
|
39
|
+
_BECH32M_CONST = 0x2BC830A3
|
|
40
|
+
_B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
|
|
41
|
+
_B58_VERSIONS = (0x00, 0x05, 0x6F, 0xC4)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _polymod(values: list[int]) -> int:
|
|
45
|
+
gen = (0x3B6A57B2, 0x26508E6D, 0x1EA119FA, 0x3D4233DD, 0x2A1462B3)
|
|
46
|
+
chk = 1
|
|
47
|
+
for v in values:
|
|
48
|
+
top = chk >> 25
|
|
49
|
+
chk = (chk & 0x1FFFFFF) << 5 ^ v
|
|
50
|
+
for i in range(5):
|
|
51
|
+
chk ^= gen[i] if ((top >> i) & 1) else 0
|
|
52
|
+
return chk
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _five_to_eight(words: list[int]) -> Optional[bytes]:
|
|
56
|
+
"""BIP-173 regrouping with strict padding: None when the padding is wrong."""
|
|
57
|
+
acc = bits = 0
|
|
58
|
+
out = bytearray()
|
|
59
|
+
for w in words:
|
|
60
|
+
acc = (acc << 5) | w
|
|
61
|
+
bits += 5
|
|
62
|
+
while bits >= 8:
|
|
63
|
+
bits -= 8
|
|
64
|
+
out.append((acc >> bits) & 0xFF)
|
|
65
|
+
if bits >= 5 or (acc << (8 - bits)) & 0xFF:
|
|
66
|
+
return None
|
|
67
|
+
return bytes(out)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _segwit_ok(address: str, hrp: str) -> bool:
|
|
71
|
+
if len(address) > 90 or address.lower() != address:
|
|
72
|
+
return False
|
|
73
|
+
sep = address.rfind("1")
|
|
74
|
+
if address[:sep] != hrp or len(address) - sep < 8:
|
|
75
|
+
return False
|
|
76
|
+
try:
|
|
77
|
+
data = [_BECH32.index(c) for c in address[sep + 1 :]]
|
|
78
|
+
except ValueError:
|
|
79
|
+
return False
|
|
80
|
+
const = _polymod([ord(c) >> 5 for c in hrp] + [0] + [ord(c) & 31 for c in hrp] + data)
|
|
81
|
+
words = data[:-6]
|
|
82
|
+
if not words:
|
|
83
|
+
return False
|
|
84
|
+
program = _five_to_eight(words[1:])
|
|
85
|
+
if program is None:
|
|
86
|
+
return False
|
|
87
|
+
if words[0] == 0:
|
|
88
|
+
return const == 1 and len(program) in (20, 32)
|
|
89
|
+
if words[0] == 1:
|
|
90
|
+
return const == _BECH32M_CONST and len(program) == 32
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _base58_ok(address: str) -> bool:
|
|
95
|
+
n = 0
|
|
96
|
+
for c in address:
|
|
97
|
+
i = _B58.find(c)
|
|
98
|
+
if i < 0:
|
|
99
|
+
return False
|
|
100
|
+
n = n * 58 + i
|
|
101
|
+
raw = b"\0" * (len(address) - len(address.lstrip("1"))) + n.to_bytes((n.bit_length() + 7) // 8, "big")
|
|
102
|
+
payload, check = raw[:-4], raw[-4:]
|
|
103
|
+
if len(payload) != 21 or payload[0] not in _B58_VERSIONS:
|
|
104
|
+
return False
|
|
105
|
+
return hashlib.sha256(hashlib.sha256(payload).digest()).digest()[:4] == check
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _address_ok(address: str) -> bool:
|
|
109
|
+
"""An address @orangecheck/bip322's decodeAddress accepts, checksum included."""
|
|
110
|
+
m = re.match(r"^(bc|tb|bcrt)1", address)
|
|
111
|
+
return _segwit_ok(address, m.group(1)) if m else _base58_ok(address)
|
|
37
112
|
|
|
38
113
|
|
|
39
114
|
@dataclass(frozen=True)
|
|
@@ -47,12 +122,13 @@ class BondFields:
|
|
|
47
122
|
|
|
48
123
|
|
|
49
124
|
def _when(s: str) -> datetime:
|
|
125
|
+
# strptime refuses times that roll over, such as T24:00:00Z or February 30.
|
|
50
126
|
return datetime.strptime(s, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc)
|
|
51
127
|
|
|
52
128
|
|
|
53
129
|
def _well_formed(f: BondFields) -> bool:
|
|
54
130
|
return bool(
|
|
55
|
-
|
|
131
|
+
_address_ok(f.address)
|
|
56
132
|
and _OUTPOINT.match(f.outpoint)
|
|
57
133
|
and _SUBJECT.match(f.subject)
|
|
58
134
|
and _NONCE.match(f.nonce)
|
|
@@ -95,11 +171,16 @@ def bond_id(message: str) -> str:
|
|
|
95
171
|
return hashlib.sha256(message.encode("utf-8")).hexdigest()
|
|
96
172
|
|
|
97
173
|
|
|
98
|
-
def bond_nullifier(audience: str, outpoint: str) -> str:
|
|
99
|
-
"""
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
174
|
+
def bond_nullifier(key: bytes, audience: str, outpoint: str) -> str:
|
|
175
|
+
"""
|
|
176
|
+
The per-audience nullifier for a coin (BOND.md §6), keyed with a secret only
|
|
177
|
+
the registry holds. Unkeyed, an audience could hash every unspent output and
|
|
178
|
+
find the coin.
|
|
179
|
+
"""
|
|
180
|
+
if len(key) < 32:
|
|
181
|
+
raise ValueError("bond nullifier key must be at least 32 bytes")
|
|
182
|
+
data = f"orangecheck-bond-nullifier\n{audience}\n{outpoint}".encode("utf-8")
|
|
183
|
+
return hmac.new(key, data, hashlib.sha256).hexdigest()
|
|
103
184
|
|
|
104
185
|
|
|
105
186
|
def bond_floors(sats: int, age_blocks: int) -> dict[str, int]:
|
|
@@ -118,6 +199,7 @@ def verify_bond_signature(
|
|
|
118
199
|
subject: Optional[str] = None,
|
|
119
200
|
now: Optional[datetime] = None,
|
|
120
201
|
skew_seconds: int = 60,
|
|
202
|
+
max_window_seconds: int = BOND_MAX_WINDOW_SECONDS,
|
|
121
203
|
) -> tuple[bool, str]:
|
|
122
204
|
"""
|
|
123
205
|
Check a signed bond message: shape, subject, signing window, signature.
|
|
@@ -131,6 +213,9 @@ def verify_bond_signature(
|
|
|
131
213
|
return False, "bond_malformed"
|
|
132
214
|
if subject is not None and fields.subject != subject:
|
|
133
215
|
return False, "bond_subject_mismatch"
|
|
216
|
+
window = (_when(fields.expires_at) - _when(fields.issued_at)).total_seconds()
|
|
217
|
+
if window > max_window_seconds:
|
|
218
|
+
return False, "bond_window_too_long"
|
|
134
219
|
at = (now or datetime.now(timezone.utc)).timestamp()
|
|
135
220
|
if _when(fields.issued_at).timestamp() - skew_seconds > at:
|
|
136
221
|
return False, "bond_not_yet_valid"
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "orangecheck"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.2.0"
|
|
8
8
|
description = "Python SDK for OrangeCheck — proof of Bitcoin stake + the OC Pledge primitive (commitments) for the open web."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A bond's address is read the way @orangecheck/bip322's decodeAddress reads it,
|
|
3
|
+
checksum included, so both SDKs give the same reason for the same message.
|
|
4
|
+
The verdicts below were produced by that function.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from orangecheck.bond import _address_ok
|
|
12
|
+
|
|
13
|
+
TS_VERDICTS = [
|
|
14
|
+
("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4", True),
|
|
15
|
+
("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5", False),
|
|
16
|
+
("BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4", False),
|
|
17
|
+
("bc1pmfr3p9j00pfxjh0zmgp99y8zftmd3s5pmedqhyptwy6lm87hf5sspknck9", True),
|
|
18
|
+
("bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3", True),
|
|
19
|
+
("tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx", True),
|
|
20
|
+
("1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2", True),
|
|
21
|
+
("1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN3", False),
|
|
22
|
+
("3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy", True),
|
|
23
|
+
# Valid checksums over bad padding (v0 and v1).
|
|
24
|
+
("bc1qlllllllllllllllllllllllllllllllllp3sej9", False),
|
|
25
|
+
("bc1plllllllllllllllllllllllllllllllllllllllllllllllllllllnjr3m8", False),
|
|
26
|
+
# bech32m on a v0 program, and witness version 2.
|
|
27
|
+
("bc1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqs9wcxj", False),
|
|
28
|
+
("bc1zqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqupf6cj", False),
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@pytest.mark.parametrize("address,ok", TS_VERDICTS)
|
|
33
|
+
def test_address_agrees_with_typescript(address: str, ok: bool) -> None:
|
|
34
|
+
assert _address_ok(address) is ok
|
|
@@ -156,7 +156,8 @@ def test_bond_nullifier(vector: dict) -> None:
|
|
|
156
156
|
from orangecheck import bond_nullifier
|
|
157
157
|
|
|
158
158
|
inp = vector["input"]
|
|
159
|
-
|
|
159
|
+
key = bytes.fromhex(inp["key"])
|
|
160
|
+
got = [bond_nullifier(key, a, inp["outpoint"]) for a in inp["audiences"]]
|
|
160
161
|
assert got == vector["expected"]["nullifiers"]
|
|
161
162
|
|
|
162
163
|
|
|
@@ -127,12 +127,12 @@
|
|
|
127
127
|
{
|
|
128
128
|
"id": "tv26",
|
|
129
129
|
"category": "bond_signature",
|
|
130
|
-
"description": "wrong signer
|
|
130
|
+
"description": "wrong signer, expired window, window over an hour, rolled-over time — MUST NOT verify"
|
|
131
131
|
},
|
|
132
132
|
{
|
|
133
133
|
"id": "tv27",
|
|
134
134
|
"category": "bond_nullifier",
|
|
135
|
-
"description": "one coin, two audiences (BOND.md §6)"
|
|
135
|
+
"description": "one coin, two audiences, one registry key (BOND.md §6)"
|
|
136
136
|
},
|
|
137
137
|
{
|
|
138
138
|
"id": "tv28",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"expires_at": "2026-10-16T12:10:00Z"
|
|
12
12
|
},
|
|
13
13
|
"expected": {
|
|
14
|
-
"canonical": "orangecheck-bond\naddress: bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4\noutpoint: 1bd631f9d28188be398bae26c018e93ec4d276d25d8f969b6cbc28eab13686c9:0\nsubject: did:oc:0123456789abcdef0123456789abcdef\nnonce: 00112233445566778899aabbccddeeff\nissued_at: 2026-10-16T12:00:00Z\nexpires_at: 2026-10-16T12:10:00Z\nack: I
|
|
15
|
-
"bond_id": "
|
|
14
|
+
"canonical": "orangecheck-bond\naddress: bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4\noutpoint: 1bd631f9d28188be398bae26c018e93ec4d276d25d8f969b6cbc28eab13686c9:0\nsubject: did:oc:0123456789abcdef0123456789abcdef\nnonce: 00112233445566778899aabbccddeeff\nissued_at: 2026-10-16T12:00:00Z\nexpires_at: 2026-10-16T12:10:00Z\nack: I back this subject with this coin. It stays in my wallet.\n",
|
|
15
|
+
"bond_id": "c4a9465c3b161eccb18c479649ef93149ac38124def8c3b9c7552bb1915d129e"
|
|
16
16
|
}
|
|
17
17
|
}
|
|
@@ -7,21 +7,21 @@
|
|
|
7
7
|
"bonds": [
|
|
8
8
|
{
|
|
9
9
|
"address": "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4",
|
|
10
|
-
"message": "orangecheck-bond\naddress: bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4\noutpoint: 1bd631f9d28188be398bae26c018e93ec4d276d25d8f969b6cbc28eab13686c9:0\nsubject: did:oc:0123456789abcdef0123456789abcdef\nnonce: 00112233445566778899aabbccddeeff\nissued_at: 2026-10-16T12:00:00Z\nexpires_at: 2026-10-16T12:10:00Z\nack: I
|
|
11
|
-
"signature": "
|
|
10
|
+
"message": "orangecheck-bond\naddress: bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4\noutpoint: 1bd631f9d28188be398bae26c018e93ec4d276d25d8f969b6cbc28eab13686c9:0\nsubject: did:oc:0123456789abcdef0123456789abcdef\nnonce: 00112233445566778899aabbccddeeff\nissued_at: 2026-10-16T12:00:00Z\nexpires_at: 2026-10-16T12:10:00Z\nack: I back this subject with this coin. It stays in my wallet.\n",
|
|
11
|
+
"signature": "AkcwRAIgBtorUyg37InmZ8kY9+iWo7557g2YD5VsH0M+yHZke9wCIG0cnfgU0o+HXaTD1Tf7/uLi6+XWGpX32wMqIaHSNzxrASECeb5mfvncu6xVoGKVzocLBwKb/NstzijZWfKBWxb4F5g="
|
|
12
12
|
},
|
|
13
13
|
{
|
|
14
14
|
"address": "bc1pmfr3p9j00pfxjh0zmgp99y8zftmd3s5pmedqhyptwy6lm87hf5sspknck9",
|
|
15
|
-
"message": "orangecheck-bond\naddress: bc1pmfr3p9j00pfxjh0zmgp99y8zftmd3s5pmedqhyptwy6lm87hf5sspknck9\noutpoint: 1bd631f9d28188be398bae26c018e93ec4d276d25d8f969b6cbc28eab13686c9:0\nsubject: did:oc:0123456789abcdef0123456789abcdef\nnonce: 00112233445566778899aabbccddeeff\nissued_at: 2026-10-16T12:00:00Z\nexpires_at: 2026-10-16T12:10:00Z\nack: I
|
|
16
|
-
"signature": "
|
|
15
|
+
"message": "orangecheck-bond\naddress: bc1pmfr3p9j00pfxjh0zmgp99y8zftmd3s5pmedqhyptwy6lm87hf5sspknck9\noutpoint: 1bd631f9d28188be398bae26c018e93ec4d276d25d8f969b6cbc28eab13686c9:0\nsubject: did:oc:0123456789abcdef0123456789abcdef\nnonce: 00112233445566778899aabbccddeeff\nissued_at: 2026-10-16T12:00:00Z\nexpires_at: 2026-10-16T12:10:00Z\nack: I back this subject with this coin. It stays in my wallet.\n",
|
|
16
|
+
"signature": "AUHt6fHusZm0RTxT2rmOw/1lDkfxgzIANvN+V1K0/llB3JXApVUlzYZhC8Er3bQo6OKluAIvF/wDUKG43PvZZMnUAQ=="
|
|
17
17
|
}
|
|
18
18
|
]
|
|
19
19
|
},
|
|
20
20
|
"expected": {
|
|
21
21
|
"valid": true,
|
|
22
22
|
"bond_ids": [
|
|
23
|
-
"
|
|
24
|
-
"
|
|
23
|
+
"c4a9465c3b161eccb18c479649ef93149ac38124def8c3b9c7552bb1915d129e",
|
|
24
|
+
"5f7e7b49acab0088da4ef49b9b95a69ea5970190bf63659e3728b821efaa72a5"
|
|
25
25
|
]
|
|
26
26
|
}
|
|
27
27
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "tv26",
|
|
3
|
+
"category": "bond_signature",
|
|
4
|
+
"description": "the P2TR signature over the P2WPKH message MUST NOT verify; the P2WPKH bond after its window MUST be refused; a window over an hour MUST be refused even with a good signature; a time that rolls over (T24:00:00Z) is malformed",
|
|
5
|
+
"input": {
|
|
6
|
+
"cases": [
|
|
7
|
+
{
|
|
8
|
+
"now": "2026-10-16T12:05:00Z",
|
|
9
|
+
"message": "orangecheck-bond\naddress: bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4\noutpoint: 1bd631f9d28188be398bae26c018e93ec4d276d25d8f969b6cbc28eab13686c9:0\nsubject: did:oc:0123456789abcdef0123456789abcdef\nnonce: 00112233445566778899aabbccddeeff\nissued_at: 2026-10-16T12:00:00Z\nexpires_at: 2026-10-16T12:10:00Z\nack: I back this subject with this coin. It stays in my wallet.\n",
|
|
10
|
+
"signature": "AUHt6fHusZm0RTxT2rmOw/1lDkfxgzIANvN+V1K0/llB3JXApVUlzYZhC8Er3bQo6OKluAIvF/wDUKG43PvZZMnUAQ==",
|
|
11
|
+
"reason": "sig_invalid"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"now": "2026-10-16T12:20:00Z",
|
|
15
|
+
"message": "orangecheck-bond\naddress: bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4\noutpoint: 1bd631f9d28188be398bae26c018e93ec4d276d25d8f969b6cbc28eab13686c9:0\nsubject: did:oc:0123456789abcdef0123456789abcdef\nnonce: 00112233445566778899aabbccddeeff\nissued_at: 2026-10-16T12:00:00Z\nexpires_at: 2026-10-16T12:10:00Z\nack: I back this subject with this coin. It stays in my wallet.\n",
|
|
16
|
+
"signature": "AkcwRAIgBtorUyg37InmZ8kY9+iWo7557g2YD5VsH0M+yHZke9wCIG0cnfgU0o+HXaTD1Tf7/uLi6+XWGpX32wMqIaHSNzxrASECeb5mfvncu6xVoGKVzocLBwKb/NstzijZWfKBWxb4F5g=",
|
|
17
|
+
"reason": "bond_expired"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"now": "2026-10-16T12:05:00Z",
|
|
21
|
+
"message": "orangecheck-bond\naddress: bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4\noutpoint: 1bd631f9d28188be398bae26c018e93ec4d276d25d8f969b6cbc28eab13686c9:0\nsubject: did:oc:0123456789abcdef0123456789abcdef\nnonce: 00112233445566778899aabbccddeeff\nissued_at: 2026-10-16T12:00:00Z\nexpires_at: 2026-10-16T14:00:00Z\nack: I back this subject with this coin. It stays in my wallet.\n",
|
|
22
|
+
"signature": "AkcwRAIgCY8Rm6wdrZkbtup43Hjii70g+Hl1byYAVM+Z0405buACIAaE+P2t5Y9dH1fb9nCC3E8CrDVqEMi3j65l4Dh6UZMNASECeb5mfvncu6xVoGKVzocLBwKb/NstzijZWfKBWxb4F5g=",
|
|
23
|
+
"reason": "bond_window_too_long"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"now": "2026-10-16T12:05:00Z",
|
|
27
|
+
"message": "orangecheck-bond\naddress: bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4\noutpoint: 1bd631f9d28188be398bae26c018e93ec4d276d25d8f969b6cbc28eab13686c9:0\nsubject: did:oc:0123456789abcdef0123456789abcdef\nnonce: 00112233445566778899aabbccddeeff\nissued_at: 2026-10-16T12:00:00Z\nexpires_at: 2026-10-16T24:00:00Z\nack: I back this subject with this coin. It stays in my wallet.\n",
|
|
28
|
+
"signature": "AkcwRAIgSem/l3mNW3RuBevxUME7srZm4mY+iBVDwwGTLyz9VD4CIGERr4Ro/HGqOivXJvUatJcp0BgbOsJ+tf2fgqsKwEfIASECeb5mfvncu6xVoGKVzocLBwKb/NstzijZWfKBWxb4F5g=",
|
|
29
|
+
"reason": "bond_malformed"
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
"expected": {
|
|
34
|
+
"valid": false
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "tv27",
|
|
3
|
+
"category": "bond_nullifier",
|
|
4
|
+
"description": "one coin, two audiences, one registry key: the same input gives the same nullifier, different audiences unrelated ones (BOND.md §6). The key is a public test key; a registry keeps its own secret.",
|
|
5
|
+
"input": {
|
|
6
|
+
"key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
|
|
7
|
+
"outpoint": "1bd631f9d28188be398bae26c018e93ec4d276d25d8f969b6cbc28eab13686c9:0",
|
|
8
|
+
"audiences": [
|
|
9
|
+
"https://example.com",
|
|
10
|
+
"https://ochk.app"
|
|
11
|
+
]
|
|
12
|
+
},
|
|
13
|
+
"expected": {
|
|
14
|
+
"nullifiers": [
|
|
15
|
+
"f9f4ac1b66f2244dfd1ba70f2a8291bdf98e5aad5d7d828d79fbd7a4d188a755",
|
|
16
|
+
"a927f6884c62e98fe5802588d026d0a041b6c8f30cea817126daae965780531e"
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "tv26",
|
|
3
|
-
"category": "bond_signature",
|
|
4
|
-
"description": "the P2TR signature presented over the P2WPKH bond message MUST NOT verify; the P2WPKH bond after its window MUST be refused",
|
|
5
|
-
"input": {
|
|
6
|
-
"cases": [
|
|
7
|
-
{
|
|
8
|
-
"now": "2026-10-16T12:05:00Z",
|
|
9
|
-
"message": "orangecheck-bond\naddress: bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4\noutpoint: 1bd631f9d28188be398bae26c018e93ec4d276d25d8f969b6cbc28eab13686c9:0\nsubject: did:oc:0123456789abcdef0123456789abcdef\nnonce: 00112233445566778899aabbccddeeff\nissued_at: 2026-10-16T12:00:00Z\nexpires_at: 2026-10-16T12:10:00Z\nack: I pledge this coin to back this subject. It stays in my wallet.\n",
|
|
10
|
-
"signature": "AUEzVjEbyrc2uc/+2/fY8wq0b80qoiQomZHb93xrjurqCi6ZPAla20fXRTg4OWIccb77clZCCa/L4lhUlWuZeSiJAQ==",
|
|
11
|
-
"reason": "sig_invalid"
|
|
12
|
-
},
|
|
13
|
-
{
|
|
14
|
-
"now": "2026-10-16T12:20:00Z",
|
|
15
|
-
"message": "orangecheck-bond\naddress: bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4\noutpoint: 1bd631f9d28188be398bae26c018e93ec4d276d25d8f969b6cbc28eab13686c9:0\nsubject: did:oc:0123456789abcdef0123456789abcdef\nnonce: 00112233445566778899aabbccddeeff\nissued_at: 2026-10-16T12:00:00Z\nexpires_at: 2026-10-16T12:10:00Z\nack: I pledge this coin to back this subject. It stays in my wallet.\n",
|
|
16
|
-
"signature": "AkcwRAIgXiBjNIVrHtzLTpQCwhreuR7CM0bM3opiP1nxPKsOI3cCIDda/wa1FZTXelNEKSSTxulsMX8iSkTlFL33O26flLg1ASECeb5mfvncu6xVoGKVzocLBwKb/NstzijZWfKBWxb4F5g=",
|
|
17
|
-
"reason": "bond_expired"
|
|
18
|
-
}
|
|
19
|
-
]
|
|
20
|
-
},
|
|
21
|
-
"expected": {
|
|
22
|
-
"valid": false
|
|
23
|
-
}
|
|
24
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "tv27",
|
|
3
|
-
"category": "bond_nullifier",
|
|
4
|
-
"description": "one coin, two audiences: same input gives the same nullifier, different audiences unrelated ones (BOND.md §6)",
|
|
5
|
-
"input": {
|
|
6
|
-
"outpoint": "1bd631f9d28188be398bae26c018e93ec4d276d25d8f969b6cbc28eab13686c9:0",
|
|
7
|
-
"audiences": [
|
|
8
|
-
"https://example.com",
|
|
9
|
-
"https://ochk.app"
|
|
10
|
-
]
|
|
11
|
-
},
|
|
12
|
-
"expected": {
|
|
13
|
-
"nullifiers": [
|
|
14
|
-
"b95726767e6c00b97fd046bc612d7518c5987dd833cb69bf3cdb54a6268e8818",
|
|
15
|
-
"d70c6f2037f30dd8579cf504aae625eb8794dffe69dcf4d92752def0c5ec4a20"
|
|
16
|
-
]
|
|
17
|
-
}
|
|
18
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|