chainlesschain 0.160.1 → 0.161.2
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.
- package/package.json +1 -1
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AIOps-BHiKMxFI.js → AIOps-CoZ9bIqF.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-5QD5uzWP.js → ActionButton-BvMi4awy.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-D1JxsGBN.js → Analytics-hRk2ziup.js} +1 -1
- package/src/assets/web-panel/assets/{AppLayout-DykU9tOE.js → AppLayout-_JR3Gko8.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-TGBqld9c.js → Audit-D8WmaHdX.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-DjpzIwA6.js → Backup-CogYVeiE.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-joQlVauq.js → BaseInput-NAp5_OPY.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-9TYfosy-.js → Chat-DkQnhjfk.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-OEOFA9GM.js → Checkbox-C9dkWb-7.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-5H5UgHJu.js → Codegen-BHyJ3j-p.js} +1 -1
- package/src/assets/web-panel/assets/{Col-BnLUipDp.js → Col-JyQOivHb.js} +1 -1
- package/src/assets/web-panel/assets/{Community-BF3R5GAl.js → Community-UMq5QuBA.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-C1EkTFek.js → Compact-DGlwooBJ.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-CpP-ODRU.js → Compliance-2rWGO55k.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-CCgGSKVR.js → Cowork-V-tDxtrt.js} +1 -1
- package/src/assets/web-panel/assets/{Cron-CZ5pjRxn.js → Cron-YgEeQvdV.js} +1 -1
- package/src/assets/web-panel/assets/{Crosschain-C0P-5sm3.js → Crosschain-Cgd5cRKn.js} +1 -1
- package/src/assets/web-panel/assets/{DID-DPZKMApP.js → DID-swdBCdMZ.js} +1 -1
- package/src/assets/web-panel/assets/{Dashboard-G-BDDAov.js → Dashboard-ClnWtxsT.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-CeywCcVQ.js → Dropdown-mlITwb7d.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-DuFRY867.js → Federation-DtUN3wQa.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-CFSiPqbu.js → FormItemContext-BYmWDwAT.js} +1 -1
- package/src/assets/web-panel/assets/{Git-CjVRJDLg.js → Git-Du1k1iHz.js} +1 -1
- package/src/assets/web-panel/assets/{Governance-C0lyocJc.js → Governance-B2TFaWsf.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-BVSAexgk.js → Inference-Cm_hmXla.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-SE4jCwIn.js → KnowledgeGraph-DLaLMo4r.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-BD5C-wTx.js → Logs-CElvIBBJ.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-CL93dFBs.js → Marketplace-BlR3RCDV.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-x-Tibae-.js → McpTools-BpHkrlka.js} +1 -1
- package/src/assets/web-panel/assets/{Memory-CR8LXq37.js → Memory-DeU9ys_m.js} +1 -1
- package/src/assets/web-panel/assets/Mtc-CCJZpnJo.js +6 -0
- package/src/assets/web-panel/assets/Mtc-Cc8OJxe_.css +1 -0
- package/src/assets/web-panel/assets/{NLProgramming-B09F6gt2.js → NLProgramming-B_Tie6j1.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-BYIn2GOe.js → Notes-BcpuirPj.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-CybJTFN9.js → Organization-B_SHESSc.js} +1 -1
- package/src/assets/web-panel/assets/{Overflow-W4YLQ7yY.js → Overflow-R2SOGT0l.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-kVj43R4j.js → P2P-IYYy3cEd.js} +1 -1
- package/src/assets/web-panel/assets/{Permissions-CfYE4XFJ.js → Permissions-CR1N42yW.js} +1 -1
- package/src/assets/web-panel/assets/{Pipeline-BVLo32Ak.js → Pipeline-nwFpKsU_.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-Efyb3xpJ.js → Privacy-BGpz72PX.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectSettings-cBqrIhNN.js → ProjectSettings-CEDhpgbs.js} +1 -1
- package/src/assets/web-panel/assets/{Projects-BYY38oZd.js → Projects-DABi6ylb.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-BsS27cWs.js → Providers-HzrcE8ma.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-BvMXwWFN.js → Recommend-BPhQwye7.js} +1 -1
- package/src/assets/web-panel/assets/{Reputation-DmwTtBfl.js → Reputation-BL6hTN1s.js} +1 -1
- package/src/assets/web-panel/assets/{Row-N-X7EJ3w.js → Row-2akLU3YS.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-D9TjnwgF.js → RssFeed-D6qNq6Ht.js} +1 -1
- package/src/assets/web-panel/assets/{Search-Hapv-QkV.js → Search-R_b-u9oL.js} +1 -1
- package/src/assets/web-panel/assets/{Security-DWbFJK10.js → Security-DdW4hu_4.js} +1 -1
- package/src/assets/web-panel/assets/{Services-BPUmhVoH.js → Services-CnzEzGFN.js} +1 -1
- package/src/assets/web-panel/assets/{Skeleton-Bo5qPHbE.js → Skeleton-D6RevdW2.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-JJ8uInMW.js → Skills-DD5ReHH7.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-CEDF9zdV.js → Sla-CaQOOsjD.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-oIoX_vCx.js → SpeechSettings-D-pGIn9Z.js} +1 -1
- package/src/assets/web-panel/assets/{Tasks-Cx5wgv5Z.js → Tasks-BbdO_i4Q.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-BomcBlkN.js → Templates-CupAugDn.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-BxSQZUNh.js → Tenant-rseAzHcY.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-BlPPoB3C.js → Tokens-DXMokNbR.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-Bhjmjsc5.js → Trigger-DkSZjOlY.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-Dsjv7rkb.js → Trust-CKb7QDH1.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-BsVR1PN8.js → VideoEditing-CSOjdBZg.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-dcRAYsdL.js → Wallet-DHbi5dHt.js} +1 -1
- package/src/assets/web-panel/assets/{WebAuthn-oqIS5PCi.js → WebAuthn-BkGDI33-.js} +1 -1
- package/src/assets/web-panel/assets/{WorkflowEditor-C_fYMBvB.js → WorkflowEditor-BFZ3RYva.js} +1 -1
- package/src/assets/web-panel/assets/{colors-D2P6CqS5.js → colors-D2tTvuDI.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-CG7qutT_.js → compact-item-CqCEUZiy.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-y4UPKgbA.js → createContext-C6HFlAQP.js} +1 -1
- package/src/assets/web-panel/assets/{hasIn-Butbu9jZ.js → hasIn-yp2CbhYc.js} +1 -1
- package/src/assets/web-panel/assets/index-4cn1LmJ9.js +1 -0
- package/src/assets/web-panel/assets/{index-BtuwtDUE.js → index-B0jkl2Zb.js} +1 -1
- package/src/assets/web-panel/assets/{index-YmGOWX7h.js → index-B2qFUwGb.js} +1 -1
- package/src/assets/web-panel/assets/{index-BEfvpbz-.js → index-B74gWYqD.js} +1 -1
- package/src/assets/web-panel/assets/{index-BJN_3RTO.js → index-B8Qxu0q2.js} +1 -1
- package/src/assets/web-panel/assets/{index-C2K61jP8.js → index-B9b_mz4I.js} +1 -1
- package/src/assets/web-panel/assets/index-BAA1SFp1.js +1 -0
- package/src/assets/web-panel/assets/{index-rIbVsjde.js → index-BJeE7n_I.js} +1 -1
- package/src/assets/web-panel/assets/{index-Ceaxjpqh.js → index-BJoK7MkB.js} +1 -1
- package/src/assets/web-panel/assets/{index-CYlDKn3O.js → index-BTL2yIvT.js} +1 -1
- package/src/assets/web-panel/assets/{index-DrVnyYpX.js → index-BlVnFOFL.js} +1 -1
- package/src/assets/web-panel/assets/{index-89HJLKZ-.js → index-Blq49aTW.js} +1 -1
- package/src/assets/web-panel/assets/{index-vC5cTycG.js → index-BxSsO6Sm.js} +1 -1
- package/src/assets/web-panel/assets/{index-38mVlGHc.js → index-CA3g3EpL.js} +1 -1
- package/src/assets/web-panel/assets/{index-DLMJy9pE.js → index-CFoFkVUt.js} +1 -1
- package/src/assets/web-panel/assets/{index-DtNHlrxp.js → index-CWhXxdyo.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bs9aHxDD.js → index-CXf0zL5i.js} +1 -1
- package/src/assets/web-panel/assets/{index-BQr8Y0o5.js → index-CcwZodUl.js} +1 -1
- package/src/assets/web-panel/assets/{index-DdgjeX4z.js → index-Cd6m6ynF.js} +1 -1
- package/src/assets/web-panel/assets/{index-BQfow_sh.js → index-Cfy9l115.js} +1 -1
- package/src/assets/web-panel/assets/{index-C1mK1Ga3.js → index-CijiVpfO.js} +1 -1
- package/src/assets/web-panel/assets/{index-hSilB_Q-.js → index-Cpfx7-LN.js} +1 -1
- package/src/assets/web-panel/assets/{index-BYZPJS7A.js → index-CrEEL63u.js} +1 -1
- package/src/assets/web-panel/assets/{index-C_8hWf5_.js → index-CucxAdwN.js} +1 -1
- package/src/assets/web-panel/assets/{index-qXvwlbkq.js → index-CvuBD5TK.js} +1 -1
- package/src/assets/web-panel/assets/{index-CyqU4Tck.js → index-Cw4v7ezB.js} +3 -3
- package/src/assets/web-panel/assets/{index-B6U6cYUa.js → index-D34iabcS.js} +1 -1
- package/src/assets/web-panel/assets/{index-BvJgRWBq.js → index-DEKuiAPQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cc77JZKd.js → index-Dj4P0iWm.js} +1 -1
- package/src/assets/web-panel/assets/{index-CAeKBs9n.js → index-DjeNNVwu.js} +1 -1
- package/src/assets/web-panel/assets/{index-B4Jfv4EB.js → index-DlgMVieO.js} +1 -1
- package/src/assets/web-panel/assets/{index-CWh3IxEh.js → index-DnehXcB-.js} +1 -1
- package/src/assets/web-panel/assets/{index-Ci6jXp3l.js → index-JszcDpsT.js} +1 -1
- package/src/assets/web-panel/assets/{index-BCQ0WlB2.js → index-KdFFI-p3.js} +1 -1
- package/src/assets/web-panel/assets/{index-DnI4Aq0q.js → index-NNymVAza.js} +1 -1
- package/src/assets/web-panel/assets/{index-gWmZm8_Q.js → index-aw2DwKj-.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dx_ZTZo_.js → index-nzDx0JAR.js} +1 -1
- package/src/assets/web-panel/assets/{index-C1ucrJLg.js → index-w1xShUDf.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-CZRZ-1bk.js → initDefaultProps-DfdVxwz6.js} +1 -1
- package/src/assets/web-panel/assets/{motion-CvU8SiWF.js → motion-CL0bdvJg.js} +1 -1
- package/src/assets/web-panel/assets/{move-ipAfWhya.js → move-Bo9Fgzv7.js} +1 -1
- package/src/assets/web-panel/assets/{omit-D6bJEjz9.js → omit-C-cc6wHr.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-Dpvzf7sL.js → pickAttrs-CjLp5RN-.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-D_tEolP1.js → placementArrow-CJa8gsqa.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-BEFI7neO.js → responsiveObserve-CcDj3P-p.js} +1 -1
- package/src/assets/web-panel/assets/{slide-Bte_KOqM.js → slide-CpvbHO26.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-K4xaDRuO.js → statusUtils-BK69kP1U.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-Cl9YgOVY.js → styleChecker-CDvBRzsG.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-DNstl1wK.js → useFlexGapSupport-CRN_hzJt.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-ChB-8cXr.js → vnode-Bzp-FsbB.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-meTNBulL.js → zoom-BHqpWXJV.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/crosschain.js +564 -8
- package/src/commands/mtc.js +1334 -0
- package/src/lib/cross-chain-mtc.js +904 -0
- package/src/assets/web-panel/assets/Mtc-C-PfF5B3.css +0 -1
- package/src/assets/web-panel/assets/Mtc-CEtRtMcc.js +0 -1
- package/src/assets/web-panel/assets/index-B5FRjJMb.js +0 -1
- package/src/assets/web-panel/assets/index-B7FV5EnN.js +0 -1
|
@@ -0,0 +1,904 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-Chain Bridge MTC integration (v0.1 — design doc 跨链桥设计 v1).
|
|
3
|
+
*
|
|
4
|
+
* Wraps core-mtc assembleBatch / verify with bridge-specific:
|
|
5
|
+
* - namespace formatting: `mtc/v1/bridge/<chain-pair>/<batch-seq>` (lex-ordered)
|
|
6
|
+
* - trust-anchor store per source chain (Independent mode, design §6.1)
|
|
7
|
+
* - bridge envelope verification (namespace prefix + bridge_op enum check)
|
|
8
|
+
*
|
|
9
|
+
* Layout under <configDir>/cross-chain-mtc/:
|
|
10
|
+
* config.json enabled, batch_interval_seconds, alg, mode (independent|federated|light-client)
|
|
11
|
+
* trust-anchors.json { chain_id → [{ pubkey_id, alg, issuer, pubkey_jwk, added_at }] }
|
|
12
|
+
* batches/<batch-id>/ one closed batch (landmark + envelopes)
|
|
13
|
+
*
|
|
14
|
+
* Per design doc §3.1: opt-in. Default config.enabled = false. Existing
|
|
15
|
+
* `cc crosschain bridge|swap|send` paths keep working unchanged when
|
|
16
|
+
* MTC integration is disabled.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import fs from "node:fs";
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
import mtcLib from "@chainlesschain/core-mtc";
|
|
22
|
+
|
|
23
|
+
const {
|
|
24
|
+
assembleBatch,
|
|
25
|
+
verify,
|
|
26
|
+
NAMESPACE_RE,
|
|
27
|
+
SCHEMA_ENVELOPE,
|
|
28
|
+
ed25519,
|
|
29
|
+
slhDsa,
|
|
30
|
+
} = mtcLib;
|
|
31
|
+
|
|
32
|
+
const SUPPORTED_BRIDGE_CHAINS = Object.freeze([
|
|
33
|
+
"ethereum",
|
|
34
|
+
"polygon",
|
|
35
|
+
"bsc",
|
|
36
|
+
"arbitrum",
|
|
37
|
+
"solana",
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
const VALID_BRIDGE_OPS = Object.freeze([
|
|
41
|
+
"lock",
|
|
42
|
+
"mint",
|
|
43
|
+
"refund",
|
|
44
|
+
"swap-init",
|
|
45
|
+
"swap-claim",
|
|
46
|
+
"swap-refund",
|
|
47
|
+
"msg-send",
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
const CONFIG_DEFAULTS = Object.freeze({
|
|
51
|
+
enabled: false,
|
|
52
|
+
// 60s default per design doc §3.5 — short-window batch suitable for
|
|
53
|
+
// cross-chain latency expectations. Operators may bump for low-traffic pairs.
|
|
54
|
+
batch_interval_seconds: 60,
|
|
55
|
+
alg: "ed25519", // or "slh-dsa-128f" (PQC opt-in)
|
|
56
|
+
mode: "independent", // or "federated" | "light-client"
|
|
57
|
+
issuer: "mtca:cc:bridge-local",
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const BRIDGE_NAMESPACE_PREFIX = "mtc/v1/bridge/";
|
|
61
|
+
const BRIDGE_NAMESPACE_RE = /^mtc\/v1\/bridge\/[a-z0-9]+-[a-z0-9]+\/[0-9]{6,}$/;
|
|
62
|
+
|
|
63
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
64
|
+
// Path helpers
|
|
65
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
export function getCrossChainMtcDir(configDir) {
|
|
68
|
+
if (!configDir) throw new Error("getCrossChainMtcDir: configDir required");
|
|
69
|
+
return path.join(configDir, "cross-chain-mtc");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function configPath(dir) {
|
|
73
|
+
return path.join(dir, "config.json");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function trustAnchorsPath(dir) {
|
|
77
|
+
return path.join(dir, "trust-anchors.json");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function batchesDir(dir) {
|
|
81
|
+
return path.join(dir, "batches");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function stagingDir(dir) {
|
|
85
|
+
return path.join(dir, "staging");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function batchSeqPath(dir) {
|
|
89
|
+
return path.join(dir, "batch-seq.json");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function ensureDirs(dir) {
|
|
93
|
+
for (const p of [dir, batchesDir(dir), stagingDir(dir)]) {
|
|
94
|
+
if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
99
|
+
// Namespace
|
|
100
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Build a cross-chain bridge namespace. Per design doc §5.2 chain-pair
|
|
104
|
+
* MUST be lexicographic (so A↔B always maps to one tree, never two).
|
|
105
|
+
*
|
|
106
|
+
* @param {string} srcChain
|
|
107
|
+
* @param {string} dstChain
|
|
108
|
+
* @param {number|string} batchSeq - integer ≥ 1 (zero-padded to 6 digits)
|
|
109
|
+
* @returns {string} namespace like "mtc/v1/bridge/ethereum-polygon/000142"
|
|
110
|
+
*/
|
|
111
|
+
export function bridgeNamespace(srcChain, dstChain, batchSeq) {
|
|
112
|
+
if (!SUPPORTED_BRIDGE_CHAINS.includes(srcChain)) {
|
|
113
|
+
throw new RangeError(
|
|
114
|
+
`bridgeNamespace: unsupported src chain "${srcChain}"`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
if (!SUPPORTED_BRIDGE_CHAINS.includes(dstChain)) {
|
|
118
|
+
throw new RangeError(
|
|
119
|
+
`bridgeNamespace: unsupported dst chain "${dstChain}"`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
if (srcChain === dstChain) {
|
|
123
|
+
throw new RangeError("bridgeNamespace: src and dst chain must differ");
|
|
124
|
+
}
|
|
125
|
+
const seqInt = Number(batchSeq);
|
|
126
|
+
if (!Number.isInteger(seqInt) || seqInt < 1) {
|
|
127
|
+
throw new RangeError("bridgeNamespace: batchSeq must be positive integer");
|
|
128
|
+
}
|
|
129
|
+
const pair = [srcChain, dstChain].sort().join("-");
|
|
130
|
+
const seq = String(seqInt).padStart(6, "0");
|
|
131
|
+
return `${BRIDGE_NAMESPACE_PREFIX}${pair}/${seq}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function isBridgeNamespace(ns) {
|
|
135
|
+
return typeof ns === "string" && BRIDGE_NAMESPACE_RE.test(ns);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
139
|
+
// Config
|
|
140
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
export function loadCrossChainMtcConfig(dir) {
|
|
143
|
+
ensureDirs(dir);
|
|
144
|
+
const p = configPath(dir);
|
|
145
|
+
if (!fs.existsSync(p)) return { ...CONFIG_DEFAULTS };
|
|
146
|
+
try {
|
|
147
|
+
const raw = JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
148
|
+
return { ...CONFIG_DEFAULTS, ...raw };
|
|
149
|
+
} catch (err) {
|
|
150
|
+
throw new Error(`cross-chain-mtc config malformed at ${p}: ${err.message}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function saveCrossChainMtcConfig(dir, patch) {
|
|
155
|
+
ensureDirs(dir);
|
|
156
|
+
const merged = { ...loadCrossChainMtcConfig(dir), ...patch };
|
|
157
|
+
if (typeof merged.enabled !== "boolean") {
|
|
158
|
+
throw new TypeError("config.enabled must be boolean");
|
|
159
|
+
}
|
|
160
|
+
if (
|
|
161
|
+
!Number.isInteger(merged.batch_interval_seconds) ||
|
|
162
|
+
merged.batch_interval_seconds < 1
|
|
163
|
+
) {
|
|
164
|
+
throw new RangeError(
|
|
165
|
+
"config.batch_interval_seconds must be positive integer",
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
if (!["ed25519", "slh-dsa-128f"].includes(merged.alg)) {
|
|
169
|
+
throw new RangeError("config.alg must be ed25519 or slh-dsa-128f");
|
|
170
|
+
}
|
|
171
|
+
if (!["independent", "federated", "light-client"].includes(merged.mode)) {
|
|
172
|
+
throw new RangeError(
|
|
173
|
+
"config.mode must be independent, federated, or light-client",
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
fs.writeFileSync(configPath(dir), JSON.stringify(merged, null, 2), "utf-8");
|
|
177
|
+
return merged;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
181
|
+
// Trust anchor store (Independent mode — design doc §6.1)
|
|
182
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
function emptyTrustAnchorStore() {
|
|
185
|
+
return { schema: "mtc-bridge-trust-anchors/v1", anchors: {} };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function loadTrustAnchors(dir) {
|
|
189
|
+
ensureDirs(dir);
|
|
190
|
+
const p = trustAnchorsPath(dir);
|
|
191
|
+
if (!fs.existsSync(p)) return emptyTrustAnchorStore();
|
|
192
|
+
try {
|
|
193
|
+
const raw = JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
194
|
+
if (raw.schema !== "mtc-bridge-trust-anchors/v1" || !raw.anchors) {
|
|
195
|
+
throw new Error("schema mismatch");
|
|
196
|
+
}
|
|
197
|
+
return raw;
|
|
198
|
+
} catch (err) {
|
|
199
|
+
throw new Error(`trust-anchors store malformed at ${p}: ${err.message}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function saveTrustAnchorStore(dir, store) {
|
|
204
|
+
fs.writeFileSync(
|
|
205
|
+
trustAnchorsPath(dir),
|
|
206
|
+
JSON.stringify(store, null, 2),
|
|
207
|
+
"utf-8",
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Register a trust anchor for a source chain.
|
|
213
|
+
*
|
|
214
|
+
* @param {string} dir
|
|
215
|
+
* @param {string} chain - one of SUPPORTED_BRIDGE_CHAINS
|
|
216
|
+
* @param {{ pubkey_id: string, alg: string, issuer: string, pubkey_jwk?: object }} anchor
|
|
217
|
+
* @returns {{ added: boolean, total_for_chain: number }}
|
|
218
|
+
*/
|
|
219
|
+
export function addTrustAnchor(dir, chain, anchor) {
|
|
220
|
+
if (!SUPPORTED_BRIDGE_CHAINS.includes(chain)) {
|
|
221
|
+
throw new RangeError(`addTrustAnchor: unsupported chain "${chain}"`);
|
|
222
|
+
}
|
|
223
|
+
if (!anchor || typeof anchor !== "object") {
|
|
224
|
+
throw new TypeError("addTrustAnchor: anchor object required");
|
|
225
|
+
}
|
|
226
|
+
if (typeof anchor.pubkey_id !== "string" || !anchor.pubkey_id) {
|
|
227
|
+
throw new TypeError("addTrustAnchor: anchor.pubkey_id required");
|
|
228
|
+
}
|
|
229
|
+
if (!["ed25519", "slh-dsa-128f"].includes(anchor.alg)) {
|
|
230
|
+
throw new RangeError(
|
|
231
|
+
"addTrustAnchor: anchor.alg must be ed25519 or slh-dsa-128f",
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
if (typeof anchor.issuer !== "string" || !anchor.issuer) {
|
|
235
|
+
throw new TypeError("addTrustAnchor: anchor.issuer required");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const store = loadTrustAnchors(dir);
|
|
239
|
+
store.anchors[chain] ||= [];
|
|
240
|
+
const existing = store.anchors[chain].find(
|
|
241
|
+
(a) => a.pubkey_id === anchor.pubkey_id,
|
|
242
|
+
);
|
|
243
|
+
if (existing) {
|
|
244
|
+
return { added: false, total_for_chain: store.anchors[chain].length };
|
|
245
|
+
}
|
|
246
|
+
store.anchors[chain].push({
|
|
247
|
+
pubkey_id: anchor.pubkey_id,
|
|
248
|
+
alg: anchor.alg,
|
|
249
|
+
issuer: anchor.issuer,
|
|
250
|
+
pubkey_jwk: anchor.pubkey_jwk || null,
|
|
251
|
+
added_at: new Date().toISOString(),
|
|
252
|
+
});
|
|
253
|
+
saveTrustAnchorStore(dir, store);
|
|
254
|
+
return { added: true, total_for_chain: store.anchors[chain].length };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function listTrustAnchors(dir, chain) {
|
|
258
|
+
const store = loadTrustAnchors(dir);
|
|
259
|
+
if (chain) {
|
|
260
|
+
if (!SUPPORTED_BRIDGE_CHAINS.includes(chain)) {
|
|
261
|
+
throw new RangeError(`listTrustAnchors: unsupported chain "${chain}"`);
|
|
262
|
+
}
|
|
263
|
+
return store.anchors[chain] || [];
|
|
264
|
+
}
|
|
265
|
+
return store.anchors;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function removeTrustAnchor(dir, chain, pubkeyId) {
|
|
269
|
+
if (!SUPPORTED_BRIDGE_CHAINS.includes(chain)) {
|
|
270
|
+
throw new RangeError(`removeTrustAnchor: unsupported chain "${chain}"`);
|
|
271
|
+
}
|
|
272
|
+
const store = loadTrustAnchors(dir);
|
|
273
|
+
const list = store.anchors[chain] || [];
|
|
274
|
+
const before = list.length;
|
|
275
|
+
store.anchors[chain] = list.filter((a) => a.pubkey_id !== pubkeyId);
|
|
276
|
+
const removed = before > store.anchors[chain].length;
|
|
277
|
+
if (removed) saveTrustAnchorStore(dir, store);
|
|
278
|
+
return { removed, total_for_chain: store.anchors[chain].length };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
282
|
+
// Bridge op leaf shape — matches design doc §5.1 payload
|
|
283
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Validate one bridge op (raw leaf) before batching. Throws on bad shape.
|
|
287
|
+
* @param {object} op
|
|
288
|
+
*/
|
|
289
|
+
export function validateBridgeOp(op) {
|
|
290
|
+
if (!op || typeof op !== "object") {
|
|
291
|
+
throw new TypeError("validateBridgeOp: op object required");
|
|
292
|
+
}
|
|
293
|
+
if (!VALID_BRIDGE_OPS.includes(op.bridge_op)) {
|
|
294
|
+
throw new RangeError(
|
|
295
|
+
`validateBridgeOp: bridge_op must be one of ${VALID_BRIDGE_OPS.join(", ")}`,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
if (!SUPPORTED_BRIDGE_CHAINS.includes(op.src_chain)) {
|
|
299
|
+
throw new RangeError(
|
|
300
|
+
`validateBridgeOp: unsupported src_chain "${op.src_chain}"`,
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
if (!SUPPORTED_BRIDGE_CHAINS.includes(op.dst_chain)) {
|
|
304
|
+
throw new RangeError(
|
|
305
|
+
`validateBridgeOp: unsupported dst_chain "${op.dst_chain}"`,
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
if (op.src_chain === op.dst_chain) {
|
|
309
|
+
throw new RangeError("validateBridgeOp: src and dst chain must differ");
|
|
310
|
+
}
|
|
311
|
+
if (typeof op.issued_at !== "string" || !op.issued_at) {
|
|
312
|
+
throw new TypeError("validateBridgeOp: issued_at (RFC 3339) required");
|
|
313
|
+
}
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
318
|
+
// Batch assembly — wraps core-mtc assembleBatch with bridge namespace
|
|
319
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Assemble a bridge batch. Pure (no fs).
|
|
323
|
+
*
|
|
324
|
+
* @param {Array<object>} bridgeOps - validated bridge ops
|
|
325
|
+
* @param {{ secretKey: Buffer, publicKey: Buffer }} keys
|
|
326
|
+
* @param {{ src_chain: string, dst_chain: string, batch_seq: number,
|
|
327
|
+
* issuer: string, alg?: string, signer?: object }} opts
|
|
328
|
+
* @returns {{ landmark, envelopes, treeHeadId, root, namespace }}
|
|
329
|
+
*/
|
|
330
|
+
export function assembleBridgeBatch(bridgeOps, keys, opts) {
|
|
331
|
+
if (!Array.isArray(bridgeOps) || bridgeOps.length === 0) {
|
|
332
|
+
throw new RangeError("assembleBridgeBatch: bridgeOps must be non-empty");
|
|
333
|
+
}
|
|
334
|
+
if (!opts || typeof opts !== "object") {
|
|
335
|
+
throw new TypeError("assembleBridgeBatch: opts required");
|
|
336
|
+
}
|
|
337
|
+
for (const op of bridgeOps) validateBridgeOp(op);
|
|
338
|
+
|
|
339
|
+
const namespace = bridgeNamespace(
|
|
340
|
+
opts.src_chain,
|
|
341
|
+
opts.dst_chain,
|
|
342
|
+
opts.batch_seq,
|
|
343
|
+
);
|
|
344
|
+
const result = assembleBatch(
|
|
345
|
+
bridgeOps,
|
|
346
|
+
keys,
|
|
347
|
+
{
|
|
348
|
+
namespace,
|
|
349
|
+
issuer: opts.issuer,
|
|
350
|
+
issuedAt: opts.issuedAt,
|
|
351
|
+
expiresAt: opts.expiresAt,
|
|
352
|
+
},
|
|
353
|
+
opts.signer,
|
|
354
|
+
);
|
|
355
|
+
return { ...result, namespace };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
359
|
+
// Envelope verification — wraps core-mtc verify with bridge-specific checks
|
|
360
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Verify a bridge envelope. Layered checks:
|
|
364
|
+
* 1. namespace conforms to bridge pattern (BRIDGE_NAMESPACE_RE)
|
|
365
|
+
* 2. envelope.leaf has valid bridge_op + src/dst_chain
|
|
366
|
+
* 3. core-mtc verify passes (inclusion proof + landmark lookup)
|
|
367
|
+
*
|
|
368
|
+
* @param {object} envelope - MTC envelope JSON
|
|
369
|
+
* @param {{ lookup: Function }} cache - LandmarkCache
|
|
370
|
+
* @param {{ now?: number }} [options]
|
|
371
|
+
* @returns {{ ok: boolean, code?: string, recoverable?: boolean,
|
|
372
|
+
* leaf?: object, treeHead?: object, bridge_op?: string }}
|
|
373
|
+
*/
|
|
374
|
+
export function verifyBridgeEnvelope(envelope, cache, options) {
|
|
375
|
+
if (!envelope || typeof envelope !== "object") {
|
|
376
|
+
return { ok: false, code: "BAD_ENVELOPE", recoverable: false };
|
|
377
|
+
}
|
|
378
|
+
if (envelope.schema !== SCHEMA_ENVELOPE) {
|
|
379
|
+
return { ok: false, code: "UNKNOWN_SCHEMA", recoverable: false };
|
|
380
|
+
}
|
|
381
|
+
if (!isBridgeNamespace(envelope.namespace)) {
|
|
382
|
+
return { ok: false, code: "BAD_BRIDGE_NAMESPACE", recoverable: false };
|
|
383
|
+
}
|
|
384
|
+
// Check leaf shape before paying merkle-proof cost
|
|
385
|
+
if (!envelope.leaf || typeof envelope.leaf !== "object") {
|
|
386
|
+
return { ok: false, code: "BAD_BRIDGE_LEAF", recoverable: false };
|
|
387
|
+
}
|
|
388
|
+
try {
|
|
389
|
+
validateBridgeOp(envelope.leaf);
|
|
390
|
+
} catch (_err) {
|
|
391
|
+
return { ok: false, code: "BAD_BRIDGE_LEAF", recoverable: false };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const result = verify(envelope, cache, options);
|
|
395
|
+
if (!result.ok) return result;
|
|
396
|
+
|
|
397
|
+
return { ...result, bridge_op: envelope.leaf.bridge_op };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
401
|
+
// Staging — `--mtc` opt-in writes one op per file; mtc-batch closes them
|
|
402
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
403
|
+
|
|
404
|
+
function stagedFileName(op) {
|
|
405
|
+
// Time-prefixed so listdir = chronological. Pair-prefix groups visually.
|
|
406
|
+
const ts = new Date(op.issued_at)
|
|
407
|
+
.toISOString()
|
|
408
|
+
.replace(/[-:T.]/g, "")
|
|
409
|
+
.slice(0, 17);
|
|
410
|
+
const pair = [op.src_chain, op.dst_chain].sort().join("-");
|
|
411
|
+
// Use a content-derived 8-char tail to dedupe accidental double-stage of same op.
|
|
412
|
+
const sigSource = `${op.bridge_op}|${op.src_tx_hash || ""}|${op.swap_id || ""}|${op.amount || ""}|${ts}`;
|
|
413
|
+
let h = 0;
|
|
414
|
+
for (let i = 0; i < sigSource.length; i++)
|
|
415
|
+
h = ((h << 5) - h + sigSource.charCodeAt(i)) | 0;
|
|
416
|
+
const tail = (h >>> 0).toString(16).padStart(8, "0");
|
|
417
|
+
return `${ts}-${pair}-${tail}.json`;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Stage one bridge op for inclusion in the next batch close.
|
|
422
|
+
* Idempotent: same op written twice -> single staging file.
|
|
423
|
+
*
|
|
424
|
+
* @param {string} dir
|
|
425
|
+
* @param {object} op
|
|
426
|
+
* @param {{ requireEnabled?: boolean }} [opts]
|
|
427
|
+
* @returns {{ staged: boolean, path: string, reason?: string }}
|
|
428
|
+
*/
|
|
429
|
+
export function stageBridgeOp(dir, op, opts = {}) {
|
|
430
|
+
ensureDirs(dir);
|
|
431
|
+
const cfg = loadCrossChainMtcConfig(dir);
|
|
432
|
+
const requireEnabled = opts.requireEnabled !== false;
|
|
433
|
+
if (requireEnabled && !cfg.enabled) {
|
|
434
|
+
return { staged: false, path: null, reason: "DISABLED" };
|
|
435
|
+
}
|
|
436
|
+
validateBridgeOp(op);
|
|
437
|
+
const file = path.join(stagingDir(dir), stagedFileName(op));
|
|
438
|
+
if (fs.existsSync(file)) {
|
|
439
|
+
return { staged: false, path: file, reason: "ALREADY_STAGED" };
|
|
440
|
+
}
|
|
441
|
+
fs.writeFileSync(file, JSON.stringify(op, null, 2), "utf-8");
|
|
442
|
+
return { staged: true, path: file };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Read all staged ops grouped by chain-pair.
|
|
447
|
+
* @returns {{ [pair: string]: Array<{ file: string, op: object }> }}
|
|
448
|
+
*/
|
|
449
|
+
export function listStagedOps(dir) {
|
|
450
|
+
ensureDirs(dir);
|
|
451
|
+
const sd = stagingDir(dir);
|
|
452
|
+
if (!fs.existsSync(sd)) return {};
|
|
453
|
+
const out = {};
|
|
454
|
+
for (const name of fs.readdirSync(sd).sort()) {
|
|
455
|
+
if (!name.endsWith(".json")) continue;
|
|
456
|
+
const file = path.join(sd, name);
|
|
457
|
+
let op;
|
|
458
|
+
try {
|
|
459
|
+
op = JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
460
|
+
validateBridgeOp(op);
|
|
461
|
+
} catch (_err) {
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
const pair = [op.src_chain, op.dst_chain].sort().join("-");
|
|
465
|
+
out[pair] ||= [];
|
|
466
|
+
out[pair].push({ file, op });
|
|
467
|
+
}
|
|
468
|
+
return out;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function nextBatchSeq(dir, pair) {
|
|
472
|
+
const p = batchSeqPath(dir);
|
|
473
|
+
let map = {};
|
|
474
|
+
if (fs.existsSync(p)) {
|
|
475
|
+
try {
|
|
476
|
+
map = JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
477
|
+
} catch (_err) {
|
|
478
|
+
map = {};
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
const next = (map[pair] || 0) + 1;
|
|
482
|
+
map[pair] = next;
|
|
483
|
+
fs.writeFileSync(p, JSON.stringify(map, null, 2), "utf-8");
|
|
484
|
+
return next;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Close all currently-staged ops into one batch per chain-pair.
|
|
489
|
+
* Writes batches/<pair>-<seq>/{landmark.json, envelope-NN.json,...} and
|
|
490
|
+
* removes the staged files atomically per pair (best-effort).
|
|
491
|
+
*
|
|
492
|
+
* @param {string} dir
|
|
493
|
+
* @param {{ alg?: string, signer?: object, issuer?: string,
|
|
494
|
+
* keys?: { secretKey: Buffer, publicKey: Buffer } }} [opts]
|
|
495
|
+
* @returns {{ batches: Array<{ pair, seq, namespace, treeHeadId, count, dir }>,
|
|
496
|
+
* skipped: { reason: string } | null }}
|
|
497
|
+
*/
|
|
498
|
+
export function closeBatch(dir, opts = {}) {
|
|
499
|
+
ensureDirs(dir);
|
|
500
|
+
const cfg = loadCrossChainMtcConfig(dir);
|
|
501
|
+
const grouped = listStagedOps(dir);
|
|
502
|
+
const pairs = Object.keys(grouped);
|
|
503
|
+
if (pairs.length === 0) {
|
|
504
|
+
return { batches: [], skipped: { reason: "NO_STAGED_OPS" } };
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const alg = opts.alg || cfg.alg;
|
|
508
|
+
const signer = opts.signer || (alg === "slh-dsa-128f" ? slhDsa : ed25519);
|
|
509
|
+
const keys = opts.keys || signer.generateKeyPair();
|
|
510
|
+
const issuer = opts.issuer || cfg.issuer;
|
|
511
|
+
|
|
512
|
+
const batches = [];
|
|
513
|
+
for (const pair of pairs) {
|
|
514
|
+
const entries = grouped[pair];
|
|
515
|
+
const ops = entries.map((e) => e.op);
|
|
516
|
+
const [src, dst] = pair.split("-");
|
|
517
|
+
const seq = nextBatchSeq(dir, pair);
|
|
518
|
+
const result = assembleBridgeBatch(ops, keys, {
|
|
519
|
+
src_chain: src,
|
|
520
|
+
dst_chain: dst,
|
|
521
|
+
batch_seq: seq,
|
|
522
|
+
issuer,
|
|
523
|
+
signer,
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
const outDir = path.join(
|
|
527
|
+
batchesDir(dir),
|
|
528
|
+
`${pair}-${String(seq).padStart(6, "0")}`,
|
|
529
|
+
);
|
|
530
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
531
|
+
fs.writeFileSync(
|
|
532
|
+
path.join(outDir, "landmark.json"),
|
|
533
|
+
JSON.stringify(result.landmark, null, 2),
|
|
534
|
+
"utf-8",
|
|
535
|
+
);
|
|
536
|
+
result.envelopes.forEach((env, i) => {
|
|
537
|
+
fs.writeFileSync(
|
|
538
|
+
path.join(outDir, `envelope-${String(i).padStart(4, "0")}.json`),
|
|
539
|
+
JSON.stringify(env, null, 2),
|
|
540
|
+
"utf-8",
|
|
541
|
+
);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
// Remove staging files only after on-disk artifacts persisted.
|
|
545
|
+
for (const e of entries) {
|
|
546
|
+
try {
|
|
547
|
+
fs.unlinkSync(e.file);
|
|
548
|
+
} catch (_err) {
|
|
549
|
+
/* best-effort cleanup */
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
batches.push({
|
|
554
|
+
pair,
|
|
555
|
+
seq,
|
|
556
|
+
namespace: result.namespace,
|
|
557
|
+
treeHeadId: result.treeHeadId,
|
|
558
|
+
count: ops.length,
|
|
559
|
+
dir: outDir,
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
return { batches, skipped: null };
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
566
|
+
// v0.2 — Multi-hop envelope (envelope-of-envelope)
|
|
567
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
568
|
+
|
|
569
|
+
const SCHEMA_MULTI_HOP_BRIDGE = "mtc-bridge-multihop/v1";
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Build a multi-hop bridge envelope by chaining single-hop envelopes.
|
|
573
|
+
* For a route A → B → C, the caller supplies the leg envelopes
|
|
574
|
+
* [A→B, B→C] and gets back a wrapper envelope referencing both legs.
|
|
575
|
+
*
|
|
576
|
+
* Verification (verifyMultiHopBridgeEnvelope below) checks each leg's
|
|
577
|
+
* inclusion proof against its respective landmark + asserts that the
|
|
578
|
+
* intermediate chain is consistent (leg[i].dst_chain == leg[i+1].src_chain).
|
|
579
|
+
*
|
|
580
|
+
* @param {Array<object>} legEnvelopes - ≥ 2 single-hop bridge envelopes
|
|
581
|
+
* @param {{ route_id?: string, total_amount?: string, asset?: string }} [meta]
|
|
582
|
+
* @returns {object} multi-hop wrapper envelope
|
|
583
|
+
*/
|
|
584
|
+
export function buildMultiHopBridgeEnvelope(legEnvelopes, meta = {}) {
|
|
585
|
+
if (!Array.isArray(legEnvelopes) || legEnvelopes.length < 2) {
|
|
586
|
+
throw new RangeError(
|
|
587
|
+
"buildMultiHopBridgeEnvelope: need at least 2 leg envelopes",
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
for (let i = 0; i < legEnvelopes.length; i++) {
|
|
591
|
+
const env = legEnvelopes[i];
|
|
592
|
+
if (!env || env.schema !== SCHEMA_ENVELOPE) {
|
|
593
|
+
throw new TypeError(`leg ${i}: not a valid mtc-envelope/v1 envelope`);
|
|
594
|
+
}
|
|
595
|
+
if (!isBridgeNamespace(env.namespace)) {
|
|
596
|
+
throw new TypeError(`leg ${i}: not a bridge namespace`);
|
|
597
|
+
}
|
|
598
|
+
validateBridgeOp(env.leaf);
|
|
599
|
+
if (i > 0) {
|
|
600
|
+
const prev = legEnvelopes[i - 1];
|
|
601
|
+
if (prev.leaf.dst_chain !== env.leaf.src_chain) {
|
|
602
|
+
throw new RangeError(
|
|
603
|
+
`route discontinuity: leg ${i - 1} dst_chain=${prev.leaf.dst_chain} but leg ${i} src_chain=${env.leaf.src_chain}`,
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
const route = legEnvelopes.map((env) => ({
|
|
609
|
+
src_chain: env.leaf.src_chain,
|
|
610
|
+
dst_chain: env.leaf.dst_chain,
|
|
611
|
+
leaf_hash: env.leaf_hash || null,
|
|
612
|
+
namespace: env.namespace,
|
|
613
|
+
tree_head_id: env.tree_head_id,
|
|
614
|
+
}));
|
|
615
|
+
const chainPath = [
|
|
616
|
+
legEnvelopes[0].leaf.src_chain,
|
|
617
|
+
...legEnvelopes.map((e) => e.leaf.dst_chain),
|
|
618
|
+
];
|
|
619
|
+
return {
|
|
620
|
+
schema: SCHEMA_MULTI_HOP_BRIDGE,
|
|
621
|
+
route_id:
|
|
622
|
+
meta.route_id ||
|
|
623
|
+
`mh-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
|
|
624
|
+
chain_path: chainPath,
|
|
625
|
+
leg_count: legEnvelopes.length,
|
|
626
|
+
legs: legEnvelopes,
|
|
627
|
+
route_summary: route,
|
|
628
|
+
total_amount: meta.total_amount || null,
|
|
629
|
+
asset: meta.asset || null,
|
|
630
|
+
issued_at: new Date().toISOString(),
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Verify a multi-hop wrapper envelope. Each leg must verify against its
|
|
636
|
+
* own landmark + the chain path must be continuous + at least one leg
|
|
637
|
+
* must be the "lock" op (the trigger).
|
|
638
|
+
*
|
|
639
|
+
* @param {object} wrapper - multi-hop envelope from buildMultiHopBridgeEnvelope
|
|
640
|
+
* @param {Array<{landmark: object}>} legCacheEntries - one per leg, in order
|
|
641
|
+
* @returns {{ ok: boolean, code?: string, leg_results?: Array<{ok, code?}> }}
|
|
642
|
+
*/
|
|
643
|
+
export function verifyMultiHopBridgeEnvelope(wrapper, legCacheEntries) {
|
|
644
|
+
if (!wrapper || wrapper.schema !== SCHEMA_MULTI_HOP_BRIDGE) {
|
|
645
|
+
return { ok: false, code: "BAD_MULTIHOP_SCHEMA" };
|
|
646
|
+
}
|
|
647
|
+
if (!Array.isArray(wrapper.legs) || wrapper.legs.length < 2) {
|
|
648
|
+
return { ok: false, code: "BAD_LEG_COUNT" };
|
|
649
|
+
}
|
|
650
|
+
if (
|
|
651
|
+
!Array.isArray(legCacheEntries) ||
|
|
652
|
+
legCacheEntries.length !== wrapper.legs.length
|
|
653
|
+
) {
|
|
654
|
+
return { ok: false, code: "LEG_CACHE_COUNT_MISMATCH" };
|
|
655
|
+
}
|
|
656
|
+
const lib = mtcLib;
|
|
657
|
+
const results = [];
|
|
658
|
+
for (let i = 0; i < wrapper.legs.length; i++) {
|
|
659
|
+
const leg = wrapper.legs[i];
|
|
660
|
+
const cache = new lib.LandmarkCache({
|
|
661
|
+
signatureVerifier: lib.alwaysAcceptSignatureVerifier,
|
|
662
|
+
});
|
|
663
|
+
try {
|
|
664
|
+
cache.ingest(legCacheEntries[i].landmark);
|
|
665
|
+
} catch (err) {
|
|
666
|
+
results.push({ ok: false, code: err.code || "LANDMARK_REJECT" });
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
const r = verifyBridgeEnvelope(leg, cache);
|
|
670
|
+
results.push(r);
|
|
671
|
+
if (i > 0) {
|
|
672
|
+
const prev = wrapper.legs[i - 1];
|
|
673
|
+
if (prev.leaf.dst_chain !== leg.leaf.src_chain) {
|
|
674
|
+
return {
|
|
675
|
+
ok: false,
|
|
676
|
+
code: "ROUTE_DISCONTINUITY",
|
|
677
|
+
leg_results: results,
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
const allOk = results.every((r) => r.ok);
|
|
683
|
+
return { ok: allOk, leg_results: results };
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
687
|
+
// v0.2 — Gas-aware batch trigger
|
|
688
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
689
|
+
|
|
690
|
+
const DEFAULT_GAS_PROFILE = Object.freeze({
|
|
691
|
+
// gas-cost units per chain, normalized to USD per tx (heuristic)
|
|
692
|
+
// Aligns with the existing fee estimator in cross-chain.js
|
|
693
|
+
ethereum: 5.0,
|
|
694
|
+
polygon: 0.01,
|
|
695
|
+
bsc: 0.1,
|
|
696
|
+
arbitrum: 0.3,
|
|
697
|
+
solana: 0.005,
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Decide whether to close the batch now or defer to the next interval
|
|
702
|
+
* based on a target chain's current gas price (USD-equivalent).
|
|
703
|
+
*
|
|
704
|
+
* Heuristic: defer when current gas > 1.5x the chain's baseline AND
|
|
705
|
+
* the staged-ops count is below the "hard close" floor. Otherwise close.
|
|
706
|
+
*
|
|
707
|
+
* @param {{
|
|
708
|
+
* target_chain: string,
|
|
709
|
+
* current_gas_usd?: number, // observed; if absent uses baseline
|
|
710
|
+
* staged_count: number,
|
|
711
|
+
* hard_close_floor?: number, // always close at or above this count
|
|
712
|
+
* defer_multiplier?: number, // gas-multiplier triggering defer
|
|
713
|
+
* }} args
|
|
714
|
+
* @returns {{ close: boolean, reason: string, baseline_usd: number, current_usd: number, staged_count: number }}
|
|
715
|
+
*/
|
|
716
|
+
export function shouldCloseBatchGasAware(args = {}) {
|
|
717
|
+
if (!args.target_chain || typeof args.target_chain !== "string") {
|
|
718
|
+
throw new TypeError("shouldCloseBatchGasAware: target_chain required");
|
|
719
|
+
}
|
|
720
|
+
const baseline = DEFAULT_GAS_PROFILE[args.target_chain];
|
|
721
|
+
if (baseline === undefined) {
|
|
722
|
+
throw new RangeError(
|
|
723
|
+
`shouldCloseBatchGasAware: unknown target_chain "${args.target_chain}"`,
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
const current =
|
|
727
|
+
typeof args.current_gas_usd === "number" ? args.current_gas_usd : baseline;
|
|
728
|
+
const staged = Number.isInteger(args.staged_count) ? args.staged_count : 0;
|
|
729
|
+
const hardFloor = Number.isInteger(args.hard_close_floor)
|
|
730
|
+
? args.hard_close_floor
|
|
731
|
+
: 50;
|
|
732
|
+
const deferMult =
|
|
733
|
+
typeof args.defer_multiplier === "number" ? args.defer_multiplier : 1.5;
|
|
734
|
+
|
|
735
|
+
if (staged >= hardFloor) {
|
|
736
|
+
return {
|
|
737
|
+
close: true,
|
|
738
|
+
reason: "STAGED_COUNT_AT_HARD_FLOOR",
|
|
739
|
+
baseline_usd: baseline,
|
|
740
|
+
current_usd: current,
|
|
741
|
+
staged_count: staged,
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
if (current > baseline * deferMult) {
|
|
745
|
+
return {
|
|
746
|
+
close: false,
|
|
747
|
+
reason: "GAS_HIGH_DEFERRED",
|
|
748
|
+
baseline_usd: baseline,
|
|
749
|
+
current_usd: current,
|
|
750
|
+
staged_count: staged,
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
return {
|
|
754
|
+
close: true,
|
|
755
|
+
reason: "GAS_NORMAL_OR_LOW",
|
|
756
|
+
baseline_usd: baseline,
|
|
757
|
+
current_usd: current,
|
|
758
|
+
staged_count: staged,
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
763
|
+
// v0.2 — SLA integration (cc sla compatible export)
|
|
764
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* Export bridge MTC operational metrics in cc sla compatible shape.
|
|
768
|
+
* Reads on-disk staging + batches + sync-stats and synthesizes:
|
|
769
|
+
* - latest_publish_age_seconds (newer than threshold = healthy)
|
|
770
|
+
* - staged_pending_count (low = healthy)
|
|
771
|
+
* - batches_per_hour (heuristic from batch dir mtimes)
|
|
772
|
+
* - sla_status: ok | degraded | down
|
|
773
|
+
*
|
|
774
|
+
* @param {string} dir - cross-chain-mtc dir
|
|
775
|
+
* @param {{ now?: number }} [opts]
|
|
776
|
+
*/
|
|
777
|
+
export function getBridgeMtcSlaMetrics(dir, opts = {}) {
|
|
778
|
+
ensureDirs(dir);
|
|
779
|
+
const now = opts.now || Date.now();
|
|
780
|
+
const status = getBridgeMtcStatus(dir);
|
|
781
|
+
|
|
782
|
+
// Compute batches_per_hour from batches/ mtimes (last hour only)
|
|
783
|
+
let batchesLastHour = 0;
|
|
784
|
+
let latestBatchMtime = null;
|
|
785
|
+
const bDir = batchesDir(dir);
|
|
786
|
+
if (fs.existsSync(bDir)) {
|
|
787
|
+
for (const name of fs.readdirSync(bDir)) {
|
|
788
|
+
try {
|
|
789
|
+
const stat = fs.statSync(path.join(bDir, name));
|
|
790
|
+
if (now - stat.mtimeMs <= 3600 * 1000) batchesLastHour++;
|
|
791
|
+
if (!latestBatchMtime || stat.mtimeMs > latestBatchMtime) {
|
|
792
|
+
latestBatchMtime = stat.mtimeMs;
|
|
793
|
+
}
|
|
794
|
+
} catch (_err) {
|
|
795
|
+
/* skip stat failures */
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
const stagedAge = latestBatchMtime
|
|
801
|
+
? Math.round((now - latestBatchMtime) / 1000)
|
|
802
|
+
: null;
|
|
803
|
+
|
|
804
|
+
// Status:
|
|
805
|
+
// - down: enabled but no batches in 24h AND staging > 0
|
|
806
|
+
// - degraded: staging > 100 OR no batch in 30 min while enabled
|
|
807
|
+
// - ok: otherwise
|
|
808
|
+
let slaStatus = "ok";
|
|
809
|
+
if (status.enabled) {
|
|
810
|
+
if (status.staging.pending > 100) slaStatus = "degraded";
|
|
811
|
+
else if (
|
|
812
|
+
stagedAge !== null &&
|
|
813
|
+
stagedAge > 30 * 60 &&
|
|
814
|
+
status.staging.pending > 0
|
|
815
|
+
) {
|
|
816
|
+
slaStatus = "degraded";
|
|
817
|
+
}
|
|
818
|
+
if (
|
|
819
|
+
stagedAge !== null &&
|
|
820
|
+
stagedAge > 24 * 3600 &&
|
|
821
|
+
status.staging.pending > 0
|
|
822
|
+
) {
|
|
823
|
+
slaStatus = "down";
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
return {
|
|
828
|
+
sla_status: slaStatus,
|
|
829
|
+
enabled: status.enabled,
|
|
830
|
+
mode: status.mode,
|
|
831
|
+
staged_pending_count: status.staging.pending,
|
|
832
|
+
batches_total: status.batches.total,
|
|
833
|
+
batches_last_hour: batchesLastHour,
|
|
834
|
+
seconds_since_last_batch: stagedAge,
|
|
835
|
+
measured_at: new Date(now).toISOString(),
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
840
|
+
// Status snapshot (for `cc crosschain mtc-status`)
|
|
841
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Read filesystem state and return a status summary suitable for CLI / IPC.
|
|
845
|
+
* No DB access — pure fs/json reads.
|
|
846
|
+
*/
|
|
847
|
+
export function getBridgeMtcStatus(dir) {
|
|
848
|
+
ensureDirs(dir);
|
|
849
|
+
const cfg = loadCrossChainMtcConfig(dir);
|
|
850
|
+
const taStore = loadTrustAnchors(dir);
|
|
851
|
+
const taChains = Object.keys(taStore.anchors);
|
|
852
|
+
const taTotal = taChains.reduce(
|
|
853
|
+
(sum, c) => sum + (taStore.anchors[c]?.length || 0),
|
|
854
|
+
0,
|
|
855
|
+
);
|
|
856
|
+
|
|
857
|
+
let batchCount = 0;
|
|
858
|
+
let latestBatch = null;
|
|
859
|
+
const bDir = batchesDir(dir);
|
|
860
|
+
if (fs.existsSync(bDir)) {
|
|
861
|
+
const entries = fs.readdirSync(bDir).sort();
|
|
862
|
+
batchCount = entries.length;
|
|
863
|
+
if (batchCount > 0) latestBatch = entries[batchCount - 1];
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
let stagedCount = 0;
|
|
867
|
+
const sDir = stagingDir(dir);
|
|
868
|
+
if (fs.existsSync(sDir)) {
|
|
869
|
+
stagedCount = fs
|
|
870
|
+
.readdirSync(sDir)
|
|
871
|
+
.filter((n) => n.endsWith(".json")).length;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
return {
|
|
875
|
+
enabled: cfg.enabled,
|
|
876
|
+
mode: cfg.mode,
|
|
877
|
+
alg: cfg.alg,
|
|
878
|
+
batch_interval_seconds: cfg.batch_interval_seconds,
|
|
879
|
+
issuer: cfg.issuer,
|
|
880
|
+
trust_anchors: {
|
|
881
|
+
chain_count: taChains.length,
|
|
882
|
+
total: taTotal,
|
|
883
|
+
by_chain: Object.fromEntries(
|
|
884
|
+
taChains.map((c) => [c, taStore.anchors[c].length]),
|
|
885
|
+
),
|
|
886
|
+
},
|
|
887
|
+
staging: {
|
|
888
|
+
pending: stagedCount,
|
|
889
|
+
},
|
|
890
|
+
batches: {
|
|
891
|
+
total: batchCount,
|
|
892
|
+
latest: latestBatch,
|
|
893
|
+
},
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// Internal exports for tests
|
|
898
|
+
export const _internal = {
|
|
899
|
+
CONFIG_DEFAULTS,
|
|
900
|
+
SUPPORTED_BRIDGE_CHAINS,
|
|
901
|
+
VALID_BRIDGE_OPS,
|
|
902
|
+
BRIDGE_NAMESPACE_PREFIX,
|
|
903
|
+
BRIDGE_NAMESPACE_RE,
|
|
904
|
+
};
|