Wallet-Native Elliptic Curve Point Multiplication as a BRC-98 Module
Some multiplayer protocols, like shuffling encrypted playing cards among several parties, need each participant to secretly multiply and later un-multiply shared points by a personal number that nobody else, including the app, is ever allowed to see. Wallets could derive keys and sign things, but had no way to just take a point you hand them and multiply it by a hidden number and hand back the result, then later undo it. Without that, these masking protocols could not be built without asking someone to expose or approximate the secret scalar.
Reference for an AI
Everything an assistant needs to answer questions about BRC-229 accurately, including what it depends on.
Summary
- Why
- Multi-party protocols that mask and later unmask shared points need a wallet to multiply an arbitrary point by a secret scalar without ever revealing that scalar to the requesting application.
- What
- BRC-229 is a BRC-98 module named ecpm that reinterprets the wallet's getPublicKey call as elliptic curve point multiplication, applying or removing a hidden wallet-derived scalar on a caller-supplied secp256k1 point.
- How
- An app calls the wallet's existing getPublicKey method with a specially formatted protocol string like p ecpm apply <pointHex> <logicalProtocolID>, and a wallet running the ecpm module derives a scalar via BRC-42/43 and returns the compressed public key of d·P or d⁻¹·P.
What this lets you do
- Apply a wallet-held secret scalar to any point you supply and get d·P back
- Remove that same mask later with the inverse operation to get P back
- Build multi-party protocols like mental poker or verifiable shuffles where masks compose and cancel in any order
- Scope permission grants by protocol, key ID, and counterparty without exposing the underlying scalar
- Reject malformed or invalid-curve points before any multiplication happens
Written by claude-sonnet-5 from the specification text. Where the two differ, the original is correct.
The specification
Abstract
This proposal defines ecpm, a BRC-98 permission-module scheme for applying a wallet-derived secp256k1 scalar to an arbitrary caller-supplied curve point, or removing that scalar by applying its modular inverse.
The operation is carried over the existing BRC-100 getPublicKey method. Inside the reserved p ecpm namespace, a supporting wallet interprets the method semantically as elliptic-curve point multiplication and returns the normal { publicKey } result. No method, call code, Wallet Wire message, or optional member is added to BRC-100.
The scheme enables commutative-masking protocols such as mental poker and verifiable shuffles while keeping the derived scalar inside the wallet. Wallets that do not install the module retain BRC-98's required behavior and reject the reserved protocol.
Motivation
A class of multi-party protocols depends on the commutativity of scalar multiplication:
a·(b·P) = b·(a·P)
Barnett-Smart mental poker is a representative use. Each participant applies a secret scalar to encoded card points, and later removes that scalar in any order. The application needs d·P and d⁻¹·P for an arbitrary point P, but it must not learn d.
Ordinary BRC-100 getPublicKey does not supply this operation. It performs BRC-42/43 child-key derivation and returns a derived public key. Naming P as the BRC-43 counterparty influences the child derivation, but does not replace the generator with P. Depending on forSelf, the result is the wallet's or counterparty's derived child public key, not the wallet's derived private scalar multiplied by the caller's point.
This distinction matters when P is an intermediate masked point whose discrete logarithm is intentionally unknown. From d·G and P, an application cannot compute d·P without solving a discrete logarithm or already knowing the scalar of P.
Existing key-linkage and encryption methods can be composed to emulate some forward-multiplication cases, but that is not an equivalent contract. It couples a point operation to linkage disclosure and encryption semantics, does not provide multiplication by d⁻¹, and grants permissions for capabilities the application did not mean to request.
The missing behavior therefore cannot be obtained from pure BRC-43 naming alone. It requires a wallet-side semantic operation, but it does not require changing BRC-100.
Relationship to BRC-98
BRC-98 reserves protocol identifiers beginning with p , requires unsupported wallets to reject them, and permits a supported scheme to define its own rules for permitted operations, key IDs, counterparties, permission attributes, and execution.
The ecpm scheme uses that reserved dispatch point to specialize the meaning of an existing BRC-100 method. This proposal does not amend BRC-98. A clarification in discussion of BRC-98 may record that semantic specialization of an existing method inside a supported module namespace is an intended use of the module system; a separate amendment to BRC-98 is not required.
The semantic boundary is safe for compatibility:
- Outside
p ecpm,getPublicKeyretains its ordinary BRC-100 meaning. - Inside
p ecpm, a wallet either implements this scheme or rejects the request as BRC-98 already requires. - Applications never infer support from a new optional wallet member. They use a configured supporting wallet or handle the standard unsupported-scheme error.
Specification
Scheme and method
The BRC-98 scheme ID is:
ecpm
A conforming implementation MUST accept this scheme only through the existing BRC-100 getPublicKey method. It MUST reject every other BRC-100 method requested under p ecpm. This prevents the ECPM scalar from being reused for signing, HMAC, encryption, or another cryptographic purpose.
The protocol-name component of GetPublicKeyArgs.protocolID has this grammar:
p ecpm <operation> <pointHex> <logicalProtocolID>
where:
operationis exactlyapplyorremove;pointHexis a lowercase, 66-character compressed secp256k1 public-key encoding;logicalProtocolIDis the application protocol whose ECPM key universe is being requested.
The outer BRC-100 call remains:
wallet.getPublicKey(
{
protocolID: [
securityLevel,
`p ecpm ${operation} ${pointHex} ${logicalProtocolID}`
],
keyID,
counterparty,
privileged,
privilegedReason,
seekPermission
},
originator
)
keyID, counterparty, privileged, privilegedReason, seekPermission, and originator retain their existing BRC-100 meanings and locations. They MUST NOT be duplicated or encoded into the protocol string.
counterparty defaults to self under the existing method rules. identityKey: true is prohibited. forSelf MUST be absent or false; it does not alter ECPM semantics.
Logical protocol ID
The logical protocol ID MUST:
- contain only lowercase ASCII letters, numbers, and single spaces;
- contain no leading, trailing, or repeated spaces;
- be at least 5 characters and no more than 273 characters; and
- not end with
protocol.
The 273-character ceiling ensures that the canonical derivation protocol p ecpm <logicalProtocolID> remains within BRC-43's 280-character protocol-ID limit.
The complete outer p ecpm string is a BRC-98 dispatch envelope, not the
protocol ID used for BRC-43 derivation. It MAY therefore exceed BRC-43's
280-character limit so that the point and operation metadata do not reduce the
logical protocol namespace. It MUST be no more than 354 characters: an
apply request can be at most 353 characters and a remove request can be at
most 354 characters. All permitted characters are ASCII, so these character
and byte counts are identical. Both bounds remain within BRC-100's
400-character protocol-string limit.
A key ID MUST satisfy the active BRC-100 getPublicKey key-ID limit.
Canonical scalar derivation
Let:
sbe the security level in the outerprotocolIDtuple;LbelogicalProtocolID;Kbe the separately suppliedkeyID; andCbe the separately suppliedcounterparty, defaulting toself.
The module MUST derive a nonzero private scalar d using BRC-42/43 with:
protocolID = [s, "p ecpm " + L]
keyID = K
counterparty = C
The point and operation MUST NOT form part of this derivation identity.
This omission is load-bearing. If the point were part of the invoice, every point would select a different scalar. If the operation were part of the invoice, remove would select a different scalar from apply. In either case round-trip removal and commutativity would fail.
The p ecpm prefix in the canonical derivation protocol isolates ECPM scalars from ordinary application keys using the same logical protocol and key ID.
Operation
After validating the point as P:
applyreturnsd·P;removereturnsd⁻¹·P, where the inverse is computed modulo the secp256k1 group ordern.
The successful result MUST use the ordinary getPublicKey result shape:
{ publicKey: PubKeyHex }
The returned value MUST be the lowercase compressed encoding of the resulting point.
For identical security level, logical protocol ID, key ID, counterparty, and root-key selection:
remove(apply(P)) = P
Implementations MUST reject, rather than encode, a result at infinity.
Privileged keys
The existing privileged and privilegedReason fields select privileged-key behavior.
When privileged is absent or false, the module derives d from the wallet's ordinary BRC-42/43 root.
When privileged is true:
privilegedReasonMUST be present and satisfy the existing BRC-100 description constraint.- The wallet MUST obtain explicit authorization even at security level 0.
- The wallet MUST derive
dfrom its privileged key material using the same canonical ECPM derivation tuple. - The privileged root or key-derivation capability MUST NOT be exposed to the application.
- The wallet MUST fail closed if privileged derivation is unavailable.
A wallet implementation SHOULD request access to privileged material only after the operation and reason have been authorized, and SHOULD release that access according to its existing privileged-key policy.
Permissions
The module applies BRC-43 permission semantics to the logical ECPM protocol:
- an ordinary security-level-0 request does not require a prompt;
- a security-level-1 grant is scoped to the originator, logical protocol ID, and ordinary-versus-privileged root selection;
- a security-level-2 grant additionally distinguishes the counterparty; and
- ordinary and privileged grants MUST NOT satisfy one another.
A wallet MAY cache a successful grant according to its normal permission lifecycle. The point, operation, and key ID SHOULD be displayed or made available to the authorization UI, but they do not change the BRC-43 protocol-level grant scope.
If permission is required and no applicable grant exists:
seekPermission: falseMUST fail without prompting;- otherwise the wallet MAY seek authorization through its normal trusted UI.
Concurrent equivalent requests SHOULD share one pending authorization decision so that high-volume protocols cannot produce duplicate prompts.
Point validation
Before multiplication, a conforming implementation MUST reject the input unless all of these conditions hold:
- The encoding is exactly 33 bytes represented by 66 lowercase hexadecimal characters.
- The first byte is
02or03. - The encoded x-coordinate is less than the secp256k1 field prime
p. - The encoding decodes to a point satisfying
y² = x³ + 7 (mod p). - The point is finite.
The x-coordinate range check MUST occur before a parser that reduces coordinates modulo p. Some curve parsers accept 02 followed by 32 ff bytes, reduce the x-coordinate, and then report the resulting different point as valid.
The same canonical validation MUST be applied when counterparty is supplied as a public key. The special values self and anyone remain valid.
Errors
A conforming wallet MUST fail without performing multiplication when:
- the installed method is not
getPublicKey; - the protocol grammar or any field constraint is invalid;
- identity-key or
forSelf: truebehavior is requested; - the point or public-key counterparty is invalid;
- authorization is unavailable or denied;
- privileged derivation is requested but unavailable; or
- the operation would return infinity.
Errors SHOULD identify the unsupported scheme, invalid field, or denied capability without revealing key material.
Conformance vectors
The vectors below pin the canonical derivation and the group operation. They were generated with the BRC-42 derivation as implemented in bsv-blockchain/go-sdk (PrivateKey.DeriveChild), which is cross-tested against the BRC-42 specification vectors.
All vectors share these parameters:
- security level:
2 - logical protocol ID:
mental poker deal - key ID:
deck mask 1 - canonical derivation protocol:
p ecpm mental poker deal - BRC-43 invoice number:
2-p ecpm mental poker deal-deck mask 1
Test wallets use transparent, minimal root keys, and the base point is the public key of the scalar 5:
wallet A root private key: 0000000000000000000000000000000000000000000000000000000000000002
wallet A identity key: 02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5
wallet B root private key: 0000000000000000000000000000000000000000000000000000000000000003
wallet B identity key: 02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9
base point P: 022f8bde4d1a07209355b4a7250a5c5128e88b84bddc619ab7cba8d569b240efe4
Vector 1 — apply, counterparty self. Wallet A receives:
protocolID = [2, "p ecpm apply 022f8bde4d1a07209355b4a7250a5c5128e88b84bddc619ab7cba8d569b240efe4 mental poker deal"]
keyID = "deck mask 1"
counterparty = "self"
With counterparty self, the BRC-42 shared secret is computed against the wallet's own identity key. The derived scalar and result are:
d_A: 0d7da380de7459582c600748e94539c408742f9d4f21153473ba2c1cb812a984
d_A·P: 023414ffa833cb89ab8cdd631998921d6a7b05b77931d0c50c80740324df6ce4f2
Vector 2 — remove round-trip. Wallet A receives the vector 1 result with operation remove and otherwise identical arguments. The output is P exactly:
d_A⁻¹·(d_A·P) = 022f8bde4d1a07209355b4a7250a5c5128e88b84bddc619ab7cba8d569b240efe4
Vector 3 — multi-party commutativity. Wallet B, with the same arguments and counterparty self, derives:
d_B: d205df1fd5f36ed688c66dda212d1178b87e11cad37469a98c7b3edd690f2b3b
Applying both masks to P yields the same point in either order:
d_B·(d_A·P) = d_A·(d_B·P) = 035eebd9a281022ce46ea790bd460f6c6320b08a9e07b560b4da36480605c0580d
Vector 4 — counterparty separation. Wallet A with counterparty set to wallet B's identity key, all else as in vector 1, derives a different scalar and result:
d_A': c3dd11d61386d969ff40e150e3039eeab9bda24ffa13936eac29d0347ec5104b
d_A'·P: 032406ec05e51aac7d0dcb1c7f1f3143b37e6336440ab728a552d668f2e397b8a4
Vector 5 — rejected encodings. Each of these pointHex values MUST be rejected without multiplication:
| Input | Reason |
|---|---|
02ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff | x-coordinate not less than the field prime; must be caught before any reducing parser |
020000000000000000000000000000000000000000000000000000000000000005 | x³ + 7 is not a quadratic residue; no such point exists |
04… (65-byte uncompressed encoding) | not the 33-byte compressed form |
| any 64-character value | wrong length |
| vector 1's point with any uppercase hex digit | not the canonical lowercase encoding |
Privileged derivation (root-specific) and unsupported-module rejection are behavioral requirements without portable vectors: a wallet MUST produce different scalars for ordinary and privileged requests with otherwise identical arguments, and a wallet without the module MUST reject the reserved protocol as BRC-98 requires.
Protocol-length boundaries are also normative: implementations MUST accept a
273-character logical protocol ID for both operations, including the resulting
354-character remove envelope, and MUST reject a 274-character logical
protocol ID.
TypeScript reference module
The TypeScript reference implementation is proposed in bsv-blockchain/ts-stack#488 as the installable @bsv/ecpm-permission-module package.
It extends the Wallet Toolbox permission-module interface with an optional semantic handler:
handleRequest?: (
request: { method: string; args: object; originator: string },
next: (args: object) => Promise<unknown>
) => Promise<unknown>
A semantic handler may return a conforming BRC-100 result directly, or invoke next at most once to use the underlying wallet method. Existing onRequest and onResponse transformation modules remain source-compatible.
The ECPM module returns the result directly and never forwards its request to ordinary getPublicKey; forwarding would derive d·G or a BRC-42 child public key, not d·P.
A wallet host installs it alongside other BRC-98 modules under the ecpm scheme and supplies:
- its ordinary BRC-42/43 key deriver;
- its trusted authorization callback; and
- optionally, a privileged-key-deriver provider.
The key derivers and derived scalars remain inside trusted wallet/module code and are never included in the BRC-100 response.
Security
Key isolation
For a counterparty point Q, d·Q is an ECDH shared secret. Reusing an identity, spending, signing, HMAC, or encryption key for ECPM could therefore disclose a capability belonging to another protocol.
For this reason, the canonical p ecpm <logicalProtocolID> derivation namespace and the method restriction are mandatory. A wallet MUST NOT substitute its identity key, spending key, or a key from an ordinary non-ECPM protocol.
Chosen-point requests
For a valid prime-order secp256k1 point P, observing P and d·P does not reveal d without solving the elliptic-curve discrete logarithm problem. An adaptively selected valid point does not weaken this assumption in the prime-order group.
That argument depends on strict validation. Accepting a point on another curve or a non-canonical encoding can create small-subgroup or invalid-curve attacks that leak information about d. Implementations must validate the encoded point before multiplication.
Permission and denial-of-service considerations
Mental-poker and shuffle protocols can require hundreds of operations, so prompting once per point is impractical. Protocol-scoped grants allow the intended throughput. Wallets SHOULD still bound request concurrency and resource use, and SHOULD make the logical protocol, originator, operation, key ID, counterparty, point, and privileged reason available to trusted policy code.
Capability containment
The public application interface exposes only getPublicKey and receives only a compressed public point. The semantic module is trusted wallet code: it may use internal key-derivation capabilities, but it MUST NOT return those capabilities, a private scalar, or privileged root material.
Why pure BRC-43 is insufficient
BRC-43 can name the counterparty and thereby determine which shared derivation universe is used. It cannot name a replacement generator for the requested group operation.
If the arbitrary point is supplied as counterparty, ordinary getPublicKey performs BRC-42 child derivation relative to that counterparty. It does not compute the wallet's derived private scalar times that point. If getPublicKey returns d·G, combining it with an arbitrary P still does not yield d·P unless the application knows the discrete logarithm of P. Intermediate points in a commutative-masking protocol are constructed specifically so that no one knows that logarithm.
Pure BRC-43 therefore provides the scalar namespace and permission vocabulary used by this proposal, but not the required group operation. BRC-98 supplies the missing semantic dispatch without changing BRC-100.
References
- BRC-2: Data Encryption and Decryption
- BRC-42: BSV Key Derivation Scheme
- BRC-43: Security Levels, Protocol IDs, Key IDs and Counterparties
- BRC-44: Admin-reserved and Prohibited Key Derivation Protocols
- BRC-73: Group Permissions for App Access
- BRC-98: P Protocols
- BRC-100: Unified Wallet-to-Application Interface
- BRC-116: Wallet Permissions and Counterparty Trust