Skip to content
Merged
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
14 changes: 9 additions & 5 deletions .github/workflows/services-watchtower.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@ jobs:
outputs:
path-filter: ${{ steps.filter.outputs.path-filter }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
if: github.event_name == 'pull_request'
with:
persist-credentials: false

- uses: dorny/paths-filter@v2
- uses: dorny/paths-filter@4512585405083f25c027a35db413c2b3b9006d50 # v2.11.1
if: github.event_name == 'pull_request'
id: filter
with:
Expand All @@ -49,17 +51,19 @@ jobs:
|| needs.watchtower-detect-changes.outputs.path-filter == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
with:
persist-credentials: false

# The watchtower is a pnpm workspace (`pnpm-workspace.yaml`: typescript +
# services/watchtower) and uses the `workspace:*` protocol, which yarn
# classic cannot resolve -- so this service is installed with pnpm, not
# the yarn flow used by `typescript.yml`.
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0
with:
version: "10.17.1"

- uses: actions/setup-node@v3
- uses: actions/setup-node@3235b876344d2a9aa001b8d1453c930bba69e610 # v3.9.1
with:
node-version: "22.x"

Expand Down
8 changes: 4 additions & 4 deletions docs/rfc/frost-migration/audit-prep-findings-2026-05-25.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# FROST migration audit-prep findings (2026-05-25)

Sweep of the merged FROST migration surface
(`contracts/frost-registry/`, `contracts/bridge/`,
`test/frost-registry/`, `test/integration/utils/`,
`docs/frost-migration/`) for residual hygiene issues prior to
(`solidity/contracts/frost-registry/`, `solidity/contracts/bridge/`,
`solidity/test/frost-registry/`, `solidity/test/integration/utils/`,
`docs/rfc/frost-migration/`) for residual hygiene issues prior to
external audit. Categorized by load-bearingness.

## Methodology
Expand All @@ -29,7 +29,7 @@ external audit. Categorized by load-bearingness.

### MUST-FIX — stale plan-doc claims that mislead readers

All four are in `docs/frost-migration/d2-ecdsa-hard-retirement-plan.md`
All four are in `docs/rfc/frost-migration/d2-ecdsa-hard-retirement-plan.md`
(the D-2.1 plan doc). After D-2.2 slice 1 (PR #447), several
claims in this doc are factually wrong about runtime behavior:

Expand Down
2 changes: 1 addition & 1 deletion docs/rfc/frost-migration/b1-implementation-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ re-checked in `submitDkgResult` to fail fast.
### B-1.4: Deploy script + Bridge fixture wiring

- New deploy script at
`contracts/tbtc-v2/deploy/46_deploy_frost_wallet_registry.ts`
`contracts/tbtc-v2/deploy/48_deploy_frost_wallet_registry.ts`
(depends on `Bridge`, `SortitionPool`, `RandomBeacon`; tags
`["FrostWalletRegistry"]`).
- Extend `test/fixtures/bridge.ts` to deploy the registry and
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ export class FileBackedP2TRWatchtowerChallengeRecordPersistence
async saveChallengeRecords(
records: P2TRWatchtowerChallengeRecordJSON[]
): Promise<void> {
const serializedRecords = `${JSON.stringify(records, null, 2)}\n`
const normalizedRecords = records.map(normalizeSerializedRecord)
const serializedRecords = `${JSON.stringify(normalizedRecords, null, 2)}\n`
await this.assertStateFileUnchangedSinceLoad()

await writeFileAtomically(this.filePath, serializedRecords)
Expand Down
21 changes: 11 additions & 10 deletions services/watchtower/src/P2TRSignatureFraudWatchtowerLoop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,16 +92,17 @@ export function abortableDelay(
}

return new Promise((resolve) => {
const timeout = setTimeout(resolve, milliseconds)

signal?.addEventListener(
"abort",
() => {
clearTimeout(timeout)
resolve()
},
{ once: true }
)
const onAbort = () => {
clearTimeout(timeout)
resolve()
}

const timeout = setTimeout(() => {
signal?.removeEventListener("abort", onAbort)
resolve()
}, milliseconds)

signal?.addEventListener("abort", onAbort, { once: true })
})
}

Expand Down
8 changes: 8 additions & 0 deletions solidity/contracts/bridge/BridgeGovernance.sol
Original file line number Diff line number Diff line change
Expand Up @@ -1854,6 +1854,14 @@ contract BridgeGovernance is Ownable {
/// classification is required since the legacy Bridge
/// mapping was shared between the two lifecycles.
/// @param challengeKeys Legacy challenge keys to migrate.
/// @dev The target `Bridge.migrateLegacyFraudChallenges` helper is
/// intentionally non-operational in the current Bridge
/// implementation: its body is stubbed out and it always
/// reverts with `MigrateLegacyFraudChallengesNotImplemented`.
/// Calls through this forwarder therefore revert until a
/// future Bridge upgrade swaps in the migration body. The
/// forwarder is kept in place so the governance ABI stays
/// stable across that upgrade (a body swap, not an ABI change).
function migrateLegacyFraudChallenges(
uint8 routerKind,
uint256[] calldata challengeKeys
Expand Down
21 changes: 13 additions & 8 deletions solidity/contracts/bridge/IBridgeLifecycleRouter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,25 @@ pragma solidity 0.8.17;
///
/// The Bridge dispatches FROST-scheme wallet lifecycle operations
/// (closeWallet, seize, isWalletMember) to a router implementing this
/// interface. The router resolves the wallet's canonical walletID from
/// the Bridge's `walletIDByWalletPubKeyHash` mapping and forwards the
/// call to the configured `frostWalletRegistry`. ECDSA-scheme lifecycle
/// interface. The router resolves the configured FROST wallet registry
/// address and the wallet's canonical walletID from the Bridge's
/// `frostLifecycleContext(walletPubKeyHash)` view and forwards the call
/// to that registry. ECDSA-scheme lifecycle
/// operations bypass the router entirely and continue to call
/// `ecdsaWalletRegistry` directly, preserving the existing
/// ownership/callback model for ECDSA wallets.
///
/// The Bridge passes only the 20-byte wallet public key hash to the
/// router; the router reads the rest of the lifecycle state from the
/// Bridge via its public view functions (lifecycleRouter,
/// frostWalletRegistry, walletIDByWalletPubKeyHash). This keeps the
/// per-call-site Bridge bytecode footprint minimal (one external call
/// with one argument) at the cost of one cross-contract view per
/// dispatch.
/// Bridge's single `frostLifecycleContext(walletPubKeyHash)` view,
/// which returns the configured `frostWalletRegistry` address and the
/// canonical walletID in one call. The Bridge does not expose separate
/// `lifecycleRouter`, `frostWalletRegistry`, or
/// `walletIDByWalletPubKeyHash` getters; folding them into
/// `frostLifecycleContext` keeps the Bridge implementation under the
/// EIP-170 deploy limit. This keeps the per-call-site Bridge bytecode
/// footprint minimal (one external call with one argument) at the cost
/// of one cross-contract view per dispatch.
///
/// The router is stateless and immutable. `Bridge.setLifecycleRouter`
/// is a one-time setter that reverts once a router has been set, so the
Expand Down
Loading