Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# npm 12+ defaults allow-git=none.
# @fabric/core itself has no Fabric git dependencies, but consumers that install
# core/http/hub from GitHub need `allow-git=all` so nested git dep preparation
# (commit-SHA fetches) is not refused. Keep this file so the monorepo default
# matches Hub / http / app packages.
allow-git=all

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium — Supply-chain: re-enables npm 12 git-dependency RCE path

allow-git=all opts this repo (and GitHub installs of it) out of npm 12’s default allow-git=none, which was added specifically to block install-time code execution via a git dependency’s .npmrc overriding the git binary (works even with --ignore-scripts).

Attack path: A compromised or malicious transitive dependency introduces a git+/github: URL → npm fetches it under all → that git checkout’s .npmrc can redirect git → arbitrary code at install time. @fabric/core currently declares no git deps, so all is broader than needed; root (or keeping the default and scoping the opt-in to Hub/http only) would preserve the boundary.

Same relaxation is also forced in package.json report:install (npm i --allow-git=all after wiping the lockfile), which further widens install-time trust during that script.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium — Supply-chain: re-enables npm 12 git-dependency RCE path

allow-git=all still opts this repo out of npm 12’s default allow-git=none, which blocks install-time code execution via a git dependency’s .npmrc overriding the git binary (even under --ignore-scripts). Confirmed still present at HEAD 91f034254; CHANGELOG notes root was rejected for nested SHA fetches.

Attack path: Compromised or malicious transitive git+/github: dep → fetched under all → that checkout’s .npmrc redirects git → arbitrary code at install time. This package still declares no git deps, so all is broader than needed.

Same relaxation remains in package.json report:install (npm i --allow-git=all after wiping the lockfile), which further widens install-time trust on a lockfile-free resolve.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium — Supply-chain: re-enables npm 12 git-dependency RCE path

allow-git=all opts this repo (and GitHub installs of it) out of npm 12’s default allow-git=none, which blocks install-time code execution via a git dependency’s .npmrc overriding the git binary (works even with --ignore-scripts). Confirmed still present at HEAD 3a4d87ab.

Attack path: Compromised or malicious transitive git+/github: dependency → fetched under all → that checkout’s .npmrc redirects git → arbitrary code at install time. @fabric/core still declares no git deps, so all is broader than needed (.npmrc is not in the published files list, but applies to clones and github: installs).

Same relaxation remains in package.json report:install (npm i --allow-git=all after wiping the lockfile), which further widens install-time trust on a lockfile-free resolve.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium — Supply-chain: re-enables npm 12 git-dependency RCE path

allow-git=all opts this repo (and GitHub installs of it) out of npm 12’s default allow-git=none, which blocks install-time code execution via a git dependency’s .npmrc overriding the git binary (works even with --ignore-scripts). Confirmed still present at HEAD b984c1f60.

Attack path: Compromised or malicious transitive git+/github: dependency → fetched under all → that checkout’s .npmrc redirects git → arbitrary code at install time. @fabric/core still declares no git deps, so all is broader than needed (.npmrc is not in the published files list, but applies to clones and github: installs).

Same relaxation remains in package.json report:install (npm i --allow-git=all after wiping the lockfile), which further widens install-time trust on a lockfile-free resolve.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium — Supply-chain: re-enables npm 12 git-dependency RCE path

allow-git=all opts this repo (and GitHub installs of it) out of npm 12’s default allow-git=none, which blocks install-time code execution via a git dependency’s .npmrc overriding the git binary (works even with --ignore-scripts). Confirmed still present at HEAD 3fc4b6602.

Attack path: Compromised or malicious transitive git+/github: dependency → fetched under all → that checkout’s .npmrc redirects git → arbitrary code at install time. @fabric/core still declares no git deps, so all is broader than needed (.npmrc is not in the published files list, but applies to clones and github: installs).

Same relaxation remains in package.json report:install (npm i --allow-git=all after wiping the lockfile), which further widens install-time trust on a lockfile-free resolve.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium — Supply-chain: re-enables npm 12 git-dependency RCE path

allow-git=all opts this repo (and GitHub installs of it) out of npm 12’s default allow-git=none, which blocks install-time code execution via a git dependency’s .npmrc overriding the git binary (works even with --ignore-scripts). Confirmed still present at HEAD a97e7523a.

