@did-btcr2/method 0.65.1 → 0.66.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/dist/.tsbuildinfo +1 -1
- package/dist/browser.js +3 -3
- package/dist/browser.mjs +3 -3
- package/dist/cjs/index.js +58 -35
- package/dist/esm/core/beacon/smt-beacon.js +37 -31
- package/dist/esm/core/beacon/smt-beacon.js.map +1 -1
- package/dist/esm/core/resolver.js +56 -21
- package/dist/esm/core/resolver.js.map +1 -1
- package/dist/types/core/beacon/smt-beacon.d.ts +8 -6
- package/dist/types/core/beacon/smt-beacon.d.ts.map +1 -1
- package/dist/types/core/interfaces.d.ts +27 -14
- package/dist/types/core/interfaces.d.ts.map +1 -1
- package/dist/types/core/resolver.d.ts.map +1 -1
- package/dist/types/core/types.d.ts +4 -2
- package/dist/types/core/types.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/core/beacon/smt-beacon.ts +40 -34
- package/src/core/interfaces.ts +27 -14
- package/src/core/resolver.ts +57 -21
- package/src/core/types.ts +4 -2
package/dist/cjs/index.js
CHANGED
|
@@ -589,6 +589,13 @@ var SingletonBeacon = class extends SinglePartyBeacon {
|
|
|
589
589
|
var import_common4 = require("@did-btcr2/common");
|
|
590
590
|
var import_smt = require("@did-btcr2/smt");
|
|
591
591
|
var import_utils2 = require("@noble/hashes/utils");
|
|
592
|
+
function proofIdHex(proof) {
|
|
593
|
+
try {
|
|
594
|
+
return (0, import_smt.hashToHex)((0, import_smt.base64UrlToHash)(proof.id));
|
|
595
|
+
} catch {
|
|
596
|
+
return void 0;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
592
599
|
var SMTBeacon = class extends SinglePartyBeacon {
|
|
593
600
|
/**
|
|
594
601
|
* Creates an instance of SMTBeacon.
|
|
@@ -601,15 +608,17 @@ var SMTBeacon = class extends SinglePartyBeacon {
|
|
|
601
608
|
/**
|
|
602
609
|
* Implements {@link https://dcdpr.github.io/did-btcr2/operations/resolve.html#process-smt-beacon | 7.2.e.1 Process SMT Beacon}.
|
|
603
610
|
*
|
|
604
|
-
* For each signal, the signalBytes contain the hex-encoded SMT root hash
|
|
605
|
-
* This method looks up the SMT Proof from the sidecar by root
|
|
606
|
-
*
|
|
607
|
-
* signed update
|
|
611
|
+
* For each signal, the signalBytes contain the hex-encoded SMT root hash
|
|
612
|
+
* (`smt_root`). This method looks up the SMT Proof from the sidecar by root
|
|
613
|
+
* hash, checks that the id of the proof is the root, verifies the proof with
|
|
614
|
+
* the SMT Proof Verification algorithm, and retrieves the signed update by the
|
|
615
|
+
* proof's updateId. A proof with no updateId announces no update for the DID.
|
|
608
616
|
*
|
|
609
617
|
* @param {Array<BeaconSignal>} signals The array of Beacon Signals to process.
|
|
610
618
|
* @param {SidecarData} sidecar The sidecar data associated with the SMT Beacon.
|
|
611
619
|
* @returns {BeaconProcessResult} Successfully resolved updates and any data needs.
|
|
612
|
-
* @throws {SMTBeaconError} if
|
|
620
|
+
* @throws {SMTBeaconError} `INVALID_SIGNAL_DATA` if the id of the proof is not the
|
|
621
|
+
* signal root, or if the proof does not verify.
|
|
613
622
|
*/
|
|
614
623
|
processSignals(signals, sidecar) {
|
|
615
624
|
const updates = new Array();
|
|
@@ -625,25 +634,21 @@ var SMTBeacon = class extends SinglePartyBeacon {
|
|
|
625
634
|
});
|
|
626
635
|
continue;
|
|
627
636
|
}
|
|
628
|
-
if (
|
|
637
|
+
if (proofIdHex(smtProof) !== signal.signalBytes) {
|
|
629
638
|
throw new SMTBeaconError(
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
{ smtProof, did }
|
|
639
|
+
`SMT proof id does not equal the signal root ${signal.signalBytes}.`,
|
|
640
|
+
import_common4.INVALID_SIGNAL_DATA,
|
|
641
|
+
{ smtProof, did, smtRootHash: signal.signalBytes }
|
|
633
642
|
);
|
|
634
643
|
}
|
|
635
|
-
|
|
636
|
-
const nonceHash = (0, import_smt.base64UrlToHash)(smtProof.nonce);
|
|
637
|
-
const candidateHash = smtProof.updateId ? (0, import_smt.blockHash)((0, import_smt.blockHash)(nonceHash), (0, import_smt.base64UrlToHash)(smtProof.updateId)) : (0, import_smt.blockHash)((0, import_smt.blockHash)(nonceHash));
|
|
638
|
-
const valid = (0, import_smt.verifySerializedProof)(smtProof, index, candidateHash);
|
|
639
|
-
if (!valid) {
|
|
644
|
+
if (!(0, import_smt.verifyProof)(smtProof, did)) {
|
|
640
645
|
throw new SMTBeaconError(
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
{ smtProof, did }
|
|
646
|
+
`SMT proof verification failed for the signal root ${signal.signalBytes}.`,
|
|
647
|
+
import_common4.INVALID_SIGNAL_DATA,
|
|
648
|
+
{ smtProof, did, smtRootHash: signal.signalBytes }
|
|
644
649
|
);
|
|
645
650
|
}
|
|
646
|
-
if (
|
|
651
|
+
if (smtProof.updateId === void 0) {
|
|
647
652
|
continue;
|
|
648
653
|
}
|
|
649
654
|
const updateHashHex = (0, import_smt.hashToHex)((0, import_smt.base64UrlToHash)(smtProof.updateId));
|
|
@@ -673,16 +678,15 @@ var SMTBeacon = class extends SinglePartyBeacon {
|
|
|
673
678
|
* @param {BitcoinConnection} bitcoin The Bitcoin network connection.
|
|
674
679
|
* @param {BroadcastOptions} [options] Optional broadcast configuration (e.g. fee estimator).
|
|
675
680
|
* @return {Promise<BroadcastResult>} The signed update, the signal txid, and the SMT
|
|
676
|
-
*
|
|
681
|
+
* proof (with the leaf nonce embedded). The proof MUST be captured for sidecar
|
|
677
682
|
* distribution: the nonce exists only here, so the on-chain signal is unresolvable without it.
|
|
678
683
|
* @throws {BeaconError} if the bitcoin address is invalid, unfunded, or UTXO cannot cover the fee.
|
|
679
684
|
*/
|
|
680
685
|
async broadcastSignal(signedUpdate, signer, bitcoin, options) {
|
|
681
686
|
const did = this.did;
|
|
682
|
-
const canonicalBytes = new TextEncoder().encode((0, import_common4.canonicalize)(signedUpdate));
|
|
683
687
|
const nonce = (0, import_utils2.randomBytes)(32);
|
|
684
688
|
const tree = new import_smt.BTCR2MerkleTree();
|
|
685
|
-
tree.addEntries([{ did, nonce,
|
|
689
|
+
tree.addEntries([{ did, nonce, updateId: (0, import_common4.canonicalHashBytes)(signedUpdate) }]);
|
|
686
690
|
tree.finalize();
|
|
687
691
|
const proof = tree.proof(did);
|
|
688
692
|
const txid = await this.buildSignAndBroadcast(tree.rootHash, signer, bitcoin, options);
|
|
@@ -2058,7 +2062,7 @@ function isSignedBTCR2Update(value) {
|
|
|
2058
2062
|
}
|
|
2059
2063
|
function isSMTProof(value) {
|
|
2060
2064
|
if (!isRecord(value)) return false;
|
|
2061
|
-
return typeof value.id === "string" && typeof value.collapsed === "string" && Array.isArray(value.hashes);
|
|
2065
|
+
return typeof value.id === "string" && typeof value.collapsed === "string" && Array.isArray(value.hashes) && value.hashes.every((h) => typeof h === "string") && (value.nonce === void 0 || typeof value.nonce === "string") && (value.updateId === void 0 || typeof value.updateId === "string");
|
|
2062
2066
|
}
|
|
2063
2067
|
function validateMinConf(value) {
|
|
2064
2068
|
if (value === void 0) return DEFAULT_MIN_CONF;
|
|
@@ -2133,15 +2137,23 @@ var Resolver = class _Resolver {
|
|
|
2133
2137
|
* The state of the specification loop, carried across every pass: the version counter
|
|
2134
2138
|
* (`current_version_id`), the update-hash history that backs duplicate confirmation
|
|
2135
2139
|
* (`update_hash_history`), the confirmations of the block that contains the most
|
|
2136
|
-
* recently applied unique update (`block_confirmations`),
|
|
2137
|
-
*
|
|
2138
|
-
*
|
|
2139
|
-
*
|
|
2140
|
+
* recently applied unique update (`block_confirmations`), the height of that block
|
|
2141
|
+
* (`current_block_height`), and the header time of that block as `updated`. A pass
|
|
2142
|
+
* that finds a new beacon address returns to discovery, so the state must not
|
|
2143
|
+
* restart: a restart would reject a linear history whose later updates are
|
|
2144
|
+
* announced on beacons that earlier updates added.
|
|
2140
2145
|
*/
|
|
2141
2146
|
#currentVersionId = 1;
|
|
2142
2147
|
#updateHashHistory = [];
|
|
2143
2148
|
#blockConfirmations = 0;
|
|
2144
2149
|
#updated;
|
|
2150
|
+
/**
|
|
2151
|
+
* The height of the block that contains the most recently applied update
|
|
2152
|
+
* (`current_block_height`). "Find Beacon Signals" keeps only the signals at or above
|
|
2153
|
+
* it: a beacon address that an update added has no signals for this DID before the
|
|
2154
|
+
* block of that update.
|
|
2155
|
+
*/
|
|
2156
|
+
#currentBlockHeight = 0;
|
|
2145
2157
|
/**
|
|
2146
2158
|
* Opt-in upper bound on multi-round beacon-discovery passes. `Infinity` (the
|
|
2147
2159
|
* default) leaves discovery unbounded; termination is already guaranteed by
|
|
@@ -2637,6 +2649,7 @@ var Resolver = class _Resolver {
|
|
|
2637
2649
|
this.#updateHashHistory.push((0, import_common12.canonicalHashBytes)(unsignedUpdate));
|
|
2638
2650
|
this.#currentVersionId++;
|
|
2639
2651
|
this.#blockConfirmations = block.confirmations;
|
|
2652
|
+
this.#currentBlockHeight = block.height;
|
|
2640
2653
|
this.#updated = import_common12.DateUtils.toISOStringNonFractional(import_common12.DateUtils.blocktimeToTimestamp(block.time));
|
|
2641
2654
|
if (this.#hasUnscannedBeacons()) {
|
|
2642
2655
|
if (++this.#discoveryRounds > this.#maxDiscoveryRounds) {
|
|
@@ -2692,8 +2705,12 @@ var Resolver = class _Resolver {
|
|
|
2692
2705
|
* malformed. It fails fast here with a typed error, in the style of the
|
|
2693
2706
|
* {@link provide} guards, and not later with an invalid date or a false
|
|
2694
2707
|
* `versionTime` comparison in the ProcessUpdate phase.
|
|
2708
|
+
*
|
|
2709
|
+
* "Find Beacon Signals" finds only the transactions at or above
|
|
2710
|
+
* `current_block_height`, the height of the block of the most recently applied
|
|
2711
|
+
* update. A signal below it is excluded: it emits no data need and applies no update.
|
|
2695
2712
|
* @param {Array<BeaconSignal>} signals The signals the caller provided for one service.
|
|
2696
|
-
* @returns {Array<BeaconSignal>} The signals at or above the threshold, in the given order.
|
|
2713
|
+
* @returns {Array<BeaconSignal>} The signals at or above the threshold and the height, in the given order.
|
|
2697
2714
|
* @throws {ResolveError} `INVALID_DID_UPDATE` for an eligible signal with no valid block metadata.
|
|
2698
2715
|
*/
|
|
2699
2716
|
#eligibleSignals(signals) {
|
|
@@ -2717,6 +2734,7 @@ var Resolver = class _Resolver {
|
|
|
2717
2734
|
}
|
|
2718
2735
|
);
|
|
2719
2736
|
}
|
|
2737
|
+
if (block.height < this.#currentBlockHeight) continue;
|
|
2720
2738
|
eligible.push(signal);
|
|
2721
2739
|
}
|
|
2722
2740
|
return eligible;
|
|
@@ -2759,7 +2777,7 @@ var Resolver = class _Resolver {
|
|
|
2759
2777
|
if (announcementHash !== need.announcementHash) {
|
|
2760
2778
|
throw new import_common12.ResolveError(
|
|
2761
2779
|
`CAS announcement hash mismatch: expected ${need.announcementHash}, got ${announcementHash}.`,
|
|
2762
|
-
import_common12.
|
|
2780
|
+
import_common12.MISSING_UPDATE_DATA,
|
|
2763
2781
|
{ expected: need.announcementHash, actual: announcementHash }
|
|
2764
2782
|
);
|
|
2765
2783
|
}
|
|
@@ -2778,7 +2796,7 @@ var Resolver = class _Resolver {
|
|
|
2778
2796
|
if (updateHash !== need.updateHash) {
|
|
2779
2797
|
throw new import_common12.ResolveError(
|
|
2780
2798
|
`Signed update hash mismatch: expected ${need.updateHash}, got ${updateHash}.`,
|
|
2781
|
-
import_common12.
|
|
2799
|
+
import_common12.INVALID_SIGNAL_DATA,
|
|
2782
2800
|
{ expected: need.updateHash, actual: updateHash }
|
|
2783
2801
|
);
|
|
2784
2802
|
}
|
|
@@ -2789,16 +2807,21 @@ var Resolver = class _Resolver {
|
|
|
2789
2807
|
if (!isSMTProof(data)) {
|
|
2790
2808
|
throw new import_common12.ResolveError(
|
|
2791
2809
|
"Provided data for NeedSMTProof is not an SMT proof.",
|
|
2792
|
-
import_common12.
|
|
2810
|
+
import_common12.INVALID_SIGNAL_DATA,
|
|
2793
2811
|
{ kind: need.kind }
|
|
2794
2812
|
);
|
|
2795
2813
|
}
|
|
2796
|
-
|
|
2797
|
-
|
|
2814
|
+
let proofIdHex2;
|
|
2815
|
+
try {
|
|
2816
|
+
proofIdHex2 = (0, import_common12.encode)((0, import_common12.decode)(data.id, "base64urlnopad"), "hex");
|
|
2817
|
+
} catch {
|
|
2818
|
+
proofIdHex2 = void 0;
|
|
2819
|
+
}
|
|
2820
|
+
if (proofIdHex2 !== need.smtRootHash) {
|
|
2798
2821
|
throw new import_common12.ResolveError(
|
|
2799
|
-
`SMT proof root hash mismatch: expected ${need.smtRootHash}, got ${
|
|
2800
|
-
import_common12.
|
|
2801
|
-
{ expected: need.smtRootHash, actual:
|
|
2822
|
+
`SMT proof root hash mismatch: expected ${need.smtRootHash}, got ${proofIdHex2 ?? "an id that does not decode"}.`,
|
|
2823
|
+
import_common12.INVALID_SIGNAL_DATA,
|
|
2824
|
+
{ expected: need.smtRootHash, actual: proofIdHex2 }
|
|
2802
2825
|
);
|
|
2803
2826
|
}
|
|
2804
2827
|
this.#sidecarData.smtMap.set(need.smtRootHash, data);
|
|
@@ -1,8 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { base64UrlToHash,
|
|
1
|
+
import { canonicalHashBytes, INVALID_SIGNAL_DATA } from '@did-btcr2/common';
|
|
2
|
+
import { base64UrlToHash, BTCR2MerkleTree, hashToHex, verifyProof } from '@did-btcr2/smt';
|
|
3
3
|
import { randomBytes } from '@noble/hashes/utils';
|
|
4
4
|
import { SinglePartyBeacon } from './beacon.js';
|
|
5
5
|
import { SMTBeaconError } from './error.js';
|
|
6
|
+
/** The hex of the base64url `id` of a proof, or `undefined` if the id does not decode to 32 bytes. */
|
|
7
|
+
function proofIdHex(proof) {
|
|
8
|
+
try {
|
|
9
|
+
return hashToHex(base64UrlToHash(proof.id));
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
6
15
|
/**
|
|
7
16
|
* Implements {@link https://dcdpr.github.io/did-btcr2/terminology.html#smt-beacon | SMT Beacon}.
|
|
8
17
|
*
|
|
@@ -27,15 +36,17 @@ export class SMTBeacon extends SinglePartyBeacon {
|
|
|
27
36
|
/**
|
|
28
37
|
* Implements {@link https://dcdpr.github.io/did-btcr2/operations/resolve.html#process-smt-beacon | 7.2.e.1 Process SMT Beacon}.
|
|
29
38
|
*
|
|
30
|
-
* For each signal, the signalBytes contain the hex-encoded SMT root hash
|
|
31
|
-
* This method looks up the SMT Proof from the sidecar by root
|
|
32
|
-
*
|
|
33
|
-
* signed update
|
|
39
|
+
* For each signal, the signalBytes contain the hex-encoded SMT root hash
|
|
40
|
+
* (`smt_root`). This method looks up the SMT Proof from the sidecar by root
|
|
41
|
+
* hash, checks that the id of the proof is the root, verifies the proof with
|
|
42
|
+
* the SMT Proof Verification algorithm, and retrieves the signed update by the
|
|
43
|
+
* proof's updateId. A proof with no updateId announces no update for the DID.
|
|
34
44
|
*
|
|
35
45
|
* @param {Array<BeaconSignal>} signals The array of Beacon Signals to process.
|
|
36
46
|
* @param {SidecarData} sidecar The sidecar data associated with the SMT Beacon.
|
|
37
47
|
* @returns {BeaconProcessResult} Successfully resolved updates and any data needs.
|
|
38
|
-
* @throws {SMTBeaconError} if
|
|
48
|
+
* @throws {SMTBeaconError} `INVALID_SIGNAL_DATA` if the id of the proof is not the
|
|
49
|
+
* signal root, or if the proof does not verify.
|
|
39
50
|
*/
|
|
40
51
|
processSignals(signals, sidecar) {
|
|
41
52
|
const updates = new Array();
|
|
@@ -43,7 +54,8 @@ export class SMTBeacon extends SinglePartyBeacon {
|
|
|
43
54
|
// The DID under resolution keys this beacon's leaf index.
|
|
44
55
|
const did = this.did;
|
|
45
56
|
for (const signal of signals) {
|
|
46
|
-
//
|
|
57
|
+
// "Process SMT Beacon": the signal bytes are smt_root, the hex SMT root hash.
|
|
58
|
+
// The smtMap is keyed by the hex of proof.id. No entry = a need for the proof.
|
|
47
59
|
const smtProof = sidecar.smtMap.get(signal.signalBytes);
|
|
48
60
|
if (!smtProof) {
|
|
49
61
|
// SMT Proof not available, emit a need
|
|
@@ -54,29 +66,23 @@ export class SMTBeacon extends SinglePartyBeacon {
|
|
|
54
66
|
});
|
|
55
67
|
continue;
|
|
56
68
|
}
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
69
|
+
// The id of the proof must equal smt_root. The resolver keys the map by the
|
|
70
|
+
// hex of the id, so its entries pass. A caller-built map is checked here too.
|
|
71
|
+
if (proofIdHex(smtProof) !== signal.signalBytes) {
|
|
72
|
+
throw new SMTBeaconError(`SMT proof id does not equal the signal root ${signal.signalBytes}.`, INVALID_SIGNAL_DATA, { smtProof, did, smtRootHash: signal.signalBytes });
|
|
60
73
|
}
|
|
61
|
-
// Verify the
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const nonceHash = base64UrlToHash(smtProof.nonce);
|
|
67
|
-
const candidateHash = smtProof.updateId
|
|
68
|
-
? blockHash(blockHash(nonceHash), base64UrlToHash(smtProof.updateId))
|
|
69
|
-
: blockHash(blockHash(nonceHash));
|
|
70
|
-
const valid = verifySerializedProof(smtProof, index, candidateHash);
|
|
71
|
-
if (!valid) {
|
|
72
|
-
throw new SMTBeaconError('SMT proof verification failed.', 'INVALID_SMT_PROOF', { smtProof, did });
|
|
74
|
+
// Verify the proof with the SMT Proof Verification algorithm. The nonce and
|
|
75
|
+
// updateId fields of the proof select the leaf value (four arms). A proof that
|
|
76
|
+
// does not decode, or that does not walk to the root, is INVALID_SIGNAL_DATA.
|
|
77
|
+
if (!verifyProof(smtProof, did)) {
|
|
78
|
+
throw new SMTBeaconError(`SMT proof verification failed for the signal root ${signal.signalBytes}.`, INVALID_SIGNAL_DATA, { smtProof, did, smtRootHash: signal.signalBytes });
|
|
73
79
|
}
|
|
74
|
-
//
|
|
75
|
-
if (
|
|
80
|
+
// No updateId: the signal announces no update for this DID. No tuple.
|
|
81
|
+
if (smtProof.updateId === undefined) {
|
|
76
82
|
continue;
|
|
77
83
|
}
|
|
78
84
|
// Look up the signed update in sidecar updateMap (keyed by hex canonical
|
|
79
|
-
// hash). The proof's updateId is
|
|
85
|
+
// hash). The proof's updateId is the same hash in base64url.
|
|
80
86
|
const updateHashHex = hashToHex(base64UrlToHash(smtProof.updateId));
|
|
81
87
|
const signedUpdate = sidecar.updateMap.get(updateHashHex);
|
|
82
88
|
if (!signedUpdate) {
|
|
@@ -105,20 +111,20 @@ export class SMTBeacon extends SinglePartyBeacon {
|
|
|
105
111
|
* @param {BitcoinConnection} bitcoin The Bitcoin network connection.
|
|
106
112
|
* @param {BroadcastOptions} [options] Optional broadcast configuration (e.g. fee estimator).
|
|
107
113
|
* @return {Promise<BroadcastResult>} The signed update, the signal txid, and the SMT
|
|
108
|
-
*
|
|
114
|
+
* proof (with the leaf nonce embedded). The proof MUST be captured for sidecar
|
|
109
115
|
* distribution: the nonce exists only here, so the on-chain signal is unresolvable without it.
|
|
110
116
|
* @throws {BeaconError} if the bitcoin address is invalid, unfunded, or UTXO cannot cover the fee.
|
|
111
117
|
*/
|
|
112
118
|
async broadcastSignal(signedUpdate, signer, bitcoin, options) {
|
|
113
119
|
// The DID keys this beacon's leaf index in the tree.
|
|
114
120
|
const did = this.did;
|
|
115
|
-
// Build a single-entry SMT
|
|
116
|
-
|
|
121
|
+
// Build a single-entry SMT in nonce mode: the leaf value is
|
|
122
|
+
// hash(hash(nonce) + updateId), with updateId the JSON Document Hash of the update.
|
|
117
123
|
const nonce = randomBytes(32);
|
|
118
124
|
const tree = new BTCR2MerkleTree();
|
|
119
|
-
tree.addEntries([{ did, nonce,
|
|
125
|
+
tree.addEntries([{ did, nonce, updateId: canonicalHashBytes(signedUpdate) }]);
|
|
120
126
|
tree.finalize();
|
|
121
|
-
// Serialize the
|
|
127
|
+
// Serialize the proof (carrying the nonce and updateId) before
|
|
122
128
|
// broadcasting: it is the only artifact that can link the on-chain root back
|
|
123
129
|
// to the update, and the nonce it embeds is irrecoverable once dropped.
|
|
124
130
|
const proof = tree.proof(did);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"smt-beacon.js","sourceRoot":"","sources":["../../../../src/core/beacon/smt-beacon.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"smt-beacon.js","sourceRoot":"","sources":["../../../../src/core/beacon/smt-beacon.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getNetwork } from '@did-btcr2/bitcoin';
|
|
2
|
-
import { canonicalHash, canonicalHashBytes, canonicalize, DateUtils, encode as encodeHash, decode as decodeHash, INTERNAL_ERROR, INVALID_DID, INVALID_DID_UPDATE, INVALID_OPTIONS, JSONPatch, JSONUtils, LATE_PUBLISHING_ERROR, NOT_FOUND, ResolveError } from '@did-btcr2/common';
|
|
2
|
+
import { canonicalHash, canonicalHashBytes, canonicalize, DateUtils, encode as encodeHash, decode as decodeHash, INTERNAL_ERROR, INVALID_DID, INVALID_DID_UPDATE, INVALID_OPTIONS, INVALID_SIGNAL_DATA, JSONPatch, JSONUtils, LATE_PUBLISHING_ERROR, MISSING_UPDATE_DATA, NOT_FOUND, ResolveError } from '@did-btcr2/common';
|
|
3
3
|
import { BTCR2_UPDATE_CONTEXT, isBtcr2UpdateContext } from './btcr2-update.js';
|
|
4
4
|
import { BIP340Cryptosuite, BIP340DataIntegrityProof, SchnorrMultikey } from '@did-btcr2/cryptosuite';
|
|
5
5
|
import { CompressedSecp256k1PublicKey } from '@did-btcr2/keypair';
|
|
@@ -44,13 +44,16 @@ function isSignedBTCR2Update(value) {
|
|
|
44
44
|
&& value.targetVersionId >= 2
|
|
45
45
|
&& isRecord(value.proof);
|
|
46
46
|
}
|
|
47
|
-
/** True if `value` has the shape of an SMT
|
|
47
|
+
/** True if `value` has the shape of an SMT proof: string fields, `hashes` an array of strings. */
|
|
48
48
|
function isSMTProof(value) {
|
|
49
49
|
if (!isRecord(value))
|
|
50
50
|
return false;
|
|
51
51
|
return typeof value.id === 'string'
|
|
52
52
|
&& typeof value.collapsed === 'string'
|
|
53
|
-
&& Array.isArray(value.hashes)
|
|
53
|
+
&& Array.isArray(value.hashes)
|
|
54
|
+
&& value.hashes.every(h => typeof h === 'string')
|
|
55
|
+
&& (value.nonce === undefined || typeof value.nonce === 'string')
|
|
56
|
+
&& (value.updateId === undefined || typeof value.updateId === 'string');
|
|
54
57
|
}
|
|
55
58
|
/**
|
|
56
59
|
* Validate `ResolutionOptions.minConf`. `undefined` selects the specification
|
|
@@ -180,15 +183,23 @@ export class Resolver {
|
|
|
180
183
|
* The state of the specification loop, carried across every pass: the version counter
|
|
181
184
|
* (`current_version_id`), the update-hash history that backs duplicate confirmation
|
|
182
185
|
* (`update_hash_history`), the confirmations of the block that contains the most
|
|
183
|
-
* recently applied unique update (`block_confirmations`),
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
186
|
+
* recently applied unique update (`block_confirmations`), the height of that block
|
|
187
|
+
* (`current_block_height`), and the header time of that block as `updated`. A pass
|
|
188
|
+
* that finds a new beacon address returns to discovery, so the state must not
|
|
189
|
+
* restart: a restart would reject a linear history whose later updates are
|
|
190
|
+
* announced on beacons that earlier updates added.
|
|
187
191
|
*/
|
|
188
192
|
#currentVersionId = 1;
|
|
189
193
|
#updateHashHistory = [];
|
|
190
194
|
#blockConfirmations = 0;
|
|
191
195
|
#updated;
|
|
196
|
+
/**
|
|
197
|
+
* The height of the block that contains the most recently applied update
|
|
198
|
+
* (`current_block_height`). "Find Beacon Signals" keeps only the signals at or above
|
|
199
|
+
* it: a beacon address that an update added has no signals for this DID before the
|
|
200
|
+
* block of that update.
|
|
201
|
+
*/
|
|
202
|
+
#currentBlockHeight = 0;
|
|
192
203
|
/**
|
|
193
204
|
* Opt-in upper bound on multi-round beacon-discovery passes. `Infinity` (the
|
|
194
205
|
* default) leaves discovery unbounded; termination is already guaranteed by
|
|
@@ -670,7 +681,7 @@ export class Resolver {
|
|
|
670
681
|
const removed = !BeaconUtils.getBeaconServices(document).some(service => BeaconUtils.parseBitcoinAddress(service.serviceEndpoint) === address);
|
|
671
682
|
if (removed)
|
|
672
683
|
continue;
|
|
673
|
-
// Step
|
|
684
|
+
// Step 6, "Check targetVersionId", first arm: targetVersionId <= currentVersionId
|
|
674
685
|
// re-announces an applied version. Confirm that it is a true duplicate, then
|
|
675
686
|
// skip it. A duplicate does not advance the version counter, does not append
|
|
676
687
|
// to the history (the slot already holds the applied update, ADR 067), and
|
|
@@ -690,22 +701,24 @@ export class Resolver {
|
|
|
690
701
|
this.#phase = ResolverPhase.Complete;
|
|
691
702
|
continue;
|
|
692
703
|
}
|
|
693
|
-
// Step
|
|
704
|
+
// Step 6, third arm: a version was skipped, so raise LATE_PUBLISHING.
|
|
694
705
|
if (update.targetVersionId !== this.#currentVersionId + 1) {
|
|
695
706
|
throw new ResolveError(`Version Id Mismatch: targetVersionId cannot be > currentVersionId + 1`, LATE_PUBLISHING_ERROR, {
|
|
696
707
|
targetVersionId: update.targetVersionId,
|
|
697
708
|
currentVersionId: this.#currentVersionId + 1
|
|
698
709
|
});
|
|
699
710
|
}
|
|
700
|
-
// Step
|
|
711
|
+
// Step 6, second arm: targetVersionId == currentVersionId + 1. Apply the update,
|
|
701
712
|
// append the unsigned update hash to the history, increment the version.
|
|
702
713
|
this.#currentDocument = Resolver.applyUpdate(document, update, block);
|
|
703
714
|
const unsignedUpdate = JSONUtils.deleteKeys(update, ['proof']);
|
|
704
715
|
this.#updateHashHistory.push(canonicalHashBytes(unsignedUpdate));
|
|
705
716
|
this.#currentVersionId++;
|
|
706
|
-
//
|
|
707
|
-
// path only: the stop above and the duplicate branch
|
|
717
|
+
// "Apply Update": block_confirmations, current_block_height, and the header time
|
|
718
|
+
// as `updated`. On the apply path only: the stop above and the duplicate branch
|
|
719
|
+
// stamp nothing.
|
|
708
720
|
this.#blockConfirmations = block.confirmations;
|
|
721
|
+
this.#currentBlockHeight = block.height;
|
|
709
722
|
this.#updated = DateUtils.toISOStringNonFractional(DateUtils.blocktimeToTimestamp(block.time));
|
|
710
723
|
// The applied update can add a beacon service. "Find Beacon Signals" runs at
|
|
711
724
|
// the top of every pass for the addresses that are not scanned yet, so the
|
|
@@ -763,8 +776,12 @@ export class Resolver {
|
|
|
763
776
|
* malformed. It fails fast here with a typed error, in the style of the
|
|
764
777
|
* {@link provide} guards, and not later with an invalid date or a false
|
|
765
778
|
* `versionTime` comparison in the ProcessUpdate phase.
|
|
779
|
+
*
|
|
780
|
+
* "Find Beacon Signals" finds only the transactions at or above
|
|
781
|
+
* `current_block_height`, the height of the block of the most recently applied
|
|
782
|
+
* update. A signal below it is excluded: it emits no data need and applies no update.
|
|
766
783
|
* @param {Array<BeaconSignal>} signals The signals the caller provided for one service.
|
|
767
|
-
* @returns {Array<BeaconSignal>} The signals at or above the threshold, in the given order.
|
|
784
|
+
* @returns {Array<BeaconSignal>} The signals at or above the threshold and the height, in the given order.
|
|
768
785
|
* @throws {ResolveError} `INVALID_DID_UPDATE` for an eligible signal with no valid block metadata.
|
|
769
786
|
*/
|
|
770
787
|
#eligibleSignals(signals) {
|
|
@@ -785,6 +802,11 @@ export class Resolver {
|
|
|
785
802
|
mediantime: block?.mediantime
|
|
786
803
|
});
|
|
787
804
|
}
|
|
805
|
+
// "Find Beacon Signals" finds only the transactions at or above current_block_height.
|
|
806
|
+
// A signal before the block of the update that added the address is not a signal
|
|
807
|
+
// of this DID.
|
|
808
|
+
if (block.height < this.#currentBlockHeight)
|
|
809
|
+
continue;
|
|
788
810
|
eligible.push(signal);
|
|
789
811
|
}
|
|
790
812
|
return eligible;
|
|
@@ -812,10 +834,12 @@ export class Resolver {
|
|
|
812
834
|
throw new ResolveError('Provided data for NeedCASAnnouncement is not a CAS announcement.', INVALID_DID_UPDATE, { kind: need.kind });
|
|
813
835
|
}
|
|
814
836
|
// Fail fast if the provided announcement is not the one the on-chain
|
|
815
|
-
// signal requested: its canonical hash must equal the need's hash.
|
|
837
|
+
// signal requested: its canonical hash must equal the need's hash. The
|
|
838
|
+
// specification ("Process CAS Beacon") treats an announcement whose hash is
|
|
839
|
+
// not map_update_hash as not available from CAS: MISSING_UPDATE_DATA.
|
|
816
840
|
const announcementHash = canonicalHash(data, { encoding: 'hex' });
|
|
817
841
|
if (announcementHash !== need.announcementHash) {
|
|
818
|
-
throw new ResolveError(`CAS announcement hash mismatch: expected ${need.announcementHash}, got ${announcementHash}.`,
|
|
842
|
+
throw new ResolveError(`CAS announcement hash mismatch: expected ${need.announcementHash}, got ${announcementHash}.`, MISSING_UPDATE_DATA, { expected: need.announcementHash, actual: announcementHash });
|
|
819
843
|
}
|
|
820
844
|
this.#sidecarData.casMap.set(announcementHash, data);
|
|
821
845
|
break;
|
|
@@ -825,22 +849,33 @@ export class Resolver {
|
|
|
825
849
|
throw new ResolveError('Provided data for NeedSignedUpdate is not a signed BTCR2 update.', INVALID_DID_UPDATE, { kind: need.kind });
|
|
826
850
|
}
|
|
827
851
|
// Fail fast if the provided update is not the one the on-chain signal
|
|
828
|
-
// requested:
|
|
852
|
+
// requested: the specification compares the JSON Document Hash of a
|
|
853
|
+
// retrieved update to update_hash, and a mismatch is INVALID_SIGNAL_DATA.
|
|
829
854
|
const updateHash = canonicalHash(data, { encoding: 'hex' });
|
|
830
855
|
if (updateHash !== need.updateHash) {
|
|
831
|
-
throw new ResolveError(`Signed update hash mismatch: expected ${need.updateHash}, got ${updateHash}.`,
|
|
856
|
+
throw new ResolveError(`Signed update hash mismatch: expected ${need.updateHash}, got ${updateHash}.`, INVALID_SIGNAL_DATA, { expected: need.updateHash, actual: updateHash });
|
|
832
857
|
}
|
|
833
858
|
this.#sidecarData.updateMap.set(updateHash, data);
|
|
834
859
|
break;
|
|
835
860
|
}
|
|
836
861
|
case 'NeedSMTProof': {
|
|
862
|
+
// A proof of another shape is data for the signal that does not agree with
|
|
863
|
+
// its Signal Bytes: INVALID_SIGNAL_DATA, as for the id and the walk below.
|
|
837
864
|
if (!isSMTProof(data)) {
|
|
838
|
-
throw new ResolveError('Provided data for NeedSMTProof is not an SMT proof.',
|
|
865
|
+
throw new ResolveError('Provided data for NeedSMTProof is not an SMT proof.', INVALID_SIGNAL_DATA, { kind: need.kind });
|
|
866
|
+
}
|
|
867
|
+
// proof.id is base64url per spec; smtRootHash is the hex on-chain signal. The
|
|
868
|
+
// specification ("Process SMT Beacon") compares the id of smt_proof to
|
|
869
|
+
// smt_root: a mismatch, or an id that does not decode, is INVALID_SIGNAL_DATA.
|
|
870
|
+
let proofIdHex;
|
|
871
|
+
try {
|
|
872
|
+
proofIdHex = encodeHash(decodeHash(data.id, 'base64urlnopad'), 'hex');
|
|
873
|
+
}
|
|
874
|
+
catch {
|
|
875
|
+
proofIdHex = undefined;
|
|
839
876
|
}
|
|
840
|
-
// proof.id is base64url per spec; smtRootHash is the hex on-chain signal.
|
|
841
|
-
const proofIdHex = encodeHash(decodeHash(data.id, 'base64urlnopad'), 'hex');
|
|
842
877
|
if (proofIdHex !== need.smtRootHash) {
|
|
843
|
-
throw new ResolveError(`SMT proof root hash mismatch: expected ${need.smtRootHash}, got ${proofIdHex}
|
|
878
|
+
throw new ResolveError(`SMT proof root hash mismatch: expected ${need.smtRootHash}, got ${proofIdHex ?? 'an id that does not decode'}.`, INVALID_SIGNAL_DATA, { expected: need.smtRootHash, actual: proofIdHex });
|
|
844
879
|
}
|
|
845
880
|
this.#sidecarData.smtMap.set(need.smtRootHash, data);
|
|
846
881
|
break;
|