Skip to content

Commit 3d3bc5f

Browse files
feat(runtime): mock-L1 auction MVP round 3 on-chain settle
Archive can post MockDleAuctionSettlement.settle after list escrow; add mock-auction-e2e, settle helper, and RULES/docs honesty for demo vs real txHash. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent d8c67cb commit 3d3bc5f

10 files changed

Lines changed: 723 additions & 19 deletions

File tree

‎docs/mock-l1-auction-mvp.md‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,19 +28,28 @@ CoNET mainnet may already host `DLEChainRegistry1155V1`; this MVP still uses **l
2828
| Local deploy script | `scripts/dle/deployMockL1AuctionLocal.ts` (`npm run dle:deploy:mock-auction-local`) |
2929
| Registration wire | `runtime/src/shared/mockL1.ts` |
3030
| Custody (Archive) | `runtime/src/shared/mockL1Custody.ts` (`MOCK_L1_RPC_URL` + `MOCK_L1_SETTLEMENT`) |
31+
| On-chain settle | `runtime/src/shared/mockL1Settle.ts` (`MOCK_L1_AUTHORITY_PRIVATE_KEY` + `MOCK_L1_SETTLE_ONCHAIN` / `executeOnChain`) |
3132
| Orders / cert wire | `runtime/src/shared/tradeMatch.ts` |
3233
| HTTP engines | `runtime/src/archive/mockL1/engine.ts`, `…/trade/engine.ts` |
3334
| Mode A | `runtime/src/archive/bft/modeA.ts` → `replayTradeMatchModeA` |
34-
| CLI / demo | `runtime/src/daemon/mock-l1-auction-cli.ts`, `mock-l1-auction-demo.ts` |
35+
| CLI / demo / e2e | `runtime/src/daemon/mock-l1-auction-cli.ts`, `mock-l1-auction-demo.ts`, `mock-l1-auction-e2e.ts` |
36+
| One-shot local | Root `scripts/dle/mockAuctionE2eLocal.sh` → `npm run dle:mock-auction-e2e` |
3537
| Web | Explorer `/mock-auction` (session keys may sign; Archive-side custody when RPC set) |
3638

3739
## MVP round 2 (2026-08)
3840

3941
1. Archive `POST /trade/check` verifies NFT `ownerOf` + `getApproved`/`isApprovedForAll` and ERC-20 `balanceOf` + `allowance` against the mock settlement when RPC env is set (client custody flags ignored).
40-
2. `npm run mock-auction-demo` runs submit→scan→candidate→check→attest→settle in-process.
42+
2. `npm run mock-auction-demo` runs submit→scan→candidate→check→attest→settle in-process (**lab / optional custody**; settle may use a demo txHash when on-chain settle is off).
4143
3. Explorer can sign sell/buy with memory-only session keys (no disk persistence).
4244
4. Root `dle:deploy:mock-auction-local` prints `MOCK_L1_*` for Archive.
4345

46+
## MVP round 3 (2026-08)
47+
48+
1. Seller `list`s NFT into `MockDleAuctionSettlement` escrow before Archive settle.
49+
2. Archive `POST /trade/settle` with `executeOnChain` / env → `certificateAuthority` calls `settle` on local RPC; phases `match_certified` → `settlement_submitted` → `settled` | `settlement_failed`.
50+
3. `npm run mock-auction-e2e` (DLE) or root `npm run dle:mock-auction-e2e` (hardhat node → deploy → e2e) produces a **real** `settlementTxHash`.
51+
4. `/health` may expose `tradeOnChainSettleConfigured` / `tradeOnChainSettleMode`.
52+
4453
## Fee
4554

