loopmarket 0.1.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.
- loopmarket-0.1.0/LICENSE +28 -0
- loopmarket-0.1.0/PKG-INFO +200 -0
- loopmarket-0.1.0/README.md +169 -0
- loopmarket-0.1.0/pyproject.toml +52 -0
- loopmarket-0.1.0/setup.cfg +4 -0
- loopmarket-0.1.0/src/loopmarket/__init__.py +43 -0
- loopmarket-0.1.0/src/loopmarket/clearing.py +147 -0
- loopmarket-0.1.0/src/loopmarket/dimensions.py +147 -0
- loopmarket-0.1.0/src/loopmarket/federation.py +314 -0
- loopmarket-0.1.0/src/loopmarket/graph.py +198 -0
- loopmarket-0.1.0/src/loopmarket/matching.py +136 -0
- loopmarket-0.1.0/src/loopmarket/ontology.py +149 -0
- loopmarket-0.1.0/src/loopmarket/registry.py +286 -0
- loopmarket-0.1.0/src/loopmarket/schema.py +364 -0
- loopmarket-0.1.0/src/loopmarket/settlement.py +17 -0
- loopmarket-0.1.0/src/loopmarket/sigs.py +82 -0
- loopmarket-0.1.0/src/loopmarket/solver/__init__.py +3 -0
- loopmarket-0.1.0/src/loopmarket/solver/agent.py +104 -0
- loopmarket-0.1.0/src/loopmarket/spacetime.py +93 -0
- loopmarket-0.1.0/src/loopmarket.egg-info/PKG-INFO +200 -0
- loopmarket-0.1.0/src/loopmarket.egg-info/SOURCES.txt +36 -0
- loopmarket-0.1.0/src/loopmarket.egg-info/dependency_links.txt +1 -0
- loopmarket-0.1.0/src/loopmarket.egg-info/requires.txt +12 -0
- loopmarket-0.1.0/src/loopmarket.egg-info/top_level.txt +1 -0
- loopmarket-0.1.0/tests/test_boundaries.py +42 -0
- loopmarket-0.1.0/tests/test_dimensions.py +122 -0
- loopmarket-0.1.0/tests/test_docs.py +26 -0
- loopmarket-0.1.0/tests/test_end_to_end.py +127 -0
- loopmarket-0.1.0/tests/test_federation.py +313 -0
- loopmarket-0.1.0/tests/test_graph.py +81 -0
- loopmarket-0.1.0/tests/test_matching.py +98 -0
- loopmarket-0.1.0/tests/test_ontology.py +17 -0
- loopmarket-0.1.0/tests/test_registry.py +169 -0
- loopmarket-0.1.0/tests/test_schema.py +82 -0
- loopmarket-0.1.0/tests/test_sigs.py +56 -0
- loopmarket-0.1.0/tests/test_spacetime.py +26 -0
- loopmarket-0.1.0/tests/test_swarm_book.py +109 -0
- loopmarket-0.1.0/tests/test_swarm_federation.py +195 -0
loopmarket-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Peter Foldiak
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: loopmarket
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Universal combinatorial marketplace: uniform offers over an OntoDAG catalogue, a versioned offer book on recordstore/Swarm, and solver agents hunting profitable loops
|
|
5
|
+
License: BSD-3-Clause
|
|
6
|
+
Project-URL: Homepage, https://github.com/petfold/loopmarket
|
|
7
|
+
Project-URL: Repository, https://github.com/petfold/loopmarket
|
|
8
|
+
Project-URL: Issues, https://github.com/petfold/loopmarket/issues
|
|
9
|
+
Keywords: marketplace,combinatorial-exchange,ontology,dag,subsumption,swarm,web3,decentralized,solver,negative-cycle,barter,mutual-credit,coincidence-of-wants
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: ontodag>=0.13.0
|
|
22
|
+
Requires-Dist: recordstore>=0.16.0
|
|
23
|
+
Provides-Extra: swarm
|
|
24
|
+
Requires-Dist: recordstore[bee,feeds]>=0.16.0; extra == "swarm"
|
|
25
|
+
Provides-Extra: sig
|
|
26
|
+
Requires-Dist: eth-keys; extra == "sig"
|
|
27
|
+
Requires-Dist: eth-hash[pycryptodome]; extra == "sig"
|
|
28
|
+
Provides-Extra: test
|
|
29
|
+
Requires-Dist: pytest; extra == "test"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# loopmarket
|
|
33
|
+
|
|
34
|
+
[](LICENSE)
|
|
35
|
+
|
|
36
|
+
A universal combinatorial marketplace over the
|
|
37
|
+
[ontodag](https://github.com/petfold/ontodag) /
|
|
38
|
+
[recordstore](https://github.com/petfold/recordstore) /
|
|
39
|
+
[Ethereum Swarm](https://www.ethswarm.org/) stack.
|
|
40
|
+
|
|
41
|
+
Every economic intention is one uniform, content-addressed **offer** — a
|
|
42
|
+
thing described as a conjunction of OntoDAG categories, with a service time
|
|
43
|
+
window and region, priced on the maker's **personal scale** (a personal
|
|
44
|
+
numeraire; the record encoding calls it the maker's personal token). A
|
|
45
|
+
distributed, versioned **offer book** holds them (recordstore keyspace;
|
|
46
|
+
Swarm-backed via `BeeBytesStore` + a signed `SwarmFeedPointer`). Competing
|
|
47
|
+
**solver agents** hunt profitable **loops** — cycles whose exchange-rate
|
|
48
|
+
product exceeds one, i.e. negative cycles under −log weights — and a
|
|
49
|
+
**clearing** layer re-verifies every leg from scratch and commits the
|
|
50
|
+
whole loop atomically.
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from recordstore import MemoryBytesStore, RecordStore
|
|
54
|
+
from loopmarket import (Ontology, OfferRegistry, MockClearing,
|
|
55
|
+
SolverAgent, Thing, give, want, ...)
|
|
56
|
+
|
|
57
|
+
ontology = Ontology().load({"produce": [], "vegetable-box": ["produce"], ...})
|
|
58
|
+
registry = OfferRegistry(RecordStore(MemoryBytesStore()))
|
|
59
|
+
registry.publish_many([...]) # gives and wants, one uniform form
|
|
60
|
+
registry.commit()
|
|
61
|
+
|
|
62
|
+
agent = SolverAgent(registry, ontology, MockClearing(registry, ontology))
|
|
63
|
+
agent.step() # snapshot → match → hunt loops → clear
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Try it
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pip install -e ".[test]" # (--break-system-packages or a venv)
|
|
70
|
+
python3 -m pytest tests/ -v # 59 tests (two need a live Bee node)
|
|
71
|
+
PYTHONPATH=src python3 examples/demo_triangle.py # P0 in one file
|
|
72
|
+
PYTHONPATH=src python3 examples/demo_federation.py # P1: books, fold, forgery, follower
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The first demo publishes the smallest nontrivial book — a piano teacher, a
|
|
76
|
+
market gardener and a bicycle mechanic, no pair of whom can trade — and
|
|
77
|
+
watches the solver find, verify and clear the triangle at a 12% surplus.
|
|
78
|
+
The second runs the federation: per-maker books, two aggregators folding
|
|
79
|
+
to byte-identical manifests, a forged offer dying at the fold, a
|
|
80
|
+
tombstoned offer staying closed, clearing provably based on the fold,
|
|
81
|
+
and a follower reading it all back — in memory by default, live against a
|
|
82
|
+
Bee node when `BEE_API` and `BEE_BATCH` are set. New here? Start with the
|
|
83
|
+
**[User Guide](docs/USER-GUIDE.md)** (tutorial) and the
|
|
84
|
+
**[Reference Manual](docs/REFERENCE.md)** (API, record formats,
|
|
85
|
+
invariants).
|
|
86
|
+
|
|
87
|
+
Candidate generation can also run through ontodag's **parametric
|
|
88
|
+
dimensions**: `DimensionIndex` files gives under their exact service window
|
|
89
|
+
and centre cell, and `candidate_matches_indexed` prunes by concept cones
|
|
90
|
+
and window overlap — provably the same matches as the exhaustive baseline
|
|
91
|
+
(the recall test enforces set-equality), with far fewer exact checks. The
|
|
92
|
+
index is a derived, per-solver copy; the shared catalogue and its pinned
|
|
93
|
+
roots never move because of it. Swap the in-memory store for
|
|
94
|
+
`recordstore.swarm_store("offers", signer=...)` (extra:
|
|
95
|
+
`pip install -e ".[swarm]"`, plus a Bee node and a postage batch) and the
|
|
96
|
+
same code runs with the book on Swarm.
|
|
97
|
+
|
|
98
|
+
## What is built, and what is designed
|
|
99
|
+
|
|
100
|
+
**Built (P0, plus the live-Swarm milestone):** the full pipeline above runs
|
|
101
|
+
in memory, and since 2026-08-01 also end-to-end on a real Gnosis-mainnet
|
|
102
|
+
Bee node — catalogue and book on Swarm, book head in a signed feed, fills
|
|
103
|
+
atomic (the gated `tests/test_swarm_book.py`). Since 2026-08-21 the
|
|
104
|
+
federation layer runs too — in memory *and* live (the gated
|
|
105
|
+
`tests/test_swarm_federation.py`, 96.5s on a Bee 2.8.1 node): per-maker
|
|
106
|
+
books under their own feeds and signers, an `Aggregator` folding them
|
|
107
|
+
under the U8 admission rules into a four-root manifest published on its
|
|
108
|
+
own feed, withdrawal tombstones, and a scorched-earth follower
|
|
109
|
+
reconstructing the cleared world from (address, topic) alone. Since
|
|
110
|
+
2026-09-04 the federation demo adopts ontodag's `core` pack as the
|
|
111
|
+
catalogue and includes a censoring aggregator: `audit_manifest` convicts
|
|
112
|
+
it from its own manifest with absence proofs, and a solver folding the
|
|
113
|
+
announced maker books itself recovers the honest fold (T14).
|
|
114
|
+
Alpha; interfaces will move.
|
|
115
|
+
|
|
116
|
+
**Designed (2026-08-07):** most of what loopmarket *is* now lives as a
|
|
117
|
+
decided, research-grounded plan corpus under `docs/plans/` — one document
|
|
118
|
+
per work package, each with measurable gates, named open problems, and a
|
|
119
|
+
closing "what this document does not promise" section. Anything implying
|
|
120
|
+
unbuilt code carries a dated marker ("decided 2026-08, lands with the v2
|
|
121
|
+
bump / P1 / P2"). Planned invariants **U8–U14** are specified in the
|
|
122
|
+
documents that motivate them and summarized across `ARCHITECTURE.md`'s
|
|
123
|
+
update notes and §11; they enter `CLAUDE.md` as binding invariants only
|
|
124
|
+
when their enforcing code and tests land. factbond's mirror corpus is
|
|
125
|
+
`factbond/docs/plans/`.
|
|
126
|
+
|
|
127
|
+
## Vocabulary
|
|
128
|
+
|
|
129
|
+
Three words carry the design and are used precisely since 2026-09-07:
|
|
130
|
+
|
|
131
|
+
- **loop** — any cleared circulation: the essay's word and the name in code (`Loop`, `LoopProposal`, `loop/`, `loop_id`).
|
|
132
|
+
- **cycle** (simple loop) — the strict circle; all the P0 solver finds, and the only case where "the product of rates around it" means anything.
|
|
133
|
+
- **circulation** — the technical name for the cleared object: a flow conserved at every maker on that maker's own scale, with composed legs where one want takes several gives (`docs/plans/P2-loop-selection.md` §10–11).
|
|
134
|
+
- **hyper-leg** — a composed leg: several givers as tails, one wanting maker as head, one flow variable, so it fills every tail or none. The maker graph with hyper-legs is a hypergraph; its flow is an LP for divisible legs and an ILP where a hyper-leg is all-or-nothing.
|
|
135
|
+
|
|
136
|
+
And two more: **clearing** is the atomic commit that fixes obligations; **settlement** is the makers delivering.
|
|
137
|
+
|
|
138
|
+
## The plan corpus
|
|
139
|
+
|
|
140
|
+
| Document | One line |
|
|
141
|
+
|---|---|
|
|
142
|
+
| [`P1-federated-book.md`](docs/plans/P1-federated-book.md) | Per-maker books under own feeds/signers; announcement, aggregation, merge discipline, lifecycle, postage economics, spam floors. |
|
|
143
|
+
| [`P2-batch-auction.md`](docs/plans/P2-batch-auction.md) | The beat: sealed proposals, numeraire-free scoring, the fairness floor, capped solver rewards, collusion resistance, fees. |
|
|
144
|
+
| [`P2-clearing-pricing.md`](docs/plans/P2-clearing-pricing.md) | Turning a winning loop's surplus into per-leg prices: equal log-surplus split under uniform directional clearing; clearing prices as node potentials (§10). |
|
|
145
|
+
| [`P2-loop-selection.md`](docs/plans/P2-loop-selection.md) | Clearing as optimization: flow LP vs packing ILP, chains, failure-aware objective, pre-commit compression; composition on the want side (§10); the cleared object is a circulation, clearing prices its node potentials (§11). |
|
|
146
|
+
| [`proof-fabric.md`](docs/plans/proof-fabric.md) | Cross-phase proofs and certificates: trie proofs vs POT, the pin table, certificate envelopes, absence proofs. |
|
|
147
|
+
| [`P3-guarantee-coupling.md`](docs/plans/P3-guarantee-coupling.md) | loopmarket's half of the factbond coupling: witness edges, reliance-capped insurance, oracle consumption, risk-priced routing. |
|
|
148
|
+
| [`P4-privacy.md`](docs/plans/P4-privacy.md) | Staged privacy: Tier 1 with zero new cryptography, the P2 format-freeze list, explicit dead/deferred rulings. |
|
|
149
|
+
| [`ontodag-coupling.md`](docs/plans/ontodag-coupling.md) | The catalogue contract: dimension terms, unit families, match degrees, the upstream-vs-local tripwire table. |
|
|
150
|
+
| [`catalogue-bootstrap.md`](docs/plans/catalogue-bootstrap.md) | Seeding and governing the shared catalogue: seed taxonomies, the import pipeline, norms as protocol rules. |
|
|
151
|
+
| [`adoption-and-thickness.md`](docs/plans/adoption-and-thickness.md) | Where the first loops come from: launch verticals, the broker surface, bridge liquidity, thickness engineering. |
|
|
152
|
+
| [`THREATS.md`](docs/plans/THREATS.md) | The threat register, T1–T9, ordered by expected damage to a young system; mirrored in factbond. |
|
|
153
|
+
| [swarm-da](https://github.com/petfold/swarm-da) (separate repo) | Memo, 2026-09-09: what building this book on Swarm taught about Swarm as a data-availability layer — the archival-DA opportunity, Bee's push-sync receipts as the half-built publication primitive, and what loopmarket sheds the day they are exposed and anchored. |
|
|
154
|
+
|
|
155
|
+
**Phase ↔ document map.** P1 (federation): `P1-federated-book.md`,
|
|
156
|
+
supported by `ontodag-coupling.md` and `catalogue-bootstrap.md`. P2
|
|
157
|
+
(verifiable clearing): the three P2 docs plus `proof-fabric.md`,
|
|
158
|
+
*constrained* by `P4-privacy.md`'s format-freeze list and gated by
|
|
159
|
+
`THREATS.md` tripwires. P3 (guarantee fabric): `P3-guarantee-coupling.md`
|
|
160
|
+
plus factbond's entire corpus — gated by factbond's Phase-0 simulation
|
|
161
|
+
going green *and* the P2 format freeze. P4 (privacy): `P4-privacy.md`,
|
|
162
|
+
whose Tier 1 may ship alongside P2. Cross-phase: `proof-fabric.md`,
|
|
163
|
+
`THREATS.md`, `adoption-and-thickness.md`, `catalogue-bootstrap.md`,
|
|
164
|
+
`ontodag-coupling.md`.
|
|
165
|
+
|
|
166
|
+
**Reading order.** First pass: `ARCHITECTURE.md` → `THREATS.md` →
|
|
167
|
+
`P1-federated-book.md`. Clearing track: `P2-loop-selection.md` →
|
|
168
|
+
`P2-clearing-pricing.md` → `P2-batch-auction.md` → `proof-fabric.md`.
|
|
169
|
+
Guarantee track: factbond `DESIGN.md` → `mechanism-design.md` →
|
|
170
|
+
`insurance-products.md` → `phase0-simulation.md` →
|
|
171
|
+
`P3-guarantee-coupling.md`. Market track: `adoption-and-thickness.md` →
|
|
172
|
+
`catalogue-bootstrap.md` → `ontodag-coupling.md`.
|
|
173
|
+
|
|
174
|
+
Order of documents is not order of construction — gates decide that; and a
|
|
175
|
+
document's existence proves nothing about feasibility. The Phase-0
|
|
176
|
+
simulation and the named empirical gates can kill designs recorded here;
|
|
177
|
+
the corpus is built so that they can.
|
|
178
|
+
|
|
179
|
+
## Documentation
|
|
180
|
+
|
|
181
|
+
- **[docs/USER-GUIDE.md](docs/USER-GUIDE.md)** — the tutorial: from your
|
|
182
|
+
first offer to a federated book on a live Swarm network, every snippet
|
|
183
|
+
runnable.
|
|
184
|
+
- **[docs/REFERENCE.md](docs/REFERENCE.md)** — the reference manual:
|
|
185
|
+
every public class and function, the keyspace, record formats,
|
|
186
|
+
invariants, environment.
|
|
187
|
+
- **[ARCHITECTURE.md](ARCHITECTURE.md)** — the design and its rationale:
|
|
188
|
+
the uniform offer form, time/place as fits-within dimensions, the book's
|
|
189
|
+
keyspace and multi-writer story, the loop arithmetic (and why prices are
|
|
190
|
+
never negative), the trust model, the proof fabric, economic security,
|
|
191
|
+
and what the architecture does not promise.
|
|
192
|
+
- **[ROADMAP.md](ROADMAP.md)** — the phase index with status:
|
|
193
|
+
P0 (built) → P1 (federated book, live since 2026-08-21) → P2 (verifiable
|
|
194
|
+
clearing, batch auctions) → P3 (guarantee fabric via factbond) →
|
|
195
|
+
P4 (privacy), each linking to its plan document.
|
|
196
|
+
- **[CLAUDE.md](CLAUDE.md)** — working rules for development: dependency
|
|
197
|
+
boundaries, core invariants U1–U7, known simplifications.
|
|
198
|
+
- **[docs/loop-economy.md](docs/loop-economy.md)** — the vision essay: the
|
|
199
|
+
loop economy, its gallery of loops, the solver ecology, judges without
|
|
200
|
+
swords, and the path in.
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# loopmarket
|
|
2
|
+
|
|
3
|
+
[](LICENSE)
|
|
4
|
+
|
|
5
|
+
A universal combinatorial marketplace over the
|
|
6
|
+
[ontodag](https://github.com/petfold/ontodag) /
|
|
7
|
+
[recordstore](https://github.com/petfold/recordstore) /
|
|
8
|
+
[Ethereum Swarm](https://www.ethswarm.org/) stack.
|
|
9
|
+
|
|
10
|
+
Every economic intention is one uniform, content-addressed **offer** — a
|
|
11
|
+
thing described as a conjunction of OntoDAG categories, with a service time
|
|
12
|
+
window and region, priced on the maker's **personal scale** (a personal
|
|
13
|
+
numeraire; the record encoding calls it the maker's personal token). A
|
|
14
|
+
distributed, versioned **offer book** holds them (recordstore keyspace;
|
|
15
|
+
Swarm-backed via `BeeBytesStore` + a signed `SwarmFeedPointer`). Competing
|
|
16
|
+
**solver agents** hunt profitable **loops** — cycles whose exchange-rate
|
|
17
|
+
product exceeds one, i.e. negative cycles under −log weights — and a
|
|
18
|
+
**clearing** layer re-verifies every leg from scratch and commits the
|
|
19
|
+
whole loop atomically.
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from recordstore import MemoryBytesStore, RecordStore
|
|
23
|
+
from loopmarket import (Ontology, OfferRegistry, MockClearing,
|
|
24
|
+
SolverAgent, Thing, give, want, ...)
|
|
25
|
+
|
|
26
|
+
ontology = Ontology().load({"produce": [], "vegetable-box": ["produce"], ...})
|
|
27
|
+
registry = OfferRegistry(RecordStore(MemoryBytesStore()))
|
|
28
|
+
registry.publish_many([...]) # gives and wants, one uniform form
|
|
29
|
+
registry.commit()
|
|
30
|
+
|
|
31
|
+
agent = SolverAgent(registry, ontology, MockClearing(registry, ontology))
|
|
32
|
+
agent.step() # snapshot → match → hunt loops → clear
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Try it
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install -e ".[test]" # (--break-system-packages or a venv)
|
|
39
|
+
python3 -m pytest tests/ -v # 59 tests (two need a live Bee node)
|
|
40
|
+
PYTHONPATH=src python3 examples/demo_triangle.py # P0 in one file
|
|
41
|
+
PYTHONPATH=src python3 examples/demo_federation.py # P1: books, fold, forgery, follower
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The first demo publishes the smallest nontrivial book — a piano teacher, a
|
|
45
|
+
market gardener and a bicycle mechanic, no pair of whom can trade — and
|
|
46
|
+
watches the solver find, verify and clear the triangle at a 12% surplus.
|
|
47
|
+
The second runs the federation: per-maker books, two aggregators folding
|
|
48
|
+
to byte-identical manifests, a forged offer dying at the fold, a
|
|
49
|
+
tombstoned offer staying closed, clearing provably based on the fold,
|
|
50
|
+
and a follower reading it all back — in memory by default, live against a
|
|
51
|
+
Bee node when `BEE_API` and `BEE_BATCH` are set. New here? Start with the
|
|
52
|
+
**[User Guide](docs/USER-GUIDE.md)** (tutorial) and the
|
|
53
|
+
**[Reference Manual](docs/REFERENCE.md)** (API, record formats,
|
|
54
|
+
invariants).
|
|
55
|
+
|
|
56
|
+
Candidate generation can also run through ontodag's **parametric
|
|
57
|
+
dimensions**: `DimensionIndex` files gives under their exact service window
|
|
58
|
+
and centre cell, and `candidate_matches_indexed` prunes by concept cones
|
|
59
|
+
and window overlap — provably the same matches as the exhaustive baseline
|
|
60
|
+
(the recall test enforces set-equality), with far fewer exact checks. The
|
|
61
|
+
index is a derived, per-solver copy; the shared catalogue and its pinned
|
|
62
|
+
roots never move because of it. Swap the in-memory store for
|
|
63
|
+
`recordstore.swarm_store("offers", signer=...)` (extra:
|
|
64
|
+
`pip install -e ".[swarm]"`, plus a Bee node and a postage batch) and the
|
|
65
|
+
same code runs with the book on Swarm.
|
|
66
|
+
|
|
67
|
+
## What is built, and what is designed
|
|
68
|
+
|
|
69
|
+
**Built (P0, plus the live-Swarm milestone):** the full pipeline above runs
|
|
70
|
+
in memory, and since 2026-08-01 also end-to-end on a real Gnosis-mainnet
|
|
71
|
+
Bee node — catalogue and book on Swarm, book head in a signed feed, fills
|
|
72
|
+
atomic (the gated `tests/test_swarm_book.py`). Since 2026-08-21 the
|
|
73
|
+
federation layer runs too — in memory *and* live (the gated
|
|
74
|
+
`tests/test_swarm_federation.py`, 96.5s on a Bee 2.8.1 node): per-maker
|
|
75
|
+
books under their own feeds and signers, an `Aggregator` folding them
|
|
76
|
+
under the U8 admission rules into a four-root manifest published on its
|
|
77
|
+
own feed, withdrawal tombstones, and a scorched-earth follower
|
|
78
|
+
reconstructing the cleared world from (address, topic) alone. Since
|
|
79
|
+
2026-09-04 the federation demo adopts ontodag's `core` pack as the
|
|
80
|
+
catalogue and includes a censoring aggregator: `audit_manifest` convicts
|
|
81
|
+
it from its own manifest with absence proofs, and a solver folding the
|
|
82
|
+
announced maker books itself recovers the honest fold (T14).
|
|
83
|
+
Alpha; interfaces will move.
|
|
84
|
+
|
|
85
|
+
**Designed (2026-08-07):** most of what loopmarket *is* now lives as a
|
|
86
|
+
decided, research-grounded plan corpus under `docs/plans/` — one document
|
|
87
|
+
per work package, each with measurable gates, named open problems, and a
|
|
88
|
+
closing "what this document does not promise" section. Anything implying
|
|
89
|
+
unbuilt code carries a dated marker ("decided 2026-08, lands with the v2
|
|
90
|
+
bump / P1 / P2"). Planned invariants **U8–U14** are specified in the
|
|
91
|
+
documents that motivate them and summarized across `ARCHITECTURE.md`'s
|
|
92
|
+
update notes and §11; they enter `CLAUDE.md` as binding invariants only
|
|
93
|
+
when their enforcing code and tests land. factbond's mirror corpus is
|
|
94
|
+
`factbond/docs/plans/`.
|
|
95
|
+
|
|
96
|
+
## Vocabulary
|
|
97
|
+
|
|
98
|
+
Three words carry the design and are used precisely since 2026-09-07:
|
|
99
|
+
|
|
100
|
+
- **loop** — any cleared circulation: the essay's word and the name in code (`Loop`, `LoopProposal`, `loop/`, `loop_id`).
|
|
101
|
+
- **cycle** (simple loop) — the strict circle; all the P0 solver finds, and the only case where "the product of rates around it" means anything.
|
|
102
|
+
- **circulation** — the technical name for the cleared object: a flow conserved at every maker on that maker's own scale, with composed legs where one want takes several gives (`docs/plans/P2-loop-selection.md` §10–11).
|
|
103
|
+
- **hyper-leg** — a composed leg: several givers as tails, one wanting maker as head, one flow variable, so it fills every tail or none. The maker graph with hyper-legs is a hypergraph; its flow is an LP for divisible legs and an ILP where a hyper-leg is all-or-nothing.
|
|
104
|
+
|
|
105
|
+
And two more: **clearing** is the atomic commit that fixes obligations; **settlement** is the makers delivering.
|
|
106
|
+
|
|
107
|
+
## The plan corpus
|
|
108
|
+
|
|
109
|
+
| Document | One line |
|
|
110
|
+
|---|---|
|
|
111
|
+
| [`P1-federated-book.md`](docs/plans/P1-federated-book.md) | Per-maker books under own feeds/signers; announcement, aggregation, merge discipline, lifecycle, postage economics, spam floors. |
|
|
112
|
+
| [`P2-batch-auction.md`](docs/plans/P2-batch-auction.md) | The beat: sealed proposals, numeraire-free scoring, the fairness floor, capped solver rewards, collusion resistance, fees. |
|
|
113
|
+
| [`P2-clearing-pricing.md`](docs/plans/P2-clearing-pricing.md) | Turning a winning loop's surplus into per-leg prices: equal log-surplus split under uniform directional clearing; clearing prices as node potentials (§10). |
|
|
114
|
+
| [`P2-loop-selection.md`](docs/plans/P2-loop-selection.md) | Clearing as optimization: flow LP vs packing ILP, chains, failure-aware objective, pre-commit compression; composition on the want side (§10); the cleared object is a circulation, clearing prices its node potentials (§11). |
|
|
115
|
+
| [`proof-fabric.md`](docs/plans/proof-fabric.md) | Cross-phase proofs and certificates: trie proofs vs POT, the pin table, certificate envelopes, absence proofs. |
|
|
116
|
+
| [`P3-guarantee-coupling.md`](docs/plans/P3-guarantee-coupling.md) | loopmarket's half of the factbond coupling: witness edges, reliance-capped insurance, oracle consumption, risk-priced routing. |
|
|
117
|
+
| [`P4-privacy.md`](docs/plans/P4-privacy.md) | Staged privacy: Tier 1 with zero new cryptography, the P2 format-freeze list, explicit dead/deferred rulings. |
|
|
118
|
+
| [`ontodag-coupling.md`](docs/plans/ontodag-coupling.md) | The catalogue contract: dimension terms, unit families, match degrees, the upstream-vs-local tripwire table. |
|
|
119
|
+
| [`catalogue-bootstrap.md`](docs/plans/catalogue-bootstrap.md) | Seeding and governing the shared catalogue: seed taxonomies, the import pipeline, norms as protocol rules. |
|
|
120
|
+
| [`adoption-and-thickness.md`](docs/plans/adoption-and-thickness.md) | Where the first loops come from: launch verticals, the broker surface, bridge liquidity, thickness engineering. |
|
|
121
|
+
| [`THREATS.md`](docs/plans/THREATS.md) | The threat register, T1–T9, ordered by expected damage to a young system; mirrored in factbond. |
|
|
122
|
+
| [swarm-da](https://github.com/petfold/swarm-da) (separate repo) | Memo, 2026-09-09: what building this book on Swarm taught about Swarm as a data-availability layer — the archival-DA opportunity, Bee's push-sync receipts as the half-built publication primitive, and what loopmarket sheds the day they are exposed and anchored. |
|
|
123
|
+
|
|
124
|
+
**Phase ↔ document map.** P1 (federation): `P1-federated-book.md`,
|
|
125
|
+
supported by `ontodag-coupling.md` and `catalogue-bootstrap.md`. P2
|
|
126
|
+
(verifiable clearing): the three P2 docs plus `proof-fabric.md`,
|
|
127
|
+
*constrained* by `P4-privacy.md`'s format-freeze list and gated by
|
|
128
|
+
`THREATS.md` tripwires. P3 (guarantee fabric): `P3-guarantee-coupling.md`
|
|
129
|
+
plus factbond's entire corpus — gated by factbond's Phase-0 simulation
|
|
130
|
+
going green *and* the P2 format freeze. P4 (privacy): `P4-privacy.md`,
|
|
131
|
+
whose Tier 1 may ship alongside P2. Cross-phase: `proof-fabric.md`,
|
|
132
|
+
`THREATS.md`, `adoption-and-thickness.md`, `catalogue-bootstrap.md`,
|
|
133
|
+
`ontodag-coupling.md`.
|
|
134
|
+
|
|
135
|
+
**Reading order.** First pass: `ARCHITECTURE.md` → `THREATS.md` →
|
|
136
|
+
`P1-federated-book.md`. Clearing track: `P2-loop-selection.md` →
|
|
137
|
+
`P2-clearing-pricing.md` → `P2-batch-auction.md` → `proof-fabric.md`.
|
|
138
|
+
Guarantee track: factbond `DESIGN.md` → `mechanism-design.md` →
|
|
139
|
+
`insurance-products.md` → `phase0-simulation.md` →
|
|
140
|
+
`P3-guarantee-coupling.md`. Market track: `adoption-and-thickness.md` →
|
|
141
|
+
`catalogue-bootstrap.md` → `ontodag-coupling.md`.
|
|
142
|
+
|
|
143
|
+
Order of documents is not order of construction — gates decide that; and a
|
|
144
|
+
document's existence proves nothing about feasibility. The Phase-0
|
|
145
|
+
simulation and the named empirical gates can kill designs recorded here;
|
|
146
|
+
the corpus is built so that they can.
|
|
147
|
+
|
|
148
|
+
## Documentation
|
|
149
|
+
|
|
150
|
+
- **[docs/USER-GUIDE.md](docs/USER-GUIDE.md)** — the tutorial: from your
|
|
151
|
+
first offer to a federated book on a live Swarm network, every snippet
|
|
152
|
+
runnable.
|
|
153
|
+
- **[docs/REFERENCE.md](docs/REFERENCE.md)** — the reference manual:
|
|
154
|
+
every public class and function, the keyspace, record formats,
|
|
155
|
+
invariants, environment.
|
|
156
|
+
- **[ARCHITECTURE.md](ARCHITECTURE.md)** — the design and its rationale:
|
|
157
|
+
the uniform offer form, time/place as fits-within dimensions, the book's
|
|
158
|
+
keyspace and multi-writer story, the loop arithmetic (and why prices are
|
|
159
|
+
never negative), the trust model, the proof fabric, economic security,
|
|
160
|
+
and what the architecture does not promise.
|
|
161
|
+
- **[ROADMAP.md](ROADMAP.md)** — the phase index with status:
|
|
162
|
+
P0 (built) → P1 (federated book, live since 2026-08-21) → P2 (verifiable
|
|
163
|
+
clearing, batch auctions) → P3 (guarantee fabric via factbond) →
|
|
164
|
+
P4 (privacy), each linking to its plan document.
|
|
165
|
+
- **[CLAUDE.md](CLAUDE.md)** — working rules for development: dependency
|
|
166
|
+
boundaries, core invariants U1–U7, known simplifications.
|
|
167
|
+
- **[docs/loop-economy.md](docs/loop-economy.md)** — the vision essay: the
|
|
168
|
+
loop economy, its gallery of loops, the solver ecology, judges without
|
|
169
|
+
swords, and the path in.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "loopmarket"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Universal combinatorial marketplace: uniform offers over an OntoDAG catalogue, a versioned offer book on recordstore/Swarm, and solver agents hunting profitable loops"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "BSD-3-Clause" }
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
keywords = [
|
|
13
|
+
"marketplace", "combinatorial-exchange", "ontology", "dag", "subsumption",
|
|
14
|
+
"swarm", "web3", "decentralized", "solver", "negative-cycle", "barter",
|
|
15
|
+
"mutual-credit", "coincidence-of-wants",
|
|
16
|
+
]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 3 - Alpha",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Intended Audience :: Science/Research",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Topic :: Office/Business :: Financial",
|
|
25
|
+
"Topic :: Software Development :: Libraries",
|
|
26
|
+
]
|
|
27
|
+
dependencies = [
|
|
28
|
+
"ontodag>=0.13.0", # catalogue: subsumption, dimensions, is_below
|
|
29
|
+
"recordstore>=0.16.0", # the book: canonical versioned KV, merge, snapshots,
|
|
30
|
+
# diff/merge_delta (O(divergence) folds), prove/verify_proof
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
# The Swarm deployment path: BeeBytesStore blobs + signed SwarmFeedPointer
|
|
35
|
+
# head, both reached through recordstore so versions stay in step with it
|
|
36
|
+
# (mirrors ontodag's own [swarm] extra).
|
|
37
|
+
swarm = ["recordstore[bee,feeds]>=0.16.0"]
|
|
38
|
+
# Detached offer signatures (U8's off-feed layer): the same secp256k1
|
|
39
|
+
# identity that owns the maker's feed, recoverable without a node.
|
|
40
|
+
sig = ["eth-keys", "eth-hash[pycryptodome]"]
|
|
41
|
+
test = ["pytest"]
|
|
42
|
+
|
|
43
|
+
[tool.setuptools.packages.find]
|
|
44
|
+
where = ["src"]
|
|
45
|
+
|
|
46
|
+
[project.urls]
|
|
47
|
+
Homepage = "https://github.com/petfold/loopmarket"
|
|
48
|
+
Repository = "https://github.com/petfold/loopmarket"
|
|
49
|
+
Issues = "https://github.com/petfold/loopmarket/issues"
|
|
50
|
+
|
|
51
|
+
[tool.pytest.ini_options]
|
|
52
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""loopmarket — a universal combinatorial marketplace over OntoDAG + recordstore + Swarm.
|
|
2
|
+
|
|
3
|
+
One uniform offer form; a shared OntoDAG catalogue in which meanings, minutes
|
|
4
|
+
and map regions are ordered by the same fits-within relation; a distributed,
|
|
5
|
+
versioned offer book over recordstore (Swarm-backed via BeeBytesStore +
|
|
6
|
+
SwarmFeedPointer); competing solver agents hunting profitable loops as
|
|
7
|
+
negative cycles; clearing that re-verifies everything and trusts no one.
|
|
8
|
+
|
|
9
|
+
Dependency direction (boundary B2, enforced by tests/test_boundaries.py):
|
|
10
|
+
|
|
11
|
+
loopmarket -> ontodag -> recordstore -> (Swarm, optional)
|
|
12
|
+
|
|
13
|
+
The core imports work with no network and no Bee node (boundary B1); Swarm
|
|
14
|
+
is a persistence backend chosen at the edges (registry.swarm_offer_book,
|
|
15
|
+
Ontology.persistent over a swarm_store), never a requirement of the model.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from .schema import (
|
|
19
|
+
ASK, BID, GIVE, WANT, GeoDisc, Offer, Thing, TimeWindow, Tokens,
|
|
20
|
+
ask, bid, give, want,
|
|
21
|
+
)
|
|
22
|
+
from .federation import Aggregator, Manifest, Omission, audit_manifest
|
|
23
|
+
from .ontology import Ontology
|
|
24
|
+
from .registry import OfferRegistry, PartialLoopError, swarm_offer_book
|
|
25
|
+
from .matching import Match, candidate_matches, check_match
|
|
26
|
+
from .sigs import maker_address, recover_maker, sign_offer, verify_offer_sig
|
|
27
|
+
from .dimensions import DimensionIndex, candidate_matches_indexed
|
|
28
|
+
from .graph import ExchangeGraph, Loop
|
|
29
|
+
from .clearing import LoopProposal, MockClearing, Receipt, Clearing
|
|
30
|
+
from .solver.agent import SolverAgent
|
|
31
|
+
|
|
32
|
+
__version__ = "0.1.0"
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"GIVE", "WANT", "ASK", "BID", "GeoDisc", "Offer", "Thing", "TimeWindow",
|
|
36
|
+
"Tokens", "give", "want", "ask", "bid",
|
|
37
|
+
"Aggregator", "Manifest", "Omission", "audit_manifest", "Ontology", "OfferRegistry",
|
|
38
|
+
"PartialLoopError", "swarm_offer_book",
|
|
39
|
+
"Match", "candidate_matches", "check_match",
|
|
40
|
+
"maker_address", "recover_maker", "sign_offer", "verify_offer_sig",
|
|
41
|
+
"DimensionIndex", "candidate_matches_indexed", "ExchangeGraph", "Loop",
|
|
42
|
+
"LoopProposal", "MockClearing", "Receipt", "Clearing", "SolverAgent",
|
|
43
|
+
]
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Clearing: where a proposed loop becomes a bundle of commitments.
|
|
2
|
+
|
|
3
|
+
Trust model (the one non-negotiable): clearing *never trusts the solver*.
|
|
4
|
+
A `LoopProposal` names the book root and ontology root it was solved
|
|
5
|
+
against; the clearing layer re-derives every leg with `check_match`, the
|
|
6
|
+
chaining, the product, and the not-already-filled status — cheap, linear in
|
|
7
|
+
the loop — before atomically marking every offer filled. Discovery is
|
|
8
|
+
expensive and competitive; verification is cheap and neutral.
|
|
9
|
+
|
|
10
|
+
`MockClearing` is the in-process stand-in: its "atomic stroke" is one
|
|
11
|
+
recordstore commit (all fills + the loop record land under a single new
|
|
12
|
+
root, or none do). The on-chain path it stands in for (roadmap P2) keeps
|
|
13
|
+
the same interface: a contract receives the loop plus *inclusion proofs*
|
|
14
|
+
that each offer is present under the pinned book root — recordstore's
|
|
15
|
+
canonical-trie `prove`/`verify_proof` (>= 0.16.0) is the primary route;
|
|
16
|
+
POT ForkPathProof is the conditional fallback only if the on-chain
|
|
17
|
+
verifier demands BMT-native proofs (docs/plans/proof-fabric.md). Batch
|
|
18
|
+
auctions across competing sealed proposals are P2 as well
|
|
19
|
+
(docs/plans/P2-batch-auction.md); the mock is first-valid-wins.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import time as _time
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
from typing import Protocol
|
|
27
|
+
|
|
28
|
+
from .graph import Loop
|
|
29
|
+
from .matching import check_match
|
|
30
|
+
from .ontology import Ontology
|
|
31
|
+
from .registry import OfferRegistry
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True, slots=True)
|
|
35
|
+
class LoopProposal:
|
|
36
|
+
loop: Loop
|
|
37
|
+
book_root: str # the registry version the loop was solved against
|
|
38
|
+
ontology_root: str # the catalogue version subsumption was checked under
|
|
39
|
+
solver: str # who found it (fee/reputation address)
|
|
40
|
+
found_at: int
|
|
41
|
+
|
|
42
|
+
def to_record(self) -> dict:
|
|
43
|
+
return {
|
|
44
|
+
"loop_id": self.loop.loop_id,
|
|
45
|
+
"solver": self.solver,
|
|
46
|
+
"found_at": self.found_at,
|
|
47
|
+
"book_root": self.book_root,
|
|
48
|
+
"ontology_root": self.ontology_root,
|
|
49
|
+
"surplus": self.loop.surplus,
|
|
50
|
+
"nodes": list(self.loop.nodes),
|
|
51
|
+
"legs": [
|
|
52
|
+
{"give": m.give.offer_id, "want": m.want.offer_id, "rate": m.rate}
|
|
53
|
+
for m in self.loop.matches
|
|
54
|
+
],
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True, slots=True)
|
|
59
|
+
class Receipt:
|
|
60
|
+
accepted: bool
|
|
61
|
+
loop_id: str
|
|
62
|
+
reason: str = ""
|
|
63
|
+
book_root: str = "" # the new root, if accepted
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class Clearing(Protocol):
|
|
67
|
+
def submit(self, proposal: LoopProposal) -> Receipt: ...
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class MockClearing:
|
|
71
|
+
"""In-process clearing over the shared registry."""
|
|
72
|
+
|
|
73
|
+
#: Oracle types this clearing knows how to verify — the P3 refusal
|
|
74
|
+
#: gate (docs/plans/P3-guarantee-coupling.md, enforcement rule 1): a leg
|
|
75
|
+
#: naming a witness type outside this set never clears here, in U7's
|
|
76
|
+
#: shape — unknown fails closed rather than silently clearing with a
|
|
77
|
+
#: guarantee nobody can check. The mock declares exactly the P0
|
|
78
|
+
#: countersign semantics.
|
|
79
|
+
VERIFIABLE_ORACLES = frozenset({"countersign"})
|
|
80
|
+
|
|
81
|
+
def __init__(self, registry: OfferRegistry, ontology: Ontology, *,
|
|
82
|
+
min_surplus: float = 0.0, require_per_node: bool = True,
|
|
83
|
+
clock=_time.time, verifiable_oracles=VERIFIABLE_ORACLES):
|
|
84
|
+
self.registry = registry
|
|
85
|
+
self.ontology = ontology
|
|
86
|
+
self.min_surplus = min_surplus
|
|
87
|
+
self.require_per_node = require_per_node
|
|
88
|
+
self.clock = clock # injectable for tests / deterministic replay
|
|
89
|
+
self.verifiable_oracles = frozenset(verifiable_oracles)
|
|
90
|
+
|
|
91
|
+
def submit(self, proposal: LoopProposal) -> Receipt:
|
|
92
|
+
loop = proposal.loop
|
|
93
|
+
lid = loop.loop_id
|
|
94
|
+
now = int(self.clock())
|
|
95
|
+
|
|
96
|
+
def reject(reason: str) -> Receipt:
|
|
97
|
+
return Receipt(False, lid, reason)
|
|
98
|
+
|
|
99
|
+
# 0. pins — the rehearsal of U10's clearing half (full enforcement,
|
|
100
|
+
# with proofs, lands with P2): the proposal's catalogue pin must
|
|
101
|
+
# *equal* this clearing's own, refused before any leg work.
|
|
102
|
+
# Plain equality covers mismatch and absence in both directions:
|
|
103
|
+
# a pinned clearing refuses unpinned proposals, an unpinned
|
|
104
|
+
# (development) one refuses proposals claiming ground it cannot
|
|
105
|
+
# confirm; '' == '' keeps the in-memory flow working.
|
|
106
|
+
if proposal.ontology_root != self.ontology.root:
|
|
107
|
+
return reject("ontology pin mismatch")
|
|
108
|
+
|
|
109
|
+
# 1. every offer must exist in the *current* book, be unfilled, and
|
|
110
|
+
# name a witness type this clearing can actually verify
|
|
111
|
+
seen: set[str] = set()
|
|
112
|
+
for oid in loop.offer_ids:
|
|
113
|
+
if oid in seen:
|
|
114
|
+
return reject(f"offer used twice: {oid[:12]}")
|
|
115
|
+
seen.add(oid)
|
|
116
|
+
try:
|
|
117
|
+
offer = self.registry.get(oid)
|
|
118
|
+
except KeyError:
|
|
119
|
+
return reject(f"unknown offer: {oid[:12]}")
|
|
120
|
+
if self.registry.is_filled(oid):
|
|
121
|
+
return reject(f"already filled: {oid[:12]}")
|
|
122
|
+
if self.registry.is_withdrawn(oid):
|
|
123
|
+
return reject(f"withdrawn: {oid[:12]}")
|
|
124
|
+
if offer.oracle not in self.verifiable_oracles:
|
|
125
|
+
return reject(f"unverifiable oracle type: {offer.oracle}")
|
|
126
|
+
|
|
127
|
+
# 2. re-derive every leg — never trust the solver's matches
|
|
128
|
+
for m in loop.matches:
|
|
129
|
+
fresh_give = self.registry.get(m.give.offer_id)
|
|
130
|
+
fresh_want = self.registry.get(m.want.offer_id)
|
|
131
|
+
if check_match(fresh_give, fresh_want, self.ontology, now=now) is None:
|
|
132
|
+
return reject(
|
|
133
|
+
f"leg fails re-verification: {m.give.offer_id[:8]}"
|
|
134
|
+
f" -> {m.want.offer_id[:8]}"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# 3. the arithmetic
|
|
138
|
+
if loop.surplus < self.min_surplus - 1e-12:
|
|
139
|
+
return reject(f"surplus {loop.surplus:.4f} below minimum")
|
|
140
|
+
if self.require_per_node and not loop.all_divisible \
|
|
141
|
+
and not loop.per_node_ok:
|
|
142
|
+
return reject("indivisible legs without per-node surplus")
|
|
143
|
+
|
|
144
|
+
# 4. atomic commitment: all fills land under one new root, or none
|
|
145
|
+
self.registry.mark_filled(loop.offer_ids, lid, proposal.to_record())
|
|
146
|
+
root = self.registry.commit()
|
|
147
|
+
return Receipt(True, lid, book_root=root)
|