Attack path: Compromised or malicious transitive git+/github: dependency → fetched under all → that checkout’s .npmrc redirects git → arbitrary code at install time. @fabric/core still declares no git deps, so all is broader than needed (.npmrc is not in the published files list, but applies to clones and github: installs).

Same relaxation remains in package.json report:install (npm i --allow-git=all after wiping the lockfile), which further widens install-time trust on a lockfile-free resolve.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium — Supply-chain: re-enables npm 12 git-dependency RCE path

allow-git=all opts this repo (and github: / clone installs) out of npm 12’s default allow-git=none, which blocks install-time code execution via a git dependency’s .npmrc overriding the git binary (works even with --ignore-scripts). Still present at HEAD c39fcdbdf; CHANGELOG notes root was rejected for nested SHA fetches.

Attack path: Compromised or malicious transitive git+/github: dependency → fetched under all → that checkout’s .npmrc redirects git → arbitrary code at install time. @fabric/core still declares no git deps, so all is broader than needed (.npmrc is not in the published files list, but applies to clones and GitHub installs).

Same relaxation remains in package.json report:install (npm i --allow-git=all after wiping the lockfile), which further widens install-time trust on a lockfile-free resolve.

256 changes: 210 additions & 46 deletions API.md

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# `@fabric/core` Changelog
Recent changes to Fabric Core.

## 2026-08-06
- **npm git deps:** add **`.npmrc`** with **`allow-git=all`** (and `report:install`) so the monorepo matches Hub/http/app consumers — npm 12+ nested git-dep preparation of commit SHAs fails under `allow-git=root`.

## 2026-07-29
JS-canonical protocol for 0.1.0; Lightning-style wire preimage; unsigned document binding; public-readiness cuts; Peer scoring; directed onion forward.

