@did-btcr2/method 0.62.0 → 0.64.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 +14 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/browser.js +3 -3
- package/dist/browser.mjs +3 -3
- package/dist/cjs/index.js +1269 -986
- package/dist/esm/core/beacon/signal-discovery.js +34 -1
- package/dist/esm/core/beacon/signal-discovery.js.map +1 -1
- package/dist/esm/core/btcr2-update.js +11 -0
- package/dist/esm/core/btcr2-update.js.map +1 -1
- package/dist/esm/core/resolver.js +345 -252
- package/dist/esm/core/resolver.js.map +1 -1
- package/dist/esm/core/updater.js +33 -3
- package/dist/esm/core/updater.js.map +1 -1
- package/dist/esm/did-btcr2.js +53 -20
- package/dist/esm/did-btcr2.js.map +1 -1
- package/dist/esm/utils/appendix.js +39 -2
- package/dist/esm/utils/appendix.js.map +1 -1
- package/dist/esm/utils/error-cause.js +16 -0
- package/dist/esm/utils/error-cause.js.map +1 -0
- package/dist/types/core/beacon/interfaces.d.ts +9 -1
- package/dist/types/core/beacon/interfaces.d.ts.map +1 -1
- package/dist/types/core/beacon/signal-discovery.d.ts +12 -0
- package/dist/types/core/beacon/signal-discovery.d.ts.map +1 -1
- package/dist/types/core/btcr2-update.d.ts +15 -8
- package/dist/types/core/btcr2-update.d.ts.map +1 -1
- package/dist/types/core/interfaces.d.ts +16 -5
- package/dist/types/core/interfaces.d.ts.map +1 -1
- package/dist/types/core/resolver.d.ts +37 -23
- package/dist/types/core/resolver.d.ts.map +1 -1
- package/dist/types/core/updater.d.ts.map +1 -1
- package/dist/types/did-btcr2.d.ts +24 -3
- package/dist/types/did-btcr2.d.ts.map +1 -1
- package/dist/types/utils/appendix.d.ts +24 -0
- package/dist/types/utils/appendix.d.ts.map +1 -1
- package/dist/types/utils/error-cause.d.ts +16 -0
- package/dist/types/utils/error-cause.d.ts.map +1 -0
- package/package.json +3 -3
- package/src/core/beacon/interfaces.ts +10 -1
- package/src/core/beacon/signal-discovery.ts +44 -1
- package/src/core/btcr2-update.ts +20 -8
- package/src/core/interfaces.ts +16 -5
- package/src/core/resolver.ts +420 -315
- package/src/core/updater.ts +41 -3
- package/src/did-btcr2.ts +70 -25
- package/src/utils/appendix.ts +48 -2
- package/src/utils/error-cause.ts +23 -0
package/dist/cjs/index.js
CHANGED
|
@@ -32,6 +32,7 @@ __export(index_exports, {
|
|
|
32
32
|
CASBeacon: () => CASBeacon,
|
|
33
33
|
CASBeaconError: () => CASBeaconError,
|
|
34
34
|
CHANGE_OUTPUT_VBYTES: () => CHANGE_OUTPUT_VBYTES,
|
|
35
|
+
DEACTIVATION_PATCH: () => DEACTIVATION_PATCH,
|
|
35
36
|
DEFAULT_FEE_ESTIMATOR: () => DEFAULT_FEE_ESTIMATOR,
|
|
36
37
|
DEFAULT_MIN_CONF: () => DEFAULT_MIN_CONF,
|
|
37
38
|
DID_REGEX: () => DID_REGEX,
|
|
@@ -764,6 +765,43 @@ var Appendix = class _Appendix {
|
|
|
764
765
|
const id = typeof entry === "object" && entry !== null && !Array.isArray(entry) ? entry.id : entry;
|
|
765
766
|
return _Appendix.absoluteDidUrl(id, did);
|
|
766
767
|
}
|
|
768
|
+
/**
|
|
769
|
+
* Finds the entry of `document.capabilityInvocation` that identifies `methodId`. A
|
|
770
|
+
* reference entry identifies it when the two DID URLs are equal. An embedded verification
|
|
771
|
+
* method object identifies it when its `id` is equal. Both spellings of a DID URL compare
|
|
772
|
+
* equal, as in {@link relationshipMethodId}. This is the lookup of the specification steps
|
|
773
|
+
* "Check `update.proof`" (the read path) and "Construct BTCR2 Signed Update" (the write
|
|
774
|
+
* path); the caller raises `INVALID_DID_UPDATE` when no entry identifies the method.
|
|
775
|
+
*
|
|
776
|
+
* @param {DidDocument} document The DID document.
|
|
777
|
+
* @param {unknown} methodId The verification method id, absolute or relative. A non-string yields `undefined`.
|
|
778
|
+
* @returns {string | DidVerificationMethod | undefined} The entry, or `undefined` if no entry identifies the id.
|
|
779
|
+
*/
|
|
780
|
+
static capabilityInvocationEntry(document, methodId) {
|
|
781
|
+
const targetId = _Appendix.relationshipMethodId(methodId, document.id);
|
|
782
|
+
if (targetId === void 0) return void 0;
|
|
783
|
+
return document.capabilityInvocation?.find(
|
|
784
|
+
(entry) => _Appendix.relationshipMethodId(entry, document.id) === targetId
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Returns the verification method that a relationship entry denotes: the object itself
|
|
789
|
+
* when the entry embeds the method, else the member of `document.verificationMethod` whose
|
|
790
|
+
* `id` equals the reference. Both spellings of a DID URL compare equal. The caller raises
|
|
791
|
+
* `INVALID_DID_UPDATE` when a reference names no member.
|
|
792
|
+
*
|
|
793
|
+
* @param {DidDocument} document The DID document.
|
|
794
|
+
* @param {string | DidVerificationMethod} entry The relationship entry: a reference, or an embedded method.
|
|
795
|
+
* @returns {DidVerificationMethod | undefined} The method, or `undefined` if a reference names no member.
|
|
796
|
+
*/
|
|
797
|
+
static verificationMethodOfEntry(document, entry) {
|
|
798
|
+
if (_Appendix.isDidVerificationMethod(entry)) return entry;
|
|
799
|
+
const targetId = _Appendix.absoluteDidUrl(entry, document.id);
|
|
800
|
+
if (targetId === void 0) return void 0;
|
|
801
|
+
return document.verificationMethod?.find(
|
|
802
|
+
(method) => _Appendix.absoluteDidUrl(method?.id, document.id) === targetId
|
|
803
|
+
);
|
|
804
|
+
}
|
|
767
805
|
/**
|
|
768
806
|
* Validates that the given object is a DidVerificationMethod
|
|
769
807
|
* @param {unknown} obj The object to validate
|
|
@@ -879,10 +917,11 @@ var Appendix = class _Appendix {
|
|
|
879
917
|
*/
|
|
880
918
|
static dereferenceZcapId(capabilityId) {
|
|
881
919
|
const rootCapability = {};
|
|
882
|
-
const
|
|
883
|
-
if (
|
|
920
|
+
const components = capabilityId.split(":");
|
|
921
|
+
if (components.length !== 4) {
|
|
884
922
|
throw new import_dids.DidError(import_dids.DidErrorCode.InvalidDid, `Invalid capabilityId: ${capabilityId}`);
|
|
885
923
|
}
|
|
924
|
+
const [urn, zcap, root, did] = components;
|
|
886
925
|
if (!urn || urn !== "urn") {
|
|
887
926
|
throw new import_dids.DidError(import_dids.DidErrorCode.InvalidDid, `Invalid capabilityId: ${capabilityId}`);
|
|
888
927
|
}
|
|
@@ -1814,6 +1853,7 @@ var BeaconSignalDiscovery = class _BeaconSignalDiscovery {
|
|
|
1814
1853
|
static async indexer(beaconServices, bitcoin) {
|
|
1815
1854
|
const beaconServiceSignals = /* @__PURE__ */ new Map();
|
|
1816
1855
|
const currentBlockCount = await bitcoin.rest.block.count();
|
|
1856
|
+
const mediantimes = /* @__PURE__ */ new Map();
|
|
1817
1857
|
for (const beaconService of beaconServices) {
|
|
1818
1858
|
beaconServiceSignals.set(beaconService, []);
|
|
1819
1859
|
const beaconAddress = BeaconUtils.parseBitcoinAddress(beaconService.serviceEndpoint);
|
|
@@ -1838,19 +1878,49 @@ var BeaconSignalDiscovery = class _BeaconSignalDiscovery {
|
|
|
1838
1878
|
continue;
|
|
1839
1879
|
}
|
|
1840
1880
|
const confirmations = currentBlockCount - status.block_height + 1;
|
|
1881
|
+
const mediantime = await _BeaconSignalDiscovery.mediantime(status.block_hash, bitcoin, mediantimes);
|
|
1841
1882
|
beaconServiceSignals.get(beaconService)?.push({
|
|
1842
1883
|
tx: beaconSignal,
|
|
1843
1884
|
signalBytes: updateHash,
|
|
1844
1885
|
blockMetadata: {
|
|
1845
1886
|
confirmations,
|
|
1846
1887
|
height: status.block_height,
|
|
1847
|
-
time: status.block_time
|
|
1888
|
+
time: status.block_time,
|
|
1889
|
+
mediantime
|
|
1848
1890
|
}
|
|
1849
1891
|
});
|
|
1850
1892
|
}
|
|
1851
1893
|
}
|
|
1852
1894
|
return beaconServiceSignals;
|
|
1853
1895
|
}
|
|
1896
|
+
/**
|
|
1897
|
+
* Return the median time past of a block, from the per-run cache or from the
|
|
1898
|
+
* Esplora block record (`GET /block/:hash`). The specification compares
|
|
1899
|
+
* `versionTime` with the block `mediantime`, and the address listing does not
|
|
1900
|
+
* carry it. One block record serves every signal in that block.
|
|
1901
|
+
* @param {string} blockhash The hash of the block that contains a signal.
|
|
1902
|
+
* @param {BitcoinConnection} bitcoin Bitcoin network connection to use for REST calls.
|
|
1903
|
+
* @param {Map<string, number>} cache The median time past values fetched so far, keyed by block hash.
|
|
1904
|
+
* @returns {Promise<number>} The median time past of the block, in Unix seconds.
|
|
1905
|
+
* @throws {ResolveError} `INTERNAL_ERROR` if the backend returns no block record or no `mediantime` for the hash.
|
|
1906
|
+
*/
|
|
1907
|
+
static async mediantime(blockhash, bitcoin, cache) {
|
|
1908
|
+
const cached = cache.get(blockhash);
|
|
1909
|
+
if (cached !== void 0) {
|
|
1910
|
+
return cached;
|
|
1911
|
+
}
|
|
1912
|
+
const block = await bitcoin.rest.block.get({ blockhash });
|
|
1913
|
+
const mediantime = block?.mediantime;
|
|
1914
|
+
if (typeof mediantime !== "number" || !Number.isFinite(mediantime)) {
|
|
1915
|
+
throw new import_common10.ResolveError(
|
|
1916
|
+
`Block ${blockhash} has no mediantime in the block record of the Bitcoin REST backend.`,
|
|
1917
|
+
import_common10.INTERNAL_ERROR,
|
|
1918
|
+
{ blockhash, block }
|
|
1919
|
+
);
|
|
1920
|
+
}
|
|
1921
|
+
cache.set(blockhash, mediantime);
|
|
1922
|
+
return mediantime;
|
|
1923
|
+
}
|
|
1854
1924
|
/**
|
|
1855
1925
|
* Traverse the full blockchain from genesis to chain top looking for beacon signals.
|
|
1856
1926
|
* @param {Array<BeaconService>} beaconServices Array of BeaconService objects to search for signals.
|
|
@@ -1920,6 +1990,7 @@ var BeaconSignalDiscovery = class _BeaconSignalDiscovery {
|
|
|
1920
1990
|
blockMetadata: {
|
|
1921
1991
|
height: block.height,
|
|
1922
1992
|
time: block.time,
|
|
1993
|
+
mediantime: block.mediantime,
|
|
1923
1994
|
confirmations: block.confirmations
|
|
1924
1995
|
}
|
|
1925
1996
|
});
|
|
@@ -1947,1127 +2018,1127 @@ var BTCR2_UPDATE_CONTEXT = Object.freeze([
|
|
|
1947
2018
|
function isBtcr2UpdateContext(value, expected = BTCR2_UPDATE_CONTEXT) {
|
|
1948
2019
|
return Array.isArray(value) && value.length === expected.length && value.every((url, i) => url === expected[i]);
|
|
1949
2020
|
}
|
|
2021
|
+
var DEACTIVATION_PATCH = Object.freeze({
|
|
2022
|
+
op: "add",
|
|
2023
|
+
path: "/deactivated",
|
|
2024
|
+
value: true
|
|
2025
|
+
});
|
|
1950
2026
|
|
|
1951
2027
|
// src/core/did-sender-resolver.ts
|
|
1952
|
-
var
|
|
1953
|
-
var
|
|
2028
|
+
var import_common13 = require("@did-btcr2/common");
|
|
2029
|
+
var import_cryptosuite2 = require("@did-btcr2/cryptosuite");
|
|
1954
2030
|
var import_keypair4 = require("@did-btcr2/keypair");
|
|
1955
2031
|
|
|
1956
2032
|
// src/core/resolver.ts
|
|
1957
2033
|
var import_bitcoin5 = require("@did-btcr2/bitcoin");
|
|
1958
|
-
var import_common13 = require("@did-btcr2/common");
|
|
1959
|
-
var import_cryptosuite2 = require("@did-btcr2/cryptosuite");
|
|
1960
|
-
var import_keypair3 = require("@did-btcr2/keypair");
|
|
1961
|
-
|
|
1962
|
-
// src/did-btcr2.ts
|
|
1963
2034
|
var import_common12 = require("@did-btcr2/common");
|
|
1964
|
-
var
|
|
2035
|
+
var import_cryptosuite = require("@did-btcr2/cryptosuite");
|
|
2036
|
+
var import_keypair3 = require("@did-btcr2/keypair");
|
|
1965
2037
|
|
|
1966
|
-
// src/
|
|
2038
|
+
// src/utils/error-cause.ts
|
|
1967
2039
|
var import_common11 = require("@did-btcr2/common");
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
2040
|
+
function errorCause(error) {
|
|
2041
|
+
if (error instanceof import_common11.DidMethodError) return { type: error.type, message: error.message };
|
|
2042
|
+
if (error instanceof Error) return { type: error.name, message: error.message };
|
|
2043
|
+
return { type: "unknown", message: String(error) };
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
// src/core/resolver.ts
|
|
2047
|
+
var import_utils7 = require("@noble/curves/utils.js");
|
|
2048
|
+
var DEFAULT_MIN_CONF = 6;
|
|
2049
|
+
function isRecord(value) {
|
|
2050
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2051
|
+
}
|
|
2052
|
+
function isCASAnnouncement(value) {
|
|
2053
|
+
return isRecord(value) && Object.values(value).every((v) => typeof v === "string");
|
|
2054
|
+
}
|
|
2055
|
+
function isSignedBTCR2Update(value) {
|
|
2056
|
+
if (!isRecord(value)) return false;
|
|
2057
|
+
return Array.isArray(value.patch) && typeof value.sourceHash === "string" && typeof value.targetHash === "string" && Number.isInteger(value.targetVersionId) && value.targetVersionId >= 2 && isRecord(value.proof);
|
|
2058
|
+
}
|
|
2059
|
+
function isSMTProof(value) {
|
|
2060
|
+
if (!isRecord(value)) return false;
|
|
2061
|
+
return typeof value.id === "string" && typeof value.collapsed === "string" && Array.isArray(value.hashes);
|
|
2062
|
+
}
|
|
2063
|
+
function validateMinConf(value) {
|
|
2064
|
+
if (value === void 0) return DEFAULT_MIN_CONF;
|
|
2065
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= 1) return value;
|
|
2066
|
+
throw new import_common12.ResolveError(
|
|
2067
|
+
`Invalid resolution option minConf: expected a positive integer (minimum 1), got ${shown(value)}.`,
|
|
2068
|
+
import_common12.INVALID_OPTIONS,
|
|
2069
|
+
{ minConf: value }
|
|
2070
|
+
);
|
|
2071
|
+
}
|
|
2072
|
+
function shown(value) {
|
|
2073
|
+
return typeof value === "string" ? JSON.stringify(value) : String(value);
|
|
2074
|
+
}
|
|
2075
|
+
var ASCII_INTEGER = /^-?[0-9]+$/;
|
|
2076
|
+
function validateVersionId(value) {
|
|
2077
|
+
if (value === void 0) return void 0;
|
|
2078
|
+
if (typeof value === "string" && ASCII_INTEGER.test(value) && Number.isSafeInteger(Number(value))) {
|
|
2079
|
+
return Number(value);
|
|
2080
|
+
}
|
|
2081
|
+
throw new import_common12.ResolveError(
|
|
2082
|
+
`Invalid resolution option versionId: expected an ASCII string of an integer, got ${shown(value)}.`,
|
|
2083
|
+
import_common12.INVALID_OPTIONS,
|
|
2084
|
+
{ versionId: value }
|
|
2085
|
+
);
|
|
2086
|
+
}
|
|
2087
|
+
var UTC_XSD_DATETIME = /^-?\d{4,}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/;
|
|
2088
|
+
var XSD_TIMEZONE = /(Z|[+-]\d{2}:\d{2})$/;
|
|
2089
|
+
function validateVersionTime(value) {
|
|
2090
|
+
if (value === void 0) return void 0;
|
|
2091
|
+
if (typeof value === "string" && UTC_XSD_DATETIME.test(value) && import_common12.DateUtils.isValidXsdDateTime(value)) {
|
|
2092
|
+
const ms = Date.parse(value);
|
|
2093
|
+
if (Number.isFinite(ms)) return ms;
|
|
2094
|
+
}
|
|
2095
|
+
throw new import_common12.ResolveError(
|
|
2096
|
+
`Invalid resolution option versionTime: expected an XML Datetime in UTC without a fraction (for example "2026-07-01T00:00:00Z"), got ${shown(value)}.`,
|
|
2097
|
+
import_common12.INVALID_OPTIONS,
|
|
2098
|
+
{ versionTime: value }
|
|
2099
|
+
);
|
|
2100
|
+
}
|
|
2101
|
+
var Resolver = class _Resolver {
|
|
2102
|
+
// --- Immutable inputs ---
|
|
2103
|
+
#didComponents;
|
|
2104
|
+
/** The parsed `ResolutionOptions.versionId`, or `undefined` when the option is absent. */
|
|
2105
|
+
#versionId;
|
|
2106
|
+
/** The parsed `ResolutionOptions.versionTime` in milliseconds since the Unix epoch, or `undefined`. */
|
|
2107
|
+
#versionTime;
|
|
1976
2108
|
/**
|
|
1977
|
-
*
|
|
2109
|
+
* The specific phase the Resolver is current in.
|
|
1978
2110
|
*/
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
// Used by generate-vector.ts and other scripts that need direct access to
|
|
1988
|
-
// individual update steps outside the state machine flow.
|
|
2111
|
+
#phase;
|
|
2112
|
+
#sidecarData;
|
|
2113
|
+
#currentDocument;
|
|
2114
|
+
#providedGenesisDocument = null;
|
|
2115
|
+
#beaconServicesSignals = /* @__PURE__ */ new Map();
|
|
2116
|
+
#processedServices = /* @__PURE__ */ new Set();
|
|
2117
|
+
/** The beacon addresses the resolver requested signals for: `scanned_beacons` of the specification. */
|
|
2118
|
+
#requestCache = /* @__PURE__ */ new Set();
|
|
1989
2119
|
/**
|
|
1990
|
-
*
|
|
1991
|
-
*
|
|
1992
|
-
*
|
|
1993
|
-
* @param {PatchOperation[]} patches The JSON Patch operations to apply.
|
|
1994
|
-
* @param {number} sourceVersionId The version ID of the source document.
|
|
1995
|
-
* @returns {UnsignedBTCR2Update} The constructed UnsignedBTCR2Update object.
|
|
1996
|
-
* @throws {UpdateError} If the target document fails DID Core validation.
|
|
2120
|
+
* The tuples of the specification's `updates` list: a signed update and the metadata of
|
|
2121
|
+
* the block that announced it. BeaconProcess appends; ProcessUpdate sorts the list and
|
|
2122
|
+
* removes one tuple per step. A tuple that one pass does not reach waits for the next.
|
|
1997
2123
|
*/
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2124
|
+
#unsortedUpdates = [];
|
|
2125
|
+
#resolvedResponse = null;
|
|
2126
|
+
/**
|
|
2127
|
+
* The state of the specification loop, carried across every pass: the version counter
|
|
2128
|
+
* (`current_version_id`), the update-hash history that backs duplicate confirmation
|
|
2129
|
+
* (`update_hash_history`), the confirmations of the block that contains the most
|
|
2130
|
+
* recently applied unique update (`block_confirmations`), and the header time of that
|
|
2131
|
+
* block as `updated`. A pass that finds a new beacon address returns to discovery, so
|
|
2132
|
+
* the state must not restart: a restart would reject a linear history whose later
|
|
2133
|
+
* updates are announced on beacons that earlier updates added.
|
|
2134
|
+
*/
|
|
2135
|
+
#currentVersionId = 1;
|
|
2136
|
+
#updateHashHistory = [];
|
|
2137
|
+
#blockConfirmations = 0;
|
|
2138
|
+
#updated;
|
|
2139
|
+
/**
|
|
2140
|
+
* Opt-in upper bound on multi-round beacon-discovery passes. `Infinity` (the
|
|
2141
|
+
* default) leaves discovery unbounded; termination is already guaranteed by
|
|
2142
|
+
* de-duplicating already-queried beacon addresses. A positive value is a
|
|
2143
|
+
* caller-imposed resource guard; a non-positive value or omission means no limit.
|
|
2144
|
+
*/
|
|
2145
|
+
#maxDiscoveryRounds;
|
|
2146
|
+
/** Count of beacon-discovery passes driven by updates adding new beacon services. */
|
|
2147
|
+
#discoveryRounds = 0;
|
|
2148
|
+
/**
|
|
2149
|
+
* Minimum block confirmations a Beacon Signal must have before this resolver
|
|
2150
|
+
* processes it: `ResolutionOptions.minConf`, default {@link DEFAULT_MIN_CONF}.
|
|
2151
|
+
* Applied at signal intake in the BeaconProcess phase. A signal below the
|
|
2152
|
+
* threshold is excluded from the resolution; the rest of the signals are
|
|
2153
|
+
* processed.
|
|
2154
|
+
*/
|
|
2155
|
+
#minConf;
|
|
2156
|
+
/**
|
|
2157
|
+
* @internal Use {@link DidBtcr2.resolve} to create instances.
|
|
2158
|
+
*/
|
|
2159
|
+
constructor(didComponents, sidecarData, currentDocument, options) {
|
|
2160
|
+
this.#didComponents = didComponents;
|
|
2161
|
+
this.#sidecarData = sidecarData;
|
|
2162
|
+
this.#currentDocument = currentDocument;
|
|
2163
|
+
if (options?.versionId !== void 0 && options?.versionTime !== void 0) {
|
|
2164
|
+
throw new import_common12.ResolveError(
|
|
2165
|
+
"Invalid resolution options: versionId and versionTime are mutually exclusive. Pass one of them.",
|
|
2166
|
+
import_common12.INVALID_OPTIONS,
|
|
2167
|
+
{ versionId: options.versionId, versionTime: options.versionTime }
|
|
2014
2168
|
);
|
|
2015
2169
|
}
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
);
|
|
2170
|
+
this.#versionId = validateVersionId(options?.versionId);
|
|
2171
|
+
this.#versionTime = validateVersionTime(options?.versionTime);
|
|
2172
|
+
const rounds = options?.maxDiscoveryRounds;
|
|
2173
|
+
this.#maxDiscoveryRounds = typeof rounds === "number" && rounds > 0 ? rounds : Infinity;
|
|
2174
|
+
this.#minConf = validateMinConf(options?.minConf);
|
|
2175
|
+
if (options?.genesisDocument) {
|
|
2176
|
+
this.#providedGenesisDocument = options.genesisDocument;
|
|
2024
2177
|
}
|
|
2025
|
-
|
|
2026
|
-
return unsignedUpdate;
|
|
2178
|
+
this.#phase = currentDocument ? "BeaconDiscovery" /* BeaconDiscovery */ : "GenesisDocument" /* GenesisDocument */;
|
|
2027
2179
|
}
|
|
2028
2180
|
/**
|
|
2029
|
-
* Implements subsection {@link
|
|
2030
|
-
*
|
|
2031
|
-
* @
|
|
2032
|
-
* @param {UnsignedBTCR2Update} unsignedUpdate The unsigned update to sign.
|
|
2033
|
-
* @param {DidVerificationMethod} verificationMethod The verification method for signing.
|
|
2034
|
-
* @param {Signer} signer Signer that produces the BIP-340 Schnorr signature.
|
|
2035
|
-
* @returns {SignedBTCR2Update} The signed update with a Data Integrity proof.
|
|
2181
|
+
* Implements subsection {@link https://dcdpr.github.io/did-btcr2/operations/resolve.html#if-genesis_bytes-is-a-secp256k1-public-key | 7.2.d.1 if genesis bytes is a secp256k1 Public Key}.
|
|
2182
|
+
* @param {DidComponents} didComponents The decoded components of the did.
|
|
2183
|
+
* @returns {DidDocument} The resolved DID Document object.
|
|
2036
2184
|
*/
|
|
2037
|
-
static
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2185
|
+
static deterministic(didComponents) {
|
|
2186
|
+
const genesisBytes = didComponents.genesisBytes;
|
|
2187
|
+
const did = Identifier.encode(genesisBytes, didComponents);
|
|
2188
|
+
const { multibase } = new import_keypair3.CompressedSecp256k1PublicKey(genesisBytes);
|
|
2189
|
+
const service = BeaconUtils.generateBeaconServices({
|
|
2190
|
+
id: did,
|
|
2191
|
+
publicKey: genesisBytes,
|
|
2192
|
+
network: (0, import_bitcoin5.getNetwork)(didComponents.network),
|
|
2193
|
+
beaconType: "SingletonBeacon"
|
|
2194
|
+
});
|
|
2195
|
+
return new DidDocument({
|
|
2196
|
+
id: did,
|
|
2197
|
+
verificationMethod: [{
|
|
2198
|
+
id: `${did}#initialKey`,
|
|
2199
|
+
type: "Multikey",
|
|
2200
|
+
controller: did,
|
|
2201
|
+
publicKeyMultibase: multibase.encoded
|
|
2202
|
+
}],
|
|
2203
|
+
service
|
|
2204
|
+
});
|
|
2205
|
+
}
|
|
2206
|
+
/**
|
|
2207
|
+
* Implements subsection {@link https://dcdpr.github.io/did-btcr2/operations/resolve.html#if-genesis_bytes-is-a-sha-256-hash | 7.2.d.2 if genesis_bytes is a SHA-256 Hash}.
|
|
2208
|
+
* @param {DidComponents} didComponents BTCR2 DID components used to resolve the DID Document
|
|
2209
|
+
* @param {object} genesisDocument The genesis document for resolving the DID Document.
|
|
2210
|
+
* @returns {DidDocument} The resolved DID Document object
|
|
2211
|
+
* @throws {ResolveError} `INVALID_DID` if the hash of the genesis document is not the genesis bytes of the identifier
|
|
2212
|
+
*/
|
|
2213
|
+
static external(didComponents, genesisDocument) {
|
|
2214
|
+
const genesisDocumentHash = (0, import_common12.canonicalHashBytes)(genesisDocument);
|
|
2215
|
+
if (!(0, import_utils7.equalBytes)(didComponents.genesisBytes, genesisDocumentHash)) {
|
|
2216
|
+
throw new import_common12.ResolveError(
|
|
2217
|
+
`Initial document mismatch: genesisBytes !== genesisDocumentHash`,
|
|
2218
|
+
import_common12.INVALID_DID,
|
|
2219
|
+
{
|
|
2220
|
+
genesisBytes: (0, import_common12.encode)(didComponents.genesisBytes, "hex"),
|
|
2221
|
+
genesisDocumentHash: (0, import_common12.encode)(genesisDocumentHash, "hex")
|
|
2222
|
+
}
|
|
2043
2223
|
);
|
|
2044
2224
|
}
|
|
2045
|
-
const
|
|
2046
|
-
const
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2225
|
+
const did = Identifier.encode(didComponents.genesisBytes, didComponents);
|
|
2226
|
+
const currentDocument = JSON.parse(
|
|
2227
|
+
JSON.stringify(genesisDocument).replaceAll(ID_PLACEHOLDER_VALUE, did)
|
|
2228
|
+
);
|
|
2229
|
+
return new DidDocument(currentDocument);
|
|
2230
|
+
}
|
|
2231
|
+
/**
|
|
2232
|
+
* Implements subsection {@link https://dcdpr.github.io/did-btcr2/operations/resolve.html#process-sidecar-data | Process Sidecar Data}
|
|
2233
|
+
* @param {Sidecar} sidecar The sidecar data to process.
|
|
2234
|
+
* @returns {SidecarData} The processed sidecar data containing maps of updates, CAS announcements, and SMT proofs.
|
|
2235
|
+
*/
|
|
2236
|
+
static sidecarData(sidecar = {}) {
|
|
2237
|
+
const updateMap = /* @__PURE__ */ new Map();
|
|
2238
|
+
if (sidecar.updates?.length)
|
|
2239
|
+
for (const update of sidecar.updates) {
|
|
2240
|
+
updateMap.set((0, import_common12.canonicalHash)(update, { encoding: "hex" }), update);
|
|
2241
|
+
}
|
|
2242
|
+
const casMap = /* @__PURE__ */ new Map();
|
|
2243
|
+
if (sidecar.casUpdates?.length)
|
|
2244
|
+
for (const update of sidecar.casUpdates) {
|
|
2245
|
+
casMap.set((0, import_common12.canonicalHash)(update, { encoding: "hex" }), update);
|
|
2246
|
+
}
|
|
2247
|
+
const smtMap = /* @__PURE__ */ new Map();
|
|
2248
|
+
if (sidecar.smtProofs?.length)
|
|
2249
|
+
for (const proof of sidecar.smtProofs) {
|
|
2250
|
+
smtMap.set((0, import_common12.encode)((0, import_common12.decode)(proof.id, "base64urlnopad"), "hex"), proof);
|
|
2251
|
+
}
|
|
2252
|
+
return { updateMap, casMap, smtMap };
|
|
2253
|
+
}
|
|
2254
|
+
/**
|
|
2255
|
+
* Implements subsection {@link https://dcdpr.github.io/did-btcr2/operations/resolve.html#confirm-duplicate-update | Confirm Duplicate Update}.
|
|
2256
|
+
* This step confirms that an update with a lower-than-expected targetVersionId is a true duplicate.
|
|
2257
|
+
* @param {SignedBTCR2Update} update The BTCR2 Signed Update to confirm as a duplicate.
|
|
2258
|
+
* @param {HashBytes[]} updateHashHistory The accumulated hash history for comparison.
|
|
2259
|
+
* @returns {void} Does not return a value, but throws an error if the update is not a valid duplicate.
|
|
2260
|
+
*/
|
|
2261
|
+
static confirmDuplicate(update, updateHashHistory) {
|
|
2262
|
+
if (!Number.isInteger(update.targetVersionId) || update.targetVersionId < 2) {
|
|
2263
|
+
throw new import_common12.ResolveError(
|
|
2264
|
+
`Invalid duplicate: targetVersionId must be an integer >= 2`,
|
|
2265
|
+
import_common12.INVALID_DID_UPDATE,
|
|
2266
|
+
{ targetVersionId: update.targetVersionId }
|
|
2052
2267
|
);
|
|
2053
2268
|
}
|
|
2054
|
-
const
|
|
2055
|
-
const
|
|
2056
|
-
const
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
import_common11.INVALID_DID_UPDATE,
|
|
2269
|
+
const { proof: _, ...unsignedUpdate } = update;
|
|
2270
|
+
const unsignedUpdateHash = (0, import_common12.canonicalHashBytes)(unsignedUpdate);
|
|
2271
|
+
const historicalUpdateHash = updateHashHistory[update.targetVersionId - 2];
|
|
2272
|
+
if (historicalUpdateHash === void 0) {
|
|
2273
|
+
throw new import_common12.ResolveError(
|
|
2274
|
+
`Invalid duplicate: no applied update in history for targetVersionId`,
|
|
2275
|
+
import_common12.LATE_PUBLISHING_ERROR,
|
|
2062
2276
|
{
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
actual: signerKey
|
|
2277
|
+
targetVersionId: update.targetVersionId,
|
|
2278
|
+
historyLength: updateHashHistory.length
|
|
2066
2279
|
}
|
|
2067
2280
|
);
|
|
2068
2281
|
}
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
// The proof names the signing method by absolute DID URL, even when the document
|
|
2077
|
-
// spells that method's own id relatively: a proof travels apart from the document
|
|
2078
|
-
// that defines the method, so a bare `#initialKey` in it resolves against nothing.
|
|
2079
|
-
// For a document that already spells its ids absolutely this is the id unchanged,
|
|
2080
|
-
// so proofs over such documents are byte-identical to before.
|
|
2081
|
-
verificationMethod: absoluteMethodId,
|
|
2082
|
-
proofPurpose: "capabilityInvocation",
|
|
2083
|
-
capability: `urn:zcap:root:${encodeURIComponent(did)}`,
|
|
2084
|
-
capabilityAction: "Write"
|
|
2085
|
-
};
|
|
2086
|
-
const diproof = multikey.toCryptosuite().toDataIntegrityProof();
|
|
2087
|
-
return diproof.addProof(unsignedUpdate, config);
|
|
2088
|
-
}
|
|
2089
|
-
/**
|
|
2090
|
-
* Implements subsection {@link https://dcdpr.github.io/did-btcr2/operations/update.html#announce-did-update | 7.3.d Announce DID Update}.
|
|
2091
|
-
* Announces a signed update to the Bitcoin blockchain via the specified beacon.
|
|
2092
|
-
*
|
|
2093
|
-
* @param {BeaconService} beaconService The beacon service to broadcast through.
|
|
2094
|
-
* @param {string} did The DID being updated. Required because a beacon service
|
|
2095
|
-
* `id` may be a relative DID URL and so cannot supply the subject.
|
|
2096
|
-
* @param {SignedBTCR2Update} update The signed update to announce.
|
|
2097
|
-
* @param {Signer} signer Signer that produces the ECDSA signature for the Bitcoin transaction.
|
|
2098
|
-
* @param {BitcoinConnection} bitcoin The Bitcoin network connection.
|
|
2099
|
-
* @param {CASBroadcastOptions} [options] Optional broadcast configuration (fee estimator,
|
|
2100
|
-
* change address, and, for CAS beacons, a `casPublish` callback invoked before the
|
|
2101
|
-
* transaction broadcast; other beacon types ignore `casPublish`).
|
|
2102
|
-
* @returns {Promise<BroadcastResult>} The broadcast artifacts: the signed update, the signal
|
|
2103
|
-
* txid, and any per-beacon-type sidecar data (CAS announcement / SMT proof).
|
|
2104
|
-
*/
|
|
2105
|
-
static async announce(beaconService, did, update, signer, bitcoin, options) {
|
|
2106
|
-
const beacon = BeaconFactory.establish(beaconService, did);
|
|
2107
|
-
return beacon.broadcastSignal(update, signer, bitcoin, options);
|
|
2108
|
-
}
|
|
2109
|
-
// Private instance wrappers
|
|
2110
|
-
// Delegate to the public statics with bound instance fields for cleaner
|
|
2111
|
-
// advance/provide code.
|
|
2112
|
-
#construct() {
|
|
2113
|
-
return _Updater.construct(this.#sourceDocument, this.#patches, this.#sourceVersionId);
|
|
2114
|
-
}
|
|
2115
|
-
/**
|
|
2116
|
-
* Advance the state machine. Returns either:
|
|
2117
|
-
* - `{ status: 'action-required', needs }` caller must provide data via {@link provide}
|
|
2118
|
-
* - `{ status: 'complete', result }` update is signed and broadcast
|
|
2119
|
-
*/
|
|
2120
|
-
advance() {
|
|
2121
|
-
while (true) {
|
|
2122
|
-
switch (this.#state.phase) {
|
|
2123
|
-
// Phase: Construct
|
|
2124
|
-
// Build the unsigned update from source doc + patches. Pure, synchronous.
|
|
2125
|
-
case "Construct": {
|
|
2126
|
-
const unsignedUpdate = this.#construct();
|
|
2127
|
-
this.#state = { phase: "Sign", unsignedUpdate };
|
|
2128
|
-
continue;
|
|
2129
|
-
}
|
|
2130
|
-
// Phase: Sign
|
|
2131
|
-
// Emit NeedSigningKey: the caller supplies the secret key (or a KMS signature).
|
|
2132
|
-
case "Sign": {
|
|
2133
|
-
return {
|
|
2134
|
-
status: "action-required",
|
|
2135
|
-
needs: [{
|
|
2136
|
-
kind: "NeedSigningKey",
|
|
2137
|
-
verificationMethodId: this.#verificationMethod.id,
|
|
2138
|
-
unsignedUpdate: this.#state.unsignedUpdate
|
|
2139
|
-
}]
|
|
2140
|
-
};
|
|
2141
|
-
}
|
|
2142
|
-
// Phase: Fund
|
|
2143
|
-
// Emit NeedFunding with the beacon address. The caller checks UTXOs,
|
|
2144
|
-
// funds the address if needed, and provides to continue.
|
|
2145
|
-
case "Fund": {
|
|
2146
|
-
const beaconAddress = this.#beaconService.serviceEndpoint.replace("bitcoin:", "");
|
|
2147
|
-
return {
|
|
2148
|
-
status: "action-required",
|
|
2149
|
-
needs: [{
|
|
2150
|
-
kind: "NeedFunding",
|
|
2151
|
-
beaconAddress,
|
|
2152
|
-
beaconService: this.#beaconService
|
|
2153
|
-
}]
|
|
2154
|
-
};
|
|
2155
|
-
}
|
|
2156
|
-
// Phase: Broadcast
|
|
2157
|
-
// Emit NeedBroadcast with the signed update + beacon service. The caller performs
|
|
2158
|
-
// the actual on-chain announcement (or hands off to the aggregation protocol).
|
|
2159
|
-
case "Broadcast": {
|
|
2160
|
-
return {
|
|
2161
|
-
status: "action-required",
|
|
2162
|
-
needs: [{
|
|
2163
|
-
kind: "NeedBroadcast",
|
|
2164
|
-
beaconService: this.#beaconService,
|
|
2165
|
-
signedUpdate: this.#state.signedUpdate,
|
|
2166
|
-
did: this.#sourceDocument.id
|
|
2167
|
-
}]
|
|
2168
|
-
};
|
|
2169
|
-
}
|
|
2170
|
-
// Phase: Complete
|
|
2171
|
-
case "Complete": {
|
|
2172
|
-
return {
|
|
2173
|
-
status: "complete",
|
|
2174
|
-
result: { signedUpdate: this.#state.signedUpdate }
|
|
2175
|
-
};
|
|
2282
|
+
if (!(0, import_utils7.equalBytes)(historicalUpdateHash, unsignedUpdateHash)) {
|
|
2283
|
+
throw new import_common12.ResolveError(
|
|
2284
|
+
`Invalid duplicate: unsigned update hash does not match historical hash`,
|
|
2285
|
+
import_common12.LATE_PUBLISHING_ERROR,
|
|
2286
|
+
{
|
|
2287
|
+
unsignedUpdateHash: (0, import_common12.encode)(unsignedUpdateHash, "hex"),
|
|
2288
|
+
historicalHash: (0, import_common12.encode)(historicalUpdateHash, "hex")
|
|
2176
2289
|
}
|
|
2177
|
-
|
|
2290
|
+
);
|
|
2178
2291
|
}
|
|
2179
2292
|
}
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
)
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
const signedUpdate = _Updater.sign(
|
|
2198
|
-
this.#sourceDocument.id,
|
|
2199
|
-
unsignedUpdate,
|
|
2200
|
-
this.#verificationMethod,
|
|
2201
|
-
data
|
|
2293
|
+
/**
|
|
2294
|
+
* Decode a hash of a BTCR2 Update (`sourceHash` or `targetHash`). The specification encodes
|
|
2295
|
+
* both with base64url without padding.
|
|
2296
|
+
* @param {unknown} value The encoded hash.
|
|
2297
|
+
* @param {'sourceHash' | 'targetHash'} field The name of the field, for the error.
|
|
2298
|
+
* @returns {HashBytes} The decoded bytes.
|
|
2299
|
+
* @throws {ResolveError} `INVALID_DID_UPDATE` if the value is not a string or does not decode.
|
|
2300
|
+
*/
|
|
2301
|
+
static decodeUpdateHash(value, field) {
|
|
2302
|
+
if (typeof value === "string") {
|
|
2303
|
+
try {
|
|
2304
|
+
return (0, import_common12.decode)(value, "base64urlnopad");
|
|
2305
|
+
} catch (error) {
|
|
2306
|
+
throw new import_common12.ResolveError(
|
|
2307
|
+
`Invalid update: ${field} does not decode as base64url: ${errorCause(error).message}`,
|
|
2308
|
+
import_common12.INVALID_DID_UPDATE,
|
|
2309
|
+
{ [field]: value, cause: errorCause(error) }
|
|
2202
2310
|
);
|
|
2203
|
-
this.#state = { phase: "Fund", unsignedUpdate, signedUpdate };
|
|
2204
|
-
break;
|
|
2205
|
-
}
|
|
2206
|
-
case "NeedFunding": {
|
|
2207
|
-
if (this.#state.phase !== "Fund") {
|
|
2208
|
-
throw new import_common11.UpdateError(
|
|
2209
|
-
`Cannot provide NeedFunding: updater phase is ${this.#state.phase}, expected Fund.`,
|
|
2210
|
-
import_common11.INVALID_DID_UPDATE,
|
|
2211
|
-
{ phase: this.#state.phase }
|
|
2212
|
-
);
|
|
2213
|
-
}
|
|
2214
|
-
if (data !== void 0) {
|
|
2215
|
-
const proof = data;
|
|
2216
|
-
if (typeof proof.utxoCount !== "number" || !Number.isFinite(proof.utxoCount) || proof.utxoCount < 1) {
|
|
2217
|
-
throw new import_common11.UpdateError(
|
|
2218
|
-
`NeedFunding proof must have utxoCount >= 1; got ${String(proof.utxoCount)}.`,
|
|
2219
|
-
import_common11.INVALID_DID_UPDATE,
|
|
2220
|
-
{ utxoCount: proof.utxoCount }
|
|
2221
|
-
);
|
|
2222
|
-
}
|
|
2223
|
-
}
|
|
2224
|
-
this.#state = {
|
|
2225
|
-
phase: "Broadcast",
|
|
2226
|
-
unsignedUpdate: this.#state.unsignedUpdate,
|
|
2227
|
-
signedUpdate: this.#state.signedUpdate
|
|
2228
|
-
};
|
|
2229
|
-
break;
|
|
2230
|
-
}
|
|
2231
|
-
case "NeedBroadcast": {
|
|
2232
|
-
if (this.#state.phase !== "Broadcast") {
|
|
2233
|
-
throw new import_common11.UpdateError(
|
|
2234
|
-
`Cannot provide NeedBroadcast: updater phase is ${this.#state.phase}, expected Broadcast.`,
|
|
2235
|
-
import_common11.INVALID_DID_UPDATE,
|
|
2236
|
-
{ phase: this.#state.phase }
|
|
2237
|
-
);
|
|
2238
|
-
}
|
|
2239
|
-
this.#state = { phase: "Complete", signedUpdate: this.#state.signedUpdate };
|
|
2240
|
-
break;
|
|
2241
2311
|
}
|
|
2242
2312
|
}
|
|
2313
|
+
throw new import_common12.ResolveError(`Invalid update: ${field} is not a string`, import_common12.INVALID_DID_UPDATE, { [field]: value });
|
|
2243
2314
|
}
|
|
2244
|
-
};
|
|
2245
|
-
|
|
2246
|
-
// src/did-btcr2.ts
|
|
2247
|
-
var DidBtcr2 = class {
|
|
2248
|
-
/**
|
|
2249
|
-
* Name of the DID method, as defined in the DID BTCR2 specification
|
|
2250
|
-
*/
|
|
2251
|
-
static methodName = "btcr2";
|
|
2252
2315
|
/**
|
|
2253
|
-
*
|
|
2254
|
-
*
|
|
2255
|
-
*
|
|
2256
|
-
* @param {
|
|
2257
|
-
* @param {
|
|
2258
|
-
* @
|
|
2259
|
-
* @
|
|
2260
|
-
* @returns {Promise<string>} Promise resolving to an identifier string.
|
|
2261
|
-
* @throws {MethodError} if any of the checks fail
|
|
2262
|
-
* @example
|
|
2263
|
-
* ```ts
|
|
2264
|
-
* const genesisBytes = SchnorrKeyPair.generate().publicKey.compressed;
|
|
2265
|
-
* const did = DidBtcr2.create(genesisBytes, { idType: 'KEY', network: 'regtest' });
|
|
2266
|
-
* ```
|
|
2316
|
+
* Parse a `created` or `expires` value of an update proof. Data Integrity types both as an
|
|
2317
|
+
* XML Schema `dateTimeStamp`: an XML Datetime with a timezone. A value without a timezone
|
|
2318
|
+
* names no fixed instant, so two resolvers would read two instants; it is rejected.
|
|
2319
|
+
* @param {Btcr2DataIntegrityProof} proof The update proof.
|
|
2320
|
+
* @param {'created' | 'expires'} field The field to parse.
|
|
2321
|
+
* @returns {number | undefined} The instant in milliseconds since the Unix epoch, or `undefined` when the field is absent.
|
|
2322
|
+
* @throws {ResolveError} `INVALID_DID_UPDATE` for a value that is not an XML Datetime with a timezone.
|
|
2267
2323
|
*/
|
|
2268
|
-
static
|
|
2269
|
-
const
|
|
2270
|
-
if (
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
options
|
|
2275
|
-
);
|
|
2324
|
+
static proofInstant(proof, field) {
|
|
2325
|
+
const value = proof[field];
|
|
2326
|
+
if (value === void 0) return void 0;
|
|
2327
|
+
if (typeof value === "string" && XSD_TIMEZONE.test(value) && import_common12.DateUtils.isValidXsdDateTime(value)) {
|
|
2328
|
+
const ms = Date.parse(value);
|
|
2329
|
+
if (Number.isFinite(ms)) return ms;
|
|
2276
2330
|
}
|
|
2277
|
-
|
|
2331
|
+
throw new import_common12.ResolveError(
|
|
2332
|
+
`Invalid update: proof.${field} is not an XML Datetime with a timezone`,
|
|
2333
|
+
import_common12.INVALID_DID_UPDATE,
|
|
2334
|
+
{ [field]: value }
|
|
2335
|
+
);
|
|
2278
2336
|
}
|
|
2279
2337
|
/**
|
|
2280
|
-
*
|
|
2281
|
-
*
|
|
2282
|
-
*
|
|
2283
|
-
*
|
|
2284
|
-
*
|
|
2285
|
-
*
|
|
2286
|
-
* @param {
|
|
2287
|
-
* @param {
|
|
2288
|
-
* @
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2338
|
+
* Spec "Check `update.proof`": the proof time window against the block that contains the
|
|
2339
|
+
* Beacon Signal. `created` must not be after the header time of the block: a controller
|
|
2340
|
+
* signs a short time before the block, and on mainnet the header time is about one hour
|
|
2341
|
+
* after the `mediantime`. `expires` must not be before the block `mediantime`: it limits a
|
|
2342
|
+
* replay, and a single miner cannot change `mediantime`. `expires` must not be before
|
|
2343
|
+
* `created`. Each comparison has no tolerance.
|
|
2344
|
+
* @param {Btcr2DataIntegrityProof} proof The update proof.
|
|
2345
|
+
* @param {BlockMetadata} block The block of the Beacon Signal.
|
|
2346
|
+
* @throws {ResolveError} `INVALID_DID_UPDATE` if a value is outside the window.
|
|
2347
|
+
*/
|
|
2348
|
+
static checkProofWindow(proof, block) {
|
|
2349
|
+
const created = _Resolver.proofInstant(proof, "created");
|
|
2350
|
+
const expires = _Resolver.proofInstant(proof, "expires");
|
|
2351
|
+
if (created !== void 0 && created > block.time * 1e3) {
|
|
2352
|
+
throw new import_common12.ResolveError(
|
|
2353
|
+
"Invalid update: proof.created is after the header time of the block that contains the Beacon Signal",
|
|
2354
|
+
import_common12.INVALID_DID_UPDATE,
|
|
2355
|
+
{ created: proof.created, blockTime: block.time }
|
|
2356
|
+
);
|
|
2357
|
+
}
|
|
2358
|
+
if (expires !== void 0 && expires < block.mediantime * 1e3) {
|
|
2359
|
+
throw new import_common12.ResolveError(
|
|
2360
|
+
"Invalid update: proof.expires is before the mediantime of the block that contains the Beacon Signal",
|
|
2361
|
+
import_common12.INVALID_DID_UPDATE,
|
|
2362
|
+
{ expires: proof.expires, mediantime: block.mediantime }
|
|
2363
|
+
);
|
|
2364
|
+
}
|
|
2365
|
+
if (created !== void 0 && expires !== void 0 && expires < created) {
|
|
2366
|
+
throw new import_common12.ResolveError(
|
|
2367
|
+
"Invalid update: proof.expires is before proof.created",
|
|
2368
|
+
import_common12.INVALID_DID_UPDATE,
|
|
2369
|
+
{ created: proof.created, expires: proof.expires }
|
|
2370
|
+
);
|
|
2371
|
+
}
|
|
2311
2372
|
}
|
|
2312
2373
|
/**
|
|
2313
|
-
*
|
|
2314
|
-
*
|
|
2315
|
-
*
|
|
2316
|
-
*
|
|
2317
|
-
*
|
|
2318
|
-
*
|
|
2319
|
-
*
|
|
2320
|
-
*
|
|
2321
|
-
*
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
*/
|
|
2335
|
-
static update({
|
|
2336
|
-
sourceDocument,
|
|
2337
|
-
patches,
|
|
2338
|
-
sourceVersionId,
|
|
2339
|
-
verificationMethodId,
|
|
2340
|
-
beaconId
|
|
2341
|
-
}) {
|
|
2342
|
-
const authorizedMethodId = Appendix.relationshipMethodId(verificationMethodId, sourceDocument.id);
|
|
2343
|
-
const authorized = authorizedMethodId !== void 0 && sourceDocument.capabilityInvocation?.some(
|
|
2344
|
-
(entry) => Appendix.relationshipMethodId(entry, sourceDocument.id) === authorizedMethodId
|
|
2345
|
-
);
|
|
2346
|
-
if (!authorized) {
|
|
2347
|
-
throw new import_common12.UpdateError(
|
|
2348
|
-
"Invalid verificationMethodId: not authorized for capabilityInvocation",
|
|
2349
|
-
import_common12.INVALID_DID_DOCUMENT,
|
|
2350
|
-
sourceDocument
|
|
2374
|
+
* Implements subsection {@link https://dcdpr.github.io/did-btcr2/operations/resolve.html#apply-update | Apply update}
|
|
2375
|
+
* and its step {@link https://dcdpr.github.io/did-btcr2/operations/resolve.html#check-update-proof | Check update.proof}.
|
|
2376
|
+
* Every failure that the specification names raises `INVALID_DID_UPDATE`. An error of the
|
|
2377
|
+
* cryptosuite, the multikey, the hash decoder, or the patch rides along as `data.cause`.
|
|
2378
|
+
* @param {DidDocument} currentDocument The current DID Document to apply the update to.
|
|
2379
|
+
* @param {SignedBTCR2Update} update The BTCR2 Signed Update to apply.
|
|
2380
|
+
* @param {BlockMetadata} block The block that contains the Beacon Signal that announced the update.
|
|
2381
|
+
* @returns {DidDocument} The updated DID Document after applying the update.
|
|
2382
|
+
* @throws {ResolveError} `INVALID_DID_UPDATE` if the update is invalid or cannot be applied.
|
|
2383
|
+
*/
|
|
2384
|
+
static applyUpdate(currentDocument, update, block) {
|
|
2385
|
+
const currentDocumentHash = (0, import_common12.canonicalHashBytes)(currentDocument);
|
|
2386
|
+
const sourceHashBytes = _Resolver.decodeUpdateHash(update.sourceHash, "sourceHash");
|
|
2387
|
+
if (!(0, import_utils7.equalBytes)(sourceHashBytes, currentDocumentHash)) {
|
|
2388
|
+
throw new import_common12.ResolveError(
|
|
2389
|
+
`Hash mismatch: update.sourceHash !== currentDocumentHash`,
|
|
2390
|
+
import_common12.INVALID_DID_UPDATE,
|
|
2391
|
+
{
|
|
2392
|
+
sourceHash: update.sourceHash,
|
|
2393
|
+
currentDocumentHash: (0, import_common12.encode)(currentDocumentHash, "hex")
|
|
2394
|
+
}
|
|
2351
2395
|
);
|
|
2352
2396
|
}
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
{ sourceDocument, verificationMethodId }
|
|
2397
|
+
if (!isBtcr2UpdateContext(update["@context"])) {
|
|
2398
|
+
throw new import_common12.ResolveError(
|
|
2399
|
+
"Invalid update: @context is not the array the specification pins for a BTCR2 Update",
|
|
2400
|
+
import_common12.INVALID_DID_UPDATE,
|
|
2401
|
+
{ context: update["@context"], expected: [...BTCR2_UPDATE_CONTEXT] }
|
|
2359
2402
|
);
|
|
2360
2403
|
}
|
|
2361
|
-
if (
|
|
2362
|
-
throw new import_common12.
|
|
2363
|
-
|
|
2364
|
-
import_common12.
|
|
2365
|
-
|
|
2404
|
+
if (!isBtcr2UpdateContext(update.proof?.["@context"], update["@context"])) {
|
|
2405
|
+
throw new import_common12.ResolveError(
|
|
2406
|
+
"Invalid update: proof @context does not equal the update @context",
|
|
2407
|
+
import_common12.INVALID_DID_UPDATE,
|
|
2408
|
+
{ proofContext: update.proof?.["@context"], context: update["@context"] }
|
|
2366
2409
|
);
|
|
2367
2410
|
}
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2411
|
+
const proof = update.proof;
|
|
2412
|
+
const expectedFields = [
|
|
2413
|
+
["type", "DataIntegrityProof"],
|
|
2414
|
+
["cryptosuite", "bip340-jcs-2025"],
|
|
2415
|
+
["proofPurpose", "capabilityInvocation"],
|
|
2416
|
+
["capabilityAction", "Write"],
|
|
2417
|
+
["capability", `urn:zcap:root:${encodeURIComponent(currentDocument.id)}`]
|
|
2418
|
+
];
|
|
2419
|
+
for (const [field, expected] of expectedFields) {
|
|
2420
|
+
const actual = proof[field];
|
|
2421
|
+
if (actual !== expected) {
|
|
2422
|
+
throw new import_common12.ResolveError(
|
|
2423
|
+
`Invalid update: proof.${field} must equal "${expected}"`,
|
|
2424
|
+
import_common12.INVALID_DID_UPDATE,
|
|
2425
|
+
{ field, expected, actual }
|
|
2426
|
+
);
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
const verificationMethodId = proof.verificationMethod;
|
|
2430
|
+
const entry = Appendix.capabilityInvocationEntry(currentDocument, verificationMethodId);
|
|
2431
|
+
if (entry === void 0) {
|
|
2432
|
+
throw new import_common12.ResolveError(
|
|
2433
|
+
"Invalid update: verificationMethod is not authorized for capabilityInvocation",
|
|
2434
|
+
import_common12.INVALID_DID_UPDATE,
|
|
2435
|
+
{
|
|
2436
|
+
verificationMethodId,
|
|
2437
|
+
capabilityInvocation: currentDocument.capabilityInvocation
|
|
2438
|
+
}
|
|
2373
2439
|
);
|
|
2374
2440
|
}
|
|
2375
|
-
const
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
"No beacon service found for provided beaconId",
|
|
2441
|
+
const vm = Appendix.verificationMethodOfEntry(currentDocument, entry);
|
|
2442
|
+
if (vm === void 0) {
|
|
2443
|
+
throw new import_common12.ResolveError(
|
|
2444
|
+
"Invalid update: verificationMethod is not found in the verificationMethod of the current document",
|
|
2380
2445
|
import_common12.INVALID_DID_UPDATE,
|
|
2381
|
-
{
|
|
2446
|
+
{ verificationMethodId }
|
|
2382
2447
|
);
|
|
2383
2448
|
}
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
* @returns {DidVerificationMethod} Promise resolving to the {@link DidVerificationMethod} object used for signing.
|
|
2400
|
-
* @throws {DidError} if the parsed did method does not match `btcr2` or signing method could not be determined.
|
|
2401
|
-
*/
|
|
2402
|
-
static getSigningMethod(didDocument, methodId) {
|
|
2403
|
-
methodId ??= "#initialKey";
|
|
2404
|
-
const parsedDid = import_dids2.Did.parse(didDocument.id);
|
|
2405
|
-
if (parsedDid && parsedDid.method !== this.methodName) {
|
|
2406
|
-
throw new import_common12.MethodError(`Method not supported: ${parsedDid.method}`, import_common12.METHOD_NOT_SUPPORTED, { identifier: didDocument.id });
|
|
2449
|
+
_Resolver.checkProofWindow(proof, block);
|
|
2450
|
+
let verified;
|
|
2451
|
+
try {
|
|
2452
|
+
const multikey = import_cryptosuite.SchnorrMultikey.fromVerificationMethod({
|
|
2453
|
+
...vm,
|
|
2454
|
+
id: Appendix.absoluteDidUrl(vm.id, currentDocument.id) ?? vm.id
|
|
2455
|
+
});
|
|
2456
|
+
const diProof = new import_cryptosuite.BIP340DataIntegrityProof(new import_cryptosuite.BIP340Cryptosuite(multikey));
|
|
2457
|
+
verified = diProof.verifyProof((0, import_common12.canonicalize)(update), "capabilityInvocation").verified;
|
|
2458
|
+
} catch (error) {
|
|
2459
|
+
throw new import_common12.ResolveError(
|
|
2460
|
+
`Invalid update: proof verification failed: ${errorCause(error).message}`,
|
|
2461
|
+
import_common12.INVALID_DID_UPDATE,
|
|
2462
|
+
{ verificationMethodId, cause: errorCause(error) }
|
|
2463
|
+
);
|
|
2407
2464
|
}
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2465
|
+
if (!verified) {
|
|
2466
|
+
throw new import_common12.ResolveError("Invalid update: proof not verified", import_common12.INVALID_DID_UPDATE, { verificationMethodId });
|
|
2467
|
+
}
|
|
2468
|
+
let updatedDocument;
|
|
2469
|
+
try {
|
|
2470
|
+
updatedDocument = import_common12.JSONPatch.apply(currentDocument, update.patch, { strict: true });
|
|
2471
|
+
} catch (error) {
|
|
2472
|
+
throw new import_common12.ResolveError(
|
|
2473
|
+
`Invalid update: ${errorCause(error).message}`,
|
|
2474
|
+
import_common12.INVALID_DID_UPDATE,
|
|
2475
|
+
{ cause: errorCause(error) }
|
|
2416
2476
|
);
|
|
2417
2477
|
}
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
}
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
var DEFAULT_MIN_CONF = 6;
|
|
2425
|
-
function isRecord(value) {
|
|
2426
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2427
|
-
}
|
|
2428
|
-
function isCASAnnouncement(value) {
|
|
2429
|
-
return isRecord(value) && Object.values(value).every((v) => typeof v === "string");
|
|
2430
|
-
}
|
|
2431
|
-
function isSignedBTCR2Update(value) {
|
|
2432
|
-
if (!isRecord(value)) return false;
|
|
2433
|
-
return Array.isArray(value.patch) && typeof value.sourceHash === "string" && typeof value.targetHash === "string" && Number.isInteger(value.targetVersionId) && value.targetVersionId >= 2 && isRecord(value.proof);
|
|
2434
|
-
}
|
|
2435
|
-
function isSMTProof(value) {
|
|
2436
|
-
if (!isRecord(value)) return false;
|
|
2437
|
-
return typeof value.id === "string" && typeof value.collapsed === "string" && Array.isArray(value.hashes);
|
|
2438
|
-
}
|
|
2439
|
-
function validateMinConf(value) {
|
|
2440
|
-
if (value === void 0) return DEFAULT_MIN_CONF;
|
|
2441
|
-
if (typeof value === "number" && Number.isInteger(value) && value >= 1) return value;
|
|
2442
|
-
const shown = typeof value === "string" ? JSON.stringify(value) : String(value);
|
|
2443
|
-
throw new import_common13.ResolveError(
|
|
2444
|
-
`Invalid resolution option minConf: expected a positive integer (minimum 1), got ${shown}.`,
|
|
2445
|
-
import_common13.INVALID_OPTIONS,
|
|
2446
|
-
{ minConf: value }
|
|
2447
|
-
);
|
|
2448
|
-
}
|
|
2449
|
-
var Resolver = class _Resolver {
|
|
2450
|
-
// --- Immutable inputs ---
|
|
2451
|
-
#didComponents;
|
|
2452
|
-
#versionId;
|
|
2453
|
-
#versionTime;
|
|
2454
|
-
/**
|
|
2455
|
-
* The specific phase the Resolver is current in.
|
|
2456
|
-
*/
|
|
2457
|
-
#phase;
|
|
2458
|
-
#sidecarData;
|
|
2459
|
-
#currentDocument;
|
|
2460
|
-
#providedGenesisDocument = null;
|
|
2461
|
-
#beaconServicesSignals = /* @__PURE__ */ new Map();
|
|
2462
|
-
#processedServices = /* @__PURE__ */ new Set();
|
|
2463
|
-
#requestCache = /* @__PURE__ */ new Set();
|
|
2464
|
-
#unsortedUpdates = [];
|
|
2465
|
-
#resolvedResponse = null;
|
|
2466
|
-
/**
|
|
2467
|
-
* Monotonic DID-document version counter and the update-hash history that backs
|
|
2468
|
-
* duplicate confirmation, both carried across the entire resolution. The spec's
|
|
2469
|
-
* read algorithm keeps a single version counter and a single update-hash history
|
|
2470
|
-
* for the whole signal-processing loop, re-deriving beacons from the contemporary
|
|
2471
|
-
* document on each pass. This sans-I/O resolver splits that one loop into discovery
|
|
2472
|
-
* rounds, so the two must persist across rounds rather than restart each pass.
|
|
2473
|
-
* Restarting them would reject a legitimate linear history whose later updates are
|
|
2474
|
-
* announced on beacons that earlier updates added: round two would forget it had
|
|
2475
|
-
* already reached version two, see version three, and raise a late-publishing error.
|
|
2476
|
-
*/
|
|
2477
|
-
#currentVersionId = 1;
|
|
2478
|
-
#updateHashHistory = [];
|
|
2479
|
-
/**
|
|
2480
|
-
* Opt-in upper bound on multi-round beacon-discovery passes. `Infinity` (the
|
|
2481
|
-
* default) leaves discovery unbounded; termination is already guaranteed by
|
|
2482
|
-
* de-duplicating already-queried beacon addresses. A positive value is a
|
|
2483
|
-
* caller-imposed resource guard; a non-positive value or omission means no limit.
|
|
2484
|
-
*/
|
|
2485
|
-
#maxDiscoveryRounds;
|
|
2486
|
-
/** Count of beacon-discovery passes driven by updates adding new beacon services. */
|
|
2487
|
-
#discoveryRounds = 0;
|
|
2488
|
-
/**
|
|
2489
|
-
* Minimum block confirmations a Beacon Signal must have before this resolver
|
|
2490
|
-
* processes it: `ResolutionOptions.minConf`, default {@link DEFAULT_MIN_CONF}.
|
|
2491
|
-
* Applied at signal intake in the BeaconProcess phase. A signal below the
|
|
2492
|
-
* threshold is excluded from the resolution; the rest of the signals are
|
|
2493
|
-
* processed.
|
|
2494
|
-
*/
|
|
2495
|
-
#minConf;
|
|
2496
|
-
/**
|
|
2497
|
-
* @internal Use {@link DidBtcr2.resolve} to create instances.
|
|
2498
|
-
*/
|
|
2499
|
-
constructor(didComponents, sidecarData, currentDocument, options) {
|
|
2500
|
-
this.#didComponents = didComponents;
|
|
2501
|
-
this.#sidecarData = sidecarData;
|
|
2502
|
-
this.#currentDocument = currentDocument;
|
|
2503
|
-
this.#versionId = options?.versionId;
|
|
2504
|
-
this.#versionTime = options?.versionTime;
|
|
2505
|
-
const rounds = options?.maxDiscoveryRounds;
|
|
2506
|
-
this.#maxDiscoveryRounds = typeof rounds === "number" && rounds > 0 ? rounds : Infinity;
|
|
2507
|
-
this.#minConf = validateMinConf(options?.minConf);
|
|
2508
|
-
if (options?.genesisDocument) {
|
|
2509
|
-
this.#providedGenesisDocument = options.genesisDocument;
|
|
2478
|
+
if (updatedDocument?.id !== currentDocument.id) {
|
|
2479
|
+
throw new import_common12.ResolveError(
|
|
2480
|
+
`Invalid update: the patch changes the document id (from "${currentDocument.id}" to "${String(updatedDocument?.id)}")`,
|
|
2481
|
+
import_common12.INVALID_DID_UPDATE,
|
|
2482
|
+
{ sourceId: currentDocument.id, targetId: updatedDocument?.id }
|
|
2483
|
+
);
|
|
2510
2484
|
}
|
|
2511
|
-
|
|
2485
|
+
try {
|
|
2486
|
+
DidDocument.validate(updatedDocument);
|
|
2487
|
+
} catch (error) {
|
|
2488
|
+
throw new import_common12.ResolveError(
|
|
2489
|
+
`Invalid update: the patched document does not conform to DID Core: ${errorCause(error).message}`,
|
|
2490
|
+
import_common12.INVALID_DID_UPDATE,
|
|
2491
|
+
{ cause: errorCause(error) }
|
|
2492
|
+
);
|
|
2493
|
+
}
|
|
2494
|
+
const updatedDocumentHash = (0, import_common12.canonicalHashBytes)(updatedDocument);
|
|
2495
|
+
const updateTargetHash = _Resolver.decodeUpdateHash(update.targetHash, "targetHash");
|
|
2496
|
+
if (!(0, import_utils7.equalBytes)(updateTargetHash, updatedDocumentHash)) {
|
|
2497
|
+
throw new import_common12.ResolveError(
|
|
2498
|
+
`Invalid update: update.targetHash !== updatedDocumentHash`,
|
|
2499
|
+
import_common12.INVALID_DID_UPDATE,
|
|
2500
|
+
{ updateTargetHash, updatedDocumentHash }
|
|
2501
|
+
);
|
|
2502
|
+
}
|
|
2503
|
+
return updatedDocument;
|
|
2512
2504
|
}
|
|
2513
2505
|
/**
|
|
2514
|
-
*
|
|
2515
|
-
*
|
|
2516
|
-
*
|
|
2506
|
+
* Advance the state machine. Returns either:
|
|
2507
|
+
* - `{ status: 'action-required', needs }` - caller must provide data via {@link provide}
|
|
2508
|
+
* - `{ status: 'resolved', result }` - resolution complete
|
|
2509
|
+
*
|
|
2510
|
+
* Analogous to Rust's `Resolver::resolve()`.
|
|
2517
2511
|
*/
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2512
|
+
resolve() {
|
|
2513
|
+
while (true) {
|
|
2514
|
+
switch (this.#phase) {
|
|
2515
|
+
// Phase: GenesisDocument
|
|
2516
|
+
// Only entered for EXTERNAL (x HRP) identifiers when genesis doc was not in sidecar.
|
|
2517
|
+
case "GenesisDocument" /* GenesisDocument */: {
|
|
2518
|
+
if (this.#providedGenesisDocument) {
|
|
2519
|
+
this.#currentDocument = _Resolver.external(
|
|
2520
|
+
this.#didComponents,
|
|
2521
|
+
this.#providedGenesisDocument
|
|
2522
|
+
);
|
|
2523
|
+
this.#providedGenesisDocument = null;
|
|
2524
|
+
this.#phase = "BeaconDiscovery" /* BeaconDiscovery */;
|
|
2525
|
+
continue;
|
|
2526
|
+
}
|
|
2527
|
+
const genesisHash = (0, import_common12.encode)(this.#didComponents.genesisBytes, "hex");
|
|
2528
|
+
return {
|
|
2529
|
+
status: "action-required",
|
|
2530
|
+
needs: [{ kind: "NeedGenesisDocument", genesisHash }]
|
|
2531
|
+
};
|
|
2532
|
+
}
|
|
2533
|
+
// Phase: BeaconDiscovery
|
|
2534
|
+
// Extract beacon services, emit NeedBeaconSignals for addresses not yet queried.
|
|
2535
|
+
case "BeaconDiscovery" /* BeaconDiscovery */: {
|
|
2536
|
+
const beaconServices = BeaconUtils.getBeaconServices(this.#currentDocument);
|
|
2537
|
+
const newServices = beaconServices.filter((service) => {
|
|
2538
|
+
const address = BeaconUtils.parseBitcoinAddress(service.serviceEndpoint);
|
|
2539
|
+
return !this.#requestCache.has(address);
|
|
2540
|
+
});
|
|
2541
|
+
if (newServices.length > 0) {
|
|
2542
|
+
for (const service of newServices) {
|
|
2543
|
+
const address = BeaconUtils.parseBitcoinAddress(service.serviceEndpoint);
|
|
2544
|
+
this.#requestCache.add(address);
|
|
2545
|
+
}
|
|
2546
|
+
return {
|
|
2547
|
+
status: "action-required",
|
|
2548
|
+
needs: [{ kind: "NeedBeaconSignals", beaconServices: newServices }]
|
|
2549
|
+
};
|
|
2550
|
+
}
|
|
2551
|
+
this.#phase = "BeaconProcess" /* BeaconProcess */;
|
|
2552
|
+
continue;
|
|
2553
|
+
}
|
|
2554
|
+
// Phase: BeaconProcess
|
|
2555
|
+
// Process each beacon's signals. Collect updates and data needs.
|
|
2556
|
+
case "BeaconProcess" /* BeaconProcess */: {
|
|
2557
|
+
const allNeeds = [];
|
|
2558
|
+
for (const [service, signals] of this.#beaconServicesSignals) {
|
|
2559
|
+
if (this.#processedServices.has(service.id) || !signals.length) continue;
|
|
2560
|
+
const eligible = this.#eligibleSignals(signals);
|
|
2561
|
+
if (!eligible.length) continue;
|
|
2562
|
+
const beacon = BeaconFactory.establish(service, this.#currentDocument.id);
|
|
2563
|
+
const result = beacon.processSignals(eligible, this.#sidecarData);
|
|
2564
|
+
if (result.needs.length > 0) {
|
|
2565
|
+
allNeeds.push(...result.needs);
|
|
2566
|
+
} else {
|
|
2567
|
+
this.#unsortedUpdates.push(...result.updates);
|
|
2568
|
+
this.#processedServices.add(service.id);
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
if (allNeeds.length > 0) {
|
|
2572
|
+
return { status: "action-required", needs: allNeeds };
|
|
2573
|
+
}
|
|
2574
|
+
this.#phase = "ProcessUpdate" /* ProcessUpdate */;
|
|
2575
|
+
continue;
|
|
2576
|
+
}
|
|
2577
|
+
// Phase: ProcessUpdate
|
|
2578
|
+
// Spec "Process Next Update": one tuple per step. The phase repeats until
|
|
2579
|
+
// the document resolves, or until an applied update adds a beacon address
|
|
2580
|
+
// that the resolver did not scan (then the pass returns to BeaconDiscovery).
|
|
2581
|
+
case "ProcessUpdate" /* ProcessUpdate */: {
|
|
2582
|
+
const document = this.#currentDocument;
|
|
2583
|
+
if (this.#versionId !== void 0 && this.#currentVersionId === this.#versionId) {
|
|
2584
|
+
this.#phase = "Complete" /* Complete */;
|
|
2585
|
+
continue;
|
|
2586
|
+
}
|
|
2587
|
+
if (this.#unsortedUpdates.length === 0 || document.deactivated) {
|
|
2588
|
+
if (this.#versionId !== void 0) {
|
|
2589
|
+
throw new import_common12.ResolveError(
|
|
2590
|
+
`Version ${this.#versionId} of the DID does not exist: the history ` + (document.deactivated ? `ends with the deactivation at version ${this.#currentVersionId}.` : `ends at version ${this.#currentVersionId}.`),
|
|
2591
|
+
import_common12.NOT_FOUND,
|
|
2592
|
+
{ versionId: this.#versionId, currentVersionId: this.#currentVersionId }
|
|
2593
|
+
);
|
|
2594
|
+
}
|
|
2595
|
+
this.#phase = "Complete" /* Complete */;
|
|
2596
|
+
continue;
|
|
2597
|
+
}
|
|
2598
|
+
this.#unsortedUpdates.sort(
|
|
2599
|
+
([upd0, blk0], [upd1, blk1]) => upd0.targetVersionId - upd1.targetVersionId || blk0.height - blk1.height
|
|
2600
|
+
);
|
|
2601
|
+
const [update, block] = this.#unsortedUpdates.shift();
|
|
2602
|
+
if (update.targetVersionId <= this.#currentVersionId) {
|
|
2603
|
+
_Resolver.confirmDuplicate(update, this.#updateHashHistory);
|
|
2604
|
+
continue;
|
|
2605
|
+
}
|
|
2606
|
+
if (this.#versionTime !== void 0 && block.mediantime * 1e3 > this.#versionTime) {
|
|
2607
|
+
this.#phase = "Complete" /* Complete */;
|
|
2608
|
+
continue;
|
|
2609
|
+
}
|
|
2610
|
+
if (update.targetVersionId !== this.#currentVersionId + 1) {
|
|
2611
|
+
throw new import_common12.ResolveError(
|
|
2612
|
+
`Version Id Mismatch: targetVersionId cannot be > currentVersionId + 1`,
|
|
2613
|
+
import_common12.LATE_PUBLISHING_ERROR,
|
|
2614
|
+
{
|
|
2615
|
+
targetVersionId: update.targetVersionId,
|
|
2616
|
+
currentVersionId: this.#currentVersionId + 1
|
|
2617
|
+
}
|
|
2618
|
+
);
|
|
2619
|
+
}
|
|
2620
|
+
this.#currentDocument = _Resolver.applyUpdate(document, update, block);
|
|
2621
|
+
const unsignedUpdate = import_common12.JSONUtils.deleteKeys(update, ["proof"]);
|
|
2622
|
+
this.#updateHashHistory.push((0, import_common12.canonicalHashBytes)(unsignedUpdate));
|
|
2623
|
+
this.#currentVersionId++;
|
|
2624
|
+
this.#blockConfirmations = block.confirmations;
|
|
2625
|
+
this.#updated = import_common12.DateUtils.toISOStringNonFractional(import_common12.DateUtils.blocktimeToTimestamp(block.time));
|
|
2626
|
+
if (this.#hasUnscannedBeacons()) {
|
|
2627
|
+
if (++this.#discoveryRounds > this.#maxDiscoveryRounds) {
|
|
2628
|
+
throw new import_common12.ResolveError(
|
|
2629
|
+
`Exceeded the configured maximum of ${this.#maxDiscoveryRounds} beacon-discovery rounds. Raise or remove ResolutionOptions.maxDiscoveryRounds to resolve this DID.`,
|
|
2630
|
+
import_common12.INTERNAL_ERROR,
|
|
2631
|
+
{ maxDiscoveryRounds: this.#maxDiscoveryRounds, discoveryRounds: this.#discoveryRounds }
|
|
2632
|
+
);
|
|
2633
|
+
}
|
|
2634
|
+
this.#phase = "BeaconDiscovery" /* BeaconDiscovery */;
|
|
2635
|
+
}
|
|
2636
|
+
continue;
|
|
2637
|
+
}
|
|
2638
|
+
// Phase: Complete
|
|
2639
|
+
// The document metadata of the specification: versionId is current_version_id,
|
|
2640
|
+
// confirmations is block_confirmations (0 when no update applied), deactivated
|
|
2641
|
+
// is the flag of the document. `updated` is present after the first apply.
|
|
2642
|
+
case "Complete" /* Complete */: {
|
|
2643
|
+
this.#resolvedResponse ??= {
|
|
2644
|
+
didDocument: this.#currentDocument,
|
|
2645
|
+
metadata: {
|
|
2646
|
+
versionId: `${this.#currentVersionId}`,
|
|
2647
|
+
confirmations: this.#blockConfirmations,
|
|
2648
|
+
...this.#updated !== void 0 ? { updated: this.#updated } : {},
|
|
2649
|
+
deactivated: this.#currentDocument.deactivated || false
|
|
2650
|
+
}
|
|
2651
|
+
};
|
|
2652
|
+
return { status: "resolved", result: this.#resolvedResponse };
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2538
2656
|
}
|
|
2539
2657
|
/**
|
|
2540
|
-
*
|
|
2541
|
-
*
|
|
2542
|
-
*
|
|
2543
|
-
* @returns {DidDocument} The resolved DID Document object
|
|
2544
|
-
* @throws {ResolveError} `INVALID_DID` if the hash of the genesis document is not the genesis bytes of the identifier
|
|
2658
|
+
* True if the current document carries a beacon service whose address the resolver
|
|
2659
|
+
* did not request signals for. "Find Beacon Signals" scans such an address on the
|
|
2660
|
+
* next pass.
|
|
2545
2661
|
*/
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
throw new import_common13.ResolveError(
|
|
2550
|
-
`Initial document mismatch: genesisBytes !== genesisDocumentHash`,
|
|
2551
|
-
import_common13.INVALID_DID,
|
|
2552
|
-
{
|
|
2553
|
-
genesisBytes: (0, import_common13.encode)(didComponents.genesisBytes, "hex"),
|
|
2554
|
-
genesisDocumentHash: (0, import_common13.encode)(genesisDocumentHash, "hex")
|
|
2555
|
-
}
|
|
2556
|
-
);
|
|
2557
|
-
}
|
|
2558
|
-
const did = Identifier.encode(didComponents.genesisBytes, didComponents);
|
|
2559
|
-
const currentDocument = JSON.parse(
|
|
2560
|
-
JSON.stringify(genesisDocument).replaceAll(ID_PLACEHOLDER_VALUE, did)
|
|
2662
|
+
#hasUnscannedBeacons() {
|
|
2663
|
+
return BeaconUtils.getBeaconServices(this.#currentDocument).some(
|
|
2664
|
+
(service) => !this.#requestCache.has(BeaconUtils.parseBitcoinAddress(service.serviceEndpoint))
|
|
2561
2665
|
);
|
|
2562
|
-
return new DidDocument(currentDocument);
|
|
2563
2666
|
}
|
|
2564
2667
|
/**
|
|
2565
|
-
*
|
|
2566
|
-
*
|
|
2567
|
-
*
|
|
2668
|
+
* Return the signals of one beacon service that resolution may process: the
|
|
2669
|
+
* signals with at least `#minConf` confirmations. The specification removes a
|
|
2670
|
+
* transaction below the threshold from the set of Beacon Signals, so an
|
|
2671
|
+
* excluded signal emits no data need and applies no update. A signal with no
|
|
2672
|
+
* integer confirmation count is excluded too: that is a mempool transaction
|
|
2673
|
+
* from a driver that did not skip it.
|
|
2674
|
+
*
|
|
2675
|
+
* An eligible signal must carry a finite block height, block time, and block
|
|
2676
|
+
* median time past. A signal that passes the count but lacks them is
|
|
2677
|
+
* malformed. It fails fast here with a typed error, in the style of the
|
|
2678
|
+
* {@link provide} guards, and not later with an invalid date or a false
|
|
2679
|
+
* `versionTime` comparison in the ProcessUpdate phase.
|
|
2680
|
+
* @param {Array<BeaconSignal>} signals The signals the caller provided for one service.
|
|
2681
|
+
* @returns {Array<BeaconSignal>} The signals at or above the threshold, in the given order.
|
|
2682
|
+
* @throws {ResolveError} `INVALID_DID_UPDATE` for an eligible signal with no valid block metadata.
|
|
2568
2683
|
*/
|
|
2569
|
-
|
|
2570
|
-
const
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
if (sidecar.casUpdates?.length)
|
|
2577
|
-
for (const update of sidecar.casUpdates) {
|
|
2578
|
-
casMap.set((0, import_common13.canonicalHash)(update, { encoding: "hex" }), update);
|
|
2684
|
+
#eligibleSignals(signals) {
|
|
2685
|
+
const eligible = [];
|
|
2686
|
+
for (const signal of signals) {
|
|
2687
|
+
const block = signal.blockMetadata;
|
|
2688
|
+
const confirmations = block?.confirmations;
|
|
2689
|
+
if (!Number.isInteger(confirmations) || confirmations < this.#minConf) {
|
|
2690
|
+
continue;
|
|
2579
2691
|
}
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2692
|
+
if (!Number.isFinite(block?.height) || !Number.isFinite(block?.time) || !Number.isFinite(block?.mediantime)) {
|
|
2693
|
+
throw new import_common12.ResolveError(
|
|
2694
|
+
`Beacon signal ${signal.signalBytes} has ${confirmations} confirmations but no valid block height, block time, or block mediantime.`,
|
|
2695
|
+
import_common12.INVALID_DID_UPDATE,
|
|
2696
|
+
{
|
|
2697
|
+
signalBytes: signal.signalBytes,
|
|
2698
|
+
confirmations,
|
|
2699
|
+
height: block?.height,
|
|
2700
|
+
time: block?.time,
|
|
2701
|
+
mediantime: block?.mediantime
|
|
2702
|
+
}
|
|
2703
|
+
);
|
|
2584
2704
|
}
|
|
2585
|
-
|
|
2705
|
+
eligible.push(signal);
|
|
2706
|
+
}
|
|
2707
|
+
return eligible;
|
|
2586
2708
|
}
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
* signal below `ResolutionOptions.minConf` before its update reaches this method,
|
|
2600
|
-
* so every tuple here comes from a block at or above the threshold.
|
|
2601
|
-
*/
|
|
2602
|
-
static updates(currentDocument, unsortedUpdates, versionTime, versionId, resolutionState = { currentVersionId: 1, updateHashHistory: [] }) {
|
|
2603
|
-
let currentVersionId = resolutionState.currentVersionId;
|
|
2604
|
-
const updateHashHistory = resolutionState.updateHashHistory;
|
|
2605
|
-
const updates = unsortedUpdates.sort(
|
|
2606
|
-
([upd0, blk0], [upd1, blk1]) => upd0.targetVersionId - upd1.targetVersionId || blk0.height - blk1.height
|
|
2607
|
-
);
|
|
2608
|
-
const response = {
|
|
2609
|
-
didDocument: currentDocument,
|
|
2610
|
-
metadata: {
|
|
2611
|
-
versionId: `${currentVersionId}`,
|
|
2612
|
-
confirmations: 0,
|
|
2613
|
-
deactivated: currentDocument.deactivated || false
|
|
2614
|
-
}
|
|
2615
|
-
};
|
|
2616
|
-
for (const [update, block] of updates) {
|
|
2617
|
-
const currentDocumentHash = (0, import_common13.canonicalHashBytes)(response.didDocument);
|
|
2618
|
-
const blocktime = import_common13.DateUtils.blocktimeToTimestamp(block.time);
|
|
2619
|
-
response.metadata.updated = import_common13.DateUtils.toISOStringNonFractional(blocktime);
|
|
2620
|
-
response.metadata.confirmations = block.confirmations;
|
|
2621
|
-
if (update.targetVersionId <= currentVersionId) {
|
|
2622
|
-
this.confirmDuplicate(update, updateHashHistory);
|
|
2623
|
-
continue;
|
|
2709
|
+
provide(need, data) {
|
|
2710
|
+
switch (need.kind) {
|
|
2711
|
+
case "NeedGenesisDocument": {
|
|
2712
|
+
if (!isRecord(data)) {
|
|
2713
|
+
throw new import_common12.ResolveError(
|
|
2714
|
+
"Provided data for NeedGenesisDocument must be a document object.",
|
|
2715
|
+
import_common12.INVALID_DID_UPDATE,
|
|
2716
|
+
{ kind: need.kind }
|
|
2717
|
+
);
|
|
2718
|
+
}
|
|
2719
|
+
this.#providedGenesisDocument = data;
|
|
2720
|
+
break;
|
|
2624
2721
|
}
|
|
2625
|
-
|
|
2626
|
-
if (
|
|
2627
|
-
|
|
2722
|
+
case "NeedBeaconSignals": {
|
|
2723
|
+
if (!(data instanceof Map)) {
|
|
2724
|
+
throw new import_common12.ResolveError(
|
|
2725
|
+
"Provided data for NeedBeaconSignals must be a Map of beacon services to signals.",
|
|
2726
|
+
import_common12.INVALID_DID_UPDATE,
|
|
2727
|
+
{ kind: need.kind }
|
|
2728
|
+
);
|
|
2729
|
+
}
|
|
2730
|
+
for (const [service, serviceSignals] of data) {
|
|
2731
|
+
this.#beaconServicesSignals.set(service, serviceSignals);
|
|
2628
2732
|
}
|
|
2733
|
+
break;
|
|
2629
2734
|
}
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
{
|
|
2637
|
-
sourceHash: update.sourceHash,
|
|
2638
|
-
currentDocumentHash: (0, import_common13.encode)(currentDocumentHash, "hex")
|
|
2639
|
-
}
|
|
2735
|
+
case "NeedCASAnnouncement": {
|
|
2736
|
+
if (!isCASAnnouncement(data)) {
|
|
2737
|
+
throw new import_common12.ResolveError(
|
|
2738
|
+
"Provided data for NeedCASAnnouncement is not a CAS announcement.",
|
|
2739
|
+
import_common12.INVALID_DID_UPDATE,
|
|
2740
|
+
{ kind: need.kind }
|
|
2640
2741
|
);
|
|
2641
2742
|
}
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
}
|
|
2653
|
-
);
|
|
2743
|
+
const announcementHash = (0, import_common12.canonicalHash)(data, { encoding: "hex" });
|
|
2744
|
+
if (announcementHash !== need.announcementHash) {
|
|
2745
|
+
throw new import_common12.ResolveError(
|
|
2746
|
+
`CAS announcement hash mismatch: expected ${need.announcementHash}, got ${announcementHash}.`,
|
|
2747
|
+
import_common12.INVALID_DID_UPDATE,
|
|
2748
|
+
{ expected: need.announcementHash, actual: announcementHash }
|
|
2749
|
+
);
|
|
2750
|
+
}
|
|
2751
|
+
this.#sidecarData.casMap.set(announcementHash, data);
|
|
2752
|
+
break;
|
|
2654
2753
|
}
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2754
|
+
case "NeedSignedUpdate": {
|
|
2755
|
+
if (!isSignedBTCR2Update(data)) {
|
|
2756
|
+
throw new import_common12.ResolveError(
|
|
2757
|
+
"Provided data for NeedSignedUpdate is not a signed BTCR2 update.",
|
|
2758
|
+
import_common12.INVALID_DID_UPDATE,
|
|
2759
|
+
{ kind: need.kind }
|
|
2760
|
+
);
|
|
2761
|
+
}
|
|
2762
|
+
const updateHash = (0, import_common12.canonicalHash)(data, { encoding: "hex" });
|
|
2763
|
+
if (updateHash !== need.updateHash) {
|
|
2764
|
+
throw new import_common12.ResolveError(
|
|
2765
|
+
`Signed update hash mismatch: expected ${need.updateHash}, got ${updateHash}.`,
|
|
2766
|
+
import_common12.INVALID_DID_UPDATE,
|
|
2767
|
+
{ expected: need.updateHash, actual: updateHash }
|
|
2768
|
+
);
|
|
2769
|
+
}
|
|
2770
|
+
this.#sidecarData.updateMap.set(updateHash, data);
|
|
2771
|
+
break;
|
|
2660
2772
|
}
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2773
|
+
case "NeedSMTProof": {
|
|
2774
|
+
if (!isSMTProof(data)) {
|
|
2775
|
+
throw new import_common12.ResolveError(
|
|
2776
|
+
"Provided data for NeedSMTProof is not an SMT proof.",
|
|
2777
|
+
import_common12.INVALID_DID_UPDATE,
|
|
2778
|
+
{ kind: need.kind }
|
|
2779
|
+
);
|
|
2780
|
+
}
|
|
2781
|
+
const proofIdHex = (0, import_common12.encode)((0, import_common12.decode)(data.id, "base64urlnopad"), "hex");
|
|
2782
|
+
if (proofIdHex !== need.smtRootHash) {
|
|
2783
|
+
throw new import_common12.ResolveError(
|
|
2784
|
+
`SMT proof root hash mismatch: expected ${need.smtRootHash}, got ${proofIdHex}`,
|
|
2785
|
+
import_common12.INVALID_DID_UPDATE,
|
|
2786
|
+
{ expected: need.smtRootHash, actual: proofIdHex }
|
|
2787
|
+
);
|
|
2788
|
+
}
|
|
2789
|
+
this.#sidecarData.smtMap.set(need.smtRootHash, data);
|
|
2790
|
+
break;
|
|
2664
2791
|
}
|
|
2665
2792
|
}
|
|
2666
|
-
return response;
|
|
2667
2793
|
}
|
|
2794
|
+
};
|
|
2795
|
+
|
|
2796
|
+
// src/core/did-sender-resolver.ts
|
|
2797
|
+
function getAggregationCommunicationKey(document) {
|
|
2798
|
+
const invocation = document.capabilityInvocation?.[0];
|
|
2799
|
+
if (invocation === void 0) {
|
|
2800
|
+
throw new import_common13.DidDocumentError(
|
|
2801
|
+
"Cannot derive aggregation communication key: capabilityInvocation is absent",
|
|
2802
|
+
import_common13.INVALID_DID_DOCUMENT,
|
|
2803
|
+
{ id: document.id }
|
|
2804
|
+
);
|
|
2805
|
+
}
|
|
2806
|
+
const invocationId = Appendix.absoluteDidUrl(invocation, document.id);
|
|
2807
|
+
const vm = typeof invocation === "string" ? invocationId === void 0 ? void 0 : document.verificationMethod?.find(
|
|
2808
|
+
(method) => Appendix.absoluteDidUrl(method.id, document.id) === invocationId
|
|
2809
|
+
) : invocation;
|
|
2810
|
+
if (!vm) {
|
|
2811
|
+
throw new import_common13.DidDocumentError(
|
|
2812
|
+
`Cannot derive aggregation communication key: capabilityInvocation[0] "${invocation}" does not resolve to a verification method`,
|
|
2813
|
+
import_common13.INVALID_DID_DOCUMENT,
|
|
2814
|
+
{ id: document.id, invocation }
|
|
2815
|
+
);
|
|
2816
|
+
}
|
|
2817
|
+
return import_cryptosuite2.SchnorrMultikey.fromVerificationMethod(vm).publicKey;
|
|
2818
|
+
}
|
|
2819
|
+
function resolveBtcr2SenderPk(did, opts) {
|
|
2820
|
+
try {
|
|
2821
|
+
const components = Identifier.decode(did);
|
|
2822
|
+
if (components.idType === "KEY") {
|
|
2823
|
+
return new import_keypair4.CompressedSecp256k1PublicKey(components.genesisBytes);
|
|
2824
|
+
}
|
|
2825
|
+
if (opts?.genesisDocument) {
|
|
2826
|
+
const document = Resolver.external(components, opts.genesisDocument);
|
|
2827
|
+
return getAggregationCommunicationKey(document);
|
|
2828
|
+
}
|
|
2829
|
+
} catch {
|
|
2830
|
+
}
|
|
2831
|
+
return void 0;
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
// src/core/updater.ts
|
|
2835
|
+
var import_common14 = require("@did-btcr2/common");
|
|
2836
|
+
var import_cryptosuite3 = require("@did-btcr2/cryptosuite");
|
|
2837
|
+
var Updater = class _Updater {
|
|
2838
|
+
#state = { phase: "Construct" };
|
|
2839
|
+
#sourceDocument;
|
|
2840
|
+
#patches;
|
|
2841
|
+
#sourceVersionId;
|
|
2842
|
+
#verificationMethod;
|
|
2843
|
+
#beaconService;
|
|
2668
2844
|
/**
|
|
2669
|
-
*
|
|
2670
|
-
* This step confirms that an update with a lower-than-expected targetVersionId is a true duplicate.
|
|
2671
|
-
* @param {SignedBTCR2Update} update The BTCR2 Signed Update to confirm as a duplicate.
|
|
2672
|
-
* @param {HashBytes[]} updateHashHistory The accumulated hash history for comparison.
|
|
2673
|
-
* @returns {void} Does not return a value, but throws an error if the update is not a valid duplicate.
|
|
2845
|
+
* @internal Use {@link DidBtcr2.update} to create instances.
|
|
2674
2846
|
*/
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2847
|
+
constructor(params) {
|
|
2848
|
+
this.#sourceDocument = params.sourceDocument;
|
|
2849
|
+
this.#patches = params.patches;
|
|
2850
|
+
this.#sourceVersionId = params.sourceVersionId;
|
|
2851
|
+
this.#verificationMethod = params.verificationMethod;
|
|
2852
|
+
this.#beaconService = params.beaconService;
|
|
2853
|
+
}
|
|
2854
|
+
// ─── Public static utility methods ─────────────────────────────────────────
|
|
2855
|
+
// Used by generate-vector.ts and other scripts that need direct access to
|
|
2856
|
+
// individual update steps outside the state machine flow.
|
|
2857
|
+
/**
|
|
2858
|
+
* Implements subsection {@link https://dcdpr.github.io/did-btcr2/operations/update.html#construct-btcr2-unsigned-update | 7.3.b Construct BTCR2 Unsigned Update}.
|
|
2859
|
+
*
|
|
2860
|
+
* @param {Btcr2DidDocument} sourceDocument The source DID document to be updated.
|
|
2861
|
+
* @param {PatchOperation[]} patches The JSON Patch operations to apply.
|
|
2862
|
+
* @param {number} sourceVersionId The version ID of the source document.
|
|
2863
|
+
* @returns {UnsignedBTCR2Update} The constructed UnsignedBTCR2Update object.
|
|
2864
|
+
* @throws {UpdateError} If the target document fails DID Core validation.
|
|
2865
|
+
*/
|
|
2866
|
+
static construct(sourceDocument, patches, sourceVersionId) {
|
|
2867
|
+
const unsignedUpdate = {
|
|
2868
|
+
// The array the specification pins, as a fresh copy: the update is a plain JSON
|
|
2869
|
+
// object that callers may edit, and the shared constant is frozen.
|
|
2870
|
+
"@context": [...BTCR2_UPDATE_CONTEXT],
|
|
2871
|
+
patch: patches,
|
|
2872
|
+
targetHash: "",
|
|
2873
|
+
targetVersionId: sourceVersionId + 1,
|
|
2874
|
+
sourceHash: (0, import_common14.canonicalHash)(sourceDocument)
|
|
2875
|
+
};
|
|
2876
|
+
let targetDocument;
|
|
2877
|
+
try {
|
|
2878
|
+
targetDocument = import_common14.JSONPatch.apply(sourceDocument, patches, { strict: true });
|
|
2879
|
+
} catch (error) {
|
|
2880
|
+
throw new import_common14.UpdateError(
|
|
2881
|
+
`Invalid patch: ${errorCause(error).message}`,
|
|
2882
|
+
import_common14.INVALID_DID_UPDATE,
|
|
2883
|
+
{ cause: errorCause(error) }
|
|
2694
2884
|
);
|
|
2695
2885
|
}
|
|
2696
|
-
if (
|
|
2697
|
-
throw new
|
|
2698
|
-
`
|
|
2699
|
-
|
|
2700
|
-
{
|
|
2701
|
-
unsignedUpdateHash: (0, import_common13.encode)(unsignedUpdateHash, "hex"),
|
|
2702
|
-
historicalHash: (0, import_common13.encode)(historicalUpdateHash, "hex")
|
|
2703
|
-
}
|
|
2886
|
+
if (targetDocument.id !== sourceDocument.id) {
|
|
2887
|
+
throw new import_common14.UpdateError(
|
|
2888
|
+
`Patches must not change the DID document id (source "${sourceDocument.id}" to target "${targetDocument.id}").`,
|
|
2889
|
+
import_common14.INVALID_DID_UPDATE,
|
|
2890
|
+
{ sourceId: sourceDocument.id, targetId: targetDocument.id }
|
|
2704
2891
|
);
|
|
2705
2892
|
}
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
* @throws {ResolveError} If the update is invalid or cannot be applied.
|
|
2714
|
-
*/
|
|
2715
|
-
static applyUpdate(currentDocument, update) {
|
|
2716
|
-
if (!isBtcr2UpdateContext(update["@context"])) {
|
|
2717
|
-
throw new import_common13.ResolveError(
|
|
2718
|
-
"Invalid update: @context is not the array the specification pins for a BTCR2 Update",
|
|
2719
|
-
import_common13.INVALID_DID_UPDATE,
|
|
2720
|
-
{ context: update["@context"], expected: [...BTCR2_UPDATE_CONTEXT] }
|
|
2893
|
+
try {
|
|
2894
|
+
DidDocument.isValid(targetDocument);
|
|
2895
|
+
} catch (error) {
|
|
2896
|
+
throw new import_common14.UpdateError(
|
|
2897
|
+
"Error validating targetDocument: " + (error instanceof Error ? error.message : String(error)),
|
|
2898
|
+
import_common14.INVALID_DID_UPDATE,
|
|
2899
|
+
targetDocument
|
|
2721
2900
|
);
|
|
2722
2901
|
}
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2902
|
+
unsignedUpdate.targetHash = (0, import_common14.canonicalHash)(targetDocument);
|
|
2903
|
+
return unsignedUpdate;
|
|
2904
|
+
}
|
|
2905
|
+
/**
|
|
2906
|
+
* Implements subsection {@link http://dcdpr.github.io/did-btcr2/operations/update.html#construct-btcr2-signed-update | 7.3.c Construct BTCR2 Signed Update }.
|
|
2907
|
+
*
|
|
2908
|
+
* @param {string} did The did-btcr2 identifier to derive the root capability from.
|
|
2909
|
+
* @param {UnsignedBTCR2Update} unsignedUpdate The unsigned update to sign.
|
|
2910
|
+
* @param {DidVerificationMethod} verificationMethod The verification method for signing.
|
|
2911
|
+
* @param {Signer} signer Signer that produces the BIP-340 Schnorr signature.
|
|
2912
|
+
* @returns {SignedBTCR2Update} The signed update with a Data Integrity proof.
|
|
2913
|
+
*/
|
|
2914
|
+
static sign(did, unsignedUpdate, verificationMethod, signer) {
|
|
2915
|
+
if (!did.startsWith("did:btcr2:")) {
|
|
2916
|
+
throw new import_common14.UpdateError(
|
|
2917
|
+
`Expected a did:btcr2 identifier for the root capability; got "${did}".`,
|
|
2918
|
+
import_common14.INVALID_DID_UPDATE,
|
|
2919
|
+
{ did }
|
|
2728
2920
|
);
|
|
2729
2921
|
}
|
|
2730
|
-
const
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
throw new import_common13.ResolveError(
|
|
2738
|
-
"Invalid root capability",
|
|
2739
|
-
import_common13.INVALID_DID_UPDATE,
|
|
2740
|
-
{ rootCapability, currentDocument }
|
|
2922
|
+
const controller = verificationMethod.controller;
|
|
2923
|
+
const hashIdx = verificationMethod.id.indexOf("#");
|
|
2924
|
+
if (hashIdx < 0) {
|
|
2925
|
+
throw new import_common14.UpdateError(
|
|
2926
|
+
`Verification method id must contain a fragment (e.g. "${verificationMethod.id}#initialKey"); got "${verificationMethod.id}".`,
|
|
2927
|
+
import_common14.INVALID_DID_UPDATE,
|
|
2928
|
+
{ verificationMethodId: verificationMethod.id }
|
|
2741
2929
|
);
|
|
2742
2930
|
}
|
|
2743
|
-
const
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
if (!authorized) {
|
|
2752
|
-
throw new import_common13.ResolveError(
|
|
2753
|
-
"Invalid update: verificationMethod is not authorized for capabilityInvocation",
|
|
2754
|
-
import_common13.INVALID_DID_UPDATE,
|
|
2931
|
+
const id = verificationMethod.id.slice(hashIdx);
|
|
2932
|
+
const multikey = import_cryptosuite3.SchnorrMultikey.fromSigner(id, controller, signer);
|
|
2933
|
+
const absoluteMethodId = hashIdx === 0 ? `${did}${id}` : verificationMethod.id;
|
|
2934
|
+
const signerKey = multikey.publicKey.multibase.encoded;
|
|
2935
|
+
if (verificationMethod.publicKeyMultibase && signerKey !== verificationMethod.publicKeyMultibase) {
|
|
2936
|
+
throw new import_common14.UpdateError(
|
|
2937
|
+
`Signing key does not match verification method "${verificationMethod.id}": the signer's public key differs from the method's published publicKeyMultibase.`,
|
|
2938
|
+
import_common14.INVALID_DID_UPDATE,
|
|
2755
2939
|
{
|
|
2756
|
-
verificationMethodId,
|
|
2757
|
-
|
|
2940
|
+
verificationMethodId: verificationMethod.id,
|
|
2941
|
+
expected: verificationMethod.publicKeyMultibase,
|
|
2942
|
+
actual: signerKey
|
|
2758
2943
|
}
|
|
2759
2944
|
);
|
|
2760
2945
|
}
|
|
2761
|
-
const
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2946
|
+
const config = {
|
|
2947
|
+
// The proof must carry the same array as the update. The cryptosuite copies the
|
|
2948
|
+
// document @context into the proof when the document has one, so the two arrays
|
|
2949
|
+
// are equal by construction; this value is the fallback for a document without one.
|
|
2950
|
+
"@context": [...BTCR2_UPDATE_CONTEXT],
|
|
2951
|
+
cryptosuite: "bip340-jcs-2025",
|
|
2952
|
+
type: "DataIntegrityProof",
|
|
2953
|
+
// The proof names the signing method by absolute DID URL, even when the document
|
|
2954
|
+
// spells that method's own id relatively: a proof travels apart from the document
|
|
2955
|
+
// that defines the method, so a bare `#initialKey` in it resolves against nothing.
|
|
2956
|
+
// For a document that already spells its ids absolutely this is the id unchanged,
|
|
2957
|
+
// so proofs over such documents are byte-identical to before.
|
|
2958
|
+
verificationMethod: absoluteMethodId,
|
|
2959
|
+
proofPurpose: "capabilityInvocation",
|
|
2960
|
+
capability: `urn:zcap:root:${encodeURIComponent(did)}`,
|
|
2961
|
+
capabilityAction: "Write"
|
|
2962
|
+
};
|
|
2963
|
+
const diproof = multikey.toCryptosuite().toDataIntegrityProof();
|
|
2964
|
+
const signedUpdate = diproof.addProof(unsignedUpdate, config);
|
|
2965
|
+
let verified;
|
|
2966
|
+
try {
|
|
2967
|
+
const verifier = import_cryptosuite3.SchnorrMultikey.fromVerificationMethod({ ...verificationMethod, id: absoluteMethodId });
|
|
2968
|
+
verified = verifier.toCryptosuite().toDataIntegrityProof().verifyProof((0, import_common14.canonicalize)(signedUpdate), "capabilityInvocation").verified;
|
|
2969
|
+
} catch (error) {
|
|
2970
|
+
throw new import_common14.UpdateError(
|
|
2971
|
+
`Invalid update: the proof does not verify with the public key of "${verificationMethod.id}": ` + errorCause(error).message,
|
|
2972
|
+
import_common14.INVALID_DID_UPDATE,
|
|
2973
|
+
{ verificationMethodId: verificationMethod.id, cause: errorCause(error) }
|
|
2772
2974
|
);
|
|
2773
2975
|
}
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
throw new import_common13.ResolveError(
|
|
2780
|
-
`Invalid update: update.targetHash !== currentDocumentHash`,
|
|
2781
|
-
import_common13.INVALID_DID_UPDATE,
|
|
2782
|
-
{ updateTargetHash, currentDocumentHash }
|
|
2976
|
+
if (!verified) {
|
|
2977
|
+
throw new import_common14.UpdateError(
|
|
2978
|
+
`Invalid update: the proof does not verify with the public key of "${verificationMethod.id}".`,
|
|
2979
|
+
import_common14.INVALID_DID_UPDATE,
|
|
2980
|
+
{ verificationMethodId: verificationMethod.id }
|
|
2783
2981
|
);
|
|
2784
2982
|
}
|
|
2785
|
-
return
|
|
2983
|
+
return signedUpdate;
|
|
2786
2984
|
}
|
|
2787
2985
|
/**
|
|
2788
|
-
*
|
|
2789
|
-
*
|
|
2790
|
-
* - `{ status: 'resolved', result }` - resolution complete
|
|
2986
|
+
* Implements subsection {@link https://dcdpr.github.io/did-btcr2/operations/update.html#announce-did-update | 7.3.d Announce DID Update}.
|
|
2987
|
+
* Announces a signed update to the Bitcoin blockchain via the specified beacon.
|
|
2791
2988
|
*
|
|
2792
|
-
*
|
|
2989
|
+
* @param {BeaconService} beaconService The beacon service to broadcast through.
|
|
2990
|
+
* @param {string} did The DID being updated. Required because a beacon service
|
|
2991
|
+
* `id` may be a relative DID URL and so cannot supply the subject.
|
|
2992
|
+
* @param {SignedBTCR2Update} update The signed update to announce.
|
|
2993
|
+
* @param {Signer} signer Signer that produces the ECDSA signature for the Bitcoin transaction.
|
|
2994
|
+
* @param {BitcoinConnection} bitcoin The Bitcoin network connection.
|
|
2995
|
+
* @param {CASBroadcastOptions} [options] Optional broadcast configuration (fee estimator,
|
|
2996
|
+
* change address, and, for CAS beacons, a `casPublish` callback invoked before the
|
|
2997
|
+
* transaction broadcast; other beacon types ignore `casPublish`).
|
|
2998
|
+
* @returns {Promise<BroadcastResult>} The broadcast artifacts: the signed update, the signal
|
|
2999
|
+
* txid, and any per-beacon-type sidecar data (CAS announcement / SMT proof).
|
|
2793
3000
|
*/
|
|
2794
|
-
|
|
3001
|
+
static async announce(beaconService, did, update, signer, bitcoin, options) {
|
|
3002
|
+
const beacon = BeaconFactory.establish(beaconService, did);
|
|
3003
|
+
return beacon.broadcastSignal(update, signer, bitcoin, options);
|
|
3004
|
+
}
|
|
3005
|
+
// Private instance wrappers
|
|
3006
|
+
// Delegate to the public statics with bound instance fields for cleaner
|
|
3007
|
+
// advance/provide code.
|
|
3008
|
+
#construct() {
|
|
3009
|
+
return _Updater.construct(this.#sourceDocument, this.#patches, this.#sourceVersionId);
|
|
3010
|
+
}
|
|
3011
|
+
/**
|
|
3012
|
+
* Advance the state machine. Returns either:
|
|
3013
|
+
* - `{ status: 'action-required', needs }` caller must provide data via {@link provide}
|
|
3014
|
+
* - `{ status: 'complete', result }` update is signed and broadcast
|
|
3015
|
+
*/
|
|
3016
|
+
advance() {
|
|
2795
3017
|
while (true) {
|
|
2796
|
-
switch (this.#phase) {
|
|
2797
|
-
// Phase:
|
|
2798
|
-
//
|
|
2799
|
-
case "
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
continue;
|
|
2808
|
-
}
|
|
2809
|
-
const genesisHash = (0, import_common13.encode)(this.#didComponents.genesisBytes, "hex");
|
|
3018
|
+
switch (this.#state.phase) {
|
|
3019
|
+
// Phase: Construct
|
|
3020
|
+
// Build the unsigned update from source doc + patches. Pure, synchronous.
|
|
3021
|
+
case "Construct": {
|
|
3022
|
+
const unsignedUpdate = this.#construct();
|
|
3023
|
+
this.#state = { phase: "Sign", unsignedUpdate };
|
|
3024
|
+
continue;
|
|
3025
|
+
}
|
|
3026
|
+
// Phase: Sign
|
|
3027
|
+
// Emit NeedSigningKey: the caller supplies the secret key (or a KMS signature).
|
|
3028
|
+
case "Sign": {
|
|
2810
3029
|
return {
|
|
2811
3030
|
status: "action-required",
|
|
2812
|
-
needs: [{
|
|
3031
|
+
needs: [{
|
|
3032
|
+
kind: "NeedSigningKey",
|
|
3033
|
+
verificationMethodId: this.#verificationMethod.id,
|
|
3034
|
+
unsignedUpdate: this.#state.unsignedUpdate
|
|
3035
|
+
}]
|
|
2813
3036
|
};
|
|
2814
3037
|
}
|
|
2815
|
-
// Phase:
|
|
2816
|
-
//
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
const
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
return {
|
|
2829
|
-
status: "action-required",
|
|
2830
|
-
needs: [{ kind: "NeedBeaconSignals", beaconServices: newServices }]
|
|
2831
|
-
};
|
|
2832
|
-
}
|
|
2833
|
-
this.#phase = "BeaconProcess" /* BeaconProcess */;
|
|
2834
|
-
continue;
|
|
2835
|
-
}
|
|
2836
|
-
// Phase: BeaconProcess
|
|
2837
|
-
// Process each beacon's signals. Collect updates and data needs.
|
|
2838
|
-
case "BeaconProcess" /* BeaconProcess */: {
|
|
2839
|
-
const allNeeds = [];
|
|
2840
|
-
for (const [service, signals] of this.#beaconServicesSignals) {
|
|
2841
|
-
if (this.#processedServices.has(service.id) || !signals.length) continue;
|
|
2842
|
-
const eligible = this.#eligibleSignals(signals);
|
|
2843
|
-
if (!eligible.length) continue;
|
|
2844
|
-
const beacon = BeaconFactory.establish(service, this.#currentDocument.id);
|
|
2845
|
-
const result = beacon.processSignals(eligible, this.#sidecarData);
|
|
2846
|
-
if (result.needs.length > 0) {
|
|
2847
|
-
allNeeds.push(...result.needs);
|
|
2848
|
-
} else {
|
|
2849
|
-
this.#unsortedUpdates.push(...result.updates);
|
|
2850
|
-
this.#processedServices.add(service.id);
|
|
2851
|
-
}
|
|
2852
|
-
}
|
|
2853
|
-
if (allNeeds.length > 0) {
|
|
2854
|
-
return { status: "action-required", needs: allNeeds };
|
|
2855
|
-
}
|
|
2856
|
-
this.#phase = "ApplyUpdates" /* ApplyUpdates */;
|
|
2857
|
-
continue;
|
|
3038
|
+
// Phase: Fund
|
|
3039
|
+
// Emit NeedFunding with the beacon address. The caller checks UTXOs,
|
|
3040
|
+
// funds the address if needed, and provides to continue.
|
|
3041
|
+
case "Fund": {
|
|
3042
|
+
const beaconAddress = this.#beaconService.serviceEndpoint.replace("bitcoin:", "");
|
|
3043
|
+
return {
|
|
3044
|
+
status: "action-required",
|
|
3045
|
+
needs: [{
|
|
3046
|
+
kind: "NeedFunding",
|
|
3047
|
+
beaconAddress,
|
|
3048
|
+
beaconService: this.#beaconService
|
|
3049
|
+
}]
|
|
3050
|
+
};
|
|
2858
3051
|
}
|
|
2859
|
-
// Phase:
|
|
2860
|
-
//
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
this.#
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
this.#unsortedUpdates = [];
|
|
2873
|
-
const beaconServices = BeaconUtils.getBeaconServices(this.#currentDocument);
|
|
2874
|
-
const hasNewServices = beaconServices.some((service) => {
|
|
2875
|
-
const address = BeaconUtils.parseBitcoinAddress(service.serviceEndpoint);
|
|
2876
|
-
return !this.#requestCache.has(address);
|
|
2877
|
-
});
|
|
2878
|
-
if (hasNewServices) {
|
|
2879
|
-
if (++this.#discoveryRounds > this.#maxDiscoveryRounds) {
|
|
2880
|
-
throw new import_common13.ResolveError(
|
|
2881
|
-
`Exceeded the configured maximum of ${this.#maxDiscoveryRounds} beacon-discovery rounds. Raise or remove ResolutionOptions.maxDiscoveryRounds to resolve this DID.`,
|
|
2882
|
-
import_common13.INTERNAL_ERROR,
|
|
2883
|
-
{ maxDiscoveryRounds: this.#maxDiscoveryRounds, discoveryRounds: this.#discoveryRounds }
|
|
2884
|
-
);
|
|
2885
|
-
}
|
|
2886
|
-
this.#phase = "BeaconDiscovery" /* BeaconDiscovery */;
|
|
2887
|
-
continue;
|
|
2888
|
-
}
|
|
2889
|
-
}
|
|
2890
|
-
this.#phase = "Complete" /* Complete */;
|
|
2891
|
-
continue;
|
|
3052
|
+
// Phase: Broadcast
|
|
3053
|
+
// Emit NeedBroadcast with the signed update + beacon service. The caller performs
|
|
3054
|
+
// the actual on-chain announcement (or hands off to the aggregation protocol).
|
|
3055
|
+
case "Broadcast": {
|
|
3056
|
+
return {
|
|
3057
|
+
status: "action-required",
|
|
3058
|
+
needs: [{
|
|
3059
|
+
kind: "NeedBroadcast",
|
|
3060
|
+
beaconService: this.#beaconService,
|
|
3061
|
+
signedUpdate: this.#state.signedUpdate,
|
|
3062
|
+
did: this.#sourceDocument.id
|
|
3063
|
+
}]
|
|
3064
|
+
};
|
|
2892
3065
|
}
|
|
2893
3066
|
// Phase: Complete
|
|
2894
|
-
case "Complete"
|
|
3067
|
+
case "Complete": {
|
|
2895
3068
|
return {
|
|
2896
|
-
status: "
|
|
2897
|
-
|
|
2898
|
-
result: this.#resolvedResponse ?? {
|
|
2899
|
-
didDocument: this.#currentDocument,
|
|
2900
|
-
metadata: {
|
|
2901
|
-
versionId: this.#versionId ?? "1",
|
|
2902
|
-
confirmations: 0,
|
|
2903
|
-
deactivated: this.#currentDocument.deactivated || false
|
|
2904
|
-
}
|
|
2905
|
-
}
|
|
3069
|
+
status: "complete",
|
|
3070
|
+
result: { signedUpdate: this.#state.signedUpdate }
|
|
2906
3071
|
};
|
|
2907
3072
|
}
|
|
2908
3073
|
}
|
|
2909
3074
|
}
|
|
2910
3075
|
}
|
|
2911
|
-
/**
|
|
2912
|
-
* Return the signals of one beacon service that resolution may process: the
|
|
2913
|
-
* signals with at least `#minConf` confirmations. The specification removes a
|
|
2914
|
-
* transaction below the threshold from the set of Beacon Signals, so an
|
|
2915
|
-
* excluded signal emits no data need and applies no update. A signal with no
|
|
2916
|
-
* integer confirmation count is excluded too: that is a mempool transaction
|
|
2917
|
-
* from a driver that did not skip it.
|
|
2918
|
-
*
|
|
2919
|
-
* An eligible signal must carry a finite block height and block time. A
|
|
2920
|
-
* signal that passes the count but lacks them is malformed. It fails fast
|
|
2921
|
-
* here with a typed error, in the style of the {@link provide} guards, and
|
|
2922
|
-
* not later with an invalid date inside {@link updates}.
|
|
2923
|
-
* @param {Array<BeaconSignal>} signals The signals the caller provided for one service.
|
|
2924
|
-
* @returns {Array<BeaconSignal>} The signals at or above the threshold, in the given order.
|
|
2925
|
-
* @throws {ResolveError} `INVALID_DID_UPDATE` for an eligible signal with no valid block metadata.
|
|
2926
|
-
*/
|
|
2927
|
-
#eligibleSignals(signals) {
|
|
2928
|
-
const eligible = [];
|
|
2929
|
-
for (const signal of signals) {
|
|
2930
|
-
const block = signal.blockMetadata;
|
|
2931
|
-
const confirmations = block?.confirmations;
|
|
2932
|
-
if (!Number.isInteger(confirmations) || confirmations < this.#minConf) {
|
|
2933
|
-
continue;
|
|
2934
|
-
}
|
|
2935
|
-
if (!Number.isFinite(block?.height) || !Number.isFinite(block?.time)) {
|
|
2936
|
-
throw new import_common13.ResolveError(
|
|
2937
|
-
`Beacon signal ${signal.signalBytes} has ${confirmations} confirmations but no valid block height or block time.`,
|
|
2938
|
-
import_common13.INVALID_DID_UPDATE,
|
|
2939
|
-
{ signalBytes: signal.signalBytes, confirmations, height: block?.height, time: block?.time }
|
|
2940
|
-
);
|
|
2941
|
-
}
|
|
2942
|
-
eligible.push(signal);
|
|
2943
|
-
}
|
|
2944
|
-
return eligible;
|
|
2945
|
-
}
|
|
2946
3076
|
provide(need, data) {
|
|
2947
3077
|
switch (need.kind) {
|
|
2948
|
-
case "
|
|
2949
|
-
if (
|
|
2950
|
-
throw new
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
{
|
|
2954
|
-
);
|
|
2955
|
-
}
|
|
2956
|
-
this.#providedGenesisDocument = data;
|
|
2957
|
-
break;
|
|
2958
|
-
}
|
|
2959
|
-
case "NeedBeaconSignals": {
|
|
2960
|
-
if (!(data instanceof Map)) {
|
|
2961
|
-
throw new import_common13.ResolveError(
|
|
2962
|
-
"Provided data for NeedBeaconSignals must be a Map of beacon services to signals.",
|
|
2963
|
-
import_common13.INVALID_DID_UPDATE,
|
|
2964
|
-
{ kind: need.kind }
|
|
2965
|
-
);
|
|
2966
|
-
}
|
|
2967
|
-
for (const [service, serviceSignals] of data) {
|
|
2968
|
-
this.#beaconServicesSignals.set(service, serviceSignals);
|
|
2969
|
-
}
|
|
2970
|
-
break;
|
|
2971
|
-
}
|
|
2972
|
-
case "NeedCASAnnouncement": {
|
|
2973
|
-
if (!isCASAnnouncement(data)) {
|
|
2974
|
-
throw new import_common13.ResolveError(
|
|
2975
|
-
"Provided data for NeedCASAnnouncement is not a CAS announcement.",
|
|
2976
|
-
import_common13.INVALID_DID_UPDATE,
|
|
2977
|
-
{ kind: need.kind }
|
|
3078
|
+
case "NeedSigningKey": {
|
|
3079
|
+
if (this.#state.phase !== "Sign") {
|
|
3080
|
+
throw new import_common14.UpdateError(
|
|
3081
|
+
`Cannot provide NeedSigningKey: updater phase is ${this.#state.phase}, expected Sign.`,
|
|
3082
|
+
import_common14.INVALID_DID_UPDATE,
|
|
3083
|
+
{ phase: this.#state.phase }
|
|
2978
3084
|
);
|
|
2979
3085
|
}
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
import_common13.INVALID_DID_UPDATE,
|
|
2985
|
-
{ expected: need.announcementHash, actual: announcementHash }
|
|
3086
|
+
if (!data) {
|
|
3087
|
+
throw new import_common14.UpdateError(
|
|
3088
|
+
"NeedSigningKey requires a Signer.",
|
|
3089
|
+
import_common14.INVALID_DID_UPDATE
|
|
2986
3090
|
);
|
|
2987
3091
|
}
|
|
2988
|
-
this.#
|
|
3092
|
+
const unsignedUpdate = this.#state.unsignedUpdate;
|
|
3093
|
+
const signedUpdate = _Updater.sign(
|
|
3094
|
+
this.#sourceDocument.id,
|
|
3095
|
+
unsignedUpdate,
|
|
3096
|
+
this.#verificationMethod,
|
|
3097
|
+
data
|
|
3098
|
+
);
|
|
3099
|
+
this.#state = { phase: "Fund", unsignedUpdate, signedUpdate };
|
|
2989
3100
|
break;
|
|
2990
3101
|
}
|
|
2991
|
-
case "
|
|
2992
|
-
if (
|
|
2993
|
-
throw new
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
{
|
|
3102
|
+
case "NeedFunding": {
|
|
3103
|
+
if (this.#state.phase !== "Fund") {
|
|
3104
|
+
throw new import_common14.UpdateError(
|
|
3105
|
+
`Cannot provide NeedFunding: updater phase is ${this.#state.phase}, expected Fund.`,
|
|
3106
|
+
import_common14.INVALID_DID_UPDATE,
|
|
3107
|
+
{ phase: this.#state.phase }
|
|
2997
3108
|
);
|
|
2998
3109
|
}
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3110
|
+
if (data !== void 0) {
|
|
3111
|
+
const proof = data;
|
|
3112
|
+
if (typeof proof.utxoCount !== "number" || !Number.isFinite(proof.utxoCount) || proof.utxoCount < 1) {
|
|
3113
|
+
throw new import_common14.UpdateError(
|
|
3114
|
+
`NeedFunding proof must have utxoCount >= 1; got ${String(proof.utxoCount)}.`,
|
|
3115
|
+
import_common14.INVALID_DID_UPDATE,
|
|
3116
|
+
{ utxoCount: proof.utxoCount }
|
|
3117
|
+
);
|
|
3118
|
+
}
|
|
3006
3119
|
}
|
|
3007
|
-
this.#
|
|
3120
|
+
this.#state = {
|
|
3121
|
+
phase: "Broadcast",
|
|
3122
|
+
unsignedUpdate: this.#state.unsignedUpdate,
|
|
3123
|
+
signedUpdate: this.#state.signedUpdate
|
|
3124
|
+
};
|
|
3008
3125
|
break;
|
|
3009
3126
|
}
|
|
3010
|
-
case "
|
|
3011
|
-
if (
|
|
3012
|
-
throw new
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
{
|
|
3016
|
-
);
|
|
3017
|
-
}
|
|
3018
|
-
const proofIdHex = (0, import_common13.encode)((0, import_common13.decode)(data.id, "base64urlnopad"), "hex");
|
|
3019
|
-
if (proofIdHex !== need.smtRootHash) {
|
|
3020
|
-
throw new import_common13.ResolveError(
|
|
3021
|
-
`SMT proof root hash mismatch: expected ${need.smtRootHash}, got ${proofIdHex}`,
|
|
3022
|
-
import_common13.INVALID_DID_UPDATE,
|
|
3023
|
-
{ expected: need.smtRootHash, actual: proofIdHex }
|
|
3127
|
+
case "NeedBroadcast": {
|
|
3128
|
+
if (this.#state.phase !== "Broadcast") {
|
|
3129
|
+
throw new import_common14.UpdateError(
|
|
3130
|
+
`Cannot provide NeedBroadcast: updater phase is ${this.#state.phase}, expected Broadcast.`,
|
|
3131
|
+
import_common14.INVALID_DID_UPDATE,
|
|
3132
|
+
{ phase: this.#state.phase }
|
|
3024
3133
|
);
|
|
3025
3134
|
}
|
|
3026
|
-
this.#
|
|
3135
|
+
this.#state = { phase: "Complete", signedUpdate: this.#state.signedUpdate };
|
|
3027
3136
|
break;
|
|
3028
3137
|
}
|
|
3029
3138
|
}
|
|
3030
3139
|
}
|
|
3031
3140
|
};
|
|
3032
3141
|
|
|
3033
|
-
// src/core/did-sender-resolver.ts
|
|
3034
|
-
function getAggregationCommunicationKey(document) {
|
|
3035
|
-
const invocation = document.capabilityInvocation?.[0];
|
|
3036
|
-
if (invocation === void 0) {
|
|
3037
|
-
throw new import_common14.DidDocumentError(
|
|
3038
|
-
"Cannot derive aggregation communication key: capabilityInvocation is absent",
|
|
3039
|
-
import_common14.INVALID_DID_DOCUMENT,
|
|
3040
|
-
{ id: document.id }
|
|
3041
|
-
);
|
|
3042
|
-
}
|
|
3043
|
-
const invocationId = Appendix.absoluteDidUrl(invocation, document.id);
|
|
3044
|
-
const vm = typeof invocation === "string" ? invocationId === void 0 ? void 0 : document.verificationMethod?.find(
|
|
3045
|
-
(method) => Appendix.absoluteDidUrl(method.id, document.id) === invocationId
|
|
3046
|
-
) : invocation;
|
|
3047
|
-
if (!vm) {
|
|
3048
|
-
throw new import_common14.DidDocumentError(
|
|
3049
|
-
`Cannot derive aggregation communication key: capabilityInvocation[0] "${invocation}" does not resolve to a verification method`,
|
|
3050
|
-
import_common14.INVALID_DID_DOCUMENT,
|
|
3051
|
-
{ id: document.id, invocation }
|
|
3052
|
-
);
|
|
3053
|
-
}
|
|
3054
|
-
return import_cryptosuite3.SchnorrMultikey.fromVerificationMethod(vm).publicKey;
|
|
3055
|
-
}
|
|
3056
|
-
function resolveBtcr2SenderPk(did, opts) {
|
|
3057
|
-
try {
|
|
3058
|
-
const components = Identifier.decode(did);
|
|
3059
|
-
if (components.idType === "KEY") {
|
|
3060
|
-
return new import_keypair4.CompressedSecp256k1PublicKey(components.genesisBytes);
|
|
3061
|
-
}
|
|
3062
|
-
if (opts?.genesisDocument) {
|
|
3063
|
-
const document = Resolver.external(components, opts.genesisDocument);
|
|
3064
|
-
return getAggregationCommunicationKey(document);
|
|
3065
|
-
}
|
|
3066
|
-
} catch {
|
|
3067
|
-
}
|
|
3068
|
-
return void 0;
|
|
3069
|
-
}
|
|
3070
|
-
|
|
3071
3142
|
// src/utils/did-document-builder.ts
|
|
3072
3143
|
var import_common15 = require("@did-btcr2/common");
|
|
3073
3144
|
var DidDocumentBuilder = class {
|
|
@@ -3122,6 +3193,217 @@ var DidDocumentBuilder = class {
|
|
|
3122
3193
|
return didDocument;
|
|
3123
3194
|
}
|
|
3124
3195
|
};
|
|
3196
|
+
|
|
3197
|
+
// src/did-btcr2.ts
|
|
3198
|
+
var import_common16 = require("@did-btcr2/common");
|
|
3199
|
+
var import_dids2 = require("@web5/dids");
|
|
3200
|
+
var DidBtcr2 = class {
|
|
3201
|
+
/**
|
|
3202
|
+
* Name of the DID method, as defined in the DID BTCR2 specification
|
|
3203
|
+
*/
|
|
3204
|
+
static methodName = "btcr2";
|
|
3205
|
+
/**
|
|
3206
|
+
* Implements section {@link https://dcdpr.github.io/did-btcr2/operations/create.html | 7.1 Create}.
|
|
3207
|
+
* @param {KeyBytes | DocumentBytes} genesisBytes The bytes used to create the genesis document for a did:btcr2 identifier.
|
|
3208
|
+
* This can be either the bytes of the genesis document itself or the bytes of a key that will be used to create the genesis document.
|
|
3209
|
+
* @param {DidCreateOptions} options Options for creating the identifier, including the idType (key or external), version, and network.
|
|
3210
|
+
* @param {string} options.idType The type of identifier to create, either 'KEY' or 'EXTERNAL'. Defaults to 'KEY'.
|
|
3211
|
+
* @param {number} options.version The version number of the did:btcr2 specification to use for creating the identifier. Defaults to 1.
|
|
3212
|
+
* @param {string} options.network The Bitcoin network to use for the identifier, e.g. 'bitcoin', 'testnet', etc. Defaults to 'bitcoin'.
|
|
3213
|
+
* @returns {Promise<string>} Promise resolving to an identifier string.
|
|
3214
|
+
* @throws {MethodError} if any of the checks fail
|
|
3215
|
+
* @example
|
|
3216
|
+
* ```ts
|
|
3217
|
+
* const genesisBytes = SchnorrKeyPair.generate().publicKey.compressed;
|
|
3218
|
+
* const did = DidBtcr2.create(genesisBytes, { idType: 'KEY', network: 'regtest' });
|
|
3219
|
+
* ```
|
|
3220
|
+
*/
|
|
3221
|
+
static create(genesisBytes, options) {
|
|
3222
|
+
const { idType, version = 1, network = "bitcoin" } = options || {};
|
|
3223
|
+
if (!idType) {
|
|
3224
|
+
throw new import_common16.MethodError(
|
|
3225
|
+
"idType is required for creating a did:btcr2 identifier",
|
|
3226
|
+
import_common16.INVALID_DID_DOCUMENT,
|
|
3227
|
+
options
|
|
3228
|
+
);
|
|
3229
|
+
}
|
|
3230
|
+
return Identifier.encode(genesisBytes, { idType, version, network });
|
|
3231
|
+
}
|
|
3232
|
+
/**
|
|
3233
|
+
* Entry point for section {@link https://dcdpr.github.io/did-btcr2/operations/resolve.html | 7.2 Resolve}.
|
|
3234
|
+
*
|
|
3235
|
+
* Factory method that performs pure setup and returns a {@link Resolver} state machine.
|
|
3236
|
+
* The caller drives resolution by calling `resolver.resolve()` and `resolver.provide()`.
|
|
3237
|
+
* Analogous to Rust's `Document::read()`.
|
|
3238
|
+
*
|
|
3239
|
+
* @param {string} did The did:btcr2 identifier to be resolved.
|
|
3240
|
+
* @param {ResolutionOptions} resolutionOptions Options used during the resolution process.
|
|
3241
|
+
* @returns {Resolver} A sans-I/O state machine the caller drives to completion.
|
|
3242
|
+
* @example
|
|
3243
|
+
* ```ts
|
|
3244
|
+
* const resolver = DidBtcr2.resolve(did, { sidecar });
|
|
3245
|
+
* let state = resolver.resolve();
|
|
3246
|
+
* while (state.status === 'action-required') {
|
|
3247
|
+
* for (const need of state.needs) { ... provide data ... }
|
|
3248
|
+
* state = resolver.resolve();
|
|
3249
|
+
* }
|
|
3250
|
+
* const { didDocument, metadata } = state.result;
|
|
3251
|
+
* ```
|
|
3252
|
+
*/
|
|
3253
|
+
static resolve(did, resolutionOptions = {}) {
|
|
3254
|
+
const didComponents = Identifier.decode(did);
|
|
3255
|
+
const sidecarData = Resolver.sidecarData(resolutionOptions.sidecar);
|
|
3256
|
+
const currentDocument = didComponents.hrp === import_common16.IdentifierHrp.k ? Resolver.deterministic(didComponents) : null;
|
|
3257
|
+
return new Resolver(didComponents, sidecarData, currentDocument, {
|
|
3258
|
+
versionId: resolutionOptions.versionId,
|
|
3259
|
+
versionTime: resolutionOptions.versionTime,
|
|
3260
|
+
genesisDocument: resolutionOptions.sidecar?.genesisDocument,
|
|
3261
|
+
maxDiscoveryRounds: resolutionOptions.maxDiscoveryRounds,
|
|
3262
|
+
minConf: resolutionOptions.minConf
|
|
3263
|
+
});
|
|
3264
|
+
}
|
|
3265
|
+
/**
|
|
3266
|
+
* Entry point for section {@link https://dcdpr.github.io/did-btcr2/#update | 7.3 Update}.
|
|
3267
|
+
*
|
|
3268
|
+
* Factory method that validates the update parameters and returns a sans-I/O
|
|
3269
|
+
* {@link Updater} state machine. The caller drives the updater through its
|
|
3270
|
+
* phases (Construct -> Sign -> Broadcast -> Complete) by calling `advance()` and
|
|
3271
|
+
* `provide()`. The method package performs **zero I/O**: signing key retrieval
|
|
3272
|
+
* (or KMS delegation) and the on-chain broadcast are the caller's responsibility.
|
|
3273
|
+
*
|
|
3274
|
+
* For a fully-wired version with Bitcoin broadcast and key handling, see
|
|
3275
|
+
* `DidMethodApi.update()` in `@did-btcr2/api`.
|
|
3276
|
+
*
|
|
3277
|
+
* @param params Update construction parameters.
|
|
3278
|
+
* @param {Btcr2DidDocument} params.sourceDocument The DID document being updated.
|
|
3279
|
+
* @param {PatchOperation[]} params.patches The JSON Patch operations to apply.
|
|
3280
|
+
* @param {number} params.sourceVersionId The version ID before applying the update.
|
|
3281
|
+
* @param {string} params.verificationMethodId The verification method ID to sign with.
|
|
3282
|
+
* @param {string} params.beaconId The beacon service ID to broadcast through.
|
|
3283
|
+
* @returns {Updater} A sans-I/O state machine for driving the update.
|
|
3284
|
+
* @throws {UpdateError} `INVALID_DID_UPDATE` if `sourceVersionId` is not an integer of at
|
|
3285
|
+
* least 1, if no entry of `capabilityInvocation` identifies the verification method, if a
|
|
3286
|
+
* reference entry names no member of `verificationMethod`, or if the beacon service is not
|
|
3287
|
+
* found. `INVALID_DID_DOCUMENT` if the method is not of type `Multikey` or does not have a
|
|
3288
|
+
* `zQ3s` publicKeyMultibase prefix.
|
|
3289
|
+
*/
|
|
3290
|
+
static update({
|
|
3291
|
+
sourceDocument,
|
|
3292
|
+
patches,
|
|
3293
|
+
sourceVersionId,
|
|
3294
|
+
verificationMethodId,
|
|
3295
|
+
beaconId
|
|
3296
|
+
}) {
|
|
3297
|
+
if (!Number.isInteger(sourceVersionId) || sourceVersionId < 1) {
|
|
3298
|
+
throw new import_common16.UpdateError(
|
|
3299
|
+
`Invalid sourceVersionId: expected an integer of at least 1, got ${String(sourceVersionId)}.`,
|
|
3300
|
+
import_common16.INVALID_DID_UPDATE,
|
|
3301
|
+
{ sourceVersionId }
|
|
3302
|
+
);
|
|
3303
|
+
}
|
|
3304
|
+
const entry = Appendix.capabilityInvocationEntry(sourceDocument, verificationMethodId);
|
|
3305
|
+
if (entry === void 0) {
|
|
3306
|
+
throw new import_common16.UpdateError(
|
|
3307
|
+
"Invalid verificationMethodId: not authorized for capabilityInvocation",
|
|
3308
|
+
import_common16.INVALID_DID_UPDATE,
|
|
3309
|
+
{ verificationMethodId, capabilityInvocation: sourceDocument.capabilityInvocation }
|
|
3310
|
+
);
|
|
3311
|
+
}
|
|
3312
|
+
const verificationMethod = Appendix.verificationMethodOfEntry(sourceDocument, entry);
|
|
3313
|
+
if (!verificationMethod) {
|
|
3314
|
+
throw new import_common16.UpdateError(
|
|
3315
|
+
"Invalid verificationMethodId: not found in source document",
|
|
3316
|
+
import_common16.INVALID_DID_UPDATE,
|
|
3317
|
+
{ verificationMethodId }
|
|
3318
|
+
);
|
|
3319
|
+
}
|
|
3320
|
+
if (verificationMethod.type !== MULTIKEY_VERIFICATION_METHOD_TYPE) {
|
|
3321
|
+
throw new import_common16.UpdateError(
|
|
3322
|
+
`Invalid verificationMethod: verificationMethod.type must be "${MULTIKEY_VERIFICATION_METHOD_TYPE}"`,
|
|
3323
|
+
import_common16.INVALID_DID_DOCUMENT,
|
|
3324
|
+
verificationMethod
|
|
3325
|
+
);
|
|
3326
|
+
}
|
|
3327
|
+
if (!verificationMethod.publicKeyMultibase?.startsWith(MULTIKEY_PUBLIC_KEY_MULTIBASE_PREFIX)) {
|
|
3328
|
+
throw new import_common16.UpdateError(
|
|
3329
|
+
`Invalid verificationMethodId: publicKeyMultibase prefix must start with "${MULTIKEY_PUBLIC_KEY_MULTIBASE_PREFIX}"`,
|
|
3330
|
+
import_common16.INVALID_DID_DOCUMENT,
|
|
3331
|
+
verificationMethod
|
|
3332
|
+
);
|
|
3333
|
+
}
|
|
3334
|
+
const targetBeaconId = Appendix.absoluteDidUrl(beaconId, sourceDocument.id);
|
|
3335
|
+
const beaconService = sourceDocument.service.filter((service) => targetBeaconId !== void 0 && Appendix.absoluteDidUrl(service.id, sourceDocument.id) === targetBeaconId).filter((service) => !!service).shift();
|
|
3336
|
+
if (!beaconService) {
|
|
3337
|
+
throw new import_common16.UpdateError(
|
|
3338
|
+
"No beacon service found for provided beaconId",
|
|
3339
|
+
import_common16.INVALID_DID_UPDATE,
|
|
3340
|
+
{ sourceDocument, beaconId }
|
|
3341
|
+
);
|
|
3342
|
+
}
|
|
3343
|
+
return new Updater({
|
|
3344
|
+
sourceDocument,
|
|
3345
|
+
patches,
|
|
3346
|
+
sourceVersionId,
|
|
3347
|
+
verificationMethod,
|
|
3348
|
+
beaconService
|
|
3349
|
+
});
|
|
3350
|
+
}
|
|
3351
|
+
/**
|
|
3352
|
+
* Entry point for section {@link https://dcdpr.github.io/did-btcr2/operations/deactivate.html | 7.4 Deactivate}.
|
|
3353
|
+
*
|
|
3354
|
+
* Deactivate is the Update operation with the predetermined patch {@link DEACTIVATION_PATCH}:
|
|
3355
|
+
* it adds the `deactivated` property with the value `true`. The factory returns the
|
|
3356
|
+
* {@link Updater} that {@link DidBtcr2.update} returns for that patch, and the caller drives
|
|
3357
|
+
* it in the same way. Resolution stops at the deactivation for good. The factory does not
|
|
3358
|
+
* refuse a source document that is deactivated already; the api does (ADR 100).
|
|
3359
|
+
*
|
|
3360
|
+
* @param params Deactivation parameters: the parameters of {@link DidBtcr2.update} without `patches`.
|
|
3361
|
+
* @returns {Updater} A sans-I/O state machine for driving the deactivation.
|
|
3362
|
+
* @throws {UpdateError} As {@link DidBtcr2.update}.
|
|
3363
|
+
*/
|
|
3364
|
+
static deactivate({
|
|
3365
|
+
sourceDocument,
|
|
3366
|
+
sourceVersionId,
|
|
3367
|
+
verificationMethodId,
|
|
3368
|
+
beaconId
|
|
3369
|
+
}) {
|
|
3370
|
+
return this.update({
|
|
3371
|
+
sourceDocument,
|
|
3372
|
+
patches: [{ ...DEACTIVATION_PATCH }],
|
|
3373
|
+
sourceVersionId,
|
|
3374
|
+
verificationMethodId,
|
|
3375
|
+
beaconId
|
|
3376
|
+
});
|
|
3377
|
+
}
|
|
3378
|
+
/**
|
|
3379
|
+
* Given the W3C DID Document of a `did:btcr2` identifier, return the signing verification method that will be used
|
|
3380
|
+
* for signing messages and credentials. If given, the `methodId` parameter is used to select the
|
|
3381
|
+
* verification method. If not given, the Identity Key's verification method with an ID fragment
|
|
3382
|
+
* of '#initialKey' is used.
|
|
3383
|
+
* @param {Btcr2DidDocument} didDocument The DID Document of the `did:btcr2` identifier.
|
|
3384
|
+
* @param {string} [methodId] Optional verification method ID to be used for signing.
|
|
3385
|
+
* @returns {DidVerificationMethod} Promise resolving to the {@link DidVerificationMethod} object used for signing.
|
|
3386
|
+
* @throws {DidError} if the parsed did method does not match `btcr2` or signing method could not be determined.
|
|
3387
|
+
*/
|
|
3388
|
+
static getSigningMethod(didDocument, methodId) {
|
|
3389
|
+
methodId ??= "#initialKey";
|
|
3390
|
+
const parsedDid = import_dids2.Did.parse(didDocument.id);
|
|
3391
|
+
if (parsedDid && parsedDid.method !== this.methodName) {
|
|
3392
|
+
throw new import_common16.MethodError(`Method not supported: ${parsedDid.method}`, import_common16.METHOD_NOT_SUPPORTED, { identifier: didDocument.id });
|
|
3393
|
+
}
|
|
3394
|
+
const targetId = Appendix.absoluteDidUrl(methodId, didDocument.id) ?? Appendix.relationshipMethodId(didDocument.assertionMethod?.[0], didDocument.id);
|
|
3395
|
+
const verificationMethod = targetId === void 0 ? void 0 : Appendix.getVerificationMethods(didDocument).find(
|
|
3396
|
+
(vm) => Appendix.absoluteDidUrl(vm.id, didDocument.id) === targetId
|
|
3397
|
+
);
|
|
3398
|
+
if (!(verificationMethod && verificationMethod.publicKeyMultibase)) {
|
|
3399
|
+
throw new import_dids2.DidError(
|
|
3400
|
+
import_dids2.DidErrorCode.InternalError,
|
|
3401
|
+
"A verification method intended for signing could not be determined from the DID Document"
|
|
3402
|
+
);
|
|
3403
|
+
}
|
|
3404
|
+
return verificationMethod;
|
|
3405
|
+
}
|
|
3406
|
+
};
|
|
3125
3407
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3126
3408
|
0 && (module.exports = {
|
|
3127
3409
|
AggregateBeaconError,
|
|
@@ -3136,6 +3418,7 @@ var DidDocumentBuilder = class {
|
|
|
3136
3418
|
CASBeacon,
|
|
3137
3419
|
CASBeaconError,
|
|
3138
3420
|
CHANGE_OUTPUT_VBYTES,
|
|
3421
|
+
DEACTIVATION_PATCH,
|
|
3139
3422
|
DEFAULT_FEE_ESTIMATOR,
|
|
3140
3423
|
DEFAULT_MIN_CONF,
|
|
3141
3424
|
DID_REGEX,
|