aotrust-protocol 2.3.7__tar.gz → 2.5.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.
- {aotrust_protocol-2.3.7 → aotrust_protocol-2.5.0}/PKG-INFO +79 -1
- {aotrust_protocol-2.3.7 → aotrust_protocol-2.5.0}/README.md +79 -1
- {aotrust_protocol-2.3.7 → aotrust_protocol-2.5.0}/agent_notary/__init__.py +7 -1
- {aotrust_protocol-2.3.7 → aotrust_protocol-2.5.0}/agent_notary/client.py +144 -2
- {aotrust_protocol-2.3.7 → aotrust_protocol-2.5.0}/agent_notary/exceptions.py +13 -1
- {aotrust_protocol-2.3.7 → aotrust_protocol-2.5.0}/agent_notary/models.py +11 -0
- {aotrust_protocol-2.3.7 → aotrust_protocol-2.5.0}/aotrust_protocol.egg-info/PKG-INFO +79 -1
- {aotrust_protocol-2.3.7 → aotrust_protocol-2.5.0}/aotrust_protocol.egg-info/SOURCES.txt +2 -1
- {aotrust_protocol-2.3.7 → aotrust_protocol-2.5.0}/pyproject.toml +1 -1
- {aotrust_protocol-2.3.7 → aotrust_protocol-2.5.0}/tests/test_live_smoke.py +5 -1
- aotrust_protocol-2.5.0/tests/test_v250_methods.py +225 -0
- {aotrust_protocol-2.3.7 → aotrust_protocol-2.5.0}/LICENSE +0 -0
- {aotrust_protocol-2.3.7 → aotrust_protocol-2.5.0}/agent_notary/tools.py +0 -0
- {aotrust_protocol-2.3.7 → aotrust_protocol-2.5.0}/aotrust_protocol.egg-info/dependency_links.txt +0 -0
- {aotrust_protocol-2.3.7 → aotrust_protocol-2.5.0}/aotrust_protocol.egg-info/requires.txt +0 -0
- {aotrust_protocol-2.3.7 → aotrust_protocol-2.5.0}/aotrust_protocol.egg-info/top_level.txt +0 -0
- {aotrust_protocol-2.3.7 → aotrust_protocol-2.5.0}/setup.cfg +0 -0
- {aotrust_protocol-2.3.7 → aotrust_protocol-2.5.0}/tests/test_client.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aotrust-protocol
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.5.0
|
|
4
4
|
Summary: Asyncio-native SDK for A&O Trust Layer — Agent Notary Service
|
|
5
5
|
Author: A&O Trust Layer
|
|
6
6
|
License: MIT
|
|
@@ -31,6 +31,9 @@ Dynamic: license-file
|
|
|
31
31
|
|
|
32
32
|
Asyncio-native Python SDK for the AOTrust Notary API — cryptographic proof-of-existence for AI agent outputs.
|
|
33
33
|
|
|
34
|
+
> **Why protocol-level proofs?** An ERC-8004 empirical study (2026) measured that **>98% of agent feedback records carry no proof of an underlying interaction** (no payment proof, no task linkage); 59–91% of reviewers on three chains participated in coordinated Sybil campaigns, and manipulating a reputation score past a trust threshold costs as little as **$0.0027**. The agentic economy is moving from *statements* to *verifiable claims*: a PDR (239 bytes, Ed25519-signed, hash-chained, NEAR-anchored) turns any output or payment into independent evidence — verifiable by anyone, without trusting the provider's database.
|
|
35
|
+
> *(Refs: arXiv:2610.12948 [cs.CR], 2026; ERC-8004 Sybil measurements, Xiong et al. 2026.)*
|
|
36
|
+
|
|
34
37
|
## Install
|
|
35
38
|
|
|
36
39
|
```bash
|
|
@@ -105,6 +108,43 @@ asyncio.run(main())
|
|
|
105
108
|
> `/v1/v1/...` → HTTP 404 on every call. This bit a real user (2026-09-29:
|
|
106
109
|
> quickstart copy-paste + status lookups by artifact text instead of job_id).
|
|
107
110
|
|
|
111
|
+
## Paid read products (2.5.0)
|
|
112
|
+
|
|
113
|
+
The SDK never signs or settles payments itself: a paid call first raises
|
|
114
|
+
`NotaryPaymentError`, whose message carries the server's x402 challenge. Pay
|
|
115
|
+
with any x402-capable client and retry with `X-PAYMENT` (HTTP) or
|
|
116
|
+
`x402/payment` (MCP); the SDK then parses the 200 response.
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from agent_notary import NotaryClient
|
|
120
|
+
from agent_notary.exceptions import NotaryPaymentError, NotaryPendingError
|
|
121
|
+
|
|
122
|
+
client = NotaryClient()
|
|
123
|
+
|
|
124
|
+
# Evidence pack — $0.02. Default = earliest issuance; seq selects one.
|
|
125
|
+
pack = await client.bundle(work_hash) # dict: picked/issuances/anchor/verify
|
|
126
|
+
pack_at_158 = await client.bundle(work_hash, seq=158)
|
|
127
|
+
|
|
128
|
+
# Signed time attestation — $0.002.
|
|
129
|
+
attest = await client.time_attest(payload_hash) # id/ts/sig_b64/leaf/log_index/anchor
|
|
130
|
+
|
|
131
|
+
# Idempotent lookup by payment tx — free. A 404 is a normal answer.
|
|
132
|
+
found = await client.pdr_by_tx("0x" + "ab" * 32) # {"found": False} when unknown
|
|
133
|
+
|
|
134
|
+
# Anchor API — free.
|
|
135
|
+
status = await client.anchor_status() # mode/latest/log_size
|
|
136
|
+
proof = await client.anchor_proof(186) # RFC 9162 §2.1.3 path
|
|
137
|
+
logproof = await client.anchor_logproof(381) # Phase B, kind 0/1
|
|
138
|
+
cons = await client.anchor_consistency(62, 64) # RFC 9162 §2.1.4
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
pack = await client.bundle(work_hash)
|
|
142
|
+
except NotaryPaymentError as e:
|
|
143
|
+
print("Pay via x402, then retry with X-PAYMENT / x402/payment:", e)
|
|
144
|
+
except NotaryPendingError as e:
|
|
145
|
+
print(f"Anchor not committed yet; retry in {e.eta_seconds}s at {e.recheck_url}")
|
|
146
|
+
```
|
|
147
|
+
|
|
108
148
|
## Configuration
|
|
109
149
|
|
|
110
150
|
| Env Var | Constructor Arg | Description |
|
|
@@ -122,6 +162,15 @@ asyncio.run(main())
|
|
|
122
162
|
| GET | `/v1/pdr/{job_id}` | Get PDR |
|
|
123
163
|
| POST | `/v1/notarize/quote` | Get price quote |
|
|
124
164
|
| GET | `/.well-known/agent.json` | MCP Server Card |
|
|
165
|
+
| POST | `/notarize` | Anonymous x402 paid PDR ($0.01) |
|
|
166
|
+
| POST | `/v1/attestation/bundle` | Evidence pack ($0.02) |
|
|
167
|
+
| POST | `/v1/attest/time` | Signed time attestation ($0.002) |
|
|
168
|
+
| GET | `/v1/pdr/verify/{pdr_b64}` | Verify a PDR |
|
|
169
|
+
| GET | `/v1/pdr/by-tx/{tx}` | PDR by payment tx |
|
|
170
|
+
| GET | `/v1/anchor/status` | Cumulative anchor status |
|
|
171
|
+
| GET | `/v1/anchor/proof/{seq}` | Inclusion proof by ledger seq |
|
|
172
|
+
| GET | `/v1/anchor/logproof/{log_index}` | Inclusion proof by log index (Phase B) |
|
|
173
|
+
| GET | `/v1/anchor/consistency?from=&to=` | Consistency proof |
|
|
125
174
|
| GET | `/openapi.json` | OpenAPI 3.0 spec |
|
|
126
175
|
|
|
127
176
|
## PDR Verification
|
|
@@ -140,6 +189,7 @@ from agent_notary.exceptions import (
|
|
|
140
189
|
NotaryAuthError, # 401 (authenticated CI surfaces; anonymous x402 gives 402 instead)
|
|
141
190
|
NotaryPaymentError, # 402 x402-challenge / 403
|
|
142
191
|
NotaryNotFoundError, # 404
|
|
192
|
+
NotaryPendingError, # 404 NOT_YET_ANCHORED — retry after eta_seconds
|
|
143
193
|
NotaryValidationError, # 400/422 — invalid params, INVALID_AGENT_SIGNATURE
|
|
144
194
|
NotaryConflictError, # 409 — tx_hash/job_id already used (idempotency)
|
|
145
195
|
NotaryServerError, # 5xx, carries status_code
|
|
@@ -166,3 +216,31 @@ except NotaryError as e:
|
|
|
166
216
|
pip install -e ".[dev]"
|
|
167
217
|
pytest tests/ -v
|
|
168
218
|
```
|
|
219
|
+
|
|
220
|
+
## Payment binding (NEAR rail)
|
|
221
|
+
|
|
222
|
+
NEAR payments must be provably linked to the account that sent them. Pass
|
|
223
|
+
`payer_account` (the NEAR account that paid) together with `bind_sig` — a
|
|
224
|
+
base64 Ed25519 **NEP-413 raw-buffer** signature (no SHA-256 pre-hash) over:
|
|
225
|
+
|
|
226
|
+
```
|
|
227
|
+
AOTrust AITP-BIND v1
|
|
228
|
+
work_hash:<64 hex>
|
|
229
|
+
tx_hash:<tx hash exactly as submitted>
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
The server resolves the account's public key via RPC and checks
|
|
233
|
+
`transaction.signer_id == payer_account`; a mismatch is rejected with
|
|
234
|
+
`403 PAYMENT_BIND_SIGNER_MISMATCH`, a bad signature with
|
|
235
|
+
`403 PAYMENT_BIND_INVALID`.
|
|
236
|
+
|
|
237
|
+
If `NEAR_PRIVATE_KEY` is set in the environment (or `near_private_key=` is
|
|
238
|
+
passed), the SDK signs the canonical message itself and fills `bind_sig`:
|
|
239
|
+
|
|
240
|
+
```python
|
|
241
|
+
req = NotarizeRequest(tx_hash=tx, work_hash=wh)
|
|
242
|
+
await client.notarize(req, payer_account="alice.near")
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Server mode `AITP_BIND_MODE=warn` (default) accepts unbound requests with a
|
|
246
|
+
`PAYMENT_UNBOUND` log line; `enforce` requires the fields.
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
Asyncio-native Python SDK for the AOTrust Notary API — cryptographic proof-of-existence for AI agent outputs.
|
|
6
6
|
|
|
7
|
+
> **Why protocol-level proofs?** An ERC-8004 empirical study (2026) measured that **>98% of agent feedback records carry no proof of an underlying interaction** (no payment proof, no task linkage); 59–91% of reviewers on three chains participated in coordinated Sybil campaigns, and manipulating a reputation score past a trust threshold costs as little as **$0.0027**. The agentic economy is moving from *statements* to *verifiable claims*: a PDR (239 bytes, Ed25519-signed, hash-chained, NEAR-anchored) turns any output or payment into independent evidence — verifiable by anyone, without trusting the provider's database.
|
|
8
|
+
> *(Refs: arXiv:2610.12948 [cs.CR], 2026; ERC-8004 Sybil measurements, Xiong et al. 2026.)*
|
|
9
|
+
|
|
7
10
|
## Install
|
|
8
11
|
|
|
9
12
|
```bash
|
|
@@ -78,6 +81,43 @@ asyncio.run(main())
|
|
|
78
81
|
> `/v1/v1/...` → HTTP 404 on every call. This bit a real user (2026-09-29:
|
|
79
82
|
> quickstart copy-paste + status lookups by artifact text instead of job_id).
|
|
80
83
|
|
|
84
|
+
## Paid read products (2.5.0)
|
|
85
|
+
|
|
86
|
+
The SDK never signs or settles payments itself: a paid call first raises
|
|
87
|
+
`NotaryPaymentError`, whose message carries the server's x402 challenge. Pay
|
|
88
|
+
with any x402-capable client and retry with `X-PAYMENT` (HTTP) or
|
|
89
|
+
`x402/payment` (MCP); the SDK then parses the 200 response.
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from agent_notary import NotaryClient
|
|
93
|
+
from agent_notary.exceptions import NotaryPaymentError, NotaryPendingError
|
|
94
|
+
|
|
95
|
+
client = NotaryClient()
|
|
96
|
+
|
|
97
|
+
# Evidence pack — $0.02. Default = earliest issuance; seq selects one.
|
|
98
|
+
pack = await client.bundle(work_hash) # dict: picked/issuances/anchor/verify
|
|
99
|
+
pack_at_158 = await client.bundle(work_hash, seq=158)
|
|
100
|
+
|
|
101
|
+
# Signed time attestation — $0.002.
|
|
102
|
+
attest = await client.time_attest(payload_hash) # id/ts/sig_b64/leaf/log_index/anchor
|
|
103
|
+
|
|
104
|
+
# Idempotent lookup by payment tx — free. A 404 is a normal answer.
|
|
105
|
+
found = await client.pdr_by_tx("0x" + "ab" * 32) # {"found": False} when unknown
|
|
106
|
+
|
|
107
|
+
# Anchor API — free.
|
|
108
|
+
status = await client.anchor_status() # mode/latest/log_size
|
|
109
|
+
proof = await client.anchor_proof(186) # RFC 9162 §2.1.3 path
|
|
110
|
+
logproof = await client.anchor_logproof(381) # Phase B, kind 0/1
|
|
111
|
+
cons = await client.anchor_consistency(62, 64) # RFC 9162 §2.1.4
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
pack = await client.bundle(work_hash)
|
|
115
|
+
except NotaryPaymentError as e:
|
|
116
|
+
print("Pay via x402, then retry with X-PAYMENT / x402/payment:", e)
|
|
117
|
+
except NotaryPendingError as e:
|
|
118
|
+
print(f"Anchor not committed yet; retry in {e.eta_seconds}s at {e.recheck_url}")
|
|
119
|
+
```
|
|
120
|
+
|
|
81
121
|
## Configuration
|
|
82
122
|
|
|
83
123
|
| Env Var | Constructor Arg | Description |
|
|
@@ -95,6 +135,15 @@ asyncio.run(main())
|
|
|
95
135
|
| GET | `/v1/pdr/{job_id}` | Get PDR |
|
|
96
136
|
| POST | `/v1/notarize/quote` | Get price quote |
|
|
97
137
|
| GET | `/.well-known/agent.json` | MCP Server Card |
|
|
138
|
+
| POST | `/notarize` | Anonymous x402 paid PDR ($0.01) |
|
|
139
|
+
| POST | `/v1/attestation/bundle` | Evidence pack ($0.02) |
|
|
140
|
+
| POST | `/v1/attest/time` | Signed time attestation ($0.002) |
|
|
141
|
+
| GET | `/v1/pdr/verify/{pdr_b64}` | Verify a PDR |
|
|
142
|
+
| GET | `/v1/pdr/by-tx/{tx}` | PDR by payment tx |
|
|
143
|
+
| GET | `/v1/anchor/status` | Cumulative anchor status |
|
|
144
|
+
| GET | `/v1/anchor/proof/{seq}` | Inclusion proof by ledger seq |
|
|
145
|
+
| GET | `/v1/anchor/logproof/{log_index}` | Inclusion proof by log index (Phase B) |
|
|
146
|
+
| GET | `/v1/anchor/consistency?from=&to=` | Consistency proof |
|
|
98
147
|
| GET | `/openapi.json` | OpenAPI 3.0 spec |
|
|
99
148
|
|
|
100
149
|
## PDR Verification
|
|
@@ -113,6 +162,7 @@ from agent_notary.exceptions import (
|
|
|
113
162
|
NotaryAuthError, # 401 (authenticated CI surfaces; anonymous x402 gives 402 instead)
|
|
114
163
|
NotaryPaymentError, # 402 x402-challenge / 403
|
|
115
164
|
NotaryNotFoundError, # 404
|
|
165
|
+
NotaryPendingError, # 404 NOT_YET_ANCHORED — retry after eta_seconds
|
|
116
166
|
NotaryValidationError, # 400/422 — invalid params, INVALID_AGENT_SIGNATURE
|
|
117
167
|
NotaryConflictError, # 409 — tx_hash/job_id already used (idempotency)
|
|
118
168
|
NotaryServerError, # 5xx, carries status_code
|
|
@@ -138,4 +188,32 @@ except NotaryError as e:
|
|
|
138
188
|
```bash
|
|
139
189
|
pip install -e ".[dev]"
|
|
140
190
|
pytest tests/ -v
|
|
141
|
-
```
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Payment binding (NEAR rail)
|
|
194
|
+
|
|
195
|
+
NEAR payments must be provably linked to the account that sent them. Pass
|
|
196
|
+
`payer_account` (the NEAR account that paid) together with `bind_sig` — a
|
|
197
|
+
base64 Ed25519 **NEP-413 raw-buffer** signature (no SHA-256 pre-hash) over:
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
AOTrust AITP-BIND v1
|
|
201
|
+
work_hash:<64 hex>
|
|
202
|
+
tx_hash:<tx hash exactly as submitted>
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
The server resolves the account's public key via RPC and checks
|
|
206
|
+
`transaction.signer_id == payer_account`; a mismatch is rejected with
|
|
207
|
+
`403 PAYMENT_BIND_SIGNER_MISMATCH`, a bad signature with
|
|
208
|
+
`403 PAYMENT_BIND_INVALID`.
|
|
209
|
+
|
|
210
|
+
If `NEAR_PRIVATE_KEY` is set in the environment (or `near_private_key=` is
|
|
211
|
+
passed), the SDK signs the canonical message itself and fills `bind_sig`:
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
req = NotarizeRequest(tx_hash=tx, work_hash=wh)
|
|
215
|
+
await client.notarize(req, payer_account="alice.near")
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Server mode `AITP_BIND_MODE=warn` (default) accepts unbound requests with a
|
|
219
|
+
`PAYMENT_UNBOUND` log line; `enforce` requires the fields.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""A&O Trust Layer — Agent Notary SDK (asyncio-native)."""
|
|
2
2
|
|
|
3
|
-
__version__ = "2.
|
|
3
|
+
__version__ = "2.5.0"
|
|
4
4
|
|
|
5
5
|
from .client import NotaryClient
|
|
6
6
|
from .models import NotarizeRequest, NotarizeResult, PdrResult, StatusResult
|
|
@@ -9,7 +9,10 @@ from .exceptions import (
|
|
|
9
9
|
NotaryAuthError,
|
|
10
10
|
NotaryNotFoundError,
|
|
11
11
|
NotaryPaymentError,
|
|
12
|
+
NotaryPendingError,
|
|
12
13
|
NotaryServerError,
|
|
14
|
+
NotaryValidationError,
|
|
15
|
+
NotaryConflictError,
|
|
13
16
|
)
|
|
14
17
|
from .tools import AOTrustTool
|
|
15
18
|
|
|
@@ -23,6 +26,9 @@ __all__ = [
|
|
|
23
26
|
"NotaryAuthError",
|
|
24
27
|
"NotaryNotFoundError",
|
|
25
28
|
"NotaryPaymentError",
|
|
29
|
+
"NotaryPendingError",
|
|
26
30
|
"NotaryServerError",
|
|
31
|
+
"NotaryValidationError",
|
|
32
|
+
"NotaryConflictError",
|
|
27
33
|
"AOTrustTool",
|
|
28
34
|
]
|
|
@@ -9,11 +9,46 @@ from typing import Optional
|
|
|
9
9
|
import aiohttp
|
|
10
10
|
|
|
11
11
|
from .models import NotarizeRequest, NotarizeResult, StatusResult, PdrResult
|
|
12
|
+
NEP413_TAG = 2147484061
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def canonical_bind_message(work_hash: str, tx_hash: str) -> bytes:
|
|
16
|
+
"""Canonical AOTrust AITP-BIND v1 message — identical to the server module."""
|
|
17
|
+
return f"AOTrust AITP-BIND v1\nwork_hash:{work_hash}\ntx_hash:{tx_hash}".encode("utf-8")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _decode_ed25519_key(raw: str) -> bytes:
|
|
21
|
+
"""Accepts hex, base58, or NEAR's 'ed25519:<base58>' key format."""
|
|
22
|
+
import base58
|
|
23
|
+
key = raw.strip()
|
|
24
|
+
if key.startswith("ed25519:"):
|
|
25
|
+
key = key.split(":", 1)[1]
|
|
26
|
+
try:
|
|
27
|
+
if len(key) == 64 and all(c in "0123456789abcdefABCDEF" for c in key):
|
|
28
|
+
return bytes.fromhex(key)
|
|
29
|
+
except Exception:
|
|
30
|
+
pass
|
|
31
|
+
return base58.b58decode(key)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _sign_bind_message(private_key: str, work_hash: str, tx_hash: str) -> str:
|
|
35
|
+
"""NEP-413 RAW buffer signature (no SHA-256 pre-hash) over the bind message."""
|
|
36
|
+
import base64
|
|
37
|
+
import struct
|
|
38
|
+
from nacl.signing import SigningKey
|
|
39
|
+
|
|
40
|
+
msg = canonical_bind_message(work_hash, tx_hash)
|
|
41
|
+
buf = struct.pack("<II", NEP413_TAG, len(msg)) + msg
|
|
42
|
+
sk = SigningKey(_decode_ed25519_key(private_key))
|
|
43
|
+
return base64.b64encode(sk.sign(buf).signature).decode()
|
|
44
|
+
|
|
45
|
+
|
|
12
46
|
from .exceptions import (
|
|
13
47
|
NotaryError,
|
|
14
48
|
NotaryAuthError,
|
|
15
49
|
NotaryNotFoundError,
|
|
16
50
|
NotaryPaymentError,
|
|
51
|
+
NotaryPendingError,
|
|
17
52
|
NotaryServerError,
|
|
18
53
|
NotaryValidationError,
|
|
19
54
|
NotaryConflictError,
|
|
@@ -108,6 +143,10 @@ class NotaryClient:
|
|
|
108
143
|
await asyncio.sleep(RETRY_DELAYS[attempt])
|
|
109
144
|
raise NotaryServerError(f"Request failed after {MAX_RETRIES} retries: {last_exc}")
|
|
110
145
|
|
|
146
|
+
async def _get_json(self, path: str) -> tuple[int, dict]:
|
|
147
|
+
"""GET helper — same retry policy as _request (502/503 backoff)."""
|
|
148
|
+
return await self._request("GET", path)
|
|
149
|
+
|
|
111
150
|
def _check_status(self, status: int, body: dict) -> None:
|
|
112
151
|
if status == 401:
|
|
113
152
|
raise NotaryAuthError(body.get("error", "Unauthorized"))
|
|
@@ -129,19 +168,42 @@ class NotaryClient:
|
|
|
129
168
|
if status >= 500:
|
|
130
169
|
raise NotaryServerError(body.get("error", f"Server error {status}"), status_code=status)
|
|
131
170
|
|
|
132
|
-
async def notarize(
|
|
171
|
+
async def notarize(
|
|
172
|
+
self,
|
|
173
|
+
request: NotarizeRequest,
|
|
174
|
+
payer_account: Optional[str] = None,
|
|
175
|
+
bind_sig: Optional[str] = None,
|
|
176
|
+
near_private_key: Optional[str] = None,
|
|
177
|
+
) -> NotarizeResult:
|
|
133
178
|
"""POST /notarize — anonymous x402 path.
|
|
134
179
|
|
|
135
180
|
Without payment the server answers 402 with the x402 challenge
|
|
136
181
|
(x402Version/accepts[0].resource=...); NotaryPaymentError carries it.
|
|
137
182
|
Payload built for the live x402 contract: work_hash at top level
|
|
138
183
|
(the CI-channel envelope with tx_hash/payload.work_hash is NOT used here).
|
|
184
|
+
|
|
185
|
+
Payment binding (V5-A', NEAR rail): pass payer_account + bind_sig, or
|
|
186
|
+
let the SDK build them. If `near_private_key` (or env NEAR_PRIVATE_KEY)
|
|
187
|
+
is set and payer_account is provided, the SDK signs the canonical
|
|
188
|
+
AOTrust AITP-BIND v1 message with the NEP-413 raw buffer and fills
|
|
189
|
+
bind_sig. Without a key, fields are simply not added (server warn mode
|
|
190
|
+
passes unbound requests).
|
|
139
191
|
"""
|
|
192
|
+
payer = payer_account or request.payer_account
|
|
193
|
+
sig = bind_sig or request.bind_sig
|
|
194
|
+
key = near_private_key or os.environ.get("NEAR_PRIVATE_KEY")
|
|
195
|
+
if not sig and key and payer and request.work_hash and request.tx_hash:
|
|
196
|
+
sig = _sign_bind_message(key, request.work_hash, request.tx_hash)
|
|
197
|
+
|
|
140
198
|
payload: dict = {"work_hash": request.work_hash or ""}
|
|
141
199
|
if request.agent_sig:
|
|
142
200
|
payload["agent_sig"] = request.agent_sig
|
|
143
201
|
if request.agent_pubkey:
|
|
144
202
|
payload["agent_pubkey"] = request.agent_pubkey
|
|
203
|
+
if payer:
|
|
204
|
+
payload["payer_account"] = payer
|
|
205
|
+
if sig:
|
|
206
|
+
payload["bind_sig"] = sig
|
|
145
207
|
status, body = await self._request("POST", "/notarize", json_data=payload)
|
|
146
208
|
self._check_status(status, body)
|
|
147
209
|
if status not in (200, 202):
|
|
@@ -234,4 +296,84 @@ class NotaryClient:
|
|
|
234
296
|
payload["agent_pubkey"] = agent_pubkey
|
|
235
297
|
status, body = await self._request("POST", "/v1/shield/free", json_data=payload)
|
|
236
298
|
self._check_status(status, body)
|
|
237
|
-
return body
|
|
299
|
+
return body
|
|
300
|
+
|
|
301
|
+
# ── 2.5.0: paid read products + anchor API ─────────────────────────────
|
|
302
|
+
|
|
303
|
+
async def bundle(self, work_hash: str, seq: int | None = None) -> dict:
|
|
304
|
+
"""POST /v1/attestation/bundle — read-only evidence pack ($0.02).
|
|
305
|
+
|
|
306
|
+
Returns the earliest issuance unless `seq` selects a concrete one.
|
|
307
|
+
Without payment the server answers 402 → NotaryPaymentError carrying
|
|
308
|
+
the x402 challenge (`x402/payment` meta on retry).
|
|
309
|
+
"""
|
|
310
|
+
payload: dict = {"work_hash": work_hash}
|
|
311
|
+
if seq is not None:
|
|
312
|
+
payload["seq"] = seq
|
|
313
|
+
status, body = await self._request("POST", "/v1/attestation/bundle", json_data=payload)
|
|
314
|
+
self._check_status(status, body)
|
|
315
|
+
return body
|
|
316
|
+
|
|
317
|
+
async def time_attest(self, payload_hash: str, nonce: str | None = None) -> dict:
|
|
318
|
+
"""POST /v1/attest/time — signed time attestation ($0.002).
|
|
319
|
+
|
|
320
|
+
Returns id/ts/ts_iso/payload_hash/nonce/kid/alg/sig_b64/leaf/log_index/
|
|
321
|
+
anchor/verify. 402 → NotaryPaymentError.
|
|
322
|
+
"""
|
|
323
|
+
payload: dict = {"payload_hash": payload_hash}
|
|
324
|
+
if nonce is not None:
|
|
325
|
+
payload["nonce"] = nonce
|
|
326
|
+
status, body = await self._request("POST", "/v1/attest/time", json_data=payload)
|
|
327
|
+
self._check_status(status, body)
|
|
328
|
+
return body
|
|
329
|
+
|
|
330
|
+
async def pdr_by_tx(self, tx: str) -> dict:
|
|
331
|
+
"""GET /v1/pdr/by-tx/{tx} — idempotent lookup by payment transaction.
|
|
332
|
+
|
|
333
|
+
A 404 is a normal answer ({"found": False}); a malformed tx is a
|
|
334
|
+
400 → NotaryValidationError.
|
|
335
|
+
"""
|
|
336
|
+
from urllib.parse import quote as _urlquote
|
|
337
|
+
status, body = await self._get_json(f"/v1/pdr/by-tx/{_urlquote(tx, safe='')}")
|
|
338
|
+
if status == 404:
|
|
339
|
+
return {"found": False}
|
|
340
|
+
self._check_status(status, body)
|
|
341
|
+
return body
|
|
342
|
+
|
|
343
|
+
async def anchor_status(self) -> dict:
|
|
344
|
+
"""GET /v1/anchor/status — cumulative Merkle log status."""
|
|
345
|
+
status, body = await self._get_json("/v1/anchor/status")
|
|
346
|
+
self._check_status(status, body)
|
|
347
|
+
return body
|
|
348
|
+
|
|
349
|
+
async def anchor_proof(self, seq: int) -> dict:
|
|
350
|
+
"""GET /v1/anchor/proof/{seq} — RFC 9162 inclusion proof.
|
|
351
|
+
|
|
352
|
+
The server answers 409 NOT_YET_ANCHORED (409 = state conflict: the leaf
|
|
353
|
+
exists in the log, the committed slice does not include it yet) — and
|
|
354
|
+
historically 404 with the same code — both raise
|
|
355
|
+
NotaryPendingError(eta_seconds, recheck_url). A 404 NOT_FOUND means the
|
|
356
|
+
seq was never issued; 404 NOT_A_LEAF is a server-side invariant signal.
|
|
357
|
+
"""
|
|
358
|
+
status, body = await self._get_json(f"/v1/anchor/proof/{int(seq)}")
|
|
359
|
+
if status in (404, 409) and (body or {}).get("error") == "NOT_YET_ANCHORED":
|
|
360
|
+
raise NotaryPendingError(
|
|
361
|
+
eta_seconds=body.get("eta_seconds"),
|
|
362
|
+
recheck_url=body.get("recheck_url"),
|
|
363
|
+
message="Anchor not published yet; retry after eta_seconds",
|
|
364
|
+
)
|
|
365
|
+
self._check_status(status, body)
|
|
366
|
+
return body
|
|
367
|
+
|
|
368
|
+
async def anchor_logproof(self, log_index: int) -> dict:
|
|
369
|
+
"""GET /v1/anchor/logproof/{log_index} — Phase B proof by opaque log_index."""
|
|
370
|
+
status, body = await self._get_json(f"/v1/anchor/logproof/{int(log_index)}")
|
|
371
|
+
self._check_status(status, body)
|
|
372
|
+
return body
|
|
373
|
+
|
|
374
|
+
async def anchor_consistency(self, from_seq: int, to_seq: int) -> dict:
|
|
375
|
+
"""GET /v1/anchor/consistency?from=&to= — RFC 9162 consistency proof."""
|
|
376
|
+
status, body = await self._get_json(
|
|
377
|
+
f"/v1/anchor/consistency?from={int(from_seq)}&to={int(to_seq)}")
|
|
378
|
+
self._check_status(status, body)
|
|
379
|
+
return body
|
|
@@ -30,4 +30,16 @@ class NotaryValidationError(NotaryError):
|
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
class NotaryConflictError(NotaryError):
|
|
33
|
-
"""409 — idempotency conflict: this tx_hash/job_id was already used."""
|
|
33
|
+
"""409 — idempotency conflict: this tx_hash/job_id was already used."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class NotaryPendingError(NotaryError):
|
|
37
|
+
"""409/404 NOT_YET_ANCHORED — the leaf/anchor is not committed yet.
|
|
38
|
+
|
|
39
|
+
Retry after `eta_seconds`; `recheck_url` points at the same endpoint.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, eta_seconds=None, recheck_url=None, message=None):
|
|
43
|
+
super().__init__(message or "Not anchored yet")
|
|
44
|
+
self.eta_seconds = eta_seconds
|
|
45
|
+
self.recheck_url = recheck_url
|
|
@@ -33,6 +33,13 @@ class NotarizeRequest(BaseModel):
|
|
|
33
33
|
work_hash: Optional[str] = Field(None, description="SHA-256 hex digest (64 chars)")
|
|
34
34
|
agent_sig: Optional[str] = Field(None, description="Base64 Ed25519 signature (sig_A)")
|
|
35
35
|
agent_pubkey: Optional[str] = Field(None, description="Hex Ed25519 public key")
|
|
36
|
+
payer_account: Optional[str] = Field(
|
|
37
|
+
None, description="NEAR account that paid/signed the tx (payment binding)"
|
|
38
|
+
)
|
|
39
|
+
bind_sig: Optional[str] = Field(
|
|
40
|
+
None,
|
|
41
|
+
description="Base64 Ed25519 NEP-413 signature over the AOTrust AITP-BIND v1 message",
|
|
42
|
+
)
|
|
36
43
|
|
|
37
44
|
def to_api_payload(self) -> dict:
|
|
38
45
|
payload: dict = {"tx_hash": self.tx_hash}
|
|
@@ -42,6 +49,10 @@ class NotarizeRequest(BaseModel):
|
|
|
42
49
|
payload["agent_sig"] = self.agent_sig
|
|
43
50
|
if self.agent_pubkey:
|
|
44
51
|
payload["agent_pubkey"] = self.agent_pubkey
|
|
52
|
+
if self.payer_account:
|
|
53
|
+
payload["payer_account"] = self.payer_account
|
|
54
|
+
if self.bind_sig:
|
|
55
|
+
payload["bind_sig"] = self.bind_sig
|
|
45
56
|
return payload
|
|
46
57
|
|
|
47
58
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aotrust-protocol
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.5.0
|
|
4
4
|
Summary: Asyncio-native SDK for A&O Trust Layer — Agent Notary Service
|
|
5
5
|
Author: A&O Trust Layer
|
|
6
6
|
License: MIT
|
|
@@ -31,6 +31,9 @@ Dynamic: license-file
|
|
|
31
31
|
|
|
32
32
|
Asyncio-native Python SDK for the AOTrust Notary API — cryptographic proof-of-existence for AI agent outputs.
|
|
33
33
|
|
|
34
|
+
> **Why protocol-level proofs?** An ERC-8004 empirical study (2026) measured that **>98% of agent feedback records carry no proof of an underlying interaction** (no payment proof, no task linkage); 59–91% of reviewers on three chains participated in coordinated Sybil campaigns, and manipulating a reputation score past a trust threshold costs as little as **$0.0027**. The agentic economy is moving from *statements* to *verifiable claims*: a PDR (239 bytes, Ed25519-signed, hash-chained, NEAR-anchored) turns any output or payment into independent evidence — verifiable by anyone, without trusting the provider's database.
|
|
35
|
+
> *(Refs: arXiv:2610.12948 [cs.CR], 2026; ERC-8004 Sybil measurements, Xiong et al. 2026.)*
|
|
36
|
+
|
|
34
37
|
## Install
|
|
35
38
|
|
|
36
39
|
```bash
|
|
@@ -105,6 +108,43 @@ asyncio.run(main())
|
|
|
105
108
|
> `/v1/v1/...` → HTTP 404 on every call. This bit a real user (2026-09-29:
|
|
106
109
|
> quickstart copy-paste + status lookups by artifact text instead of job_id).
|
|
107
110
|
|
|
111
|
+
## Paid read products (2.5.0)
|
|
112
|
+
|
|
113
|
+
The SDK never signs or settles payments itself: a paid call first raises
|
|
114
|
+
`NotaryPaymentError`, whose message carries the server's x402 challenge. Pay
|
|
115
|
+
with any x402-capable client and retry with `X-PAYMENT` (HTTP) or
|
|
116
|
+
`x402/payment` (MCP); the SDK then parses the 200 response.
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from agent_notary import NotaryClient
|
|
120
|
+
from agent_notary.exceptions import NotaryPaymentError, NotaryPendingError
|
|
121
|
+
|
|
122
|
+
client = NotaryClient()
|
|
123
|
+
|
|
124
|
+
# Evidence pack — $0.02. Default = earliest issuance; seq selects one.
|
|
125
|
+
pack = await client.bundle(work_hash) # dict: picked/issuances/anchor/verify
|
|
126
|
+
pack_at_158 = await client.bundle(work_hash, seq=158)
|
|
127
|
+
|
|
128
|
+
# Signed time attestation — $0.002.
|
|
129
|
+
attest = await client.time_attest(payload_hash) # id/ts/sig_b64/leaf/log_index/anchor
|
|
130
|
+
|
|
131
|
+
# Idempotent lookup by payment tx — free. A 404 is a normal answer.
|
|
132
|
+
found = await client.pdr_by_tx("0x" + "ab" * 32) # {"found": False} when unknown
|
|
133
|
+
|
|
134
|
+
# Anchor API — free.
|
|
135
|
+
status = await client.anchor_status() # mode/latest/log_size
|
|
136
|
+
proof = await client.anchor_proof(186) # RFC 9162 §2.1.3 path
|
|
137
|
+
logproof = await client.anchor_logproof(381) # Phase B, kind 0/1
|
|
138
|
+
cons = await client.anchor_consistency(62, 64) # RFC 9162 §2.1.4
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
pack = await client.bundle(work_hash)
|
|
142
|
+
except NotaryPaymentError as e:
|
|
143
|
+
print("Pay via x402, then retry with X-PAYMENT / x402/payment:", e)
|
|
144
|
+
except NotaryPendingError as e:
|
|
145
|
+
print(f"Anchor not committed yet; retry in {e.eta_seconds}s at {e.recheck_url}")
|
|
146
|
+
```
|
|
147
|
+
|
|
108
148
|
## Configuration
|
|
109
149
|
|
|
110
150
|
| Env Var | Constructor Arg | Description |
|
|
@@ -122,6 +162,15 @@ asyncio.run(main())
|
|
|
122
162
|
| GET | `/v1/pdr/{job_id}` | Get PDR |
|
|
123
163
|
| POST | `/v1/notarize/quote` | Get price quote |
|
|
124
164
|
| GET | `/.well-known/agent.json` | MCP Server Card |
|
|
165
|
+
| POST | `/notarize` | Anonymous x402 paid PDR ($0.01) |
|
|
166
|
+
| POST | `/v1/attestation/bundle` | Evidence pack ($0.02) |
|
|
167
|
+
| POST | `/v1/attest/time` | Signed time attestation ($0.002) |
|
|
168
|
+
| GET | `/v1/pdr/verify/{pdr_b64}` | Verify a PDR |
|
|
169
|
+
| GET | `/v1/pdr/by-tx/{tx}` | PDR by payment tx |
|
|
170
|
+
| GET | `/v1/anchor/status` | Cumulative anchor status |
|
|
171
|
+
| GET | `/v1/anchor/proof/{seq}` | Inclusion proof by ledger seq |
|
|
172
|
+
| GET | `/v1/anchor/logproof/{log_index}` | Inclusion proof by log index (Phase B) |
|
|
173
|
+
| GET | `/v1/anchor/consistency?from=&to=` | Consistency proof |
|
|
125
174
|
| GET | `/openapi.json` | OpenAPI 3.0 spec |
|
|
126
175
|
|
|
127
176
|
## PDR Verification
|
|
@@ -140,6 +189,7 @@ from agent_notary.exceptions import (
|
|
|
140
189
|
NotaryAuthError, # 401 (authenticated CI surfaces; anonymous x402 gives 402 instead)
|
|
141
190
|
NotaryPaymentError, # 402 x402-challenge / 403
|
|
142
191
|
NotaryNotFoundError, # 404
|
|
192
|
+
NotaryPendingError, # 404 NOT_YET_ANCHORED — retry after eta_seconds
|
|
143
193
|
NotaryValidationError, # 400/422 — invalid params, INVALID_AGENT_SIGNATURE
|
|
144
194
|
NotaryConflictError, # 409 — tx_hash/job_id already used (idempotency)
|
|
145
195
|
NotaryServerError, # 5xx, carries status_code
|
|
@@ -166,3 +216,31 @@ except NotaryError as e:
|
|
|
166
216
|
pip install -e ".[dev]"
|
|
167
217
|
pytest tests/ -v
|
|
168
218
|
```
|
|
219
|
+
|
|
220
|
+
## Payment binding (NEAR rail)
|
|
221
|
+
|
|
222
|
+
NEAR payments must be provably linked to the account that sent them. Pass
|
|
223
|
+
`payer_account` (the NEAR account that paid) together with `bind_sig` — a
|
|
224
|
+
base64 Ed25519 **NEP-413 raw-buffer** signature (no SHA-256 pre-hash) over:
|
|
225
|
+
|
|
226
|
+
```
|
|
227
|
+
AOTrust AITP-BIND v1
|
|
228
|
+
work_hash:<64 hex>
|
|
229
|
+
tx_hash:<tx hash exactly as submitted>
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
The server resolves the account's public key via RPC and checks
|
|
233
|
+
`transaction.signer_id == payer_account`; a mismatch is rejected with
|
|
234
|
+
`403 PAYMENT_BIND_SIGNER_MISMATCH`, a bad signature with
|
|
235
|
+
`403 PAYMENT_BIND_INVALID`.
|
|
236
|
+
|
|
237
|
+
If `NEAR_PRIVATE_KEY` is set in the environment (or `near_private_key=` is
|
|
238
|
+
passed), the SDK signs the canonical message itself and fills `bind_sig`:
|
|
239
|
+
|
|
240
|
+
```python
|
|
241
|
+
req = NotarizeRequest(tx_hash=tx, work_hash=wh)
|
|
242
|
+
await client.notarize(req, payer_account="alice.near")
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Server mode `AITP_BIND_MODE=warn` (default) accepts unbound requests with a
|
|
246
|
+
`PAYMENT_UNBOUND` log line; `enforce` requires the fields.
|
|
@@ -76,8 +76,12 @@ def test_live_get_status_404_on_unknown_job():
|
|
|
76
76
|
assert msg is not None, "get_status() на несуществующем job должен дать 404"
|
|
77
77
|
|
|
78
78
|
|
|
79
|
-
def test_live_pubkey_lazy_fetch():
|
|
79
|
+
def test_live_pubkey_lazy_fetch(monkeypatch):
|
|
80
80
|
_live_guard()
|
|
81
|
+
# Isolation: root tests do os.environ.setdefault("NOTARY_PUBKEY", ...), and in a
|
|
82
|
+
# combined run that makes NotaryClient take the "provided key" path (no fetch),
|
|
83
|
+
# so _pubkey_fetched stays None. This test is specifically about lazy fetch.
|
|
84
|
+
monkeypatch.delenv("NOTARY_PUBKEY", raising=False)
|
|
81
85
|
from agent_notary import NotaryClient
|
|
82
86
|
|
|
83
87
|
client = NotaryClient()
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""aotrust-protocol 2.5.0 — paid read products and anchor API.
|
|
2
|
+
|
|
3
|
+
Mock transport only (aioresponses): bundle/time_attest, pdr_by_tx, and the
|
|
4
|
+
anchor status/proof/logproof/consistency methods, including the 402 path and
|
|
5
|
+
NOT_YET_ANCHORED → NotaryPendingError mapping.
|
|
6
|
+
"""
|
|
7
|
+
import pytest
|
|
8
|
+
import aioresponses.core as _arc
|
|
9
|
+
from aioresponses import aioresponses
|
|
10
|
+
from aiohttp import ClientResponse as _AiohttpClientResponse
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class _CompatClientResponse(_AiohttpClientResponse):
|
|
14
|
+
"""aiohttp 3.14 requires stream_writer; aioresponses 0.7.9 predates it."""
|
|
15
|
+
|
|
16
|
+
class _Writer:
|
|
17
|
+
output_size = 0
|
|
18
|
+
|
|
19
|
+
def __init__(self, *args, **kwargs):
|
|
20
|
+
kwargs.setdefault("stream_writer", self._Writer())
|
|
21
|
+
super().__init__(*args, **kwargs)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# _build_response() resolves this module-level name at call time.
|
|
25
|
+
_arc.ClientResponse = _CompatClientResponse
|
|
26
|
+
|
|
27
|
+
from agent_notary import NotaryClient
|
|
28
|
+
from agent_notary.exceptions import (
|
|
29
|
+
NotaryNotFoundError,
|
|
30
|
+
NotaryPaymentError,
|
|
31
|
+
NotaryPendingError,
|
|
32
|
+
NotaryValidationError,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
BASE = "https://api.aotrust.test"
|
|
36
|
+
WORK_HASH = "9" * 64
|
|
37
|
+
PAYLOAD_HASH = "a" * 64
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@pytest.fixture
|
|
41
|
+
def client():
|
|
42
|
+
return NotaryClient(base_url=BASE)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@pytest.mark.asyncio
|
|
46
|
+
async def test_bundle_200_and_optional_seq(client):
|
|
47
|
+
with aioresponses() as m:
|
|
48
|
+
m.post(f"{BASE}/v1/attestation/bundle", status=200, payload={
|
|
49
|
+
"work_hash": WORK_HASH,
|
|
50
|
+
"seq": 80,
|
|
51
|
+
"picked": "earliest",
|
|
52
|
+
"issuance_count": 2,
|
|
53
|
+
"issuances": [{"seq": 80}, {"seq": 158}],
|
|
54
|
+
"pdr_b64": "AAA=",
|
|
55
|
+
"anchor": {"mode": "cumulative", "size": 191},
|
|
56
|
+
})
|
|
57
|
+
out = await client.bundle(WORK_HASH)
|
|
58
|
+
assert out["seq"] == 80
|
|
59
|
+
assert out["picked"] == "earliest"
|
|
60
|
+
assert isinstance(out["issuances"], list)
|
|
61
|
+
key = next(k for k in m.requests if k[0].upper() == "POST")
|
|
62
|
+
calls = m.requests[key]
|
|
63
|
+
assert calls[0].kwargs["json"] == {"work_hash": WORK_HASH}
|
|
64
|
+
|
|
65
|
+
with aioresponses() as m:
|
|
66
|
+
m.post(f"{BASE}/v1/attestation/bundle", status=200, payload={
|
|
67
|
+
"work_hash": WORK_HASH, "seq": 158, "picked": "requested"})
|
|
68
|
+
out = await client.bundle(WORK_HASH, seq=158)
|
|
69
|
+
assert out["seq"] == 158
|
|
70
|
+
key = next(k for k in m.requests if k[0].upper() == "POST")
|
|
71
|
+
calls = m.requests[key]
|
|
72
|
+
assert calls[0].kwargs["json"] == {"work_hash": WORK_HASH, "seq": 158}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@pytest.mark.asyncio
|
|
76
|
+
async def test_bundle_402_is_payment_error(client):
|
|
77
|
+
with aioresponses() as m:
|
|
78
|
+
m.post(f"{BASE}/v1/attestation/bundle", status=402, payload={
|
|
79
|
+
"x402Version": 1,
|
|
80
|
+
"accepts": [{"scheme": "exact", "amount": "20000"}],
|
|
81
|
+
"error": "Payment required",
|
|
82
|
+
})
|
|
83
|
+
with pytest.raises(NotaryPaymentError) as exc:
|
|
84
|
+
await client.bundle(WORK_HASH)
|
|
85
|
+
assert "Payment required" in str(exc.value)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@pytest.mark.asyncio
|
|
89
|
+
async def test_time_attest_200_omits_nonce(client):
|
|
90
|
+
with aioresponses() as m:
|
|
91
|
+
m.post(f"{BASE}/v1/attest/time", status=200, payload={
|
|
92
|
+
"id": 7, "ts": 1760000000000000000, "payload_hash": PAYLOAD_HASH,
|
|
93
|
+
"sig_b64": "c2ln", "leaf": "bGVhZg==", "log_index": 381,
|
|
94
|
+
"anchor": {"mode": "cumulative", "log_index": 381},
|
|
95
|
+
})
|
|
96
|
+
out = await client.time_attest(PAYLOAD_HASH)
|
|
97
|
+
assert out["id"] == 7
|
|
98
|
+
assert out["log_index"] == 381
|
|
99
|
+
assert out["anchor"]["mode"] == "cumulative"
|
|
100
|
+
key = next(k for k in m.requests if k[0].upper() == "POST")
|
|
101
|
+
calls = m.requests[key]
|
|
102
|
+
assert calls[0].kwargs["json"] == {"payload_hash": PAYLOAD_HASH}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@pytest.mark.asyncio
|
|
106
|
+
async def test_time_attest_402_is_payment_error(client):
|
|
107
|
+
with aioresponses() as m:
|
|
108
|
+
m.post(f"{BASE}/v1/attest/time", status=402, payload={"error": "Payment required"})
|
|
109
|
+
with pytest.raises(NotaryPaymentError) as exc:
|
|
110
|
+
await client.time_attest(PAYLOAD_HASH)
|
|
111
|
+
assert "Payment required" in str(exc.value)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@pytest.mark.asyncio
|
|
115
|
+
async def test_pdr_by_tx_200(client):
|
|
116
|
+
tx = "0x" + "ab" * 32
|
|
117
|
+
with aioresponses() as m:
|
|
118
|
+
m.get(f"{BASE}/v1/pdr/by-tx/{tx}", status=200, payload={
|
|
119
|
+
"found": True, "seq": 186, "payment_hash": tx[2:],
|
|
120
|
+
"payment_anchor_type": 5, "work_hash": WORK_HASH, "pdr_b64": "AAA="})
|
|
121
|
+
out = await client.pdr_by_tx(tx)
|
|
122
|
+
assert out["found"] is True
|
|
123
|
+
assert out["seq"] == 186
|
|
124
|
+
assert out["payment_hash"] == tx[2:]
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@pytest.mark.asyncio
|
|
128
|
+
async def test_pdr_by_tx_404_is_found_false(client):
|
|
129
|
+
with aioresponses() as m:
|
|
130
|
+
m.get(f"{BASE}/v1/pdr/by-tx/0xunknown", status=404, payload={"found": False})
|
|
131
|
+
assert await client.pdr_by_tx("0xunknown") == {"found": False}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@pytest.mark.asyncio
|
|
135
|
+
async def test_pdr_by_tx_400_is_validation_error(client):
|
|
136
|
+
with aioresponses() as m:
|
|
137
|
+
m.get(f"{BASE}/v1/pdr/by-tx/garbage", status=400, payload={"error": "INVALID_TX"})
|
|
138
|
+
with pytest.raises(NotaryValidationError):
|
|
139
|
+
await client.pdr_by_tx("garbage")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@pytest.mark.asyncio
|
|
143
|
+
async def test_anchor_status_200(client):
|
|
144
|
+
with aioresponses() as m:
|
|
145
|
+
m.get(f"{BASE}/v1/anchor/status", status=200, payload={
|
|
146
|
+
"mode": "cumulative",
|
|
147
|
+
"latest": {"anchor_seq": 64, "size": 191, "root": "ff" * 32,
|
|
148
|
+
"near_tx": "tx", "ts": 1},
|
|
149
|
+
"log_size": 191,
|
|
150
|
+
})
|
|
151
|
+
out = await client.anchor_status()
|
|
152
|
+
assert out["mode"] == "cumulative"
|
|
153
|
+
assert out["latest"]["size"] == 191
|
|
154
|
+
assert out["log_size"] == 191
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@pytest.mark.asyncio
|
|
158
|
+
async def test_anchor_proof_200(client):
|
|
159
|
+
with aioresponses() as m:
|
|
160
|
+
m.get(f"{BASE}/v1/anchor/proof/190", status=200, payload={
|
|
161
|
+
"mode": "cumulative", "seq": 190, "anchor_seq": 64, "size": 191,
|
|
162
|
+
"index": 189, "path": ["ab" * 32], "root": "ff" * 32,
|
|
163
|
+
})
|
|
164
|
+
out = await client.anchor_proof(190)
|
|
165
|
+
assert out["seq"] == 190
|
|
166
|
+
assert out["index"] == 189
|
|
167
|
+
assert isinstance(out["path"], list)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@pytest.mark.asyncio
|
|
171
|
+
async def test_anchor_proof_not_yet_anchored_is_pending(client):
|
|
172
|
+
# Real server semantics: 409 (state conflict) with error=NOT_YET_ANCHORED.
|
|
173
|
+
with aioresponses() as m:
|
|
174
|
+
m.get(f"{BASE}/v1/anchor/proof/999", status=409, payload={
|
|
175
|
+
"error": "NOT_YET_ANCHORED", "eta_seconds": 3600,
|
|
176
|
+
"recheck_url": "/v1/anchor/proof/999"})
|
|
177
|
+
with pytest.raises(NotaryPendingError) as exc:
|
|
178
|
+
await client.anchor_proof(999)
|
|
179
|
+
assert exc.value.eta_seconds == 3600
|
|
180
|
+
assert exc.value.recheck_url == "/v1/anchor/proof/999"
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@pytest.mark.asyncio
|
|
184
|
+
async def test_anchor_proof_pending_404_compat(client):
|
|
185
|
+
# Older deployments answered 404 with the same code — keep the mapping.
|
|
186
|
+
with aioresponses() as m:
|
|
187
|
+
m.get(f"{BASE}/v1/anchor/proof/998", status=404, payload={
|
|
188
|
+
"error": "NOT_YET_ANCHORED", "eta_seconds": 60,
|
|
189
|
+
"recheck_url": "/v1/anchor/proof/998"})
|
|
190
|
+
with pytest.raises(NotaryPendingError):
|
|
191
|
+
await client.anchor_proof(998)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@pytest.mark.asyncio
|
|
195
|
+
async def test_anchor_proof_not_found_is_notary_not_found(client):
|
|
196
|
+
# A seq that was never issued must NOT look like "pending".
|
|
197
|
+
with aioresponses() as m:
|
|
198
|
+
m.get(f"{BASE}/v1/anchor/proof/7777", status=404, payload={"error": "NOT_FOUND"})
|
|
199
|
+
with pytest.raises(NotaryNotFoundError):
|
|
200
|
+
await client.anchor_proof(7777)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@pytest.mark.asyncio
|
|
204
|
+
async def test_anchor_logproof_path_and_200(client):
|
|
205
|
+
with aioresponses() as m:
|
|
206
|
+
m.get(f"{BASE}/v1/anchor/logproof/381", status=200, payload={
|
|
207
|
+
"log_index": 381, "kind": 1, "ref_id": 7, "anchor_seq": 64,
|
|
208
|
+
"size": 191, "path": [], "root": "ff" * 32})
|
|
209
|
+
out = await client.anchor_logproof(381)
|
|
210
|
+
assert out["log_index"] == 381
|
|
211
|
+
assert out["kind"] == 1
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@pytest.mark.asyncio
|
|
215
|
+
async def test_anchor_consistency_query(client):
|
|
216
|
+
with aioresponses() as m:
|
|
217
|
+
m.get(f"{BASE}/v1/anchor/consistency?from=62&to=64", status=200, payload={
|
|
218
|
+
"from": {"anchor_seq": 62, "size": 189, "root": "aa" * 32},
|
|
219
|
+
"to": {"anchor_seq": 64, "size": 191, "root": "ff" * 32},
|
|
220
|
+
"proof": ["cd" * 32], "how_to_verify": "RFC 9162 §2.1.4",
|
|
221
|
+
})
|
|
222
|
+
out = await client.anchor_consistency(62, 64)
|
|
223
|
+
assert out["from"]["size"] == 189
|
|
224
|
+
assert out["to"]["anchor_seq"] == 64
|
|
225
|
+
assert isinstance(out["proof"], list)
|
|
File without changes
|
|
File without changes
|
{aotrust_protocol-2.3.7 → aotrust_protocol-2.5.0}/aotrust_protocol.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|