Expand Down
3 changes: 2 additions & 1 deletion DEVELOPERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ Read **[VISION.md](VISION.md)** first for what Fabric is building, how **`@fabri
See also [`QUICKSTART.md`][quickstart-guide] for up-to-date instructions.

0. `nvm use 24.15.0` (install [`nvm`][nvm-official] if needed; matches `.nvmrc` / `package.json` engines)
1. From a clone of this repo: `npm install` (or `npm install -g @fabric/core` to put `fabric` on your `PATH`)
0b. Ensure **npm 12+** (`npm -v`). Node 24.15.0 may ship npm 11.x — upgrade with `npm install -g npm@12` (or newer) before installing. Downstream packages that install Fabric from GitHub need **`.npmrc` `allow-git=all`** (npm 12+); see Hub / `@fabric/http` / GoonCitizen.
1. From a clone of this repo: `npm install` (or `npm install -g @fabric/core` to put `fabric` on your `PATH`).
2. (optional) `fabric setup` to generate a master key and local config
3. (optional) `fabric keygen` to generate a new master key without saving to disk (ephemeral)
4. Run `fabric` — the CLI entry is wired through `types/cli.js` and extends **`Service.FabricShell`**. **Contracts** (HTLCs, document sessions, programs, shell packs, …) are documented in **[docs/CONTRACTS.md](docs/CONTRACTS.md)**; terminal UX in **[docs/CLI.md](docs/CLI.md)**.
Expand Down
2 changes: 1 addition & 1 deletion MESSAGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ Primary sources:
| `BITCOIN_TRANSACTION_HASH` | 22100 | `0x5654` | Announces/propagates a Bitcoin transaction hash. |
| `LOG_MESSAGE` | 3235156080 | `0xc0d3f330` | Debug/log transport message for diagnostics. |
| `GENERIC_LIST` | 3235170158 | `0xc0d42e2e` | Generic list/queue-style payload container. |
| `SIDECHAIN_STATE_PATCH` | 997 | `0x03e5` | Typed-field sidechain/registry update (`basisClock` / `basisDigest` / `catalogCanonical`). HTTP may map RFC6902 ↔ fields. |
| `SIDECHAIN_STATE_PATCH` | 997 | `0x03e5` | Typed-field sidechain/registry update (`basisClock` / `basisDigest` / `catalogCanonical` / optional `patchesCanonical`). HTTP may map RFC6902 ↔ fields. |
| `DOCUMENT_PUBLISH` | 998 | `0x03e6` | Publishes a document descriptor/content reference. |
| `DOCUMENT_REQUEST` | 999 | `0x03e7` | Requests a document from peers/services. |
| `BLOCK_CANDIDATE` | 3 | `0x0003` | Candidate block announcement in peer coordination. |
Expand Down
21 changes: 19 additions & 2 deletions docs/APPLICATION_NAMESPACES.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,24 @@ These `type` strings ride inside `CONTRACT_MESSAGE` (not outer opcodes):
| `GroupChat` | GoonCitizen Group Federation | Group channel chat |
| `GroupChange` | GoonCitizen Group Federation | Membership / meta |
| `GroupShare` | GoonCitizen Group Federation | Group-scoped shares (mission offers; `kind: GroupOffer` for opaque `fabric:<hex>` join offers) |
| `GroupActivityTree` | GoonCitizen Group Federation | Merkle root + digests of cumulative history under a Group namespace |
| `GroupJournalRequest` | GoonCitizen Group Federation | Request missing Statechain journal entries (`fromClock` → tip) |
| `GroupJournalBatch` | GoonCitizen Group Federation | Catch-up batch of journal rows + tip Schnorr (`ContractStateTip`) |
| `GroupStateJournal` | GoonCitizen Group Federation | Optional tip attestation: folded `stateDigest` signed to threshold |
| `ContractCapabilityGrant` | Hub, GoonCitizen | Token-backed reader/signer grant (`OP_CONTRACT_READ` / `OP_CONTRACT_SIGN`) |
| `ContractWithdrawalRequest` | Hub, GoonCitizen | Spend or decay-migrate from contract Taproot UTXO |
| `ContractWithdrawalWitness` | Hub, GoonCitizen | Co-signer witness for withdrawal / migration |
| `GameStateSnapshot` | GoonCitizen → Hub sidechain | Cumulative analytics snapshot for Beacon seal (also listed under `ACTIVITY_TYPES`) |

**Tip attestation:** journal tips use
[`functions/contractStateSigning`](../functions/contractStateSigning.js)
(`kind: ContractStateTip`, same k-of-n witness shape as Beacon epochs). Hub must
track this module when sealing contract-namespace sidechains.

**Taproot spend ladder:** [`functions/contractTaproot`](../functions/contractTaproot.js)
builds deterministic P2TR trees from author-defined failover tiers (`after` / `until`
decay + optional migrate). See DISTRIBUTED_EXECUTION.md.

### Shared activity / GenericMessage types

Not outer opcodes; not always `CONTRACT_MESSAGE` bodies. Catalogued as
Expand Down Expand Up @@ -71,5 +87,6 @@ it is hashed into the contract `Actor` id (GoonCitizen network genesis).
1. **New mesh features** use the outer types above — not new one-off opcodes per app.
2. **App-specific semantics** go in `CONTRACT_MESSAGE` body `type` + `object` under a published contract id.
3. **Ignore unknown namespaces** — never crash the Peer on unfamiliar `contract` ids.
4. **Hub invite JSON** (`FederationContractInvite` v2) is the shared join/policy shape.
5. Prefer importing names from `@fabric/core/functions/applicationNamespaces` rather than duplicating string literals.
4. **Invite JSON** (`FederationContractInvite` v2) is the shared join/policy shape — parse/build lives in **`@fabric/http/functions/federationContractInvite`** (keep JSON bridges out of core).
5. Prefer importing body-type names from `@fabric/core/functions/applicationNamespaces` rather than duplicating string literals.
6. **`contract:message` events** expose `wireMessage` / `messageHex` so apps can attach bit-identical AMP frames to journal rows.
11 changes: 8 additions & 3 deletions docs/DISTRIBUTED_EXECUTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ Hub’s longer narrative (Beacon, delegation, signing rounds): hub.fabric.pub
|--------|------|
| `fabricCanonicalJson` | Deterministic digests (Actor / sidechain / Program) |
| `beaconFederationSigning` | Epoch commitment strings + federation witness verify |
| `contractStateSigning` | Contract-namespace tip Schnorr (`ContractStateTip`); same witness shape as Beacon |
| `contractTaproot` | Failover ladder → P2TR (`toAddress` / `toTaprootContract`); `after`/`until` decay + migrate |
| `contractTierWhen` | Off-chain `when` predicates for tier activation |
| `contractCapability` | Token issue/verify for `OP_CONTRACT_READ` / `OP_CONTRACT_SIGN` |
| `fabricProgramManifest` | Manifest v1 (`programId` / `programHash` / allowed types / optional `sidechainPolicy`) |
| **`sidechainState`** | Sealed JSON document: digests, RFC6902 patches, path policy, journal, snapshots, Beacon tip restore, contract namespace seals |

Expand Down Expand Up @@ -85,11 +89,12 @@ Accepted `CONTRACT_PUBLISH` ids reuse the **same** document helpers under

`SIDECHAIN_STATE_PATCH` uses the **same opcode / type name** across Peer, Beacon,
and HTTP. Core prefers **typed fields** (`basisClock`, `basisDigest`,
`catalogCanonical`) via `messageBodyCodec` /
`catalogCanonical`, optional `patchesCanonical`) via `messageBodyCodec` /
`functions/documentRegistrySidechain`. **RFC6902 JSON patch arrays are an
`@fabric/http` edge transform** (`messageBodyJsonBridge`) of those fields — not
the core/simulator primary API. Digests of `sidechain/STATE` still use
`fabricCanonicalJson` (digest ≠ wire body).
the core/simulator primary API. Multi-op sequences round-trip through
`patchesCanonical` (UTF-8 JSON array); `/registry` still seeds `catalogCanonical`.
Digests of `sidechain/STATE` still use `fabricCanonicalJson` (digest ≠ wire body).

A dedicated numeric outer opcode may be allocated later.

Expand Down
3 changes: 2 additions & 1 deletion docs/MESSAGE_BODY.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ API: `types/message.js` body codec helpers (`Message.encodeBody` /
- HTTP / Bridge may map field structs ↔ JSON for REST and browsers
(`functions/messageBodyJsonBridge.js` in `@fabric/http`), including
RFC6902 patch arrays ↔ `SIDECHAIN_STATE_PATCH` fields
(`basisClock` / `basisDigest` / `catalogCanonical`).
(`basisClock` / `basisDigest` / `catalogCanonical` / optional
`patchesCanonical` for multi-op fidelity).
- **Legacy:** object bodies without a registered schema still
`JSON.stringify` (deprecated transitional path for GenericMessage and
unmigrated types). New opcodes **must** ship a field schema.
Expand Down
14 changes: 13 additions & 1 deletion functions/applicationNamespaces.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,19 @@ const CONTRACT_BODY_TYPES = Object.freeze({
GroupChange: 'GroupChange',
GroupShare: 'GroupShare',
/** Merkle root + digests of cumulative local history leaves under a Group namespace. */
GroupActivityTree: 'GroupActivityTree'
GroupActivityTree: 'GroupActivityTree',
/** Request missing Statechain journal entries (fromClock → tip). */
GroupJournalRequest: 'GroupJournalRequest',
/** Catch-up batch of accepted journal rows + tip Schnorr (k-of-n members). */
GroupJournalBatch: 'GroupJournalBatch',
/** Optional tip attestation: folded stateDigest signed by member threshold. */
GroupStateJournal: 'GroupStateJournal',
/** Token-backed contract role grant (reader / signer). */
ContractCapabilityGrant: 'ContractCapabilityGrant',
/** Propose spend or decay-migrate from contract Taproot UTXO. */
ContractWithdrawalRequest: 'ContractWithdrawalRequest',
/** Co-signer witness for a withdrawal / migration. */
ContractWithdrawalWitness: 'ContractWithdrawalWitness'
});

/**
Expand Down
118 changes: 118 additions & 0 deletions functions/contractCapability.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
'use strict';

/**
* Contract-scoped Token capabilities (read-only member vs signer).
*/

const Key = require('../types/key');
const Token = require('../types/token');

const OP_CONTRACT_READ = 'OP_CONTRACT_READ';
const OP_CONTRACT_SIGN = 'OP_CONTRACT_SIGN';

/**
* @param {object} opts
* @param {object|Key} opts.issuerKey Fabric Key (or settings) of issuer
* @param {string} opts.subject Subject compressed pubkey
* @param {string} opts.contractId Contract / namespace id
* @param {string} [opts.capability=OP_CONTRACT_READ]
* @param {number} [opts.expiresInSeconds]
* @returns {string} Token.toSignedString()
*/
function issueContractCapability (opts = {}) {
const capability = opts.capability || OP_CONTRACT_READ;
if (capability !== OP_CONTRACT_READ && capability !== OP_CONTRACT_SIGN) {
throw new Error(`unsupported contract capability: ${capability}`);
}
const contractId = String(opts.contractId || '').trim().toLowerCase();
if (!contractId) throw new Error('contractId required');
const subject = String(opts.subject || '').trim().toLowerCase();
if (!subject) throw new Error('subject required');
const issuer = opts.issuerKey && typeof opts.issuerKey.sign === 'function'
? opts.issuerKey
: new Key(opts.issuerKey || {});
const token = new Token({
capability,
issuer,
subject,
ctx: { contractId }
});
return token.toSignedString({
expiresInSeconds: opts.expiresInSeconds,
ctx: { contractId }
});
}

/**
* @param {string} tokenString
* @param {object} expect
* @param {string} expect.contractId
* @param {string} [expect.expectedCap]
* @param {string} [expect.subject]
* @param {Key|object} [expect.issuerKey] Verify Schnorr against this key (auth path)
* @param {boolean} [expect.allowUnverified] Opt-in parse-only (no sig check); result has verified:false
* @returns {{ cap: string, iss: string, sub: string, iat: number, exp: number, ctx?: object, verified?: boolean }|null}
*/
function verifyContractCapability (tokenString, expect = {}) {
const contractId = String(expect.contractId || '').trim().toLowerCase();
if (!contractId) return null;
let payload = null;
let verified = false;
if (expect.issuerKey) {
const key = expect.issuerKey && typeof expect.issuerKey.verify === 'function'
? expect.issuerKey
: new Key(expect.issuerKey);
payload = Token.verifySigned(tokenString, key);
verified = !!payload;
} else if (expect.allowUnverified === true) {
// Parse-only: not authorization. Callers must not treat this as a verified grant.
if (!tokenString || typeof tokenString !== 'string') return null;
const parts = tokenString.split('.');
if (parts.length !== 2) return null;
try {
const { tryParseWireJson } = require('./wireJson');
const payloadStr = Token.base64UrlDecode(parts[0]);
const pr = tryParseWireJson(payloadStr);
if (!pr.ok) return null;
payload = pr.value;
if (!payload || payload.exp == null || Date.now() / 1000 > payload.exp) return null;
verified = false;
} catch (_) {
return null;
}
} else {
return null;
}
if (!payload) return null;
const ctxId = payload.ctx && payload.ctx.contractId
? String(payload.ctx.contractId).trim().toLowerCase()
: '';
if (ctxId !== contractId) return null;
if (expect.expectedCap && payload.cap !== expect.expectedCap) return null;
if (expect.subject) {
const sub = String(payload.sub || '').trim().toLowerCase();
if (sub !== String(expect.subject).trim().toLowerCase()) return null;
}
return Object.assign({}, payload, { verified });
}

function roleToCapability (role) {
const r = String(role || '').toLowerCase();
if (r === 'signer' || r === 'sign') return OP_CONTRACT_SIGN;
return OP_CONTRACT_READ;
}

function capabilityToRole (cap) {
if (cap === OP_CONTRACT_SIGN) return 'signer';
if (cap === OP_CONTRACT_READ) return 'reader';
return null;
}

module.exports = {
OP_CONTRACT_READ,
OP_CONTRACT_SIGN,
issueContractCapability,
verifyContractCapability,
roleToCapability,
capabilityToRole
};
143 changes: 143 additions & 0 deletions functions/contractStateSigning.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
'use strict';

/**
* Contract-namespace tip attestation (k-of-n Schnorr).
*
* Same witness shape as {@link beaconFederationSigning}: members / validators
* Schnorr-sign a canonical tip string. Used by GoonCitizen Group Statechain
* journals today; Hub contract sidechains SHOULD reuse this when sealing
* per-namespace tips (keep Hub docs / RPC in sync if the tip kind changes).
*
* @see functions/beaconFederationSigning.js
* @see docs/APPLICATION_NAMESPACES.md
* @see docs/DISTRIBUTED_EXECUTION.md
*/

const crypto = require('crypto');
const Key = require('../types/key');
const fabricCanonicalJson = require('./fabricCanonicalJson');
const { verifyFederationWitnessOnMessage } = require('./beaconFederationSigning');

/** Canonical tip kind — Hub and apps must agree; bump only with protocol note. */
const CONTRACT_STATE_TIP_KIND = 'ContractStateTip';

/**
* UTF-8 string members Schnorr-sign for a contract-namespace tip.
* @param {object} fields
* @param {string} fields.contractId
* @param {number} fields.clock
* @param {string} fields.stateDigest
* @returns {string}
*/
function signingStringForContractStateTip (fields = {}) {
return fabricCanonicalJson({
version: 1,
kind: CONTRACT_STATE_TIP_KIND,
contractId: String(fields.contractId || '').trim().toLowerCase(),
clock: Number(fields.clock) || 0,
stateDigest: String(fields.stateDigest || '').trim().toLowerCase()
});
}

/**
* @param {string} contractId
* @param {number} clock
* @param {string} stateDigest
* @returns {Buffer}
*/
function tipMessageBuffer (contractId, clock, stateDigest) {
return Buffer.from(signingStringForContractStateTip({ contractId, clock, stateDigest }), 'utf8');
}

/**
* Sign a tip with a Fabric Key (or `{ xprv }` / Key-like).
* @param {object} keyOrSettings Key instance or settings for `new Key(...)`
* @param {string} contractId
* @param {number} clock
* @param {string} stateDigest
* @returns {{ pubkey: string, signature: string, message: string }}
*/
function signContractStateTip (keyOrSettings, contractId, clock, stateDigest) {
const key = keyOrSettings && typeof keyOrSettings.signSchnorr === 'function'
? keyOrSettings
: new Key(keyOrSettings || {});
const message = signingStringForContractStateTip({ contractId, clock, stateDigest });
const signature = Buffer.from(key.signSchnorr(Buffer.from(message, 'utf8'))).toString('hex');
return { pubkey: key.pubkey, signature, message };
}

/**
* Verify k-of-n tip signatures (Federation witness shape).
* @param {string[]} validatorPubkeys
* @param {number} threshold
* @param {string} contractId
* @param {number} clock
* @param {string} stateDigest
* @param {{ [pubkey: string]: string }|{ signatures: object }} signaturesOrWitness
* @returns {boolean}
*/
function verifyContractStateTip (
validatorPubkeys,
threshold,
contractId,
clock,
stateDigest,
signaturesOrWitness
) {
const thr = Number(threshold);
if (!Number.isInteger(thr) || thr < 1) {
throw new Error('threshold must be a positive integer');
}
const witness = signaturesOrWitness && signaturesOrWitness.signatures
&& typeof signaturesOrWitness.signatures === 'object'
? signaturesOrWitness
: { signatures: signaturesOrWitness || {} };
return verifyFederationWitnessOnMessage(
tipMessageBuffer(contractId, clock, stateDigest),
witness,
validatorPubkeys,
thr
);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* Merge signature maps (pubkey → sig hex), last write wins per key.
* @param {...object} maps
* @returns {{ [pubkey: string]: string }}
*/
function mergeTipSignatures (...maps) {
const out = {};
for (const m of maps) {
if (!m || typeof m !== 'object') continue;
const src = m.signatures && typeof m.signatures === 'object' ? m.signatures : m;
for (const [pk, sig] of Object.entries(src)) {
if (typeof pk === 'string' && pk && typeof sig === 'string' && sig) {
out[pk] = sig;
}
}
}
return out;
}

/**
* Stable digest helper for tests / logging.
* @param {string} contractId
* @param {number} clock
* @param {string} stateDigest
* @returns {string} hex sha256 of tip message
*/
function tipDigestHex (contractId, clock, stateDigest) {
return crypto.createHash('sha256')
.update(tipMessageBuffer(contractId, clock, stateDigest))
.digest('hex');
}

module.exports = {
CONTRACT_STATE_TIP_KIND,
signingStringForContractStateTip,
tipMessageBuffer,
signContractStateTip,
verifyContractStateTip,
mergeTipSignatures,
tipDigestHex
};
Loading
Loading