@bsv/dpp-overlay-topics 0.4.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/LICENSE +58 -0
  2. package/README.md +169 -0
  3. package/dist/anchorStorage.d.ts +58 -0
  4. package/dist/anchorStorage.d.ts.map +1 -0
  5. package/dist/anchorStorage.js +72 -0
  6. package/dist/anchorStorage.js.map +1 -0
  7. package/dist/attestationAnchor.d.ts +10 -0
  8. package/dist/attestationAnchor.d.ts.map +1 -0
  9. package/dist/attestationAnchor.js +10 -0
  10. package/dist/attestationAnchor.js.map +1 -0
  11. package/dist/attestationStorage.d.ts +49 -0
  12. package/dist/attestationStorage.d.ts.map +1 -0
  13. package/dist/attestationStorage.js +78 -0
  14. package/dist/attestationStorage.js.map +1 -0
  15. package/dist/capabilities.d.ts +127 -0
  16. package/dist/capabilities.d.ts.map +1 -0
  17. package/dist/capabilities.js +210 -0
  18. package/dist/capabilities.js.map +1 -0
  19. package/dist/engineStorage.d.ts +106 -0
  20. package/dist/engineStorage.d.ts.map +1 -0
  21. package/dist/engineStorage.js +253 -0
  22. package/dist/engineStorage.js.map +1 -0
  23. package/dist/evidenceExport.d.ts +189 -0
  24. package/dist/evidenceExport.d.ts.map +1 -0
  25. package/dist/evidenceExport.js +517 -0
  26. package/dist/evidenceExport.js.map +1 -0
  27. package/dist/history.d.ts +114 -0
  28. package/dist/history.d.ts.map +1 -0
  29. package/dist/history.js +215 -0
  30. package/dist/history.js.map +1 -0
  31. package/dist/index.d.ts +269 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +1223 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/lib.d.ts +31 -0
  36. package/dist/lib.d.ts.map +1 -0
  37. package/dist/lib.js +31 -0
  38. package/dist/lib.js.map +1 -0
  39. package/dist/limits.d.ts +44 -0
  40. package/dist/limits.d.ts.map +1 -0
  41. package/dist/limits.js +44 -0
  42. package/dist/limits.js.map +1 -0
  43. package/dist/lsAttestation.d.ts +21 -0
  44. package/dist/lsAttestation.d.ts.map +1 -0
  45. package/dist/lsAttestation.js +45 -0
  46. package/dist/lsAttestation.js.map +1 -0
  47. package/dist/lsDpp.d.ts +53 -0
  48. package/dist/lsDpp.d.ts.map +1 -0
  49. package/dist/lsDpp.js +148 -0
  50. package/dist/lsDpp.js.map +1 -0
  51. package/dist/lsUoraDpp.d.ts +55 -0
  52. package/dist/lsUoraDpp.d.ts.map +1 -0
  53. package/dist/lsUoraDpp.js +152 -0
  54. package/dist/lsUoraDpp.js.map +1 -0
  55. package/dist/policyConfig.d.ts +31 -0
  56. package/dist/policyConfig.d.ts.map +1 -0
  57. package/dist/policyConfig.js +112 -0
  58. package/dist/policyConfig.js.map +1 -0
  59. package/dist/retraction.d.ts +41 -0
  60. package/dist/retraction.d.ts.map +1 -0
  61. package/dist/retraction.js +147 -0
  62. package/dist/retraction.js.map +1 -0
  63. package/dist/storage.d.ts +117 -0
  64. package/dist/storage.d.ts.map +1 -0
  65. package/dist/storage.js +158 -0
  66. package/dist/storage.js.map +1 -0
  67. package/dist/sync.d.ts +75 -0
  68. package/dist/sync.d.ts.map +1 -0
  69. package/dist/sync.js +118 -0
  70. package/dist/sync.js.map +1 -0
  71. package/dist/tmAttestation.d.ts +36 -0
  72. package/dist/tmAttestation.d.ts.map +1 -0
  73. package/dist/tmAttestation.js +50 -0
  74. package/dist/tmAttestation.js.map +1 -0
  75. package/dist/tmDpp.d.ts +154 -0
  76. package/dist/tmDpp.d.ts.map +1 -0
  77. package/dist/tmDpp.js +397 -0
  78. package/dist/tmDpp.js.map +1 -0
  79. package/dist/tmUoraDpp.d.ts +73 -0
  80. package/dist/tmUoraDpp.d.ts.map +1 -0
  81. package/dist/tmUoraDpp.js +150 -0
  82. package/dist/tmUoraDpp.js.map +1 -0
  83. package/dist/uoraAnchor.d.ts +209 -0
  84. package/dist/uoraAnchor.d.ts.map +1 -0
  85. package/dist/uoraAnchor.js +433 -0
  86. package/dist/uoraAnchor.js.map +1 -0
  87. package/package.json +51 -0