4655
```

‎explorer/RULES.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,10 +106,12 @@ Detail page (footer hidden). Local mock-L1 `/mockl1/*` + `/trade/*` against the
106106

107107
**MVP round 2:** session-only lab private keys may **sign** sell/buy/attest in the browser (`ethers`); keys are **never** written to localStorage / IndexedDB. Order / candidate hashing is mirrored in `explorer/src/lib/mockAuctionWire.ts` (no runtime import). Archive legality remains Archive-side when RPC custody is configured — the page may pass lab flags only for Archive without `MOCK_L1_*`.
108108

109+
**MVP round 3:** when Archive has on-chain settle configured, match timeline may show a **real** `settlementTxHash` from `MockDleAuctionSettlement.settle` (local Hardhat/Anvil). Do **not** paint lab demo fake hashes as mainnet settles. UI still does **not** call `settle` from the browser — Archive posts the authority tx.
110+
109111
| Rule | Detail |
110112
|---|---|
111113
| Scope | Local Hardhat/Anvil mock network only |
112-
| UI | May sign client orders/attests with session keys; does **not** self-claim Archive legality / quorum |
114+
| UI | May sign client orders/attests with session keys; does **not** self-claim Archive legality / quorum; does **not** submit settlement txs |
113115
| Labels | Always show `mockL1Only` / `notProductionDepin` pills |
114116
| Forbidden | Painting as CoNET mainnet, production DePIN, or live CL RANDAO; persisting private keys |
115117

‎package.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@
5454
"lab:keep-p11-joiner": "npm run runtime:build && npm --prefix pilot run lab:keep-p11-joiner",
5555
"lab:deploy-p11-joiner": "npm run runtime:build && npm --prefix pilot run lab:deploy-p11-joiner",
5656
"lab:accept-p11-join": "npm --prefix pilot run lab:accept-p11-join",
57-
"lab:p11-full-open-join": "npm run runtime:build && npm --prefix pilot run lab:p11-full-open-join"
57+
"lab:p11-full-open-join": "npm run runtime:build && npm --prefix pilot run lab:p11-full-open-join",
58+
"mock-auction-e2e": "tsx runtime/src/daemon/mock-l1-auction-e2e.ts"
5859
},
5960
"dependencies": {
6061
"ethers": "^6.15.0"

‎runtime/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,6 @@ P1 联网 BFT(独立包 `runtime/src/archive/bft`):5 个 active 对冻结
6161
- `POST /bft/message` / `GET /bft/status` — 实验室 prevote/precommit 交换(复用 27101)
6262
- `GET /ondemand/pool` / `GET /ondemand/selection` / `POST /ondemand/hook` / `POST /ondemand/freeze` / `POST /ondemand/message` — 实验室等待池与 SelectionLog(复用 27101)
6363
- `POST /newchain/request` / `GET /newchain/chains` — 实验室 Mode A 三类新链创世;新 \(Q_V\) 为 EIP-712 `ArchiveValidatorQuorumAttest`(**P18**;**不是** 生产 secp256k1 / L1 NFT / 30 天资格)。随机用户:`70.35.205.77:/home/peter/dle-newchain-user`(`npm run lab:deploy-newchain-user`)
64-
- `POST /mockl1/register` / `GET /mockl1/chains` + `POST /trade/*` — **Mock-L1 拍卖 MVP**(本地 Hardhat/Anvil ERC-1155 真实 `tokenId`;**禁止**把 lab `notL1Nft` 升格;**禁止**用 WaitingPool / `POST /ondemand/hook` 当 trade ingress)。CLI:`npm run mock-auction-cli`;一键演示:`npm run mock-auction-demo`。Archive 可选 RPC custody:`MOCK_L1_RPC_URL` + `MOCK_L1_SETTLEMENT`。设计 / wire:`docs/mock-l1-auction-mvp.md`、`docs/mock-l1-auction-wire.md`。Explorer:`/mock-auction`(会话密钥可签名,不落盘)。**不是** CoNET 主网接线,**不是** 生产 CL RANDAO / DePIN。
64+
- `POST /mockl1/register` / `GET /mockl1/chains` + `POST /trade/*` — **Mock-L1 拍卖 MVP**(本地 Hardhat/Anvil ERC-1155 真实 `tokenId`;**禁止**把 lab `notL1Nft` 升格;**禁止**用 WaitingPool / `POST /ondemand/hook` 当 trade ingress)。CLI:`npm run mock-auction-cli`;演示:`npm run mock-auction-demo`;真上链 settle:`npm run mock-auction-e2e`(需 `MOCK_L1_*`;根仓一键 `npm run dle:mock-auction-e2e`)。Archive 可选 RPC custody:`MOCK_L1_RPC_URL` + `MOCK_L1_SETTLEMENT`;on-chain settle:`MOCK_L1_AUTHORITY_PRIVATE_KEY` + `MOCK_L1_SETTLE_ONCHAIN`。设计 / wire:`docs/mock-l1-auction-mvp.md`、`docs/mock-l1-auction-wire.md`。Explorer:`/mock-auction`(会话密钥可签名,不落盘;不代签 settle)。**不是** CoNET 主网接线,**不是** 生产 CL RANDAO / DePIN。
6565

6666
本地 UI:`npm run explorer:dev`(见 `explorer/README.md`)。Home / Certificates 展示 waiting pool、`poolRoot`、7+2、`endorsed`;首屏 seed 七主机 P3 验收,仅可信 live 覆盖。`eth_chainId` 仍是 CoNET-DLE Testnet `0x44c45`,不是 L1 `224422`。`liveGroupIds` 含引导组 L1 登记交易 hash;实验室 M6 另含 G2 L1 `registerLiveGroup` tx(Home Clusters 下打开 Blockscout `/tx/`;实验室 keccak 仅为别名)。`dle_locateHash` 在 \(G_e \ge 2\) 走 `locatePlane`;全平面 JSON-RPC `null` 仅当每个活跃组都返回可信本组 `notFound`。一组超时是 `unavailable`,不是 `null`。实验室 M7:`tipStateRoot` / `membershipRoot` 为一等 `HashObjectKind`,命中返回 typed 对象而不是 AC;同 hash 后一 height 为 first-write-wins;`ZERO32` 不编目。这 **不是** 30 天资格。

‎runtime/RULES.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,12 +111,12 @@ Independent of NFT 42 Tendermint / `bft-state.json`. **Not** an L1 birth certifi
111111
| `POST /trade/scan` / `POST /trade/candidate` | Scanner proposes `MockL1MatchCandidateV1` only |
112112
| `POST /trade/check` | Archive legality: when `MOCK_L1_RPC_URL` + `MOCK_L1_SETTLEMENT` (or `verifyL1Custody` hook) are set, **eth_call / hook custody** — client `l1EscrowCustody` flags are **ignored**. Otherwise lab flags fallback. Then freeze-then-draw → propose `TradeMatchCertificateV1` |
113113
| `POST /trade/attest` | Committee signatures (lab keccak beacon; `notProductionBeacon`) |
114-
| `POST /trade/settle` | Mode A settlement states; mock L1 atomic settle is separate |
114+
| `POST /trade/settle` | Mode A settlement states. **MVP round 3:** `executeOnChain: true` / `MOCK_L1_SETTLE_ONCHAIN` / `submitL1SettlementTx` hook → authority calls `MockDleAuctionSettlement.settle` (seller must have `list`ed first). Success → `settlement_submitted` → `settled` with real `settlementTxHash`; failure → `settlement_failed` |
115115
| `GET /trade/orders` / `/trade/matches` / `/trade/timeline` | Read-only status |
116116

117117
Fee: **1 bps** of clearing price; **50% scanner / 50% committee**. Mode A events `0x1302–0x1306` (MatchProposed → … → Settled / SettlementFailed); keep `TradeOpened` (`0x1301`) replay compatible.
118118

119-
`/health` may expose `tradeRpcCustodyConfigured` / `tradeRpcCustodyMode` (`rpc` | `hook` | `clientFlags`). **MVP round 2 (2026-08):** Archive-side custody via `shared/mockL1Custody.ts`; in-process demo `npm run mock-auction-demo`.
119+
`/health` may expose `tradeRpcCustodyConfigured` / `tradeRpcCustodyMode` (`rpc` | `hook` | `clientFlags`) and `tradeOnChainSettleConfigured` / `tradeOnChainSettleMode`. **MVP round 2 (2026-08):** Archive-side custody via `shared/mockL1Custody.ts`; in-process demo `npm run mock-auction-demo`. **MVP round 3 (2026-08):** on-chain settle via `shared/mockL1Settle.ts`; E2E `npm run mock-auction-e2e` (needs `MOCK_L1_*`); root one-shot `npm run dle:mock-auction-e2e` (hardhat node → deploy → e2e).
120120

121121
Live keep evidence (2026-08-16): `pilot/evidence/conet-dle-p6-genesis-2026-08/p6-live-accept.json` — trade `chainNftId=326990096`, 7/7 `DleLabArchiveCertificateV1` in ~10s, NFT 42 `eth_blockNumber` stayed `0x1`, `liveGroupCount=2`. Completing P6 is **not** 30-day qualification.
122122

‎runtime/src/archive/trade/engine.ts‎

Lines changed: 119 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
import type { Hex } from '../../shared/bytes.js'
1616
import { mockL1FeePolicyHash } from '../../shared/mockL1.js'
1717
import { mockL1CustodyEnv, verifyMockL1Custody } from '../../shared/mockL1Custody.js'
18+
import { mockL1SettleEnv, settleMockL1Auction } from '../../shared/mockL1Settle.js'
1819
import {
1920
MATCH_CANDIDATE_SCHEMA,
2021
ORDER_SIDE_BUY,
@@ -107,6 +108,20 @@ export interface TradeEngineOptions {
107108
buy: MockL1TradeOrderV1
108109
clearingPrice: string
109110
}) => Promise<{ ok: true } | { ok: false; reason: string }>
111+
/**
112+
* When set (or via MOCK_L1_AUTHORITY_PRIVATE_KEY + MOCK_L1_SETTLE_ONCHAIN),
113+
* Archive may broadcast MockDleAuctionSettlement.settle as certificateAuthority.
114+
*/
115+
l1AuthorityPrivateKey?: string
116+
/** Test hook — overrides on-chain settle broadcast. */
117+
submitL1SettlementTx?: (args: {
118+
certificateHash: Hex
119+
sellerOrderHash: Hex
120+
buyer: Hex
121+
clearingAmount: string
122+
scanner: Hex
123+
committee: readonly Hex[]
124+
}) => Promise<{ ok: true; txHash: Hex } | { ok: false; reason: string }>
110125
}
111126

