feat(wallet): add BRC-98 ECPM semantic module - #488
Open
connormurray2 wants to merge 6 commits into
Open
Conversation
…-side Supersedes bsv-blockchain#487, which was overbuilt. That branch added two methods, a wire call code and substrate plumbing across six transports. This is the single operation actually missing, and nothing else. Context. Masking a point through a BRC-100 wallet is already possible today, via revealCounterpartyKeyLinkage plus decrypt, and masks produced that way commute across independent wallets. What has no route through the interface is removing a mask: that needs multiplication by the modular inverse of the derived key. Feeding a*C back through the linkage recipe yields a^2*C, not C. The primitives already exist and this method composes them rather than introducing anything new: const masked = new PublicKey(key.deriveSharedSecret(point)) const inverse = new PrivateKey(key.invm(new Curve().n)) const unmasked = inverse.deriveSharedSecret(masked) // === point That composition requires the private key in application memory. WalletInterface exposes 29 methods and no route to a scalar -- keyDeriver is a property of the in-process class, not part of the interface, so over a substrate there is none. An application whose keys live in a wallet therefore cannot complete the second step. This method runs both steps where the key already is. The first test asserts the output is identical to the composition above, so the behaviour is pinned to the existing primitives rather than to a new definition. Optional, deliberately. BRC-100's value is that it does not change, so a method added later cannot be mandatory: declaring it required on WalletInterface broke 23 call sites across every substrate plus the KV store, registry and identity clients, and declaring it required on ProtoWallet broke @bsv/wallet-toolbox, where Wallet, PrivilegedKeyManager and the wallet managers satisfy the class structurally without extending it. Applications feature-detect and degrade. Wire substrate support is deliberately excluded here; it needs a call code, which is an interface-version decision rather than a library one. Key derivation is mandatory rather than stylistic. For a counterparty point Q, d*Q IS the ECDH shared secret with Q, so performing this with a spending or identity key would hand any caller that secret and break encryption to that counterparty. The key is always derived from protocolID/keyID/counterparty and no identityKey option is offered. On validation: PublicKey.fromString accepts '02' + 'ff'.repeat(32), an x-coordinate greater than the field prime, reduces it silently to 0x1000003d0, and validate() then returns true. A test asserts both halves of that so the reason for the range check is visible. The check runs before the parser, since the parser performs the reduction. go-sdk has the same behaviour independently. Verified: tsc -b clean, oxlint --deny-warnings clean, prettier clean on the files this adds to (the two existing warnings in Wallet.interfaces.ts predate it), full sdk suite green at 157 suites / 5925 tests, and wallet-toolbox at its 4 pre-existing TS2307 baseline with 0 attributable here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
connormurray2
requested review from
BraydenLangley,
sirdeggen,
tonesnotes and
ty-everett
as code owners
August 20, 2026 13:59
CI failure on bsv-blockchain#488 was six lanes -- merge-gate, build-and-test, browser packages, SDK coverage, wallet browser and wallet mobile -- all one cause: bundle size budgets, and zero type errors anywhere. Measured rather than guessed. Building the UMD bundle from main's SDK gives 554475 bytes against a 555000 budget, so the ratchet was sitting 525 bytes above head. multiplyPoint adds 963, which crosses it. These budgets are deliberate ratchets set just above current size, so any real addition trips them and the fix is to advance them by what the addition actually costs. Reduced the cost before raising anything: the error messages carried a redundant 'the supplied'/'the result is' phrasing that bought no diagnostic value, since the stack already names the function. Trimming those took the delta from 1043 to 963 bytes. Tests still pass -- they match on the specific part of each message, not the prose. Six raw budgets advanced to the next 1000-byte boundary above the observed size, matching the existing convention: sdk umd 555000 -> 556000 (observed 555438) sdk esbuild 560000 -> 561000 (observed 560710) sdk vite 742000 -> 743000 (observed 742268) message-box umd 510000 -> 511000 (observed 510105) wallet client 1607000 -> 1608000 (observed 1607943) wallet mobile 3367000 -> 3368000 (observed 3367997) Only the raw dimension moves. The checker throws on the first dimension over budget, which would have meant discovering these one CI round at a time, so I instrumented it locally to print every measurement at once and then restored it unmodified. That surfaced the esbuild and vite overages before CI reported them. Compressed dimensions have far more slack -- gzip sits 2808 under and brotli 4044 under, against 562 for raw -- because a kilobyte of new source compresses to a few hundred bytes. CI agrees: every failing lane named raw and nothing else. Verified: sdk test:browser passes the full exact-tarball browser contract, tsc -b clean, oxlint clean repo-wide, prettier clean, and the multiplyPoint suite green at 10 tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…mension Second round on the same cause, so this time it covers the whole surface rather than only what CI named. CI reported two more overages after the first budget commit: esbuild browser raw at 1253340 against 1252500, and Hermes mobile bytecode gzip at 1366133 against 1366000. Zero type errors, again -- these are purely size ratchets. The Hermes gzip breach is the informative one. My earlier reasoning was that only the raw dimension could realistically breach, because a kilobyte of new source compresses to a few hundred bytes. That held for the SDK, where gzip had 2808 bytes of slack, but it is wrong here: Hermes gzip was cut to 133 bytes above head. These budgets are set that fine on every dimension, so whichever is tightest breaches first and patching one at a time invites another CI round for the same kilobyte of code. So both breached dimensions are raised to the next 500-byte step above the observed value, and the sibling dimensions on the same bundles get the same small allowance -- vite and esbuild gzip/brotli on the client, hermes brotli and the whole metro triple on mobile. Every increase is 500 to 1500 bytes, proportional to the roughly one kilobyte of source multiplyPoint adds, and none of them loosens a budget beyond what that growth accounts for. I tried to measure these locally rather than infer them, instrumenting check-wallet-toolbox-platform.mjs to print every dimension the way I did for the SDK checker. The wallet lanes pack a tarball and resolve it as an external consumer, which needs CI's setup, so the run fails before measuring. The script is restored unmodified -- confirmed by an empty diff under scripts/. Verified: oxlint clean repo-wide, sdk tsc -b clean, the multiplyPoint suite green at 10 tests, prettier clean on both budget files, the SDK exact-tarball browser contract still passing at raw 555438 / gzip 159192 / brotli 131956, and the diff containing nothing but the two budget files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Collaborator
|
Revised this existing PR/branch in place; no replacement PR was opened.
Local verification is recorded in the updated PR description, including the The remaining repository-health failure is base governance state, not a feature |
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Revision
This PR now implements BRC-229 as the
ecpmBRC-98 semantic module, replacing the earlier optionalWalletInterface.multiplyPointapproach.The existing branch and review history are preserved. Commit
a1eaaa6c0reverses the public SDK method, SDK test, and associated bundle-budget increases from the first three commits, then adds the module-based implementation.The companion full-text BRC rewrite is posted as a review suggestion on BRCs #230.
Protocol
Applications continue to call the fixed BRC-100
getPublicKeymethod:keyID,counterparty,privileged,privilegedReason, andseekPermissionremain in their existingGetPublicKeyArgsfields.The module derives one scalar under the canonical namespace:
The operation and point are deliberately excluded from the derivation identity:
applyreturnsd·P;removereturnsd⁻¹·P;The logical protocol ID is capped at 273 characters so the canonical
p ecpmderivation namespace remains within BRC-43's 280-character limit.Wallet Toolbox semantic module hook
PermissionsModulegains an optional, backwards-compatible handler:A semantic module can return the conforming BRC-100 result directly or invoke the underlying operation once through guarded
next. ExistingonRequest/onResponsemodules remain unchanged.ECPM returns
{ publicKey }directly and does not call ordinarygetPublicKey, because ordinary BRC-43 derivation returns a child public key over the fixed generator (or the named counterparty's child key), notd·P.Installable reference package
This adds
@bsv/ecpm-permission-module@0.1.0alongside BTMS and the existing wallet module packages.Wallet hosts register it as:
The privileged provider is optional and is acquired only after authorization. Ordinary and privileged permission grants are isolated. Level 2 grants are counterparty-scoped,
seekPermission: falsefails without a cached grant, and concurrent equivalent prompts are deduplicated.Security boundary
getPublicKeyis accepted underp ecpm; signing, HMAC, and encryption cannot reuse the scalar.forSelf: truerequests are rejected.{ publicKey }; key derivers, roots, and private scalars remain in trusted wallet/module code.Verification
The repository health run exposed four expired documentation reviews with real
source drift. They were reverified against the current route registries,
Dockerfiles, environment examples, package manifests, and intervening commits;
the corrected service versions, health routes, deployment guidance, and cloud
environment names are included in
4632f51f9. Documentation policy and thecomplete rendered docs/link build now pass.
The final repository-health evaluator is still blocked by three pre-existing
governance exceptions whose owner review dates expired on 2026-08-23/24:
scorecard-release-token-permission,image-size-unpublished-advisory-patch, andlegacy-sonar-analysis-suppressions. Renewing or resolving security andquality exceptions requires an explicit maintainer governance review; this PR
does not extend them merely to make CI green.