@mikeargento/bitgraph-audit 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/anchors.d.ts +0 -24
- package/dist/anchors.d.ts.map +1 -1
- package/dist/anchors.js +53 -11
- package/dist/anchors.js.map +1 -1
- package/dist/audit.d.ts +1 -1
- package/dist/audit.js +1 -1
- package/package.json +2 -2
- package/src/__tests__/anchors.test.ts +139 -0
- package/src/anchors.ts +30 -11
- package/src/audit.ts +1 -1
package/dist/anchors.d.ts
CHANGED
|
@@ -1,27 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* bitgraph-audit anchor identification (G5)
|
|
3
|
-
*
|
|
4
|
-
* Ethereum anchors are ordinary chain members minted by the anchor
|
|
5
|
-
* service: same counter sequence, same prevB64 links, same signing key as
|
|
6
|
-
* every other proof on the chain. What makes a proof an anchor is the
|
|
7
|
-
* SIGNED attribution:
|
|
8
|
-
*
|
|
9
|
-
* - attribution.name === "Ethereum Anchor" is the discriminator.
|
|
10
|
-
* - attribution.message carries the Ethereum block hash string.
|
|
11
|
-
* - attribution.title carries the block number, only inside the signed
|
|
12
|
-
* Etherscan URL.
|
|
13
|
-
* - artifact.digestB64 is SHA-256 over the block-hash STRING.
|
|
14
|
-
*
|
|
15
|
-
* The unsigned metadata.type === "ethereum-anchor" is corroboration only:
|
|
16
|
-
* agreement and disagreement are recorded, and metadata alone never makes
|
|
17
|
-
* a proof an anchor. The unsigned metadata.anchor block timestamp is
|
|
18
|
-
* never read; wall-clock evidence comes exclusively from verified anchor
|
|
19
|
-
* witnesses (witness.ts). No wall-clock time is ever derived from a block
|
|
20
|
-
* number.
|
|
21
|
-
*
|
|
22
|
-
* Run after verifyObservedProofs so the records carry the run
|
|
23
|
-
* verification status; identification itself reads only the signed body.
|
|
24
|
-
*/
|
|
25
1
|
import type { AnchorIdentification, IngestResult } from "./types.js";
|
|
26
2
|
/**
|
|
27
3
|
* Identify anchor proofs among the observed proofs. Read-only and
|
package/dist/anchors.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"anchors.d.ts","sourceRoot":"","sources":["../src/anchors.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"anchors.d.ts","sourceRoot":"","sources":["../src/anchors.ts"],"names":[],"mappings":"AA4BA,OAAO,KAAK,EACV,oBAAoB,EAGpB,YAAY,EAEb,MAAM,YAAY,CAAC;AAYpB;;;GAGG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,YAAY,GAAG,oBAAoB,CAyG1E"}
|
package/dist/anchors.js
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
// Copyright (c) 2024-2026 Mike Argento. Licensed under the MIT License. See LICENSE.
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* bitgraph-audit anchor identification (G5)
|
|
4
|
+
*
|
|
5
|
+
* Ethereum anchors are ordinary chain members minted by the anchor
|
|
6
|
+
* service: same counter sequence, same prevB64 links, same signing key as
|
|
7
|
+
* every other proof on the chain. What makes a proof an anchor is the
|
|
8
|
+
* SIGNED attribution:
|
|
9
|
+
*
|
|
10
|
+
* - attribution.name === "Ethereum Anchor" is the discriminator.
|
|
11
|
+
* - attribution.message carries the Ethereum block hash string.
|
|
12
|
+
* - attribution.title carries the block number, only inside the signed
|
|
13
|
+
* Etherscan URL.
|
|
14
|
+
* - artifact.digestB64 is SHA-256 over the block-hash STRING.
|
|
15
|
+
*
|
|
16
|
+
* The unsigned metadata.type === "ethereum-anchor" is corroboration only:
|
|
17
|
+
* agreement and disagreement are recorded, and metadata alone never makes
|
|
18
|
+
* a proof an anchor. The unsigned metadata.anchor block timestamp is
|
|
19
|
+
* never read; wall-clock evidence comes exclusively from verified anchor
|
|
20
|
+
* witnesses (witness.ts). No wall-clock time is ever derived from a block
|
|
21
|
+
* number.
|
|
22
|
+
*
|
|
23
|
+
* Run after verifyObservedProofs so the records carry the run
|
|
24
|
+
* verification status; identification itself reads only the signed body.
|
|
25
|
+
*/
|
|
26
|
+
import { anchorKindOf, anchorMarkOf } from "@mikeargento/bitgraph-verify";
|
|
3
27
|
const ANCHOR_METADATA_TYPE = "ethereum-anchor";
|
|
4
28
|
/**
|
|
5
29
|
* Strict Etherscan block URL, the exact form the anchor service signs
|
|
@@ -17,19 +41,24 @@ export function identifyAnchors(ingest) {
|
|
|
17
41
|
const metadataOnlyProofHashes = [];
|
|
18
42
|
const findings = [];
|
|
19
43
|
for (const proof of ingest.proofs) {
|
|
20
|
-
const attributionName = readAttributionField(proof, "name");
|
|
21
44
|
const metadataType = readMetadataType(proof);
|
|
22
|
-
|
|
23
|
-
|
|
45
|
+
// ⚠️ Two forms, and they are not equally strong. "authenticated" means the
|
|
46
|
+
// enclave wrote commit.anchor after verifying the anchor service's
|
|
47
|
+
// signature over the claim; "legacy-attribution" means only the signed
|
|
48
|
+
// name says so, which is every anchor written before enclave v7
|
|
49
|
+
// (2026-09-06) and most of the ledger. Both are anchors; only one is
|
|
50
|
+
// backed by something inside the proof.
|
|
51
|
+
const kind = anchorKindOf(proof.proof);
|
|
52
|
+
if (kind === null) {
|
|
24
53
|
if (metadataType === ANCHOR_METADATA_TYPE) {
|
|
25
54
|
// Unsigned claim with no signed backing: recorded, never trusted.
|
|
26
55
|
metadataOnlyProofHashes.push(proof.proofHash);
|
|
27
56
|
findings.push({
|
|
28
57
|
code: "anchor-metadata-only-claim",
|
|
29
58
|
...pathOf(proof),
|
|
30
|
-
message: "unsigned metadata.type claims ethereum-anchor, but the
|
|
31
|
-
"
|
|
32
|
-
"not treated as an anchor.",
|
|
59
|
+
message: "unsigned metadata.type claims ethereum-anchor, but the proof carries neither a signed " +
|
|
60
|
+
"commit.anchor nor the signed attribution name. Metadata is advisory and unsigned; the " +
|
|
61
|
+
"proof is not treated as an anchor.",
|
|
33
62
|
details: { proofHash: proof.proofHash },
|
|
34
63
|
});
|
|
35
64
|
}
|
|
@@ -47,15 +76,28 @@ export function identifyAnchors(ingest) {
|
|
|
47
76
|
findings.push({
|
|
48
77
|
code: "anchor-metadata-disagreement",
|
|
49
78
|
...pathOf(proof),
|
|
50
|
-
message: `
|
|
79
|
+
message: `this proof is a signed Ethereum anchor, but unsigned metadata.type is ` +
|
|
51
80
|
`${JSON.stringify(metadataType)}. The signed field governs; the disagreement is recorded.`,
|
|
52
81
|
details: { proofHash: proof.proofHash, metadataType: metadataType },
|
|
53
82
|
});
|
|
54
83
|
}
|
|
55
|
-
|
|
84
|
+
// An authenticated anchor carries its block in commit.anchor, which the
|
|
85
|
+
// enclave signed. Reading it from there rather than from attribution is
|
|
86
|
+
// what lets an anchor's attribution be used for something else — a
|
|
87
|
+
// bitgraph-fuse/1 marker, for instance — without the block becoming
|
|
88
|
+
// unreadable. Legacy anchors keep the old path exactly.
|
|
89
|
+
const authenticated = anchorMarkOf(proof.proof);
|
|
90
|
+
const blockHash = authenticated?.blockHash ?? readAttributionField(proof, "message");
|
|
56
91
|
const title = readAttributionField(proof, "title");
|
|
57
|
-
const blockNumber =
|
|
58
|
-
|
|
92
|
+
const blockNumber = authenticated !== null
|
|
93
|
+
? String(authenticated.blockNumber)
|
|
94
|
+
: title !== undefined
|
|
95
|
+
? (ETHERSCAN_BLOCK_URL.exec(title)?.[1] ?? undefined)
|
|
96
|
+
: undefined;
|
|
97
|
+
// Only a legacy anchor depends on the title for its block number; an
|
|
98
|
+
// authenticated one has already answered, so an absent title is not a
|
|
99
|
+
// finding against it.
|
|
100
|
+
if (blockNumber === undefined && kind === "legacy-attribution") {
|
|
59
101
|
findings.push({
|
|
60
102
|
code: "anchor-title-unparseable",
|
|
61
103
|
...pathOf(proof),
|
package/dist/anchors.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"anchors.js","sourceRoot":"","sources":["../src/anchors.ts"],"names":[],"mappings":"AAAA,qFAAqF;
|
|
1
|
+
{"version":3,"file":"anchors.js","sourceRoot":"","sources":["../src/anchors.ts"],"names":[],"mappings":"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"}
|
package/dist/audit.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type { AuditOptions, AuditResult, ExitFlags, IngestResult } from "./types
|
|
|
8
8
|
* this equals package.json's version, so the constant cannot drift silently
|
|
9
9
|
* across releases.
|
|
10
10
|
*/
|
|
11
|
-
export declare const AUDIT_VERSION = "0.
|
|
11
|
+
export declare const AUDIT_VERSION = "0.5.0";
|
|
12
12
|
/** The audit package's own version. */
|
|
13
13
|
export declare function auditToolVersion(): string;
|
|
14
14
|
/** Options for auditIngest(): the audit options plus the run stamp. */
|
package/dist/audit.js
CHANGED
|
@@ -33,7 +33,7 @@ import { validateAttestations } from "./attestation.js";
|
|
|
33
33
|
* this equals package.json's version, so the constant cannot drift silently
|
|
34
34
|
* across releases.
|
|
35
35
|
*/
|
|
36
|
-
export const AUDIT_VERSION = "0.
|
|
36
|
+
export const AUDIT_VERSION = "0.5.0";
|
|
37
37
|
/** The audit package's own version. */
|
|
38
38
|
export function auditToolVersion() {
|
|
39
39
|
return AUDIT_VERSION;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikeargento/bitgraph-audit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Offline audit of BitGraph proof bundles: ingest, tiered verification, causal reconstruction, divergence reporting.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"node": ">=20"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@mikeargento/bitgraph-verify": "^1.
|
|
34
|
+
"@mikeargento/bitgraph-verify": "^1.11.0",
|
|
35
35
|
"@noble/hashes": "^1.4.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// Copyright (c) 2024-2026 Mike Argento.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What counts as an anchor, and where its block is read from.
|
|
5
|
+
*
|
|
6
|
+
* This package's job is rigour about anchors, and until now it had no test at
|
|
7
|
+
* all for the discriminator it documents. The discriminator also just changed:
|
|
8
|
+
* `attribution.name === "Ethereum Anchor"` was the only test, and it is now the
|
|
9
|
+
* fallback behind the signed `commit.anchor` that enclave v7 introduced.
|
|
10
|
+
*
|
|
11
|
+
* The case that matters is the last one. An anchor fused like every other
|
|
12
|
+
* artifact needs its attribution for the `bitgraph-fuse/1` marker, so the name
|
|
13
|
+
* is gone and the block has to come from `commit.anchor`. If that stops
|
|
14
|
+
* working, fused anchors become invisible to an audit.
|
|
15
|
+
*
|
|
16
|
+
* Run: node --test packages/audit/src/__tests__/anchors.test.ts
|
|
17
|
+
*/
|
|
18
|
+
import { test } from "node:test";
|
|
19
|
+
import assert from "node:assert/strict";
|
|
20
|
+
import { identifyAnchors } from "../anchors.ts";
|
|
21
|
+
import type { IngestResult, ObservedProof } from "../types.ts";
|
|
22
|
+
|
|
23
|
+
const BLOCK = 25928913;
|
|
24
|
+
const HASH = "0x0ce4f044093cdd272ce4d7f47903a2f77e9195c4a5267a3eb7200e3a7d9ba7fa";
|
|
25
|
+
|
|
26
|
+
function observed(proof: Record<string, unknown>, proofHash: string): ObservedProof {
|
|
27
|
+
return {
|
|
28
|
+
proofHash,
|
|
29
|
+
chainHash: `chain-${proofHash}`,
|
|
30
|
+
proof: proof as never,
|
|
31
|
+
sources: [],
|
|
32
|
+
version: "bitgraph/1",
|
|
33
|
+
chainId: "bitgraph:main",
|
|
34
|
+
} as unknown as ObservedProof;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function ingest(proofs: ObservedProof[]): IngestResult {
|
|
38
|
+
return { bundlePath: "test", container: "directory", proofs } as unknown as IngestResult;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Pre-v7: the signed name is the only thing saying anchor. Most of the ledger. */
|
|
42
|
+
const legacy = observed({
|
|
43
|
+
commit: { counter: "10956" },
|
|
44
|
+
attribution: {
|
|
45
|
+
name: "Ethereum Anchor",
|
|
46
|
+
title: `https://etherscan.io/block/${BLOCK}`,
|
|
47
|
+
message: HASH,
|
|
48
|
+
},
|
|
49
|
+
metadata: { type: "ethereum-anchor" },
|
|
50
|
+
}, "legacy");
|
|
51
|
+
|
|
52
|
+
/** v7 onward: both the signed mark and the name. */
|
|
53
|
+
const authenticated = observed({
|
|
54
|
+
commit: { counter: "10957", anchor: { blockNumber: BLOCK, blockHash: HASH } },
|
|
55
|
+
attribution: {
|
|
56
|
+
name: "Ethereum Anchor",
|
|
57
|
+
title: `https://etherscan.io/block/${BLOCK}`,
|
|
58
|
+
message: HASH,
|
|
59
|
+
},
|
|
60
|
+
metadata: { type: "ethereum-anchor" },
|
|
61
|
+
}, "authenticated");
|
|
62
|
+
|
|
63
|
+
/** Fused: attribution spent on the fuse marker, block only in commit.anchor. */
|
|
64
|
+
const fused = observed({
|
|
65
|
+
commit: { counter: "10958", anchor: { blockNumber: BLOCK, blockHash: HASH } },
|
|
66
|
+
attribution: { name: "bitgraph-fuse/1", title: "container/2" },
|
|
67
|
+
metadata: { type: "ethereum-anchor" },
|
|
68
|
+
}, "fused");
|
|
69
|
+
|
|
70
|
+
/** An unsigned claim with nothing behind it. Never an anchor. */
|
|
71
|
+
const metadataOnly = observed({
|
|
72
|
+
commit: { counter: "10959" },
|
|
73
|
+
attribution: { name: "Someone" },
|
|
74
|
+
metadata: { type: "ethereum-anchor" },
|
|
75
|
+
}, "metadata-only");
|
|
76
|
+
|
|
77
|
+
const notAnAnchor = observed({
|
|
78
|
+
commit: { counter: "10960" },
|
|
79
|
+
attribution: { name: "Someone" },
|
|
80
|
+
}, "ordinary");
|
|
81
|
+
|
|
82
|
+
test("a legacy anchor is still an anchor, and its block still comes from attribution", () => {
|
|
83
|
+
const { anchors, findings } = identifyAnchors(ingest([legacy]));
|
|
84
|
+
assert.equal(anchors.length, 1, "the attribution name must keep identifying pre-v7 anchors");
|
|
85
|
+
assert.equal(anchors[0].blockHash, HASH);
|
|
86
|
+
assert.equal(anchors[0].blockNumber, String(BLOCK));
|
|
87
|
+
assert.equal(findings.length, 0);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("an authenticated anchor reads its block from the signed commit.anchor", () => {
|
|
91
|
+
const { anchors, findings } = identifyAnchors(ingest([authenticated]));
|
|
92
|
+
assert.equal(anchors.length, 1);
|
|
93
|
+
assert.equal(anchors[0].blockHash, HASH);
|
|
94
|
+
assert.equal(anchors[0].blockNumber, String(BLOCK));
|
|
95
|
+
assert.equal(findings.length, 0);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("a fused anchor is identified with no attribution name at all", () => {
|
|
99
|
+
// The whole point of the migration: attribution is spent on the fuse marker.
|
|
100
|
+
const { anchors, findings } = identifyAnchors(ingest([fused]));
|
|
101
|
+
assert.equal(anchors.length, 1, "commit.anchor must identify an anchor on its own");
|
|
102
|
+
assert.equal(anchors[0].blockHash, HASH, "the block must be readable without attribution");
|
|
103
|
+
assert.equal(anchors[0].blockNumber, String(BLOCK));
|
|
104
|
+
assert.equal(
|
|
105
|
+
findings.filter((f) => f.code === "anchor-title-unparseable").length,
|
|
106
|
+
0,
|
|
107
|
+
"an authenticated anchor does not need a parseable title, so absence is not a finding",
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("an unsigned metadata claim is recorded and refused", () => {
|
|
112
|
+
const { anchors, metadataOnlyProofHashes, findings } = identifyAnchors(ingest([metadataOnly]));
|
|
113
|
+
assert.equal(anchors.length, 0);
|
|
114
|
+
assert.deepEqual(metadataOnlyProofHashes, ["metadata-only"]);
|
|
115
|
+
assert.equal(findings.filter((f) => f.code === "anchor-metadata-only-claim").length, 1);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("an ordinary proof is not an anchor and raises nothing", () => {
|
|
119
|
+
const { anchors, metadataOnlyProofHashes, findings } = identifyAnchors(ingest([notAnAnchor]));
|
|
120
|
+
assert.equal(anchors.length, 0);
|
|
121
|
+
assert.equal(metadataOnlyProofHashes.length, 0);
|
|
122
|
+
assert.equal(findings.length, 0);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("a legacy anchor without a parseable title still reports that", () => {
|
|
126
|
+
const noTitle = observed({
|
|
127
|
+
commit: { counter: "1" },
|
|
128
|
+
attribution: { name: "Ethereum Anchor", message: HASH },
|
|
129
|
+
}, "legacy-no-title");
|
|
130
|
+
const { anchors, findings } = identifyAnchors(ingest([noTitle]));
|
|
131
|
+
assert.equal(anchors.length, 1);
|
|
132
|
+
assert.equal(anchors[0].blockNumber, undefined);
|
|
133
|
+
assert.equal(findings.filter((f) => f.code === "anchor-title-unparseable").length, 1);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("all forms are found together, in observation order", () => {
|
|
137
|
+
const { anchors } = identifyAnchors(ingest([legacy, notAnAnchor, authenticated, metadataOnly, fused]));
|
|
138
|
+
assert.deepEqual(anchors.map((a) => a.proofHash), ["legacy", "authenticated", "fused"]);
|
|
139
|
+
});
|
package/src/anchors.ts
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
* verification status; identification itself reads only the signed body.
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
+
import { anchorKindOf, anchorMarkOf } from "@mikeargento/bitgraph-verify";
|
|
28
29
|
import type {
|
|
29
30
|
AnchorIdentification,
|
|
30
31
|
AnchorRecord,
|
|
@@ -33,7 +34,6 @@ import type {
|
|
|
33
34
|
ObservedProof,
|
|
34
35
|
} from "./types.js";
|
|
35
36
|
|
|
36
|
-
const ANCHOR_ATTRIBUTION_NAME = "Ethereum Anchor";
|
|
37
37
|
const ANCHOR_METADATA_TYPE = "ethereum-anchor";
|
|
38
38
|
|
|
39
39
|
/**
|
|
@@ -54,11 +54,16 @@ export function identifyAnchors(ingest: IngestResult): AnchorIdentification {
|
|
|
54
54
|
const findings: AuditFinding[] = [];
|
|
55
55
|
|
|
56
56
|
for (const proof of ingest.proofs) {
|
|
57
|
-
const attributionName = readAttributionField(proof, "name");
|
|
58
57
|
const metadataType = readMetadataType(proof);
|
|
59
|
-
|
|
58
|
+
// ⚠️ Two forms, and they are not equally strong. "authenticated" means the
|
|
59
|
+
// enclave wrote commit.anchor after verifying the anchor service's
|
|
60
|
+
// signature over the claim; "legacy-attribution" means only the signed
|
|
61
|
+
// name says so, which is every anchor written before enclave v7
|
|
62
|
+
// (2026-09-06) and most of the ledger. Both are anchors; only one is
|
|
63
|
+
// backed by something inside the proof.
|
|
64
|
+
const kind = anchorKindOf(proof.proof);
|
|
60
65
|
|
|
61
|
-
if (
|
|
66
|
+
if (kind === null) {
|
|
62
67
|
if (metadataType === ANCHOR_METADATA_TYPE) {
|
|
63
68
|
// Unsigned claim with no signed backing: recorded, never trusted.
|
|
64
69
|
metadataOnlyProofHashes.push(proof.proofHash);
|
|
@@ -66,9 +71,9 @@ export function identifyAnchors(ingest: IngestResult): AnchorIdentification {
|
|
|
66
71
|
code: "anchor-metadata-only-claim",
|
|
67
72
|
...pathOf(proof),
|
|
68
73
|
message:
|
|
69
|
-
"unsigned metadata.type claims ethereum-anchor, but the
|
|
70
|
-
"
|
|
71
|
-
"not treated as an anchor.",
|
|
74
|
+
"unsigned metadata.type claims ethereum-anchor, but the proof carries neither a signed " +
|
|
75
|
+
"commit.anchor nor the signed attribution name. Metadata is advisory and unsigned; the " +
|
|
76
|
+
"proof is not treated as an anchor.",
|
|
72
77
|
details: { proofHash: proof.proofHash },
|
|
73
78
|
});
|
|
74
79
|
}
|
|
@@ -89,16 +94,30 @@ export function identifyAnchors(ingest: IngestResult): AnchorIdentification {
|
|
|
89
94
|
code: "anchor-metadata-disagreement",
|
|
90
95
|
...pathOf(proof),
|
|
91
96
|
message:
|
|
92
|
-
`
|
|
97
|
+
`this proof is a signed Ethereum anchor, but unsigned metadata.type is ` +
|
|
93
98
|
`${JSON.stringify(metadataType)}. The signed field governs; the disagreement is recorded.`,
|
|
94
99
|
details: { proofHash: proof.proofHash, metadataType: metadataType as string },
|
|
95
100
|
});
|
|
96
101
|
}
|
|
97
102
|
|
|
98
|
-
|
|
103
|
+
// An authenticated anchor carries its block in commit.anchor, which the
|
|
104
|
+
// enclave signed. Reading it from there rather than from attribution is
|
|
105
|
+
// what lets an anchor's attribution be used for something else — a
|
|
106
|
+
// bitgraph-fuse/1 marker, for instance — without the block becoming
|
|
107
|
+
// unreadable. Legacy anchors keep the old path exactly.
|
|
108
|
+
const authenticated = anchorMarkOf(proof.proof);
|
|
109
|
+
const blockHash = authenticated?.blockHash ?? readAttributionField(proof, "message");
|
|
99
110
|
const title = readAttributionField(proof, "title");
|
|
100
|
-
const blockNumber =
|
|
101
|
-
|
|
111
|
+
const blockNumber =
|
|
112
|
+
authenticated !== null
|
|
113
|
+
? String(authenticated.blockNumber)
|
|
114
|
+
: title !== undefined
|
|
115
|
+
? (ETHERSCAN_BLOCK_URL.exec(title)?.[1] ?? undefined)
|
|
116
|
+
: undefined;
|
|
117
|
+
// Only a legacy anchor depends on the title for its block number; an
|
|
118
|
+
// authenticated one has already answered, so an absent title is not a
|
|
119
|
+
// finding against it.
|
|
120
|
+
if (blockNumber === undefined && kind === "legacy-attribution") {
|
|
102
121
|
findings.push({
|
|
103
122
|
code: "anchor-title-unparseable",
|
|
104
123
|
...pathOf(proof),
|
package/src/audit.ts
CHANGED
|
@@ -37,7 +37,7 @@ import type { AuditOptions, AuditResult, ExitFlags, IngestResult } from "./types
|
|
|
37
37
|
* this equals package.json's version, so the constant cannot drift silently
|
|
38
38
|
* across releases.
|
|
39
39
|
*/
|
|
40
|
-
export const AUDIT_VERSION = "0.
|
|
40
|
+
export const AUDIT_VERSION = "0.5.0";
|
|
41
41
|
|
|
42
42
|
/** The audit package's own version. */
|
|
43
43
|
export function auditToolVersion(): string {
|