@@ -0,0 +1,152 @@
1
+ import { tryParseUoraAnchor, UORA_ANCHOR_PREFIX } from './uoraAnchor.js';
2
+ export const UORA_TOPIC = 'tm_uora_dpp';
3
+ export const UORA_SERVICE = 'ls_uora_dpp';
4
+ /**
5
+ * `ls_uora_dpp`: the DID-keyed lookup.
6
+ *
7
+ * The question this exists to answer is "what has this party attested", keyed
8
+ * on the issuer's `did:key`, and it is the question no index over the v1 anchor
9
+ * format could answer at all: a v1 anchor names no issuer, so an indexer
10
+ * holding one has a digest and no idea whose claim it covers.
11
+ *
12
+ * Four other selectors come free from the same fields and are worth having:
13
+ * the subject (every claim about one passport, from every party), the
14
+ * attestation id (the anchor for one known claim), the digest (given an
15
+ * attestation in hand, has anyone anchored exactly this), and the UORA type,
16
+ * which narrows any of the others.
17
+ *
18
+ * ## What the answer is, and what it is not
19
+ *
20
+ * The answer is outputs, as BRC-24 requires, so the caller receives the anchors
21
+ * as chain data and checks them without trusting this service. That matters
22
+ * more here than for a passport lookup: this index is derived from the outputs,
23
+ * so an index that lied would be caught by the caller reading the same outputs
24
+ * it just returned.
25
+ *
26
+ * The answer is **not** the attestations. Those are never on chain, by the
27
+ * privacy rule, so a caller who wants the claim itself fetches it from the
28
+ * resolver and checks its digest against the anchor this returned. The anchor
29
+ * is the proof; the resolver is merely convenient.
30
+ */
31
+ export class UoraAnchorLookupService {
32
+ storage;
33
+ admissionMode = 'locking-script';
34
+ spendNotificationMode = 'txid';
35
+ constructor(storage) {
36
+ this.storage = storage;
37
+ }
38
+ async outputAdmittedByTopic(payload) {
39
+ if (payload.mode !== 'locking-script')
40
+ throw new Error('Invalid admission mode');
41
+ if (payload.topic !== UORA_TOPIC)
42
+ return;
43
+ const anchor = tryParseUoraAnchor(payload.lockingScript);
44
+ // Admission already validated this; a null here would mean the topic
45
+ // manager and this service disagree, and indexing a half-read output is
46
+ // worse than not indexing it.
47
+ if (anchor == null)
48
+ return;
49
+ await this.storage.insert({
50
+ txid: payload.txid,
51
+ outputIndex: payload.outputIndex,
52
+ digest: anchor.digest,
53
+ attestationId: anchor.attestationId,
54
+ issuer: anchor.issuer,
55
+ issuerKey: anchor.issuerKey,
56
+ subject: anchor.subject,
57
+ uoraType: anchor.uoraType,
58
+ anchoredBy: anchor.anchoredBy,
59
+ lockingKey: anchor.lockingKey,
60
+ createdAt: new Date(),
61
+ });
62
+ }
63
+ /**
64
+ * An anchor is a leaf and should never be spent. If one is, the claim it
65
+ * carries is unaffected: the digest sat at that point in the chain's order
66
+ * whatever later happened to the satoshi. So the record stays, and the spend
67
+ * is not recorded, because nothing about this index turns on it.
68
+ */
69
+ async outputSpent(payload) {
70
+ if (payload.mode !== 'txid')
71
+ throw new Error('Invalid spend notification mode');
72
+ }
73
+ async outputNoLongerRetainedInHistory(txid, outputIndex, topic) {
74
+ if (topic !== UORA_TOPIC)
75
+ return;
76
+ await this.storage.delete(txid, outputIndex);
77
+ }
78
+ async outputEvicted(txid, outputIndex) {
79
+ await this.storage.delete(txid, outputIndex);
80
+ }
81
+ async lookup(question) {
82
+ if (question.service !== UORA_SERVICE) {
83
+ throw new Error(`Unsupported service ${question.service}`);
84
+ }
85
+ const query = (question.query ?? {});
86
+ // Refused rather than ignored: `selectors()` in the storage layer would
87
+ // drop a non-string field, and a query that silently loses a selector
88
+ // answers broader than it was asked, up to a full scan under the cap.
89
+ const fields = [
90
+ 'issuer',
91
+ 'issuerKey',
92
+ 'subject',
93
+ 'attestationId',
94
+ 'digest',
95
+ 'uoraType',
96
+ 'anchoredBy',
97
+ ];
98
+ for (const field of fields) {
99
+ const value = query[field];
100
+ if (value != null && typeof value !== 'string') {
101
+ throw new Error(`Query field ${field} must be a string`);
102
+ }
103
+ }
104
+ // `uoraType` alone is every anchor of a common type, which is a table scan
105
+ // wearing a query, so it narrows and never selects.
106
+ const selective = [
107
+ query.issuer,
108
+ query.issuerKey,
109
+ query.subject,
110
+ query.attestationId,
111
+ query.digest,
112
+ query.anchoredBy,
113
+ ].some((value) => typeof value === 'string' && value !== '');
114
+ if (!selective) {
115
+ throw new Error('Query must provide issuer, issuerKey, subject, attestationId, digest or anchoredBy');
116
+ }
117
+ const records = await this.storage.find(query);
118
+ return records.map((record) => ({ txid: record.txid, outputIndex: record.outputIndex }));
119
+ }
120
+ async getDocumentation() {
121
+ return [
122
+ `# ${UORA_SERVICE}`,
123
+ '',
124
+ `Lookup for ${UORA_ANCHOR_PREFIX} attestation anchors, keyed on the issuer DID.`,
125
+ '',
126
+ 'Query with at least one of `issuer` (a did:key), `issuerKey` (the same',
127
+ 'key as hex), `subject` (a passport id), `attestationId`, `digest`, or',
128
+ '`anchoredBy` (the treasury that wrote the anchor). `uoraType` narrows any',
129
+ 'of them and cannot select on its own. All are',
130
+ 'exact matches. `limit` caps the answer and is itself capped.',
131
+ '',
132
+ 'Every indexed field is one the anchoring service signed, at the boundary',
133
+ 'it is read at: the signature covers each field behind its length, so the',
134
+ 'subject this index is keyed on is the subject that was written. Anchors',
135
+ 'in the superseded uora-anchor-v2 layout are not admitted and are',
136
+ 'therefore not indexed, because their signature left that boundary open.',
137
+ '',
138
+ 'The answer is the anchor outputs as BEEF, so the caller verifies them',
139
+ 'against the chain rather than trusting this index. The attestations',
140
+ 'themselves are never on chain: fetch one from the resolver and check its',
141
+ "canonical digest against the anchor's.",
142
+ ].join('\n');
143
+ }
144
+ async getMetaData() {
145
+ return {
146
+ name: UORA_SERVICE,
147
+ shortDescription: 'UORA attestation anchor lookup, keyed on issuer DID',
148
+ version: '1.0.0',
149
+ };
150
+ }
151
+ }
152
+ //# sourceMappingURL=lsUoraDpp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lsUoraDpp.js","sourceRoot":"","sources":["../src/lsUoraDpp.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ import { type PublisherPolicy, type PublisherRole } from '@bsv/dpp-core';
2
+ export interface PublisherPolicyConfig {
3
+ /** The verified chain, oldest first. */
4
+ chain: PublisherPolicy[];
5
+ /** Operator identifier to compressed identity key, from the environment. */
6
+ operators: Record<string, string>;
7
+ /** The versions `verifyPolicyChain` accepted, in chain order. */
8
+ versions: number[];
9
+ /** Where the chain came from, for logs. */
10
+ source: string;
11
+ }
12
+ /** `operator=compressedKey` pairs, comma-separated. Malformed input stops the boot rather than naming no operator. */
13
+ export declare function parseOperatorIdentityKeys(value: string | undefined): Record<string, string>;
14
+ /** Read and verify a chain file. Every failure is an Error whose message names the file, the version and the reason. */
15
+ export declare function loadPublisherPolicy(file: string, operators: Record<string, string>): PublisherPolicyConfig;
16
+ /** The policy the environment configures, or undefined when it configures none. */
17
+ export declare function publisherPolicyFromEnvironment(): PublisherPolicyConfig | undefined;
18
+ /**
19
+ * The publisher keys a topic accepts at an instant under the chain.
20
+ *
21
+ * `undefined` means the version in force does not cover the topic: a policy
22
+ * may name `scope.topics`, and a topic outside them keeps its static
23
+ * configuration (the identity key, or ANCHOR_SERVICE_KEYS). An empty array
24
+ * means the policy covers the topic and no key is active, which admits
25
+ * nothing, and is also the answer for an instant before the first version was
26
+ * issued: a key admits nothing dated before its activation.
27
+ */
28
+ export declare function policyKeysFor(chain: PublisherPolicy[], at: string | Date, role: PublisherRole, topic: string): string[] | undefined;
29
+ /** The newest version of the chain, the one that names the operator profile and the current keys. */
30
+ export declare function newestPolicy(config: PublisherPolicyConfig): PublisherPolicy;
31
+ //# sourceMappingURL=policyConfig.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policyConfig.d.ts","sourceRoot":"","sources":["../src/policyConfig.ts"],"names":[],"mappings":"AAkBA,OAAO,EAIL,KAAK,eAAe,EACpB,KAAK,aAAa,EACnB,MAAM,eAAe,CAAA;AAEtB,MAAM,WAAW,qBAAqB;IACpC,wCAAwC;IACxC,KAAK,EAAE,eAAe,EAAE,CAAA;IACxB,4EAA4E;IAC5E,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACjC,iEAAiE;IACjE,QAAQ,EAAE,MAAM,EAAE,CAAA;IAClB,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAA;CACf;AAID,sHAAsH;AACtH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAyB3F;AAED,wHAAwH;AACxH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,qBAAqB,CAyB1G;AAED,mFAAmF;AACnF,wBAAgB,8BAA8B,IAAI,qBAAqB,GAAG,SAAS,CAalF;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,eAAe,EAAE,EACxB,EAAE,EAAE,MAAM,GAAG,IAAI,EACjB,IAAI,EAAE,aAAa,EACnB,KAAK,EAAE,MAAM,GACZ,MAAM,EAAE,GAAG,SAAS,CAKtB;AAED,qGAAqG;AACrG,wBAAgB,YAAY,CAAC,MAAM,EAAE,qBAAqB,GAAG,eAAe,CAE3E"}
@@ -0,0 +1,112 @@
1
+ /**
2
+ * The publisher key policy as this node is configured with it
3
+ * (`spec/services.md` section 1, `contracts/publisher-policy.schema.json`).
4
+ *
5
+ * `PUBLISHER_POLICY_FILE` names a JSON file holding the chain of
6
+ * `dpp-publisher-policy@1` versions, oldest first, and `OPERATOR_IDENTITY_KEYS`
7
+ * names each operator's identity key, because the chain's genesis is signed
8
+ * by an operator and the policy itself is never the source of the key that
9
+ * authorises it. The chain is verified once, at boot, with the core's
10
+ * `verifyPolicyChain`; a chain that does not verify stops the boot and names
11
+ * the version and the reason, since admitting under an unverified list would
12
+ * be admitting under whatever a file said.
13
+ *
14
+ * Without a policy file nothing here is consulted: the single
15
+ * `SERVICE_IDENTITY_KEY` is an implicit single-operator policy with no
16
+ * rotation history, and the capability document says so.
17
+ */
18
+ import { readFileSync } from 'node:fs';
19
+ import { policyInForceAt, publisherKeysAt, verifyPolicyChain, } from '@bsv/dpp-core';
20
+ const COMPRESSED_KEY = /^0[23][0-9a-f]{64}$/;
21
+ /** `operator=compressedKey` pairs, comma-separated. Malformed input stops the boot rather than naming no operator. */
22
+ export function parseOperatorIdentityKeys(value) {
23
+ const operators = {};
24
+ const pairs = (value ?? '')
25
+ .split(',')
26
+ .map((pair) => pair.trim())
27
+ .filter((pair) => pair !== '');
28
+ for (const pair of pairs) {
29
+ const separator = pair.indexOf('=');
30
+ if (separator <= 0) {
31
+ throw new Error(`OPERATOR_IDENTITY_KEYS entry "${pair}" is not of the form operator=compressedKey`);
32
+ }
33
+ const operator = pair.slice(0, separator).trim();
34
+ const key = pair.slice(separator + 1).trim();
35
+ if (!COMPRESSED_KEY.test(key)) {
36
+ throw new Error(`OPERATOR_IDENTITY_KEYS: the key given for ${operator} is not a compressed public key`);
37
+ }
38
+ if (operators[operator] != null)
39
+ throw new Error(`OPERATOR_IDENTITY_KEYS names ${operator} twice`);
40
+ // One key under two names would let it sign a federation rotation and
41
+ // countersign it as "another operator": the whole point of the
42
+ // countersignature is that it comes from a different key.
43
+ const holder = Object.entries(operators).find(([, existing]) => existing === key)?.[0];
44
+ if (holder != null)
45
+ throw new Error(`OPERATOR_IDENTITY_KEYS gives ${operator} the same key as ${holder}`);
46
+ operators[operator] = key;
47
+ }
48
+ return operators;
49
+ }
50
+ /** Read and verify a chain file. Every failure is an Error whose message names the file, the version and the reason. */
51
+ export function loadPublisherPolicy(file, operators) {
52
+ let text;
53
+ try {
54
+ text = readFileSync(file, 'utf8');
55
+ }
56
+ catch (cause) {
57
+ throw new Error(`PUBLISHER_POLICY_FILE ${file} could not be read: ${cause instanceof Error ? cause.message : String(cause)}`);
58
+ }
59
+ let parsed;
60
+ try {
61
+ parsed = JSON.parse(text);
62
+ }
63
+ catch {
64
+ throw new Error(`PUBLISHER_POLICY_FILE ${file} is not JSON`);
65
+ }
66
+ if (!Array.isArray(parsed) || parsed.some((p) => typeof p !== 'object' || p === null || Array.isArray(p))) {
67
+ throw new Error(`PUBLISHER_POLICY_FILE ${file} must hold a JSON array of dpp-publisher-policy@1 documents, oldest first`);
68
+ }
69
+ const chain = parsed;
70
+ const result = verifyPolicyChain(chain, operators);
71
+ if (!result.ok || result.failure != null) {
72
+ const failure = result.failure ?? { version: 0, reason: 'format', detail: 'the chain did not verify' };
73
+ throw new Error(`PUBLISHER_POLICY_FILE ${file}: policy version ${failure.version} refused, ${failure.reason}: ${failure.detail}`);
74
+ }
75
+ return { chain, operators, versions: result.versions, source: file };
76
+ }
77
+ /** The policy the environment configures, or undefined when it configures none. */
78
+ export function publisherPolicyFromEnvironment() {
79
+ const file = (process.env.PUBLISHER_POLICY_FILE ?? '').trim();
80
+ const operators = process.env.OPERATOR_IDENTITY_KEYS;
81
+ if (file === '') {
82
+ if (operators != null && operators.trim() !== '') {
83
+ console.warn('OPERATOR_IDENTITY_KEYS is set but PUBLISHER_POLICY_FILE is not: the operator keys are ignored, ' +
84
+ 'because there is no policy chain for them to authorise');
85
+ }
86
+ return undefined;
87
+ }
88
+ return loadPublisherPolicy(file, parseOperatorIdentityKeys(operators));
89
+ }
90
+ /**
91
+ * The publisher keys a topic accepts at an instant under the chain.
92
+ *
93
+ * `undefined` means the version in force does not cover the topic: a policy
94
+ * may name `scope.topics`, and a topic outside them keeps its static
95
+ * configuration (the identity key, or ANCHOR_SERVICE_KEYS). An empty array
96
+ * means the policy covers the topic and no key is active, which admits
97
+ * nothing, and is also the answer for an instant before the first version was
98
+ * issued: a key admits nothing dated before its activation.
99
+ */
100
+ export function policyKeysFor(chain, at, role, topic) {
101
+ const inForce = policyInForceAt(chain, at);
102
+ if (inForce == null)
103
+ return [];
104
+ if (inForce.scope.topics != null && !inForce.scope.topics.includes(topic))
105
+ return undefined;
106
+ return publisherKeysAt(chain, at, role);
107
+ }
108
+ /** The newest version of the chain, the one that names the operator profile and the current keys. */
109
+ export function newestPolicy(config) {
110
+ return config.chain[config.chain.length - 1];
111
+ }
112
+ //# sourceMappingURL=policyConfig.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policyConfig.js","sourceRoot":"","sources":["../src/policyConfig.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ import type { LookupService } from '@bsv/overlay';
2
+ import type { RetractableStorage } from './engineStorage.js';
3
+ import type { DppRecordStore } from './storage.js';
4
+ export type RetractionRefusalCode = 'output-unknown' | 'retraction-refused' | 'chain-tracker-unavailable';
5
+ export declare class RetractionRefused extends Error {
6
+ readonly status: number;
7
+ readonly code: RetractionRefusalCode;
8
+ constructor(status: number, code: RetractionRefusalCode, message: string);
9
+ }
10
+ export interface RetractionRequest {
11
+ storage: RetractableStorage;
12
+ lookupServices: Record<string, LookupService>;
13
+ records: DppRecordStore;
14
+ topic: string;
15
+ txid: string;
16
+ outputIndex: number;
17
+ reason: string;
18
+ /**
19
+ * Whether the network knows the transaction, asked of the configured chain
20
+ * tracker's source. Undefined when the deployment has no header source
21
+ * (CHAIN_TRACKER=scripts-only): the check is then not run, and the answer
22
+ * says so.
23
+ */
24
+ knownOnChain?: (txid: string) => Promise<boolean>;
25
+ }
26
+ export interface Retraction {
27
+ txid: string;
28
+ outputIndex: number;
29
+ reason: string;
30
+ /** Whether the network was asked; false under scripts-only, where the local proof check is the whole check. */
31
+ networkChecked: boolean;
32
+ /** The predecessor made the tip again, when the retracted state had one. */
33
+ restoredTip?: {
34
+ txid: string;
35
+ outputIndex: number;
36
+ };
37
+ /** Repairs the engine storage could not make, by name, when it is not one of this package's. */
38
+ unrepaired: string[];
39
+ }
40
+ export declare function retractOutput(request: RetractionRequest): Promise<Retraction>;
41
+ //# sourceMappingURL=retraction.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retraction.d.ts","sourceRoot":"","sources":["../src/retraction.ts"],"names":[],"mappings":"AAiCA,OAAO,KAAK,EAAE,aAAa,EAAU,MAAM,cAAc,CAAA;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AAC5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAElD,MAAM,MAAM,qBAAqB,GAAG,gBAAgB,GAAG,oBAAoB,GAAG,2BAA2B,CAAA;AAEzG,qBAAa,iBAAkB,SAAQ,KAAK;IAExC,QAAQ,CAAC,MAAM,EAAE,MAAM;IACvB,QAAQ,CAAC,IAAI,EAAE,qBAAqB;gBAD3B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,qBAAqB,EACpC,OAAO,EAAE,MAAM;CAIlB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,kBAAkB,CAAA;IAC3B,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAA;IAC7C,OAAO,EAAE,cAAc,CAAA;IACvB,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;IACd;;;;;OAKG;IACH,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;CAClD;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,+GAA+G;IAC/G,cAAc,EAAE,OAAO,CAAA;IACvB,4EAA4E;IAC5E,WAAW,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAA;IACnD,gGAAgG;IAChG,UAAU,EAAE,MAAM,EAAE,CAAA;CACrB;AAyBD,wBAAsB,aAAa,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,UAAU,CAAC,CAiFnF"}
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Retraction (`POST /retract`): removing a state the index admitted on
3
+ * announcement and the network then refused.
4
+ *
5
+ * `spec/writing.md` lets a writer announce a state before it sends it, so the
6
+ * index can hold an output no block will ever contain. That phantom sits on
7
+ * the passport as its tip: the writer's next state builds on nothing, and a
8
+ * reader's `chainFromBeef` finds two successors of one state and can order
9
+ * neither. The writer, who alone knows the network's answer, asks the index
10
+ * to forget the output.
11
+ *
12
+ * What is never retracted: a state with a merkle path (the network did take
13
+ * it), a state the configured chain tracker knows (same), and a spent state
14
+ * (its successor was admitted against it; retracting the middle of a history
15
+ * would leave the successor pointing at nothing).
16
+ *
17
+ * How the removal is done. The Engine's own removal path is its stale-output
18
+ * eviction, `deleteUTXODeep`: delete the output, tell every lookup service it
19
+ * is no longer retained, then recurse into the outputs it consumed and delete
20
+ * each of those too once nothing else consumes them. That recursion is right
21
+ * for the Engine's own case, a topic that did not retain a spent coin, and
22
+ * wrong here: on a history-retaining topic the consumed lineage is the whole
23
+ * lifecycle, and the retraction of one phantom would erase the passport back
24
+ * to its genesis. `deleteUTXODeep` is also private to the Engine. So this
25
+ * module performs the eviction's first step through the same storage and
26
+ * lookup-service calls (`deleteOutput`, then the eviction hook), repairs the
27
+ * predecessor's `consumedBy` exactly as the eviction does, and stops there;
28
+ * then it makes the two repairs the eviction never needs, clearing the
29
+ * predecessor's spent mark and the phantom's applied-transaction record, so
30
+ * the real tip is a tip again and the same transaction can be announced
31
+ * again should the network take it after all.
32
+ */
33
+ import { Beef } from '@bsv/sdk';
34
+ export class RetractionRefused extends Error {
35
+ status;
36
+ code;
37
+ constructor(status, code, message) {
38
+ super(message);
39
+ this.status = status;
40
+ this.code = code;
41
+ }
42
+ }
43
+ /** Whether the stored BEEF proves the transaction: a bump for its txid, or a merkle path on it. */
44
+ function beefProves(beef, txid) {
45
+ if (beef == null)
46
+ return false;
47
+ try {
48
+ const parsed = Beef.fromBinary(beef);
49
+ const held = parsed.findTxid(txid);
50
+ return held != null && (held.bumpIndex != null || held.tx?.merklePath != null);
51
+ }
52
+ catch {
53
+ return false;
54
+ }
55
+ }
56
+ /** Why an output as it stands may not be retracted, or undefined when it may. */
57
+ function refusalFor(output, txid) {
58
+ if (output.spent || output.consumedBy.length > 0) {
59
+ return 'the output is spent: a later state was admitted against it, and a history is never cut in the middle';
60
+ }
61
+ if (output.blockHeight != null || beefProves(output.beef, txid)) {
62
+ return 'this index holds a merkle path for the output: the network took the transaction, so there is nothing to retract';
63
+ }
64
+ return undefined;
65
+ }
66
+ export async function retractOutput(request) {
67
+ const { storage, topic, txid, outputIndex } = request;
68
+ const output = await storage.findOutput(txid, outputIndex, topic, undefined, true);
69
+ if (output == null) {
70
+ throw new RetractionRefused(404, 'output-unknown', `this index holds no ${topic} output ${txid}:${outputIndex}`);
71
+ }
72
+ const refusal = refusalFor(output, txid);
73
+ if (refusal != null)
74
+ throw new RetractionRefused(409, 'retraction-refused', refusal);
75
+ let networkChecked = false;
76
+ if (request.knownOnChain != null) {
77
+ let known;
78
+ try {
79
+ known = await request.knownOnChain(txid);
80
+ }
81
+ catch (cause) {
82
+ throw new RetractionRefused(503, 'chain-tracker-unavailable', `the chain tracker could not be asked whether it knows ${txid}; the retraction was not applied, try again: ${cause instanceof Error ? cause.message : String(cause)}`);
83
+ }
84
+ if (known) {
85
+ throw new RetractionRefused(409, 'retraction-refused', 'the chain tracker knows this transaction: the network took it, so there is nothing to retract');
86
+ }
87
+ networkChecked = true;
88
+ }
89
+ // Nothing here is atomic with the Engine: while the network was asked, a
90
+ // /submit may have admitted a successor or an /arc-ingest may have proven
91
+ // the output. So the output is read again immediately before the delete
92
+ // and refused if it no longer qualifies; the window left is the delete
93
+ // itself, which the storage interface gives no way to close.
94
+ const current = await storage.findOutput(txid, outputIndex, topic, undefined, true);
95
+ if (current == null) {
96
+ throw new RetractionRefused(404, 'output-unknown', `this index no longer holds ${topic} output ${txid}:${outputIndex}`);
97
+ }
98
+ const changed = refusalFor(current, txid);
99
+ if (changed != null) {
100
+ throw new RetractionRefused(409, 'retraction-refused', `the output changed while the retraction was being checked and nothing was removed: ${changed}`);
101
+ }
102
+ // The eviction's first step, through the same calls it makes.
103
+ await storage.deleteOutput(txid, outputIndex, topic);
104
+ for (const service of Object.values(request.lookupServices)) {
105
+ try {
106
+ await service.outputEvicted(txid, outputIndex);
107
+ }
108
+ catch (cause) {
109
+ console.error(`retraction of ${txid}:${outputIndex}: a lookup service failed to evict it:`, cause);
110
+ }
111
+ }
112
+ // The eviction's repair of the predecessor, then the two it never needs.
113
+ const unrepaired = [];
114
+ let restoredTip;
115
+ for (const consumed of output.outputsConsumed) {
116
+ const predecessor = await storage.findOutput(consumed.txid, consumed.outputIndex, topic);
117
+ if (predecessor == null)
118
+ continue;
119
+ const consumedBy = predecessor.consumedBy.filter((c) => !(c.txid === txid && c.outputIndex === outputIndex));
120
+ await storage.updateConsumedBy(consumed.txid, consumed.outputIndex, topic, consumedBy);
121
+ if (consumedBy.length > 0)
122
+ continue;
123
+ if (storage.markUTXOAsUnspent != null) {
124
+ await storage.markUTXOAsUnspent(consumed.txid, consumed.outputIndex, topic);
125
+ }
126
+ else {
127
+ unrepaired.push(`engine storage still marks ${consumed.txid}:${consumed.outputIndex} spent`);
128
+ }
129
+ await request.records.markUnspent(consumed.txid, consumed.outputIndex);
130
+ restoredTip = { txid: consumed.txid, outputIndex: consumed.outputIndex };
131
+ }
132
+ if (storage.deleteAppliedTransaction != null) {
133
+ await storage.deleteAppliedTransaction(txid, topic);
134
+ }
135
+ else {
136
+ unrepaired.push(`engine storage still records ${txid} as applied to ${topic}, so a re-announcement would be skipped as a duplicate`);
137
+ }
138
+ return {
139
+ txid,
140
+ outputIndex,
141
+ reason: request.reason,
142
+ networkChecked,
143
+ ...(restoredTip == null ? {} : { restoredTip }),
144
+ unrepaired,
145
+ };
146
+ }
147
+ //# sourceMappingURL=retraction.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retraction.js","sourceRoot":"","sources":["../src/retraction.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,117 @@
1
+ import type { Db } from 'mongodb';
2
+ /** One indexed DPP state (admitted output, kept after spend as history). */
3
+ export interface DppRecord {
4
+ txid: string;
5
+ outputIndex: number;
6
+ passportId: string;
7
+ /** Chip UID from the payload's data-carrier property; '' when absent. */
8
+ uid: string;
9
+ op: string;
10
+ timestamp: string;
11
+ previousTxid: string;
12
+ spent: boolean;
13
+ /** txid of the state that spent this one; '' while unspent. */
14
+ spendingTxid: string;
15
+ createdAt: Date;
16
+ /**
17
+ * The store's own insertion sequence: assigned once, at insert, and never
18
+ * rewritten. It is the order `spec/portable-evidence.md` section 1 pages by,
19
+ * so two readers of one snapshot receive the same pages and a cursor names
20
+ * a position that a concurrent insert cannot shift. Rows written before the
21
+ * field existed are numbered at boot in createdAt, txid, outputIndex order,
22
+ * which is the order the bounded lookup has always answered them in.
23
+ */
24
+ sequence: number;
25
+ }
26
+ /** What the lookup service hands the store; the store assigns `sequence`. */
27
+ export type DppRecordInput = Omit<DppRecord, 'sequence'>;
28
+ /** Which records a page or an export is about: one of the two keys, non-empty. */
29
+ export interface RecordSelector {
30
+ passportId?: string;
31
+ uid?: string;
32
+ }
33
+ /** The sequences a page reads: after < sequence <= upTo. */
34
+ export interface SequenceRange {
35
+ after: number;
36
+ upTo: number;
37
+ }
38
+ /**
39
+ * Record store for ls_dpp. Abstract so tests (and the backend dev mode) can
40
+ * run in memory; production uses Mongo via the overlay's lookup-service
41
+ * factory.
42
+ */
43
+ export interface DppRecordStore {
44
+ insert: (record: DppRecordInput) => Promise<void>;
45
+ markSpent: (txid: string, outputIndex: number, spendingTxid: string) => Promise<void>;
46
+ /**
47
+ * Undo `markSpent`: the state that spent this one was retracted, an announced
48
+ * transaction the network refused (`POST /retract`), so this one is the tip
49
+ * again.
50
+ */
51
+ markUnspent: (txid: string, outputIndex: number) => Promise<void>;
52
+ delete: (txid: string, outputIndex: number) => Promise<void>;
53
+ findByPassport: (passportId: string) => Promise<DppRecord[]>;
54
+ findByUid: (uid: string) => Promise<DppRecord[]>;
55
+ /**
56
+ * The highest sequence assigned so far, 0 when nothing was ever inserted:
57
+ * what a snapshot pins. A deleted row's sequence stays assigned, so the
58
+ * answer never goes backwards.
59
+ */
60
+ highestSequence: () => Promise<number>;
61
+ /**
62
+ * The records matching the selector whose sequence lies in the range,
63
+ * ascending by sequence, at most `limit` of them. The one query a page is
64
+ * made of, so both stores answer it from an index rather than a scan.
65
+ */
66
+ findPage: (selector: RecordSelector, range: SequenceRange, limit: number) => Promise<DppRecord[]>;
67
+ }
68
+ /**
69
+ * The one accepted spelling of a selector: exactly one key, a non-empty string.
70
+ * Checked in the store and not only at the HTTP handler, because the Mongo
71
+ * store would run whatever object it is handed as a filter.
72
+ */
73
+ export declare function selectorFilter(selector: RecordSelector): {
74
+ passportId: string;
75
+ } | {
76
+ uid: string;
77
+ };
78
+ export declare class MongoDppStorage implements DppRecordStore {
79
+ private readonly records;
80
+ private readonly counters;
81
+ private ready?;
82
+ constructor(db: Db);
83
+ /**
84
+ * Indexes, and the one migration this store has ever needed: rows written
85
+ * before `sequence` existed are numbered in createdAt, txid, outputIndex
86
+ * order, so every row has a position before the first page is served and the
87
+ * pages of an old deployment read in the order its lookups always answered.
88
+ * Runs once per process on first use, and `main()` awaits it at boot so the
89
+ * numbering never races a request. Idempotent: a second boot finds nothing
90
+ * to number.
91
+ */
92
+ ensureReady(): Promise<void>;
93
+ /** One atomic increment per insert: the counter is the order, gaps included. */
94
+ private nextSequence;
95
+ insert(record: DppRecordInput): Promise<void>;
96
+ markSpent(txid: string, outputIndex: number, spendingTxid: string): Promise<void>;
97
+ markUnspent(txid: string, outputIndex: number): Promise<void>;
98
+ delete(txid: string, outputIndex: number): Promise<void>;
99
+ findByPassport(passportId: string): Promise<DppRecord[]>;
100
+ findByUid(uid: string): Promise<DppRecord[]>;
101
+ highestSequence(): Promise<number>;
102
+ findPage(selector: RecordSelector, range: SequenceRange, limit: number): Promise<DppRecord[]>;
103
+ }
104
+ export declare class InMemoryDppStorage implements DppRecordStore {
105
+ private readonly records;
106
+ private assigned;
107
+ private key;
108
+ insert(record: DppRecordInput): Promise<void>;
109
+ markSpent(txid: string, outputIndex: number, spendingTxid: string): Promise<void>;
110
+ markUnspent(txid: string, outputIndex: number): Promise<void>;
111
+ delete(txid: string, outputIndex: number): Promise<void>;
112
+ findByPassport(passportId: string): Promise<DppRecord[]>;
113
+ findByUid(uid: string): Promise<DppRecord[]>;
114
+ highestSequence(): Promise<number>;
115
+ findPage(selector: RecordSelector, range: SequenceRange, limit: number): Promise<DppRecord[]>;
116
+ }
117
+ //# sourceMappingURL=storage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"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"}