112127
export type TradePostResult = { status: number; body: unknown }
@@ -160,9 +175,14 @@ export function createTradeEngine(options: TradeEngineOptions): TradeEngine {
160175
('0x0000000000000000000000000000000000000001' as Hex)).toLowerCase() as Hex
161176
const nowSec = options.nowSec ?? (() => BigInt(Math.floor(Date.now() / 1000)))
162177
const envCustody = mockL1CustodyEnv()
163-
const l1RpcUrl = options.l1RpcUrl ?? envCustody.rpcUrl
164-
const l1Settlement = options.l1SettlementAddress ?? envCustody.settlement
178+
const envSettle = mockL1SettleEnv()
179+
const l1RpcUrl = options.l1RpcUrl ?? envCustody.rpcUrl ?? envSettle.rpcUrl
180+
const l1Settlement = options.l1SettlementAddress ?? envCustody.settlement ?? envSettle.settlement
181+
const l1AuthorityPk = options.l1AuthorityPrivateKey ?? envSettle.authorityPrivateKey
165182
const rpcCustodyConfigured = Boolean(l1RpcUrl && l1Settlement) || options.verifyL1Custody !== undefined
183+
const onChainSettleConfigured =
184+
options.submitL1SettlementTx !== undefined ||
185+
Boolean(l1RpcUrl && l1Settlement && l1AuthorityPk)
166186

