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.
Files changed (62) hide show
  1. {orangecheck-2.1.0 → orangecheck-2.2.0}/CHANGELOG.md +23 -0
  2. {orangecheck-2.1.0 → orangecheck-2.2.0}/PKG-INFO +1 -1
  3. {orangecheck-2.1.0 → orangecheck-2.2.0}/orangecheck/__init__.py +3 -1
  4. {orangecheck-2.1.0 → orangecheck-2.2.0}/orangecheck/bond.py +98 -13
  5. {orangecheck-2.1.0 → orangecheck-2.2.0}/pyproject.toml +1 -1
  6. orangecheck-2.2.0/tests/test_bond_address.py +34 -0
  7. {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/test_conformance.py +2 -1
  8. {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/index.json +2 -2
  9. {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/tv24.json +2 -2
  10. {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/tv25.json +6 -6
  11. orangecheck-2.2.0/tests/vectors/tv26.json +36 -0
  12. orangecheck-2.2.0/tests/vectors/tv27.json +19 -0
  13. orangecheck-2.1.0/tests/vectors/tv26.json +0 -24
  14. orangecheck-2.1.0/tests/vectors/tv27.json +0 -18
  15. {orangecheck-2.1.0 → orangecheck-2.2.0}/.gitignore +0 -0
  16. {orangecheck-2.1.0 → orangecheck-2.2.0}/README.md +0 -0
  17. {orangecheck-2.1.0 → orangecheck-2.2.0}/orangecheck/__main__.py +0 -0
  18. {orangecheck-2.1.0 → orangecheck-2.2.0}/orangecheck/canonical.py +0 -0
  19. {orangecheck-2.1.0 → orangecheck-2.2.0}/orangecheck/client.py +0 -0
  20. {orangecheck-2.1.0 → orangecheck-2.2.0}/orangecheck/errors.py +0 -0
  21. {orangecheck-2.1.0 → orangecheck-2.2.0}/orangecheck/pledge/__init__.py +0 -0
  22. {orangecheck-2.1.0 → orangecheck-2.2.0}/orangecheck/pledge/bond.py +0 -0
  23. {orangecheck-2.1.0 → orangecheck-2.2.0}/orangecheck/pledge/canonical.py +0 -0
  24. {orangecheck-2.1.0 → orangecheck-2.2.0}/orangecheck/pledge/delegation.py +0 -0
  25. {orangecheck-2.1.0 → orangecheck-2.2.0}/orangecheck/pledge/envelopes.py +0 -0
  26. {orangecheck-2.1.0 → orangecheck-2.2.0}/orangecheck/pledge/resolution.py +0 -0
  27. {orangecheck-2.1.0 → orangecheck-2.2.0}/orangecheck/pledge/state.py +0 -0
  28. {orangecheck-2.1.0 → orangecheck-2.2.0}/orangecheck/py.typed +0 -0
  29. {orangecheck-2.1.0 → orangecheck-2.2.0}/orangecheck/top_level.py +0 -0
  30. {orangecheck-2.1.0 → orangecheck-2.2.0}/orangecheck/types.py +0 -0
  31. {orangecheck-2.1.0 → orangecheck-2.2.0}/orangecheck/verify_sig.py +0 -0
  32. {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/pledge/__init__.py +0 -0
  33. {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/pledge/test_authority.py +0 -0
  34. {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/pledge/test_delegation.py +0 -0
  35. {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/pledge/test_vectors.py +0 -0
  36. {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/pledge/test_wrap.py +0 -0
  37. {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/test_client.py +0 -0
  38. {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/test_client_failure_modes.py +0 -0
  39. {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/tv01.json +0 -0
  40. {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/tv02.json +0 -0
  41. {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/tv03.json +0 -0
  42. {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/tv04.json +0 -0
  43. {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/tv05.json +0 -0
  44. {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/tv06.json +0 -0
  45. {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/tv07.json +0 -0
  46. {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/tv08.json +0 -0
  47. {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/tv09.json +0 -0
  48. {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/tv10.json +0 -0
  49. {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/tv11.json +0 -0
  50. {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/tv12.json +0 -0
  51. {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/tv13.json +0 -0
  52. {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/tv14.json +0 -0
  53. {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/tv15.json +0 -0
  54. {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/tv16.json +0 -0
  55. {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/tv17.json +0 -0
  56. {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/tv18.json +0 -0
  57. {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/tv19.json +0 -0
  58. {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/tv20.json +0 -0
  59. {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/tv21.json +0 -0
  60. {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/tv22.json +0 -0
  61. {orangecheck-2.1.0 → orangecheck-2.2.0}/tests/vectors/tv23.json +0 -0
  62. {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.1.0
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.1.0"
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, pledged by its holder
3
- to back one subject. The coin never moves and nobody holds it; spending it
4
- ends the bond. An audience sees floors and a per-audience nullifier, never
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 pledge this coin to back this subject. It stays in my wallet."
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
- # Shape only; the signature check is what binds the address.
36
- _ADDRESS = re.compile(r"^(bc1|tb1|bcrt1)[02-9ac-hj-np-z]{8,87}$|^[123mn][1-9A-HJ-NP-Za-km-z]{25,34}$")
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
- _ADDRESS.match(f.address)
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
- """The per-audience nullifier for a coin (BOND.md §6)."""
100
- return hashlib.sha256(
101
- f"orangecheck-bond-nullifier\n{audience}\n{outpoint}".encode("utf-8")
102
- ).hexdigest()
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.1.0"
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
- got = [bond_nullifier(a, inp["outpoint"]) for a in inp["audiences"]]
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 and expired window — MUST NOT verify"
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 pledge this coin to back this subject. It stays in my wallet.\n",
15
- "bond_id": "25fdfb7dd6211e74329eb533c2760f41c6e477e04bea71d270a4a1feb3f1cd42"
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 pledge this coin to back this subject. It stays in my wallet.\n",
11
- "signature": "AkcwRAIgXiBjNIVrHtzLTpQCwhreuR7CM0bM3opiP1nxPKsOI3cCIDda/wa1FZTXelNEKSSTxulsMX8iSkTlFL33O26flLg1ASECeb5mfvncu6xVoGKVzocLBwKb/NstzijZWfKBWxb4F5g="
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 pledge this coin to back this subject. It stays in my wallet.\n",
16
- "signature": "AUEzVjEbyrc2uc/+2/fY8wq0b80qoiQomZHb93xrjurqCi6ZPAla20fXRTg4OWIccb77clZCCa/L4lhUlWuZeSiJAQ=="
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
- "25fdfb7dd6211e74329eb533c2760f41c6e477e04bea71d270a4a1feb3f1cd42",
24
- "82fbacfc4e64f6a82e5b0efc67fa31a43b0e57f2d8d341080eec7c2594df9e0b"
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