@did-btcr2/method 0.55.0 → 0.58.0
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/README.md +10 -4
- package/dist/.tsbuildinfo +1 -1
- package/dist/browser.js +3 -3
- package/dist/browser.mjs +3 -3
- package/dist/cjs/index.js +172 -66
- package/dist/esm/core/beacon/beacon.js +11 -1
- package/dist/esm/core/beacon/beacon.js.map +1 -1
- package/dist/esm/core/beacon/cas-beacon.js +7 -6
- package/dist/esm/core/beacon/cas-beacon.js.map +1 -1
- package/dist/esm/core/beacon/factory.js +6 -4
- package/dist/esm/core/beacon/factory.js.map +1 -1
- package/dist/esm/core/beacon/signal-discovery.js +124 -38
- package/dist/esm/core/beacon/signal-discovery.js.map +1 -1
- package/dist/esm/core/beacon/singleton-beacon.js +2 -2
- package/dist/esm/core/beacon/singleton-beacon.js.map +1 -1
- package/dist/esm/core/beacon/smt-beacon.js +7 -6
- package/dist/esm/core/beacon/smt-beacon.js.map +1 -1
- package/dist/esm/core/beacon/utils.js +0 -10
- package/dist/esm/core/beacon/utils.js.map +1 -1
- package/dist/esm/core/did-sender-resolver.js +8 -2
- package/dist/esm/core/did-sender-resolver.js.map +1 -1
- package/dist/esm/core/resolver.js +20 -1
- package/dist/esm/core/resolver.js.map +1 -1
- package/dist/esm/core/updater.js +17 -4
- package/dist/esm/core/updater.js.map +1 -1
- package/dist/esm/did-btcr2.js +23 -7
- package/dist/esm/did-btcr2.js.map +1 -1
- package/dist/esm/utils/appendix.js +36 -5
- package/dist/esm/utils/appendix.js.map +1 -1
- package/dist/types/core/beacon/beacon.d.ts +10 -1
- package/dist/types/core/beacon/beacon.d.ts.map +1 -1
- package/dist/types/core/beacon/cas-beacon.d.ts +2 -1
- package/dist/types/core/beacon/cas-beacon.d.ts.map +1 -1
- package/dist/types/core/beacon/factory.d.ts +3 -1
- package/dist/types/core/beacon/factory.d.ts.map +1 -1
- package/dist/types/core/beacon/signal-discovery.d.ts +36 -9
- package/dist/types/core/beacon/signal-discovery.d.ts.map +1 -1
- package/dist/types/core/beacon/singleton-beacon.d.ts +1 -1
- package/dist/types/core/beacon/singleton-beacon.d.ts.map +1 -1
- package/dist/types/core/beacon/smt-beacon.d.ts +2 -1
- package/dist/types/core/beacon/smt-beacon.d.ts.map +1 -1
- package/dist/types/core/beacon/utils.d.ts +0 -6
- package/dist/types/core/beacon/utils.d.ts.map +1 -1
- package/dist/types/core/did-sender-resolver.d.ts.map +1 -1
- package/dist/types/core/interfaces.d.ts.map +1 -1
- package/dist/types/core/resolver.d.ts.map +1 -1
- package/dist/types/core/updater.d.ts +11 -4
- package/dist/types/core/updater.d.ts.map +1 -1
- package/dist/types/did-btcr2.d.ts.map +1 -1
- package/dist/types/utils/appendix.d.ts +30 -4
- package/dist/types/utils/appendix.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/core/beacon/beacon.ts +12 -1
- package/src/core/beacon/cas-beacon.ts +7 -6
- package/src/core/beacon/factory.ts +6 -4
- package/src/core/beacon/interfaces.ts +1 -1
- package/src/core/beacon/signal-discovery.ts +141 -42
- package/src/core/beacon/singleton-beacon.ts +2 -2
- package/src/core/beacon/smt-beacon.ts +7 -6
- package/src/core/beacon/utils.ts +0 -13
- package/src/core/did-sender-resolver.ts +10 -2
- package/src/core/interfaces.ts +1 -0
- package/src/core/resolver.ts +33 -2
- package/src/core/updater.ts +25 -5
- package/src/did-btcr2.ts +28 -8
- package/src/utils/appendix.ts +37 -5
|
@@ -544,8 +544,19 @@ export abstract class SinglePartyBeacon {
|
|
|
544
544
|
*/
|
|
545
545
|
readonly service: BeaconService;
|
|
546
546
|
|
|
547
|
-
|
|
547
|
+
/**
|
|
548
|
+
* The absolute did:btcr2 identifier this beacon instance serves.
|
|
549
|
+
*
|
|
550
|
+
* Injected by the caller rather than recovered from {@link service}. A beacon
|
|
551
|
+
* service `id` is permitted to be a relative DID URL (`#beacon-1`), which
|
|
552
|
+
* carries no DID to strip a fragment from, so deriving the subject from it is
|
|
553
|
+
* only correct for the absolute spelling.
|
|
554
|
+
*/
|
|
555
|
+
readonly did: string;
|
|
556
|
+
|
|
557
|
+
constructor(service: BeaconService, did: string) {
|
|
548
558
|
this.service = service;
|
|
559
|
+
this.did = did;
|
|
549
560
|
}
|
|
550
561
|
|
|
551
562
|
/**
|
|
@@ -57,9 +57,10 @@ export class CASBeacon extends SinglePartyBeacon {
|
|
|
57
57
|
/**
|
|
58
58
|
* Creates an instance of CASBeacon.
|
|
59
59
|
* @param {BeaconService} service The service of the Beacon.
|
|
60
|
+
* @param {string} did The absolute did:btcr2 identifier this beacon serves.
|
|
60
61
|
*/
|
|
61
|
-
constructor(service: BeaconService) {
|
|
62
|
-
super({ ...service, type: 'CASBeacon' });
|
|
62
|
+
constructor(service: BeaconService, did: string) {
|
|
63
|
+
super({ ...service, type: 'CASBeacon' }, did);
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
/**
|
|
@@ -82,8 +83,8 @@ export class CASBeacon extends SinglePartyBeacon {
|
|
|
82
83
|
const updates = new Array<[SignedBTCR2Update, BlockMetadata]>();
|
|
83
84
|
const needs = new Array<DataNeed>();
|
|
84
85
|
|
|
85
|
-
//
|
|
86
|
-
const did = this.
|
|
86
|
+
// The DID under resolution keys this beacon's announcement entry.
|
|
87
|
+
const did = this.did;
|
|
87
88
|
|
|
88
89
|
for(const signal of signals) {
|
|
89
90
|
// Signal bytes are hex, matches hex-keyed sidecar maps directly
|
|
@@ -160,8 +161,8 @@ export class CASBeacon extends SinglePartyBeacon {
|
|
|
160
161
|
bitcoin: BitcoinConnection,
|
|
161
162
|
options?: CASBroadcastOptions
|
|
162
163
|
): Promise<BroadcastResult> {
|
|
163
|
-
//
|
|
164
|
-
const did = this.
|
|
164
|
+
// The DID this beacon serves keys its announcement entry.
|
|
165
|
+
const did = this.did;
|
|
165
166
|
|
|
166
167
|
// Hash the signed update (base64urlnopad for the CAS Announcement entry per spec)
|
|
167
168
|
const updateHash = canonicalHash(signedUpdate);
|
|
@@ -14,16 +14,18 @@ export class BeaconFactory {
|
|
|
14
14
|
/**
|
|
15
15
|
* Establish a Beacon instance based on the provided service and optional sidecar data.
|
|
16
16
|
* @param {BeaconService} service The beacon service configuration.
|
|
17
|
+
* @param {string} did The absolute did:btcr2 identifier the beacon serves. Supplied
|
|
18
|
+
* by the caller because a beacon service `id` may be a relative DID URL.
|
|
17
19
|
* @returns {SinglePartyBeacon} The established Beacon instance.
|
|
18
20
|
*/
|
|
19
|
-
static establish(service: BeaconService): SinglePartyBeacon {
|
|
21
|
+
static establish(service: BeaconService, did: string): SinglePartyBeacon {
|
|
20
22
|
switch (service.type) {
|
|
21
23
|
case 'SingletonBeacon':
|
|
22
|
-
return new SingletonBeacon(service);
|
|
24
|
+
return new SingletonBeacon(service, did);
|
|
23
25
|
case 'CASBeacon':
|
|
24
|
-
return new CASBeacon(service);
|
|
26
|
+
return new CASBeacon(service, did);
|
|
25
27
|
case 'SMTBeacon':
|
|
26
|
-
return new SMTBeacon(service);
|
|
28
|
+
return new SMTBeacon(service, did);
|
|
27
29
|
default:
|
|
28
30
|
throw new MethodError('Invalid Beacon Type', 'INVALID_BEACON_ERROR', service);
|
|
29
31
|
}
|
|
@@ -76,4 +76,4 @@ export interface BeaconSignal {
|
|
|
76
76
|
*
|
|
77
77
|
* @param announcement The CAS Announcement object (DID to update hash mapping).
|
|
78
78
|
*/
|
|
79
|
-
export type CasPublishFn = (announcement: Record<string, string>) => Promise<void>;
|
|
79
|
+
export type CasPublishFn = (announcement: Record<string, string>) => Promise<void>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
BitcoinConnection,
|
|
3
3
|
BlockV3,
|
|
4
|
+
RawTransactionRest,
|
|
4
5
|
RawTransactionV2} from '@did-btcr2/bitcoin';
|
|
5
6
|
import {
|
|
6
7
|
GENESIS_TX_ID,
|
|
@@ -11,37 +12,44 @@ import type { BeaconService, BeaconSignal } from './interfaces.js';
|
|
|
11
12
|
import { BeaconUtils } from './utils.js';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
15
|
+
* The serialized form of a Beacon Signal output: `OP_RETURN` (`0x6a`), the 32-byte push
|
|
16
|
+
* opcode (`0x20`), then the 32-byte hash. This is the exact inverse of `opReturnScript`,
|
|
17
|
+
* which encodes signals as `0x6a 0x20 <32 bytes>` and is pinned byte for byte by
|
|
18
|
+
* `op-return-script.spec.ts`.
|
|
19
|
+
*/
|
|
20
|
+
const BEACON_SIGNAL_SCRIPT = /^6a20([0-9a-f]{64})$/i;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Parses a serialized scriptPubKey and returns the beacon signal hash if and only if the
|
|
24
|
+
* output is exactly `OP_RETURN OP_PUSHBYTES_32 <32-byte hash>`.
|
|
16
25
|
*
|
|
17
|
-
* Beacon signals encode a single 32-byte update or announcement hash in an
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* malformed or adversarial on-chain output cannot be mistaken for a real
|
|
21
|
-
* downstream.
|
|
26
|
+
* Beacon signals encode a single 32-byte update or announcement hash in an OP_RETURN data
|
|
27
|
+
* push. Any other shape (a bare `OP_RETURN`, a push of the wrong size, a second push, or a
|
|
28
|
+
* non-canonical push opcode such as `OP_PUSHDATA1`) is not a valid signal and returns
|
|
29
|
+
* `null`, so a malformed or adversarial on-chain output cannot be mistaken for a real
|
|
30
|
+
* signal downstream.
|
|
22
31
|
*
|
|
23
|
-
*
|
|
32
|
+
* The input is the script itself, not a rendered `asm` string, because `asm` is a
|
|
33
|
+
* human-readable rendering whose dialect differs per backend: Bitcoin Core prints
|
|
34
|
+
* `OP_RETURN <hash>` while Esplora prints `OP_RETURN OP_PUSHBYTES_32 <hash>`. Both return
|
|
35
|
+
* the identical serialized script (Esplora as `scriptpubkey`, Core as `scriptPubKey.hex`),
|
|
36
|
+
* so decoding the script is backend-agnostic and matches the bytes actually committed to
|
|
37
|
+
* the chain.
|
|
38
|
+
*
|
|
39
|
+
* @param {string | undefined} scriptPubKey Hex-encoded scriptPubKey of the output to parse.
|
|
24
40
|
* @returns {string | null} The lowercased 32-byte hex hash, or `null` if not a valid signal.
|
|
25
41
|
*/
|
|
26
|
-
export function
|
|
27
|
-
if(!
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// A standard NULL_DATA beacon output is exactly three asm tokens: the OP_RETURN
|
|
32
|
-
// opcode, the 32-byte push opcode, and the 64-character hex payload.
|
|
33
|
-
const tokens = asm.trim().split(/\s+/);
|
|
34
|
-
if(tokens.length !== 3 || tokens[0] !== 'OP_RETURN' || tokens[1] !== 'OP_PUSHBYTES_32') {
|
|
42
|
+
export function extractOpReturnSignalHash(scriptPubKey: string | undefined): string | null {
|
|
43
|
+
if(!scriptPubKey) {
|
|
35
44
|
return null;
|
|
36
45
|
}
|
|
37
46
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if(!/^[0-9a-fA-F]{64}$/.test(signalHash)) {
|
|
47
|
+
const signal = BEACON_SIGNAL_SCRIPT.exec(scriptPubKey.trim());
|
|
48
|
+
if(!signal) {
|
|
41
49
|
return null;
|
|
42
50
|
}
|
|
43
51
|
|
|
44
|
-
return
|
|
52
|
+
return signal[1].toLowerCase();
|
|
45
53
|
}
|
|
46
54
|
|
|
47
55
|
/**
|
|
@@ -52,6 +60,52 @@ export function extractOpReturnSignal(asm: string | undefined): string | null {
|
|
|
52
60
|
*/
|
|
53
61
|
export class BeaconSignalDiscovery {
|
|
54
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Determines whether a candidate transaction spends an output controlled by the given
|
|
65
|
+
* beacon address.
|
|
66
|
+
*
|
|
67
|
+
* A Beacon Signal is a transaction that *spends from* a Beacon Address, but an address
|
|
68
|
+
* transaction listing returns every transaction touching the address in either
|
|
69
|
+
* direction. Without this check, anyone able to pay dust to a beacon address could
|
|
70
|
+
* attach an arbitrary 32-byte OP_RETURN and have it read as a signal, so the input side
|
|
71
|
+
* has to be inspected before a transaction is treated as one.
|
|
72
|
+
*
|
|
73
|
+
* Esplora embeds the spent output in `vin[].prevout`; when a backend omits it the
|
|
74
|
+
* funding transaction is fetched instead, so a missing field cannot silently drop a
|
|
75
|
+
* real signal.
|
|
76
|
+
*
|
|
77
|
+
* @param {RawTransactionRest} tx The candidate transaction.
|
|
78
|
+
* @param {string} address The beacon address the transaction must spend from.
|
|
79
|
+
* @param {BitcoinConnection} bitcoin Bitcoin network connection to use for REST calls.
|
|
80
|
+
* @returns {Promise<boolean>} True if at least one input spends an output of the beacon address.
|
|
81
|
+
*/
|
|
82
|
+
private static async spendsFromAddress(
|
|
83
|
+
tx: RawTransactionRest,
|
|
84
|
+
address: string,
|
|
85
|
+
bitcoin: BitcoinConnection
|
|
86
|
+
): Promise<boolean> {
|
|
87
|
+
for(const vin of tx.vin ?? []) {
|
|
88
|
+
// A coinbase input spends no prior output, so it can never spend from a beacon.
|
|
89
|
+
if(vin.is_coinbase) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let prevout = vin.prevout;
|
|
94
|
+
|
|
95
|
+
// Fall back to the funding transaction when the backend does not embed the prevout.
|
|
96
|
+
if(!prevout && vin.txid) {
|
|
97
|
+
const fundingTx = await bitcoin.rest.transaction.get(vin.txid);
|
|
98
|
+
prevout = fundingTx?.vout?.[vin.vout];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if(prevout?.scriptpubkey_address === address) {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
55
109
|
/**
|
|
56
110
|
* Retrieves the beacon signals for the given array of BeaconService objects
|
|
57
111
|
* using an esplora/electrs REST API connection via a bitcoin I/O driver.
|
|
@@ -71,10 +125,9 @@ export class BeaconSignalDiscovery {
|
|
|
71
125
|
// Iterate over each beacon
|
|
72
126
|
for (const beaconService of beaconServices) {
|
|
73
127
|
beaconServiceSignals.set(beaconService, []);
|
|
128
|
+
const beaconAddress = BeaconUtils.parseBitcoinAddress(beaconService.serviceEndpoint as string);
|
|
74
129
|
// Get the transactions for the beacon address via REST
|
|
75
|
-
const beaconSignals = await bitcoin.rest.address.getTxs(
|
|
76
|
-
BeaconUtils.parseBitcoinAddress(beaconService.serviceEndpoint as string)
|
|
77
|
-
);
|
|
130
|
+
const beaconSignals = await bitcoin.rest.address.getTxs(beaconAddress);
|
|
78
131
|
|
|
79
132
|
// If no signals are found, continue
|
|
80
133
|
if (!beaconSignals || !beaconSignals.length) {
|
|
@@ -84,10 +137,11 @@ export class BeaconSignalDiscovery {
|
|
|
84
137
|
// Iterate over each signal
|
|
85
138
|
for (const beaconSignal of beaconSignals) {
|
|
86
139
|
// Get the last vout in the transaction
|
|
87
|
-
const
|
|
140
|
+
const lastSignalVout = beaconSignal.vout.slice(-1)[0];
|
|
88
141
|
|
|
89
142
|
/**
|
|
90
|
-
*
|
|
143
|
+
* Decode the signal from the serialized script, not from `scriptpubkey_asm`: the
|
|
144
|
+
* asm rendering is backend-specific, the script is not.
|
|
91
145
|
* Vout (rest) format:
|
|
92
146
|
* {
|
|
93
147
|
* scriptpubkey: '6a20570f177c65e64fb5cf61180b664cdddf09ab76153c2b192e22006e5b22a3917a',
|
|
@@ -96,18 +150,24 @@ export class BeaconSignalDiscovery {
|
|
|
96
150
|
* value: 0
|
|
97
151
|
* }
|
|
98
152
|
*/
|
|
99
|
-
if(!
|
|
153
|
+
if(!lastSignalVout) {
|
|
100
154
|
continue;
|
|
101
155
|
}
|
|
102
156
|
|
|
103
157
|
// A beacon signal output must be exactly `OP_RETURN OP_PUSHBYTES_32 <32-byte hash>`.
|
|
104
158
|
// Reject any other shape (bare OP_RETURN, wrong push size, non-hex payload) so a
|
|
105
159
|
// malformed on-chain output cannot masquerade as a phantom signal downstream.
|
|
106
|
-
const updateHash =
|
|
160
|
+
const updateHash = extractOpReturnSignalHash(lastSignalVout.scriptpubkey);
|
|
107
161
|
if(!updateHash) {
|
|
108
162
|
continue;
|
|
109
163
|
}
|
|
110
164
|
|
|
165
|
+
// The address listing returns inbound payments too, so require the transaction to
|
|
166
|
+
// spend from the beacon before treating its OP_RETURN as a signal.
|
|
167
|
+
if(!await BeaconSignalDiscovery.spendsFromAddress(beaconSignal, beaconAddress, bitcoin)) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
111
171
|
// Use the pre-fetched block count instead of calling per-signal
|
|
112
172
|
const confirmations = currentBlockCount - beaconSignal.status.block_height + 1;
|
|
113
173
|
|
|
@@ -154,8 +214,16 @@ export class BeaconSignalDiscovery {
|
|
|
154
214
|
// Get the current block height once before the loop
|
|
155
215
|
const targetHeight = await rpc.getBlockCount();
|
|
156
216
|
|
|
157
|
-
|
|
158
|
-
|
|
217
|
+
/**
|
|
218
|
+
* Hoist the beacon address lookup before the loop, mapping each address to the caller's
|
|
219
|
+
* own service object. The results below are keyed by object identity, so the map has to
|
|
220
|
+
* hold those exact instances rather than copies of them. Addresses are parsed the same
|
|
221
|
+
* way as on the indexer path, so a BIP21 endpoint carrying query parameters resolves to
|
|
222
|
+
* the bare address a spent output reports.
|
|
223
|
+
*/
|
|
224
|
+
const beaconServicesMap = new Map<string, BeaconService>(
|
|
225
|
+
beaconServices.map(service => [BeaconUtils.parseBitcoinAddress(service.serviceEndpoint as string), service])
|
|
226
|
+
);
|
|
159
227
|
|
|
160
228
|
// Set genesis height
|
|
161
229
|
let height = 0;
|
|
@@ -167,11 +235,50 @@ export class BeaconSignalDiscovery {
|
|
|
167
235
|
while (block.height <= targetHeight) {
|
|
168
236
|
// Iterate over each transaction in the block
|
|
169
237
|
for (const tx of block.tx) {
|
|
170
|
-
// If the txid is a
|
|
238
|
+
// If the txid is a genesis transaction, continue ...
|
|
171
239
|
if (tx.txid === GENESIS_TX_ID) {
|
|
172
240
|
continue;
|
|
173
241
|
}
|
|
174
242
|
|
|
243
|
+
/**
|
|
244
|
+
* A Beacon Signal announces its update hash in the last output of the *spending*
|
|
245
|
+
* transaction, so the hash is resolved once per transaction, before the input side
|
|
246
|
+
* is inspected. The output being spent carries a plain locking script and never a
|
|
247
|
+
* signal. A beacon signal output must be exactly
|
|
248
|
+
* `OP_RETURN OP_PUSHBYTES_32 <32-byte hash>`; rejecting any other shape here also
|
|
249
|
+
* keeps the free filter ahead of the prevout lookups below.
|
|
250
|
+
*
|
|
251
|
+
* The signal is decoded from `scriptPubKey.hex`, the same serialized script the
|
|
252
|
+
* indexer path reads as `scriptpubkey`. Core's `asm` renders a data push as bare
|
|
253
|
+
* hex (`OP_RETURN <hash>`) where Esplora names the push opcode
|
|
254
|
+
* (`OP_RETURN OP_PUSHBYTES_32 <hash>`), so an asm-shaped check written against one
|
|
255
|
+
* backend silently discards every signal from the other.
|
|
256
|
+
* Vout (rpc) format:
|
|
257
|
+
* {
|
|
258
|
+
* value: 0,
|
|
259
|
+
* n: 1,
|
|
260
|
+
* scriptPubKey: {
|
|
261
|
+
* asm: "OP_RETURN 7af2be9fcb371dfcc5465a74373d499c11b1f7bba47e0507fce892ea12ec1cd6",
|
|
262
|
+
* desc: "raw(6a207af2be9fcb371dfcc5465a74373d499c11b1f7bba47e0507fce892ea12ec1cd6)#g064zwfr",
|
|
263
|
+
* hex: "6a207af2be9fcb371dfcc5465a74373d499c11b1f7bba47e0507fce892ea12ec1cd6",
|
|
264
|
+
* type: "nulldata",
|
|
265
|
+
* },
|
|
266
|
+
* }
|
|
267
|
+
*/
|
|
268
|
+
const lastSignalVout = tx.vout.slice(-1)[0];
|
|
269
|
+
if (!lastSignalVout) {
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const updateHash = extractOpReturnSignalHash(lastSignalVout.scriptPubKey?.hex);
|
|
274
|
+
if (!updateHash) {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// One transaction is one signal per beacon, however many of that beacon's UTXOs
|
|
279
|
+
// it spends, so track the services already credited with this transaction.
|
|
280
|
+
const signaled = new Set<BeaconService>();
|
|
281
|
+
|
|
175
282
|
// Iterate over each input in the transaction
|
|
176
283
|
for (const vin of tx.vin) {
|
|
177
284
|
|
|
@@ -213,21 +320,13 @@ export class BeaconSignalDiscovery {
|
|
|
213
320
|
|
|
214
321
|
// Use the hoisted beaconServicesMap instead of rebuilding per-vin
|
|
215
322
|
const beaconService = beaconServicesMap.get(scriptPubKey.address);
|
|
216
|
-
if (!beaconService) {
|
|
217
|
-
continue;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// A beacon signal output must be exactly `OP_RETURN OP_PUSHBYTES_32 <32-byte hash>`.
|
|
221
|
-
// Reject any other shape so a malformed on-chain output cannot masquerade as a
|
|
222
|
-
// phantom signal downstream.
|
|
223
|
-
const txVoutScriptPubkeyAsm = prevout.vout[vin.vout].scriptPubKey.asm;
|
|
224
|
-
const updateHash = extractOpReturnSignal(txVoutScriptPubkeyAsm);
|
|
225
|
-
if(!updateHash) {
|
|
323
|
+
if (!beaconService || signaled.has(beaconService)) {
|
|
226
324
|
continue;
|
|
227
325
|
}
|
|
326
|
+
signaled.add(beaconService);
|
|
228
327
|
|
|
229
328
|
// Log the found txid and beacon
|
|
230
|
-
console.info(`Tx ${tx.txid} contains beacon
|
|
329
|
+
console.info(`Tx ${tx.txid} contains beacon address ${scriptPubKey.address}`);
|
|
231
330
|
|
|
232
331
|
// Push the beacon signal object to the beacon signals array for that beacon service
|
|
233
332
|
beaconServiceSignals.get(beaconService)?.push({
|
|
@@ -20,8 +20,8 @@ export class SingletonBeacon extends SinglePartyBeacon {
|
|
|
20
20
|
* Creates an instance of SingletonBeacon.
|
|
21
21
|
* @param {BeaconService} service The BeaconService object representing the funded beacon to announce the update to.
|
|
22
22
|
*/
|
|
23
|
-
constructor(service: BeaconService) {
|
|
24
|
-
super({ ...service, type: 'SingletonBeacon' });
|
|
23
|
+
constructor(service: BeaconService, did: string) {
|
|
24
|
+
super({ ...service, type: 'SingletonBeacon' }, did);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/**
|
|
@@ -27,9 +27,10 @@ export class SMTBeacon extends SinglePartyBeacon {
|
|
|
27
27
|
/**
|
|
28
28
|
* Creates an instance of SMTBeacon.
|
|
29
29
|
* @param {BeaconService} service The Beacon service.
|
|
30
|
+
* @param {string} did The absolute did:btcr2 identifier this beacon serves.
|
|
30
31
|
*/
|
|
31
|
-
constructor(service: BeaconService) {
|
|
32
|
-
super({ ...service, type: 'SMTBeacon' });
|
|
32
|
+
constructor(service: BeaconService, did: string) {
|
|
33
|
+
super({ ...service, type: 'SMTBeacon' }, did);
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
/**
|
|
@@ -52,8 +53,8 @@ export class SMTBeacon extends SinglePartyBeacon {
|
|
|
52
53
|
const updates = new Array<[SignedBTCR2Update, BlockMetadata]>();
|
|
53
54
|
const needs = new Array<DataNeed>();
|
|
54
55
|
|
|
55
|
-
//
|
|
56
|
-
const did = this.
|
|
56
|
+
// The DID under resolution keys this beacon's leaf index.
|
|
57
|
+
const did = this.did;
|
|
57
58
|
|
|
58
59
|
for(const signal of signals) {
|
|
59
60
|
// Signal bytes are the hex-encoded SMT root hash; smtMap is keyed by proof.id (also hex)
|
|
@@ -144,8 +145,8 @@ export class SMTBeacon extends SinglePartyBeacon {
|
|
|
144
145
|
bitcoin: BitcoinConnection,
|
|
145
146
|
options?: BroadcastOptions
|
|
146
147
|
): Promise<BroadcastResult> {
|
|
147
|
-
//
|
|
148
|
-
const did = this.
|
|
148
|
+
// The DID keys this beacon's leaf index in the tree.
|
|
149
|
+
const did = this.did;
|
|
149
150
|
|
|
150
151
|
// Build a single-entry SMT from the signed update
|
|
151
152
|
const canonicalBytes = new TextEncoder().encode(canonicalize(signedUpdate));
|
package/src/core/beacon/utils.ts
CHANGED
|
@@ -172,19 +172,6 @@ export class BeaconUtils {
|
|
|
172
172
|
return { ...beacon, serviceEndpoint: beacon.serviceEndpoint.replace('bitcoin:', '')};
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
/**
|
|
176
|
-
* Create a map of address => beaconService with address field.
|
|
177
|
-
* @param {Array<BeaconService>} beacons The list of beacon services.
|
|
178
|
-
* @returns {Map<string, BeaconService>} A map of address => beaconService.
|
|
179
|
-
*/
|
|
180
|
-
static getBeaconServicesMap(beacons: Array<BeaconService>): Map<string, BeaconService> {
|
|
181
|
-
return new Map<string, BeaconService>(
|
|
182
|
-
beacons
|
|
183
|
-
.map(this.parseBeaconServiceEndpoint)
|
|
184
|
-
.map((beacon) => ([beacon.serviceEndpoint as string, beacon]))
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
175
|
/**
|
|
189
176
|
* Get the beacon service ids from a list of beacon services.
|
|
190
177
|
* @param {DidDocument} didDocument The DID Document to extract the services from.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { DidDocumentError, INVALID_DID_DOCUMENT } from '@did-btcr2/common';
|
|
2
2
|
import { SchnorrMultikey } from '@did-btcr2/cryptosuite';
|
|
3
3
|
import { CompressedSecp256k1PublicKey } from '@did-btcr2/keypair';
|
|
4
|
+
import { Appendix } from '../utils/appendix.js';
|
|
4
5
|
import type { DidDocument, DidVerificationMethod } from '../utils/did-document.js';
|
|
5
6
|
import { Identifier } from './identifier.js';
|
|
6
7
|
import { Resolver } from './resolver.js';
|
|
@@ -40,9 +41,16 @@ export function getAggregationCommunicationKey(
|
|
|
40
41
|
// Resolve capabilityInvocation[0] to a verification method: a string reference is
|
|
41
42
|
// dereferenced by id against verificationMethod; an embedded method is used directly.
|
|
42
43
|
// A local id lookup is used rather than getSigningMethod, which defaults to #initialKey
|
|
43
|
-
// and does not resolve embedded methods.
|
|
44
|
+
// and does not resolve embedded methods. Reference and method id are each resolved to an
|
|
45
|
+
// absolute DID URL first, because either may be spelled as a relative DID URL; an
|
|
46
|
+
// unusable reference resolves to nothing rather than to the first unusable method id.
|
|
47
|
+
const invocationId = Appendix.absoluteDidUrl(invocation, document.id);
|
|
44
48
|
const vm: DidVerificationMethod | undefined = typeof invocation === 'string'
|
|
45
|
-
?
|
|
49
|
+
? (invocationId === undefined
|
|
50
|
+
? undefined
|
|
51
|
+
: document.verificationMethod?.find(
|
|
52
|
+
method => Appendix.absoluteDidUrl(method.id, document.id) === invocationId
|
|
53
|
+
))
|
|
46
54
|
: invocation;
|
|
47
55
|
|
|
48
56
|
if(!vm) {
|
package/src/core/interfaces.ts
CHANGED
package/src/core/resolver.ts
CHANGED
|
@@ -225,6 +225,7 @@ export class Resolver {
|
|
|
225
225
|
/** Count of beacon-discovery passes driven by updates adding new beacon services. */
|
|
226
226
|
#discoveryRounds = 0;
|
|
227
227
|
|
|
228
|
+
|
|
228
229
|
/**
|
|
229
230
|
* @internal Use {@link DidBtcr2.resolve} to create instances.
|
|
230
231
|
*/
|
|
@@ -232,7 +233,12 @@ export class Resolver {
|
|
|
232
233
|
didComponents: DidComponents,
|
|
233
234
|
sidecarData: SidecarData,
|
|
234
235
|
currentDocument: DidDocument | null,
|
|
235
|
-
options?: {
|
|
236
|
+
options?: {
|
|
237
|
+
versionId?: string;
|
|
238
|
+
versionTime?: string;
|
|
239
|
+
genesisDocument?: object;
|
|
240
|
+
maxDiscoveryRounds?: number;
|
|
241
|
+
}
|
|
236
242
|
) {
|
|
237
243
|
this.#didComponents = didComponents;
|
|
238
244
|
this.#sidecarData = sidecarData;
|
|
@@ -601,6 +607,29 @@ export class Resolver {
|
|
|
601
607
|
throw new ResolveError('No verificationMethod found in update', INVALID_DID_UPDATE, update);
|
|
602
608
|
}
|
|
603
609
|
|
|
610
|
+
// Spec "Check update.proof": raise INVALID_DID_UPDATE if
|
|
611
|
+
// currentDocument.capabilityInvocation does not contain
|
|
612
|
+
// update.proof.verificationMethod. Locating the method in verificationMethod[] and
|
|
613
|
+
// verifying its signature is not sufficient on its own: a key the controller
|
|
614
|
+
// published only for authentication (or for no relationship at all) must not be
|
|
615
|
+
// able to authorize a DID update. The write path enforces this in DidBtcr2.update();
|
|
616
|
+
// without it here the read path applies an update signed by any key in the document.
|
|
617
|
+
// Checked before the method is located so an unauthorized method always fails with
|
|
618
|
+
// this typed error, whether or not it also appears in verificationMethod[].
|
|
619
|
+
const authorizedMethodId = Appendix.relationshipMethodId(verificationMethodId, currentDocument.id);
|
|
620
|
+
const authorized = authorizedMethodId !== undefined && currentDocument.capabilityInvocation?.some(
|
|
621
|
+
entry => Appendix.relationshipMethodId(entry, currentDocument.id) === authorizedMethodId
|
|
622
|
+
);
|
|
623
|
+
if(!authorized) {
|
|
624
|
+
throw new ResolveError(
|
|
625
|
+
'Invalid update: verificationMethod is not authorized for capabilityInvocation',
|
|
626
|
+
INVALID_DID_UPDATE, {
|
|
627
|
+
verificationMethodId,
|
|
628
|
+
capabilityInvocation : currentDocument.capabilityInvocation
|
|
629
|
+
}
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
|
|
604
633
|
// Get the verificationMethod from the DID Document using the verificationMethodId.
|
|
605
634
|
const vm = DidBtcr2.getSigningMethod(currentDocument, verificationMethodId);
|
|
606
635
|
|
|
@@ -724,7 +753,9 @@ export class Resolver {
|
|
|
724
753
|
if(this.#processedServices.has(service.id) || !signals.length) continue;
|
|
725
754
|
|
|
726
755
|
// Establish a typed beacon and process its signals
|
|
727
|
-
|
|
756
|
+
// The beacon is bound to the DID under resolution: a beacon service
|
|
757
|
+
// `id` may be a relative DID URL, so it cannot supply the subject.
|
|
758
|
+
const beacon = BeaconFactory.establish(service, this.#currentDocument!.id);
|
|
728
759
|
const result = beacon.processSignals(signals, this.#sidecarData);
|
|
729
760
|
|
|
730
761
|
if(result.needs.length > 0) {
|
package/src/core/updater.ts
CHANGED
|
@@ -58,8 +58,8 @@ export interface FundingProof {
|
|
|
58
58
|
* The updater needs the caller to broadcast the signed update via the beacon.
|
|
59
59
|
*
|
|
60
60
|
* The caller decides how: for single-party beacons, call
|
|
61
|
-
* `Updater.announce(beaconService, signedUpdate, signer, bitcoin, options?)` or
|
|
62
|
-
* `BeaconFactory.establish(beaconService).broadcastSignal(...)`. For multi-party
|
|
61
|
+
* `Updater.announce(beaconService, did, signedUpdate, signer, bitcoin, options?)` or
|
|
62
|
+
* `BeaconFactory.establish(beaconService, did).broadcastSignal(...)`. For multi-party
|
|
63
63
|
* aggregate beacons, hand off to the aggregation protocol.
|
|
64
64
|
*
|
|
65
65
|
* Both single-party paths return a `BroadcastResult`; capture it. Beyond the txid,
|
|
@@ -76,6 +76,11 @@ export interface NeedBroadcast {
|
|
|
76
76
|
readonly beaconService: BeaconService;
|
|
77
77
|
/** The signed update ready for broadcast. */
|
|
78
78
|
readonly signedUpdate: SignedBTCR2Update;
|
|
79
|
+
/**
|
|
80
|
+
* The DID being updated. Supplied because a beacon service `id` may be a
|
|
81
|
+
* relative DID URL and so cannot be used to recover the subject.
|
|
82
|
+
*/
|
|
83
|
+
readonly did: string;
|
|
79
84
|
}
|
|
80
85
|
|
|
81
86
|
/** Discriminated union of all data needs the updater may request from the caller. */
|
|
@@ -148,7 +153,7 @@ export interface UpdaterParams {
|
|
|
148
153
|
* // Capture the BroadcastResult: broadcast.txid, plus broadcast.announcement
|
|
149
154
|
* // (CAS beacons) and broadcast.proof (SMT beacons; must be retained, the
|
|
150
155
|
* // proof's nonce exists nowhere else).
|
|
151
|
-
* const broadcast = await Updater.announce(need.beaconService, need.signedUpdate, signer, bitcoin);
|
|
156
|
+
* const broadcast = await Updater.announce(need.beaconService, need.did, need.signedUpdate, signer, bitcoin);
|
|
152
157
|
* updater.provide(need);
|
|
153
158
|
* break;
|
|
154
159
|
* }
|
|
@@ -278,6 +283,12 @@ export class Updater {
|
|
|
278
283
|
const id = verificationMethod.id.slice(hashIdx);
|
|
279
284
|
const multikey = SchnorrMultikey.fromSigner(id, controller, signer);
|
|
280
285
|
|
|
286
|
+
// The absolute spelling of the method id, for the proof to name it by. `hashIdx === 0`
|
|
287
|
+
// means the document writes this method's own id as a relative DID URL, which per DID
|
|
288
|
+
// Core denotes that fragment of the document's `id`: the same rule as
|
|
289
|
+
// Appendix.absoluteDidUrl, whose `undefined` case the fragment check above excludes.
|
|
290
|
+
const absoluteMethodId = hashIdx === 0 ? `${did}${id}` : verificationMethod.id;
|
|
291
|
+
|
|
281
292
|
// Fail fast if the signer's public key is not the key published in the named
|
|
282
293
|
// verification method. Signing with the wrong key yields a Data Integrity
|
|
283
294
|
// proof that cannot verify against the method, so the resulting on-chain
|
|
@@ -308,7 +319,12 @@ export class Updater {
|
|
|
308
319
|
],
|
|
309
320
|
cryptosuite : 'bip340-jcs-2025',
|
|
310
321
|
type : 'DataIntegrityProof',
|
|
311
|
-
|
|
322
|
+
// The proof names the signing method by absolute DID URL, even when the document
|
|
323
|
+
// spells that method's own id relatively: a proof travels apart from the document
|
|
324
|
+
// that defines the method, so a bare `#initialKey` in it resolves against nothing.
|
|
325
|
+
// For a document that already spells its ids absolutely this is the id unchanged,
|
|
326
|
+
// so proofs over such documents are byte-identical to before.
|
|
327
|
+
verificationMethod : absoluteMethodId,
|
|
312
328
|
proofPurpose : 'capabilityInvocation',
|
|
313
329
|
capability : `urn:zcap:root:${encodeURIComponent(did)}`,
|
|
314
330
|
capabilityAction : 'Write',
|
|
@@ -323,6 +339,8 @@ export class Updater {
|
|
|
323
339
|
* Announces a signed update to the Bitcoin blockchain via the specified beacon.
|
|
324
340
|
*
|
|
325
341
|
* @param {BeaconService} beaconService The beacon service to broadcast through.
|
|
342
|
+
* @param {string} did The DID being updated. Required because a beacon service
|
|
343
|
+
* `id` may be a relative DID URL and so cannot supply the subject.
|
|
326
344
|
* @param {SignedBTCR2Update} update The signed update to announce.
|
|
327
345
|
* @param {Signer} signer Signer that produces the ECDSA signature for the Bitcoin transaction.
|
|
328
346
|
* @param {BitcoinConnection} bitcoin The Bitcoin network connection.
|
|
@@ -334,12 +352,13 @@ export class Updater {
|
|
|
334
352
|
*/
|
|
335
353
|
static async announce(
|
|
336
354
|
beaconService: BeaconService,
|
|
355
|
+
did: string,
|
|
337
356
|
update: SignedBTCR2Update,
|
|
338
357
|
signer: Signer,
|
|
339
358
|
bitcoin: BitcoinConnection,
|
|
340
359
|
options?: CASBroadcastOptions
|
|
341
360
|
): Promise<BroadcastResult> {
|
|
342
|
-
const beacon = BeaconFactory.establish(beaconService);
|
|
361
|
+
const beacon = BeaconFactory.establish(beaconService, did);
|
|
343
362
|
return beacon.broadcastSignal(update, signer, bitcoin, options);
|
|
344
363
|
}
|
|
345
364
|
|
|
@@ -406,6 +425,7 @@ export class Updater {
|
|
|
406
425
|
kind : 'NeedBroadcast',
|
|
407
426
|
beaconService : this.#beaconService,
|
|
408
427
|
signedUpdate : this.#state.signedUpdate,
|
|
428
|
+
did : this.#sourceDocument.id,
|
|
409
429
|
}],
|
|
410
430
|
};
|
|
411
431
|
}
|