167187
function persist(): void {
168188
const state: PersistedState = {
@@ -227,6 +247,12 @@ export function createTradeEngine(options: TradeEngineOptions): TradeEngine {
227247
: l1RpcUrl && l1Settlement
228248
? 'eth_call'
229249
: 'client_assert_fallback',
250+
tradeOnChainSettleConfigured: onChainSettleConfigured,
251+
tradeOnChainSettleMode: options.submitL1SettlementTx !== undefined
252+
? 'hook'
253+
: onChainSettleConfigured
254+
? 'rpc'
255+
: 'off',
230256
tradeOrderCount: orders.size,
231257
tradeMatchCount: matches.size,
232258
tradeByPhase: byPhase,
@@ -615,7 +641,7 @@ export function createTradeEngine(options: TradeEngineOptions): TradeEngine {
615641
return { status: 200, body: { ok: true, match: record } }
616642
}
617643

618-
function settleStatus(body: unknown): { status: number; body: Record<string, unknown> } {
644+
async function settleStatus(body: unknown): Promise<{ status: number; body: Record<string, unknown> }> {
619645
if (!isRecord(body) || typeof body.candidateHash !== 'string') {
620646
return { status: 400, body: { ok: false, error: 'candidateHash required' } }
621647
}
@@ -625,16 +651,98 @@ export function createTradeEngine(options: TradeEngineOptions): TradeEngine {
625651
return { status: 400, body: { ok: false, error: 'certificate not ready for settlement' } }
626652
}
627653
const outcome = typeof body.outcome === 'string' ? body.outcome : 'submitted'
628-
let eventType: number
629-
if (outcome === 'submitted') {
630-
eventType = EVENT_TRADE_SETTLEMENT_SUBMITTED
631-
} else if (outcome === 'settled') {
632-
eventType = EVENT_TRADE_SETTLED
633-
} else if (outcome === 'failed') {
634-
eventType = EVENT_TRADE_SETTLEMENT_FAILED
635-
} else {
654+
if (outcome !== 'submitted' && outcome !== 'settled' && outcome !== 'failed') {
636655
return { status: 400, body: { ok: false, error: 'outcome must be submitted|settled|failed' } }
637656
}
657+
658+
const executeOnChain =
659+
body.executeOnChain === true || envSettle.settleOnChain || options.submitL1SettlementTx !== undefined
660+
661+
if (
662+
executeOnChain &&
663+
outcome !== 'failed' &&
664+
record.phase === 'match_certified' &&
665+
record.certificate !== undefined
666+
) {
667+
const cert = record.certificate
668+
const committee =
669+
cert.signers.length > 0
670+
? cert.signers
671+
: (cert.committee ?? record.committee ?? [])
672+
const onChain =
673+
options.submitL1SettlementTx !== undefined
674+
? await options.submitL1SettlementTx({
675+
certificateHash: cert.certificateHash,
676+
sellerOrderHash: record.sell.orderHash,
677+
buyer: record.buy.maker,
678+
clearingAmount: record.candidate.clearingPrice,
679+
scanner: record.candidate.scanner,
680+
committee,
681+
})
682+
: l1RpcUrl && l1Settlement && l1AuthorityPk
683+
? await settleMockL1Auction({
684+
rpcUrl: l1RpcUrl,
685+
settlement: l1Settlement,
686+
authorityPrivateKey: l1AuthorityPk,
687+
certificateHash: cert.certificateHash,
688+
sellerOrderHash: record.sell.orderHash,
689+
buyer: record.buy.maker,
690+
clearingAmount: record.candidate.clearingPrice,
691+
scanner: record.candidate.scanner,
692+
committee,
693+
})
694+
: { ok: false as const, reason: 'on-chain settle not configured (need MOCK_L1_RPC_URL + MOCK_L1_SETTLEMENT + MOCK_L1_AUTHORITY_PRIVATE_KEY)' }
695+
696+
if (!onChain.ok) {
697+
const failTransition = applyMatchTransition(record, EVENT_TRADE_SETTLEMENT_FAILED)
698+
if (failTransition.ok) {
699+
record.phase = 'settlement_failed'
700+
record.settlementError = onChain.reason
701+
record.updatedAt = new Date().toISOString()
702+
persist()
703+
options.store.appendWal({
704+
type: 'trade-settlement',
705+
candidateHash: record.candidateHash,
706+
phase: record.phase,
707+
tipStateRoot: record.tipStateRoot,
708+
valueHash: record.valueHash,
709+
})
710+
}
711+
return { status: 400, body: { ok: false, error: onChain.reason, match: record } }
712+
}
713+
record.settlementTxHash = onChain.txHash
714+
// Mode A: certified → submitted → settled when caller asked for settled.
715+
const submitted = applyMatchTransition(record, EVENT_TRADE_SETTLEMENT_SUBMITTED)
716+
if (!submitted.ok) return { status: 400, body: { ok: false, error: submitted.error } }
717+
record.phase = 'settlement_submitted'
718+
if (outcome === 'settled') {
719+
const settled = applyMatchTransition(record, EVENT_TRADE_SETTLED)
720+
if (!settled.ok) return { status: 400, body: { ok: false, error: settled.error } }
721+
record.phase = 'settled'
722+
record.sell.cancelled = true
723+
record.buy.cancelled = true
724+
orders.set(record.sell.orderHash.toLowerCase(), record.sell)
725+
orders.set(record.buy.orderHash.toLowerCase(), record.buy)
726+
}
727+
record.updatedAt = new Date().toISOString()
728+
persist()
729+
options.store.appendWal({
730+
type: 'trade-settlement',
731+
candidateHash: record.candidateHash,
732+
phase: record.phase,
733+
tipStateRoot: record.tipStateRoot,
734+
valueHash: record.valueHash,
735+
settlementTxHash: record.settlementTxHash,
736+
})
737+
return { status: 200, body: { ok: true, match: record, onChain: true, mockL1Only: true } }
738+
}
739+
740+
const eventType =
741+
outcome === 'submitted'
742+
? EVENT_TRADE_SETTLEMENT_SUBMITTED
743+
: outcome === 'settled'
744+
? EVENT_TRADE_SETTLED
745+
: EVENT_TRADE_SETTLEMENT_FAILED
638746
const transition = applyMatchTransition(record, eventType)
639747
if (!transition.ok) return { status: 400, body: { ok: false, error: transition.error } }
640748
if (outcome === 'submitted') {

‎runtime/src/daemon/RULES.md‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,9 @@ Local Hardhat/Anvil MVP only. **Not** CoNET 224422 mainnet, **not** production D
4444

4545
```bash
4646
npm run mock-auction-cli -- --archive http://127.0.0.1:27101 <command> …
47-
npm run mock-auction-demo # in-process submit→scan→candidate→check→attest→settle
47+
npm run mock-auction-demo # in-process submit→scan→candidate→check→attest→settle (lab / optional custody)
48+
npm run mock-auction-e2e # list → match → Archive settle() on live local RPC (MOCK_L1_*)
49+
# monorepo root: npm run dle:mock-auction-e2e # hardhat node + deploy + e2e
4850
```
4951

5052
| Command | Role |
@@ -53,6 +55,7 @@ npm run mock-auction-demo # in-process submit→scan→candidate→check→att
5355
| `submit` / `cancel` | EIP-191 signed sell/buy orders → `POST /trade/submit` / `/trade/cancel` |
5456
| `scan` / `candidate` | Scanner proposes `MockL1MatchCandidateV1` only — **cannot** settle |
5557
| `check` / `attest` | Archive legality + freeze-then-draw committee attest → `TradeMatchCertificateV1`. With Archive `MOCK_L1_RPC_URL` + `MOCK_L1_SETTLEMENT`, client custody flags are **ignored** |
58+
| `settle` (round 3) | With `MOCK_L1_SETTLE_ONCHAIN` / authority key / `executeOnChain`, Archive posts real `settle` tx; seller must `list` first |
5659
| `settle` | Report mock L1 settlement outcome (`submitted` / `settled` / `failed`) |
5760
| `status` | `GET /mockl1/chains` + `/trade/orders` + `/trade/matches` |
5861

0 commit comments

Comments
 (0)