Skip to content

BRC-229: Wallet-Native Elliptic Curve Point Multiplication - #230

Draft
connormurray2 wants to merge 2 commits into
bsv-blockchain:masterfrom
connormurray2:brc-229-multiply-point
Draft

BRC-229: Wallet-Native Elliptic Curve Point Multiplication#230
connormurray2 wants to merge 2 commits into
bsv-blockchain:masterfrom
connormurray2:brc-229-multiply-point

Conversation

@connormurray2

@connormurray2 connormurray2 commented Aug 19, 2026

Copy link
Copy Markdown

Adds one method to the BRC-100 interface: multiplyPoint, which multiplies a caller-supplied secp256k1 point by a BRC-42/43 derived private key and returns the result, without disclosing the key. An invert flag multiplies by the modular inverse, so a mask the wallet applies is a mask the wallet can remove.

Why

A class of multi-party protocols rests on the commutativity of scalar multiplication, a·(b·P) == b·(a·P): Barnett–Smart mental poker, verifiable shuffles, several oblivious-transfer constructions. BRC-100 exposes no way to reach that group operation. getPublicKey gives d·G, but nothing gives d·P for an arbitrary P.

So an application that needs commutative masking has to generate and hold its own secp256k1 keys outside the wallet. In the mental-poker case those keys are precisely what the privacy of the user's hand depends on — whoever holds the masking scalars can read every card. The user ends up with a wallet that protects their keys and an application holding a second set that nothing protects, which is the outcome BRC-100 exists to prevent.

No companion derivePoint is proposed. getPublicKey already returns d·G for the same derivation arguments, so a second method would be redundant.

On the obvious objection

"Multiply this arbitrary point by my key" reads like a signing oracle. The spec argues it is not, on two independent grounds: recovering the scalar from P and d·P is ECDLP, and adaptively chosen P = k·G only ever returns k·(d·G), which the caller could compute from getPublicKey's output alone. Separately, ECDSA is fragile under leakage because the same key sits in s = k⁻¹(z + r·d); a key reachable only through multiplyPoint never signs, so there is no such equation to attack.

The risk that is real gets named plainly: for a counterparty point Q, d·Q is the ECDH shared secret. Implemented over a spending or identity key, this method would hand any caller that secret and break BRC-2 encryption to that counterparty. Mandatory BRC-42/43 derivation is what prevents it — which is why that rule is a MUST and why no identityKey option is offered by analogy with getPublicKey.

Validation rules come from implementation, not from theory

The normative validation rules include a canonical-encoding check on both coordinates, and the spec states that this is not implied by an on-curve check. That is in there because the reference implementation shipped with exactly that hole: a test submitting a compressed point whose x-coordinate is thirty-two 0xff bytes — greater than the field prime — was accepted, because the parser reduced the coordinate silently and the on-curve test then passed. Easy bug to ship, so the spec makes the check mandatory rather than advisory.

Reference implementation

https://github.com/connormurray2/brc100-poker/tree/main/internal/brc/points

Go, with tests asserting the properties the spec depends on: masks from independent wallets commute, invert recovers the original point, a three-way mask strips in any order, distinct protocol and key IDs derive independently, two wallets never derive the same protocol key, and a whole-deck mask with a selective unmask leaves the other positions unreadable.

Written as part of a non-custodial poker application, where a dealerless deal is the motivating use case. No wallet implements multiplyPoint today; applications needing commutative masking must currently either hold keys outside the wallet or fall back to a trusted dealer.

Happy to revise the shape, the naming, or the permission semantics — particularly the call-code assignment (29) and whether per-protocol grants covering repeated calls is the right permission model for a method this high-volume.

Adds one method, multiplyPoint, letting an application ask a BRC-100 wallet to
multiply a caller-supplied secp256k1 point by a BRC-42/43 derived key without
disclosing that key. An invert flag multiplies by the modular inverse, so a
mask the wallet applies is a mask the wallet can remove.

Motivation: commutative-masking protocols -- Barnett-Smart mental poker,
verifiable shuffles, oblivious transfer -- rest on a.(b.P) == b.(a.P). BRC-100
exposes no way to reach the group operation, so an application needing it must
generate and hold secp256k1 keys outside the wallet. In the mental-poker case
those keys are exactly what privacy of the user's hand depends on. That is the
outcome BRC-100 exists to prevent.

No companion derivePoint method is proposed: getPublicKey already returns d.G
for the same derivation arguments.

The security section argues the method is not a signing oracle -- recovering the
scalar is ECDLP, adaptively chosen points yield only what getPublicKey already
gives, and a key that never signs has no signature equation to attack. It then
names the risk that is real: d.Q IS the ECDH shared secret with Q, so
implementing this over a spending or identity key would break BRC-2 encryption
to that counterparty. Mandatory BRC-42/43 derivation is what prevents it, which
is why that rule is a MUST and no identityKey option is offered.

Validation rules are normative and drawn from implementation experience rather
than asserted in advance. A canonical-encoding check on both coordinates is
required and is NOT implied by an on-curve check: some libraries accept an
x-coordinate greater than the field prime, reduce it silently, and then report
the point as on-curve. The reference implementation shipped with exactly that
hole until a test submitting thirty-two 0xff bytes caught it.

Reference implementation and test suite in Go:
https://github.com/connormurray2/brc100-poker/tree/main/internal/brc/points

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@connormurray2 connormurray2 changed the title BRC-229: Wallet-Native Elliptic Curve Point Multiplication BRC-230: Wallet-Native Elliptic Curve Point Multiplication Aug 20, 2026
@connormurray2 connormurray2 changed the title BRC-230: Wallet-Native Elliptic Curve Point Multiplication BRC-229: Wallet-Native Elliptic Curve Point Multiplication Aug 20, 2026
…dence

Two corrections, both discovered by implementing the spec rather than by
rereading it.

multiplyPoint is now declared optional, with feature detection required. The
first draft said wallets MUST expose it, which is unimplementable: BRC-100 is
specified as an unchanging interface, so a method declared mandatory after the
fact retroactively invalidates every wallet and substrate already shipped
against it. Declaring it required in @bsv/sdk broke 23 call sites --
WalletClient, HTTPWalletJSON, WalletWireTransceiver, window.CWI, XDM,
ReactNativeWebView, plus the KV store, registry and identity clients -- none of
which have any reason to multiply an elliptic curve point. The new Optionality
section states the rule and generalises it: any future BRC adding a method to
this interface faces the same constraint.

The canonical-encoding rule is upgraded from an anecdote to a table of measured
behaviour. '02' + 'ff'*32 is accepted, silently reduced to 0x1000003d0, and then
reported on-curve by BOTH @bsv/sdk (PublicKey.fromString / validate) and go-sdk
(PublicKeyFromString / IsOnCurve). Two independently written libraries sharing
the defect makes it an interoperability hazard rather than one library's quirk.
The spec now also requires the range check to run BEFORE the parser, since the
parser is what performs the reduction -- validating afterwards is too late,
because the out-of-range value has already become a different valid point.

Implementations section now cites the TypeScript reference implementation
proposed at bsv-blockchain/ts-stack#487, verified against that repository's own
tooling: tsc -b clean, oxlint --deny-warnings clean, 157 suites / 5924 tests
green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sirdeggen
sirdeggen marked this pull request as draft August 21, 2026 16:21
@sirdeggen

Copy link
Copy Markdown
Collaborator

This should be reframed as a proposal to change the wallet interface rather than attempting to change BRC 100 itself, which is fixed by definition.

@ty-everett

Copy link
Copy Markdown
Collaborator

I think the primitive is valuable, but I do not think it should add a new method or call code to BRC-100. This looks like exactly the kind of extension the BRC-98 P-protocol/module namespace was created to support.

BRC-100 is intentionally fixed. Making multiplyPoint optional avoids invalidating existing structural implementations, but it still expands the interface, wallet wire, substrates, clients, processors, and capability-detection machinery. It also creates a precedent where every new cryptographic primitive adds another optional method to the supposedly unchanging interface.

Instead, I suggest reframing BRC-229 as the definition of a BRC-98 module scheme, perhaps ecpm, and routing the operation through an existing BRC-100 method such as getPublicKey.

BRC-229 itself can define the ecpm scheme and clarify that P-modules are semantic extension points, not merely alternate permission filters. Within a declared p <scheme> namespace, a supporting wallet may assign scheme-specific semantics to existing methods. Unsupported wallets already have the correct behavior: they reject the P-protocol because they do not have the named module.

Proposed application-facing form

For example:

await wallet.getPublicKey({
  protocolID: [
    2,
    `p ecpm apply ${pointHex} mental poker deal`
  ],
  keyID: 'deck-mask-1',
  counterparty: 'self',
  privileged: false,
  seekPermission: true
})

would return:

{
  publicKey: dP
}

To remove the transformation:

await wallet.getPublicKey({
  protocolID: [
    2,
    `p ecpm remove ${maskedPointHex} mental poker deal`
  ],
  keyID: 'deck-mask-1',
  counterparty: 'self',
  privileged: false,
  seekPermission: true
})

would return:

{
  publicKey: d⁻¹P
}

The concrete grammar could be:

p ecpm <operation> <pointHex> <logicalProtocolID>

where:

  • <operation> is apply or remove;
  • <pointHex> is a canonical 33-byte compressed secp256k1 point;
  • <logicalProtocolID> is the remaining protocol-specific string and may contain spaces;
  • the BRC-43 security level remains the first element of the normal protocolID tuple;
  • keyID remains the ordinary, separately supplied BRC-43 key ID;
  • counterparty remains the ordinary, separately supplied BRC-43 counterparty;
  • privileged, privilegedReason, and seekPermission retain their existing BRC-100 meanings.

If retaining the shorter form is preferable, apply could be the default:

p ecpm <pointHex> <logicalProtocolID>

with an explicit form for removal. I slightly prefer always including apply or remove, because it makes parsing and auditing unambiguous.

The result still conforms to the existing getPublicKey return shape. A compressed public key is simply an encoded secp256k1 point, so { publicKey: PubKeyHex } carries the result without a new type or wire representation.

Internal derivation semantics

The point and operation must be excluded from the derived key’s identity.

Given:

p ecpm apply <pointHex> <logicalProtocolID>

the module should derive the scalar using a canonical module-specific derivation namespace equivalent to:

protocolID: [
  securityLevel,
  `p ecpm ${logicalProtocolID}`
]
keyID
counterparty
privileged

It then computes:

apply:   d · P
remove:  d⁻¹ · P

This distinction is load-bearing. If <pointHex> or apply/remove is included in the BRC-42 invoice used to derive d, the wallet derives a different scalar for every intermediate point. Masks would no longer commute, and applying and removing a mask would not select the same key.

The canonical derivation domain should nevertheless retain p ecpm so these keys cannot collide with ordinary signing, encryption, HMAC, payment, or application keys using the same logical protocol and key ID.

For privileged requests, the same operation should be performed using the wallet’s privileged derivation root, exactly as existing key-related methods already do. Applying and removing a privileged mask must use the same privileged value, counterparty, logical protocol, and key ID.

identityKey: true should be rejected. forSelf should either be rejected or required to be absent/false because the module already defines exactly which derived scalar is being used, and overloading forSelf would introduce needless ambiguity.

Why this is an intended P-module use

The module system already routes P-protocol uses of getPublicKey, encryption, HMAC, and signature methods through scheme-specific request and response handlers. Those handlers may validate or transform the request and transform the response.

It would be useful for this BRC to state explicitly that “transform” includes scheme-defined method semantics. A P-module is therefore not limited to deciding whether a normal BRC-100 operation is authorized. It may define what an existing method means inside its reserved namespace, subject to the method’s existing transport shape.

That interpretation gives BRC-98 an important architectural purpose: it becomes the extension mechanism for wallet capabilities without changing BRC-100 itself. Otherwise the ecosystem will continue adding optional members and call codes whenever a new wallet-native operation is required.

The current PermissionsModule adapter may need a wallet-internal execution hook because request/response transformation alone cannot calculate (dP) from (dG) and arbitrary (P). That is an implementation concern rather than an application-interface change. A wallet could expose a restricted internal capability to the trusted module, such as:

multiplyDerivedPoint({
  protocolID,
  keyID,
  counterparty,
  privileged,
  point,
  invert
})

The module should receive this narrow capability, not unrestricted access to the wallet’s root key or raw KeyDeriver.

This also fixes a key-isolation problem

The current security argument says that a key reachable through multiplyPoint never signs or encrypts. That is the desired rule, but ordinary BRC-43 derivation does not enforce it.

Without a module-specific namespace and method policy, an application can call createSignature, encrypt, decrypt, or HMAC methods with exactly the same:

protocolID + keyID + counterparty + privileged

Those methods derive the same private key. This matters because (dQ) is the ECDH shared secret for derived private key (d) and point (Q). The proposal correctly identifies key reuse as the real security risk, but the proposed standalone method does not itself prevent that reuse.

An ecpm module can enforce the required key type:

  • permit only the declared EC point operations;
  • reject signing under p ecpm;
  • reject ordinary BRC-2 encryption/decryption under p ecpm;
  • reject HMAC operations under p ecpm;
  • reject linkage revelation unless BRC-229 deliberately defines it;
  • never use the EC point multiplication key for spending or identity functions.

Thus the module does more than preserve interface compatibility: it makes the proposal’s “this key is only used for point multiplication” security invariant enforceable.

Clarification concerning getPublicKey

The current text says that no companion derivation method is required because getPublicKey returns (dG) for the same derivation arguments.

That needs qualification when an explicit counterparty is supplied. In the current BRC-42 implementation, normal getPublicKey behavior may return the counterparty’s derived public key:

P + hG

rather than the public key corresponding to the wallet’s derived private scalar:

(x + h)G = dG

The latter ordinarily requires self-derivation or forSelf: true.

The module proposal avoids that ambiguity. Under p ecpm, the input point is explicit and the result is defined directly:

apply(P)  = dP
remove(P) = d⁻¹P

Calling apply with the generator (G) naturally returns (dG), so no additional method is needed.

Why counterparty naming alone is insufficient

BRC-43 lets the caller name (P) as the counterparty, but that does not make existing derivation operate with (P) as the base point.

Let the wallet identity scalar be (x), the named counterparty point be (P), and:

h = HMAC(xP, invoice)
d = x + h

Existing BRC-42 public derivation can produce:

dG = (x + h)G

or the counterparty child:

P + hG

The required operation is:

dP = (x + h)P = xP + hP

Naming P causes it to participate in the ECDH value used by the HMAC. It does not replace (G) with (P) in the final scalar multiplication.

If the caller knows (P=pG) and knows (p), it can compute:

dP = p(dG)

That handles points whose discrete logarithm is already known, such as initial small card encodings. It does not handle arbitrary intermediate points after another participant has masked them, and it provides no way to calculate (d^{-1}P). The module primitive is therefore still necessary.

Validation and permissions

The current validation requirements should be retained unchanged:

  • exactly 33 bytes;
  • compressed prefix 02 or 03;
  • canonical encoded coordinate checked before invoking a parser that may reduce modulo (p);
  • valid secp256k1 point;
  • not the point at infinity;
  • reject a result at infinity.

The module should own the authorization semantics for its P-protocol, including originator identification, security levels, counterparty-specific grants, privileged access, privilegedReason, and seekPermission.

Because these protocols require hundreds of operations, the module should support protocol-level or grouped authorization instead of prompting for each point. This is another reason the P-module model is appropriate: the permission behavior belongs with the capability.

Compatibility and feature detection

This design requires:

  • no new BRC-100 member;
  • no call code 29;
  • no changes to Wallet Wire serialization;
  • no changes to every substrate and structural implementer;
  • no claim that old wallets have become non-conforming.

A wallet with the ecpm module processes the request. A wallet without it rejects p ecpm as an unsupported P-scheme, as BRC-98 already requires. Applications can handle that error and fall back to an application-held key or a trusted-dealer protocol.

In short, I recommend retaining the cryptographic and validation work in this proposal, but defining it as the ecpm BRC-98 module rather than adding multiplyPoint to BRC-100. This preserves BRC-100’s immutability, gives BRC-98 a concrete capability-extension role, provides enforceable key isolation, carries privileged and counterparty semantics through existing arguments, and avoids changing every wallet transport for one optional primitive.

@ty-everett ty-everett left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested full replacement: specify BRC-229 as the ecpm BRC-98 semantic module rather than adding an optional BRC-100 method. This keeps BRC-100 and its Wallet Wire unchanged, retains key ID/counterparty/privileged controls in their existing fields, defines apply/remove over one canonical derived scalar, and explains precisely why naming the point as a pure BRC-43 counterparty is not equivalent. The companion reference implementation has been revised in bsv-blockchain/ts-stack#488.

Comment thread wallet/0229.md
Comment on lines +1 to +211
# BRC-229: Wallet-Native Elliptic Curve Point Multiplication

Connor Murray (connor.murray@bsvassociation.org)

## Abstract

This proposal adds a single method, `multiplyPoint`, to the [BRC-100](./0100.md) wallet interface. It multiplies a caller-supplied secp256k1 point by a [BRC-42](../key-derivation/0042.md)/[BRC-43](../key-derivation/0043.md) derived private key and returns the resulting point, without ever disclosing the key. An `invert` flag multiplies by the modular inverse of that key, so a transformation applied by the wallet can be removed by the wallet.

This is the smallest addition that lets commutative-masking protocols — mental poker, verifiable shuffles, oblivious transfer, threshold key exchange — run inside a BRC-100 wallet instead of requiring an application to hold raw private keys of its own.

## Motivation

BRC-100 gives an application a complete vocabulary for asking a wallet to act on the user's keys: sign a transaction, produce a signature, encrypt, HMAC, derive a public key. Every one of those operations is defined over data the wallet chooses to interpret. None of them expose the underlying group operation.

That gap has a concrete consequence. A class of multi-party protocols is built on the commutativity of scalar multiplication:

```
a·(b·P) == b·(a·P)
```

Barnett–Smart mental poker is the canonical example. Each participant masks every card with a secret scalar; because masking commutes, the participants can apply their masks in any order and later strip them in any order. No participant ever learns a card another participant was dealt, and no dealer exists to trust. This is the standard construction for trustless card games, and the same primitive underlies verifiable shuffles and several oblivious-transfer constructions.

A BRC-100 wallet cannot participate in any of it. `getPublicKey` returns `d·G` for a derived key — the generator multiplied by the scalar — but there is no way to ask for `d·P` for an arbitrary point `P`. So an application that needs commutative masking must generate and hold its own secp256k1 keys outside the wallet, in application storage, with application-grade key hygiene.

The result is the outcome BRC-100 exists to prevent. The user has a wallet that protects their keys, and the application keeps a second set of keys next to it that nothing protects. In the mental-poker case those keys are exactly what privacy of the user's hand depends on: whoever holds the masking scalars can read every card. Moving them into the wallet moves them behind the wallet's existing consent, storage, and backup guarantees.

`multiplyPoint` closes the gap with one method and no new cryptographic assumptions. It exposes the group operation the curve already provides, under the key derivation BRC-100 already mandates.

## Specification

### Method

A wallet implementing this specification exposes the following method, in keeping with the
[BRC-100](./0100.md) interface conventions. It is declared **optional** on the interface, for the
reason given in [Optionality](#optionality) — that requirement is normative and is not a matter of
implementation convenience:

```ts
/**
* Multiplies a caller-supplied secp256k1 point by a derived private key, returning the
* resulting point. The private key is never revealed.
*
* @param {Object} args - Contains the point, the protocol and key IDs for derivation, and options.
* @param {PubKeyHex} args.point - The point to multiply, as a compressed DER-encoded secp256k1 point.
* @param {[0 | 1 | 2, ProtocolString5To400Characters]} args.protocolID - BRC-43 security level and protocol ID.
* @param {KeyIDStringUnder800Characters} args.keyID - BRC-43 key ID.
* @param {PubKeyHex | 'self' | 'anyone'} [args.counterparty] - Counterparty for derivation. Default 'self'.
* @param {BooleanDefaultFalse} [args.invert] - Multiply by the modular inverse of the derived key instead.
* @param {BooleanDefaultFalse} [args.privileged] - Whether this is a privileged request.
* @param {DescriptionString5to50Characters} [args.privilegedReason] - Reason for privileged access.
* @param {BooleanDefaultTrue} [args.seekPermission] - Whether to seek user permission if required.
* @param {OriginatorDomainNameString} [originator] - FQDN of the originating application.
* @returns {Promise<Object>} Resolves to the resulting point, or an error response.
*/
multiplyPoint?: (
args: {
point: PubKeyHex
protocolID: [0 | 1 | 2, ProtocolString5To400Characters]
keyID: KeyIDStringUnder800Characters
counterparty?: PubKeyHex | 'self' | 'anyone'
invert?: BooleanDefaultFalse
privileged?: BooleanDefaultFalse
privilegedReason?: DescriptionString5to50Characters
seekPermission?: BooleanDefaultTrue
},
originator?: OriginatorDomainNameString
) => Promise<{ point: PubKeyHex }>
```

No companion "derive this point" method is specified, because `getPublicKey` already provides it: with the same `protocolID`, `keyID`, and `counterparty`, `getPublicKey` returns `d·G`, which is the derived point for the generator.

### Optionality

`multiplyPoint` **MUST** be declared as an optional member of the wallet interface, and an
application **MUST** feature-detect it rather than assume its presence:

```ts
if (typeof wallet.multiplyPoint === 'function') {
// wallet-native masking is available
} else {
// degrade: the protocol cannot use a wallet-held masking key
}
```

A wallet that does not implement the method is fully conformant with [BRC-100](./0100.md); it is
simply not usable for the protocols this specification enables.

This is not a stylistic preference, and the requirement was discovered rather than assumed. BRC-100
is specified as an *unchanging* interface, which has a consequence for every later addition: a
method declared mandatory retroactively invalidates every wallet and every substrate already
shipped against the interface. When the reference implementation first declared `multiplyPoint`
required, the TypeScript compiler rejected **23 call sites** across the SDK — `WalletClient`,
`HTTPWalletJSON`, `WalletWireTransceiver`, `window.CWI`, `XDM` and `ReactNativeWebView`, together
with the key-value store, registry and identity clients — none of which have any reason to perform
elliptic curve point multiplication.

Any future BRC adding a method to the BRC-100 interface faces the same constraint. Optionality plus
feature detection is the only way to extend an interface whose central promise is that it does not
change.

### Key derivation

1. The private key used MUST be derived per [BRC-42](../key-derivation/0042.md) and [BRC-43](../key-derivation/0043.md) from `protocolID`, `keyID`, and `counterparty`, using the same derivation `getPublicKey` uses for the same arguments.
2. Wallets MUST NOT use the user's identity key, any change or spending key, or any [BRC-44](../key-derivation/0044.md) internal protocol key.
3. Wallets MUST reject protocol IDs reserved by [BRC-98](./0098.md) unless they support the named scheme, as for any other BRC-43 operation.

Point 2 is the load-bearing requirement, not a stylistic preference. See [Security](#security).

### Input validation

Given a supplied point, a conforming wallet MUST reject the request unless all of the following hold. Each check corresponds to a real attack; none may be skipped.

1. The encoding is a valid compressed DER secp256k1 point (33 bytes, leading byte `0x02` or `0x03`).
2. Both decoded affine coordinates are canonical field elements — each in the range `[0, p)`, where `p` is the secp256k1 field prime.
3. The decoded point satisfies the curve equation `y² = x³ + 7 (mod p)`.
4. The point is not the identity (point at infinity).

Check 2 is **not** implied by check 3, and omitting it is the most likely way to build a
non-conforming implementation.

This is verified rather than hypothesised. In both reference implementations, the compressed point
`02` followed by thirty-two `0xff` bytes — an x-coordinate numerically greater than the field prime —
is accepted by the library's point parser, silently reduced modulo `p` (to `0x1000003d0`), and then
reported as on-curve:

| Library | Parser | Result of on-curve check |
| --- | --- | --- |
| `@bsv/sdk` (TypeScript) | `PublicKey.fromString` | `validate()` returns `true` |
| `go-sdk` (Go) | `ec.PublicKeyFromString` | `IsOnCurve` returns `true` |

Two independently written libraries share the behaviour, so an implementer following only the curve
equation will accept a point that was never validly encoded. That is the entry point for the
invalid-curve attack described under [Security](#security). Both reference implementations were
first written with this defect and a test caught it in each.

Because the parser is what performs the reduction, a conforming implementation **MUST** perform the
range check on the encoded coordinate *before* handing it to the parser. Validating the parsed
point is too late: by then the out-of-range value has already become a different, valid point.

### Result

1. On success the wallet returns the resulting point, compressed DER-encoded.
2. When `invert` is false, the result is `d·P`, where `d` is the derived key and `P` the supplied point.
3. When `invert` is true, the result is `d⁻¹·P`, the inverse taken modulo the curve order `n`.
4. `multiplyPoint` with `invert: true` applied to the output of `multiplyPoint` with `invert: false` under the same derivation arguments MUST return the original point.
5. If the operation would produce the identity, the wallet MUST return an error rather than an encoding of the identity.

### Permissions

1. The operation is subject to the same permission and consent machinery as any other BRC-100 protocol operation, including [BRC-73](./0073.md) grouped permissions and [BRC-116](./0116.md) permission lifecycle behavior.
2. A wallet MAY treat a granted permission for a protocol as covering repeated calls under that protocol. Protocols of this kind are inherently high-volume: dealing a 52-card deck among 6 players is several hundred point multiplications, and prompting per operation would make the method unusable.
3. Because the derived key is protocol-scoped, a permission grant for one protocol conveys no ability to operate under another.

## Security

### This is not a signing oracle

The obvious objection to "multiply this arbitrary point by my key" is that it looks like an oracle an attacker can query to recover the key. It is not, for two independent reasons.

**Recovering the scalar from the output is the elliptic curve discrete logarithm problem.** Given `P` and `d·P`, finding `d` is exactly the assumption secp256k1 rests on. An attacker who could do this could equally take any public key and recover its private key. Choosing `P` adaptively does not help: for any chosen `P = k·G` the response is `k·(d·G)`, which the attacker could have computed from `getPublicKey`'s output alone. Chosen-point queries therefore yield nothing that public information does not already yield.

**There is no signature equation to attack.** ECDSA is fragile under partial-information leakage because the same private key appears in `s = k⁻¹(z + r·d)`, so biased or leaked nonces yield lattice attacks recovering `d`. A key reachable only through `multiplyPoint` never signs. There is no `k`, no `s`, and no equation relating the key to a message. The lattice attacks that make signing oracles dangerous have no analogue here.

### The real risk is key reuse, and it is why derivation is mandatory

The genuine danger is not the operation but the key it is performed with. Note that for a point `Q` belonging to a counterparty, `d·Q` **is** the ECDH shared secret between `d` and `Q`.

If a wallet implemented `multiplyPoint` over a key used for anything else, the method would hand any caller the shared secret for that key — breaking [BRC-2](./0002.md) encryption to that counterparty, and any HMAC or key-linkage guarantee derived from it. Implemented over the identity key, it would compromise the user's identity-level ECDH secrets wholesale.

Mandatory BRC-42/43 derivation is precisely what prevents this. A key that exists only under a protocol-scoped derivation, and is never used to sign or to encrypt, has no other security property to lose. Its only capability is the one the application asked for. This is why point 2 under [Key derivation](#key-derivation) is a MUST rather than a SHOULD, and why no `identityKey` option is offered by analogy with `getPublicKey`.

### Key linkage

`multiplyPoint` reveals no more about key relationships than `getPublicKey` already does. Deliberate disclosure of key linkage remains the province of `revealCounterpartyKeyLinkage` and `revealSpecificKeyLinkage`, protected as described in [BRC-72](../key-derivation/0072.md). A wallet MUST NOT treat a `multiplyPoint` grant as authority to reveal linkage.

### Invalid-curve attacks

The validation rules above are the defense against the standard invalid-curve attack, in which a point on a different curve — one with smooth order — is submitted so that the response leaks the key modulo small factors. Rejecting non-canonical encodings, off-curve points, and the identity closes it. This is why those rules are normative requirements rather than implementation advice.

## Implementations

Two reference implementations exist.

**TypeScript**, in the BSV Association SDK, proposed at
https://github.com/bsv-blockchain/ts-stack/pull/487 — implemented in `ProtoWallet` using the
`KeyDeriver`, `Point.mul` and `BigNumber.invm` primitives the package already provides. Verified
against that repository's own tooling: `tsc -b` clean, `oxlint --deny-warnings` clean, and the full
SDK suite green at 157 suites and 5924 tests.

**Go**, at
https://github.com/connormurray2/brc100-poker/tree/main/internal/brc/points

It was written against `go-sdk` curve primitives as part of a non-custodial poker application, where a dealerless deal is the motivating use case. The tests assert that masks applied by independent wallets commute, that `invert` recovers the original point, that a three-way mask can be stripped in any order, that distinct protocol and key IDs yield independent derivations, that two different wallets never derive the same protocol key, and that a whole-deck mask with a selective unmask leaves the other positions unreadable.

The validation rules in this specification are drawn from that work rather than asserted in advance. The first draft validated points with an on-curve check alone, and a test submitting a compressed point whose x-coordinate is thirty-two `0xff` bytes — a value greater than the secp256k1 field prime — was accepted: the parser reduced the coordinate silently and the on-curve test then passed. The canonical-encoding requirement exists because that hole is easy to ship.

No wallet implements `multiplyPoint` at the time of writing. Applications needing commutative masking today must either hold keys outside the wallet or degrade to a trusted dealer; this proposal exists to remove that choice.

## References

- [BRC-2: Data Encryption and Decryption](./0002.md)
- [BRC-42: BSV Key Derivation Scheme (BKDS)](../key-derivation/0042.md)
- [BRC-43: Security Levels, Protocol IDs, Key IDs and Counterparties](../key-derivation/0043.md)
- [BRC-44: Admin-reserved and Prohibited Key Derivation Protocols](../key-derivation/0044.md)
- [BRC-72: Protecting BRC-69 Key Linkage Information in Transit](../key-derivation/0072.md)
- [BRC-73: Group Permissions for App Access](./0073.md)
- [BRC-98: P Protocols: Allowing Future Wallet Protocol Permission Schemes](./0098.md)
- [BRC-100: Unified, Vendor-Neutral, Unchanging, and Open BSV Blockchain Standard Wallet-to-Application Interface](./0100.md)
- [BRC-116: Wallet Permissions and Counterparty Trust](./0116.md)
- <a name="footnote-1">1</a>: Barnett, A. and Smart, N. (2003). Mental Poker Revisited. Cryptography and Coding, LNCS 2898, pp. 370–383.
- <a name="footnote-2">2</a>: Biehl, I., Meyer, B. and Müller, V. (2000). Differential Fault Attacks on Elliptic Curve Cryptosystems. CRYPTO 2000, LNCS 1880, pp. 131–146. (Invalid-curve attacks.)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
# BRC-229: Wallet-Native Elliptic Curve Point Multiplication
Connor Murray (connor.murray@bsvassociation.org)
## Abstract
This proposal adds a single method, `multiplyPoint`, to the [BRC-100](./0100.md) wallet interface. It multiplies a caller-supplied secp256k1 point by a [BRC-42](../key-derivation/0042.md)/[BRC-43](../key-derivation/0043.md) derived private key and returns the resulting point, without ever disclosing the key. An `invert` flag multiplies by the modular inverse of that key, so a transformation applied by the wallet can be removed by the wallet.
This is the smallest addition that lets commutative-masking protocols — mental poker, verifiable shuffles, oblivious transfer, threshold key exchange — run inside a BRC-100 wallet instead of requiring an application to hold raw private keys of its own.
## Motivation
BRC-100 gives an application a complete vocabulary for asking a wallet to act on the user's keys: sign a transaction, produce a signature, encrypt, HMAC, derive a public key. Every one of those operations is defined over data the wallet chooses to interpret. None of them expose the underlying group operation.
That gap has a concrete consequence. A class of multi-party protocols is built on the commutativity of scalar multiplication:
```
a·(b·P) == b·(a·P)
```
Barnett–Smart mental poker is the canonical example. Each participant masks every card with a secret scalar; because masking commutes, the participants can apply their masks in any order and later strip them in any order. No participant ever learns a card another participant was dealt, and no dealer exists to trust. This is the standard construction for trustless card games, and the same primitive underlies verifiable shuffles and several oblivious-transfer constructions.
A BRC-100 wallet cannot participate in any of it. `getPublicKey` returns `d·G` for a derived key — the generator multiplied by the scalar — but there is no way to ask for `d·P` for an arbitrary point `P`. So an application that needs commutative masking must generate and hold its own secp256k1 keys outside the wallet, in application storage, with application-grade key hygiene.
The result is the outcome BRC-100 exists to prevent. The user has a wallet that protects their keys, and the application keeps a second set of keys next to it that nothing protects. In the mental-poker case those keys are exactly what privacy of the user's hand depends on: whoever holds the masking scalars can read every card. Moving them into the wallet moves them behind the wallet's existing consent, storage, and backup guarantees.
`multiplyPoint` closes the gap with one method and no new cryptographic assumptions. It exposes the group operation the curve already provides, under the key derivation BRC-100 already mandates.
## Specification
### Method
A wallet implementing this specification exposes the following method, in keeping with the
[BRC-100](./0100.md) interface conventions. It is declared **optional** on the interface, for the
reason given in [Optionality](#optionality) — that requirement is normative and is not a matter of
implementation convenience:
```ts
/**
* Multiplies a caller-supplied secp256k1 point by a derived private key, returning the
* resulting point. The private key is never revealed.
*
* @param {Object} args - Contains the point, the protocol and key IDs for derivation, and options.
* @param {PubKeyHex} args.point - The point to multiply, as a compressed DER-encoded secp256k1 point.
* @param {[0 | 1 | 2, ProtocolString5To400Characters]} args.protocolID - BRC-43 security level and protocol ID.
* @param {KeyIDStringUnder800Characters} args.keyID - BRC-43 key ID.
* @param {PubKeyHex | 'self' | 'anyone'} [args.counterparty] - Counterparty for derivation. Default 'self'.
* @param {BooleanDefaultFalse} [args.invert] - Multiply by the modular inverse of the derived key instead.
* @param {BooleanDefaultFalse} [args.privileged] - Whether this is a privileged request.
* @param {DescriptionString5to50Characters} [args.privilegedReason] - Reason for privileged access.
* @param {BooleanDefaultTrue} [args.seekPermission] - Whether to seek user permission if required.
* @param {OriginatorDomainNameString} [originator] - FQDN of the originating application.
* @returns {Promise<Object>} Resolves to the resulting point, or an error response.
*/
multiplyPoint?: (
args: {
point: PubKeyHex
protocolID: [0 | 1 | 2, ProtocolString5To400Characters]
keyID: KeyIDStringUnder800Characters
counterparty?: PubKeyHex | 'self' | 'anyone'
invert?: BooleanDefaultFalse
privileged?: BooleanDefaultFalse
privilegedReason?: DescriptionString5to50Characters
seekPermission?: BooleanDefaultTrue
},
originator?: OriginatorDomainNameString
) => Promise<{ point: PubKeyHex }>
```
No companion "derive this point" method is specified, because `getPublicKey` already provides it: with the same `protocolID`, `keyID`, and `counterparty`, `getPublicKey` returns `d·G`, which is the derived point for the generator.
### Optionality
`multiplyPoint` **MUST** be declared as an optional member of the wallet interface, and an
application **MUST** feature-detect it rather than assume its presence:
```ts
if (typeof wallet.multiplyPoint === 'function') {
// wallet-native masking is available
} else {
// degrade: the protocol cannot use a wallet-held masking key
}
```
A wallet that does not implement the method is fully conformant with [BRC-100](./0100.md); it is
simply not usable for the protocols this specification enables.
This is not a stylistic preference, and the requirement was discovered rather than assumed. BRC-100
is specified as an *unchanging* interface, which has a consequence for every later addition: a
method declared mandatory retroactively invalidates every wallet and every substrate already
shipped against the interface. When the reference implementation first declared `multiplyPoint`
required, the TypeScript compiler rejected **23 call sites** across the SDK — `WalletClient`,
`HTTPWalletJSON`, `WalletWireTransceiver`, `window.CWI`, `XDM` and `ReactNativeWebView`, together
with the key-value store, registry and identity clients — none of which have any reason to perform
elliptic curve point multiplication.
Any future BRC adding a method to the BRC-100 interface faces the same constraint. Optionality plus
feature detection is the only way to extend an interface whose central promise is that it does not
change.
### Key derivation
1. The private key used MUST be derived per [BRC-42](../key-derivation/0042.md) and [BRC-43](../key-derivation/0043.md) from `protocolID`, `keyID`, and `counterparty`, using the same derivation `getPublicKey` uses for the same arguments.
2. Wallets MUST NOT use the user's identity key, any change or spending key, or any [BRC-44](../key-derivation/0044.md) internal protocol key.
3. Wallets MUST reject protocol IDs reserved by [BRC-98](./0098.md) unless they support the named scheme, as for any other BRC-43 operation.
Point 2 is the load-bearing requirement, not a stylistic preference. See [Security](#security).
### Input validation
Given a supplied point, a conforming wallet MUST reject the request unless all of the following hold. Each check corresponds to a real attack; none may be skipped.
1. The encoding is a valid compressed DER secp256k1 point (33 bytes, leading byte `0x02` or `0x03`).
2. Both decoded affine coordinates are canonical field elements — each in the range `[0, p)`, where `p` is the secp256k1 field prime.
3. The decoded point satisfies the curve equation `y² = x³ + 7 (mod p)`.
4. The point is not the identity (point at infinity).
Check 2 is **not** implied by check 3, and omitting it is the most likely way to build a
non-conforming implementation.
This is verified rather than hypothesised. In both reference implementations, the compressed point
`02` followed by thirty-two `0xff` bytes — an x-coordinate numerically greater than the field prime —
is accepted by the library's point parser, silently reduced modulo `p` (to `0x1000003d0`), and then
reported as on-curve:
| Library | Parser | Result of on-curve check |
| --- | --- | --- |
| `@bsv/sdk` (TypeScript) | `PublicKey.fromString` | `validate()` returns `true` |
| `go-sdk` (Go) | `ec.PublicKeyFromString` | `IsOnCurve` returns `true` |
Two independently written libraries share the behaviour, so an implementer following only the curve
equation will accept a point that was never validly encoded. That is the entry point for the
invalid-curve attack described under [Security](#security). Both reference implementations were
first written with this defect and a test caught it in each.
Because the parser is what performs the reduction, a conforming implementation **MUST** perform the
range check on the encoded coordinate *before* handing it to the parser. Validating the parsed
point is too late: by then the out-of-range value has already become a different, valid point.
### Result
1. On success the wallet returns the resulting point, compressed DER-encoded.
2. When `invert` is false, the result is `d·P`, where `d` is the derived key and `P` the supplied point.
3. When `invert` is true, the result is `d⁻¹·P`, the inverse taken modulo the curve order `n`.
4. `multiplyPoint` with `invert: true` applied to the output of `multiplyPoint` with `invert: false` under the same derivation arguments MUST return the original point.
5. If the operation would produce the identity, the wallet MUST return an error rather than an encoding of the identity.
### Permissions
1. The operation is subject to the same permission and consent machinery as any other BRC-100 protocol operation, including [BRC-73](./0073.md) grouped permissions and [BRC-116](./0116.md) permission lifecycle behavior.
2. A wallet MAY treat a granted permission for a protocol as covering repeated calls under that protocol. Protocols of this kind are inherently high-volume: dealing a 52-card deck among 6 players is several hundred point multiplications, and prompting per operation would make the method unusable.
3. Because the derived key is protocol-scoped, a permission grant for one protocol conveys no ability to operate under another.
## Security
### This is not a signing oracle
The obvious objection to "multiply this arbitrary point by my key" is that it looks like an oracle an attacker can query to recover the key. It is not, for two independent reasons.
**Recovering the scalar from the output is the elliptic curve discrete logarithm problem.** Given `P` and `d·P`, finding `d` is exactly the assumption secp256k1 rests on. An attacker who could do this could equally take any public key and recover its private key. Choosing `P` adaptively does not help: for any chosen `P = k·G` the response is `k·(d·G)`, which the attacker could have computed from `getPublicKey`'s output alone. Chosen-point queries therefore yield nothing that public information does not already yield.
**There is no signature equation to attack.** ECDSA is fragile under partial-information leakage because the same private key appears in `s = k⁻¹(z + r·d)`, so biased or leaked nonces yield lattice attacks recovering `d`. A key reachable only through `multiplyPoint` never signs. There is no `k`, no `s`, and no equation relating the key to a message. The lattice attacks that make signing oracles dangerous have no analogue here.
### The real risk is key reuse, and it is why derivation is mandatory
The genuine danger is not the operation but the key it is performed with. Note that for a point `Q` belonging to a counterparty, `d·Q` **is** the ECDH shared secret between `d` and `Q`.
If a wallet implemented `multiplyPoint` over a key used for anything else, the method would hand any caller the shared secret for that key — breaking [BRC-2](./0002.md) encryption to that counterparty, and any HMAC or key-linkage guarantee derived from it. Implemented over the identity key, it would compromise the user's identity-level ECDH secrets wholesale.
Mandatory BRC-42/43 derivation is precisely what prevents this. A key that exists only under a protocol-scoped derivation, and is never used to sign or to encrypt, has no other security property to lose. Its only capability is the one the application asked for. This is why point 2 under [Key derivation](#key-derivation) is a MUST rather than a SHOULD, and why no `identityKey` option is offered by analogy with `getPublicKey`.
### Key linkage
`multiplyPoint` reveals no more about key relationships than `getPublicKey` already does. Deliberate disclosure of key linkage remains the province of `revealCounterpartyKeyLinkage` and `revealSpecificKeyLinkage`, protected as described in [BRC-72](../key-derivation/0072.md). A wallet MUST NOT treat a `multiplyPoint` grant as authority to reveal linkage.
### Invalid-curve attacks
The validation rules above are the defense against the standard invalid-curve attack, in which a point on a different curve — one with smooth order — is submitted so that the response leaks the key modulo small factors. Rejecting non-canonical encodings, off-curve points, and the identity closes it. This is why those rules are normative requirements rather than implementation advice.
## Implementations
Two reference implementations exist.
**TypeScript**, in the BSV Association SDK, proposed at
https://github.com/bsv-blockchain/ts-stack/pull/487 — implemented in `ProtoWallet` using the
`KeyDeriver`, `Point.mul` and `BigNumber.invm` primitives the package already provides. Verified
against that repository's own tooling: `tsc -b` clean, `oxlint --deny-warnings` clean, and the full
SDK suite green at 157 suites and 5924 tests.
**Go**, at
https://github.com/connormurray2/brc100-poker/tree/main/internal/brc/points
It was written against `go-sdk` curve primitives as part of a non-custodial poker application, where a dealerless deal is the motivating use case. The tests assert that masks applied by independent wallets commute, that `invert` recovers the original point, that a three-way mask can be stripped in any order, that distinct protocol and key IDs yield independent derivations, that two different wallets never derive the same protocol key, and that a whole-deck mask with a selective unmask leaves the other positions unreadable.
The validation rules in this specification are drawn from that work rather than asserted in advance. The first draft validated points with an on-curve check alone, and a test submitting a compressed point whose x-coordinate is thirty-two `0xff` bytes — a value greater than the secp256k1 field prime — was accepted: the parser reduced the coordinate silently and the on-curve test then passed. The canonical-encoding requirement exists because that hole is easy to ship.
No wallet implements `multiplyPoint` at the time of writing. Applications needing commutative masking today must either hold keys outside the wallet or degrade to a trusted dealer; this proposal exists to remove that choice.
## References
- [BRC-2: Data Encryption and Decryption](./0002.md)
- [BRC-42: BSV Key Derivation Scheme (BKDS)](../key-derivation/0042.md)
- [BRC-43: Security Levels, Protocol IDs, Key IDs and Counterparties](../key-derivation/0043.md)
- [BRC-44: Admin-reserved and Prohibited Key Derivation Protocols](../key-derivation/0044.md)
- [BRC-72: Protecting BRC-69 Key Linkage Information in Transit](../key-derivation/0072.md)
- [BRC-73: Group Permissions for App Access](./0073.md)
- [BRC-98: P Protocols: Allowing Future Wallet Protocol Permission Schemes](./0098.md)
- [BRC-100: Unified, Vendor-Neutral, Unchanging, and Open BSV Blockchain Standard Wallet-to-Application Interface](./0100.md)
- [BRC-116: Wallet Permissions and Counterparty Trust](./0116.md)
- <a name="footnote-1">1</a>: Barnett, A. and Smart, N. (2003). Mental Poker Revisited. Cryptography and Coding, LNCS 2898, pp. 370–383.
- <a name="footnote-2">2</a>: Biehl, I., Meyer, B. and Müller, V. (2000). Differential Fault Attacks on Elliptic Curve Cryptosystems. CRYPTO 2000, LNCS 1880, pp. 131–146. (Invalid-curve attacks.)
# BRC-229: Wallet-Native Elliptic Curve Point Multiplication as a BRC-98 Module
Connor Murray (connor.murray@bsvassociation.org)
## Abstract
This proposal defines `ecpm`, a [BRC-98](./0098.md) 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](./0100.md) `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:

1. Outside `p ecpm`, `getPublicKey` retains its ordinary BRC-100 meaning.
2. Inside `p ecpm`, a wallet either implements this scheme or rejects the request as BRC-98 already requires.
3. 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


where:

- `operation` is exactly `apply` or `remove`;
- `pointHex` is a lowercase, 66-character compressed secp256k1 public-key encoding;
- `logicalProtocolID` is the application protocol whose ECPM key universe is being requested.

The outer BRC-100 call remains:

```ts
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 protocol string also MUST fit the active BRC-100 protocol-string limit. A key ID MUST satisfy the active BRC-100 getPublicKey key-ID limit.

Canonical scalar derivation

Let:

  • s be the security level in the outer protocolID tuple;
  • L be logicalProtocolID;
  • K be the separately supplied keyID; and
  • C be the separately supplied counterparty, defaulting to self.

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:

  • apply returns d·P;
  • remove returns d⁻¹·P, where the inverse is computed modulo the secp256k1 group order n.

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:

  1. privilegedReason MUST be present and satisfy the existing BRC-100 description constraint.
  2. The wallet MUST obtain explicit authorization even at security level 0.
  3. The wallet MUST derive d from its privileged key material using the same canonical ECPM derivation tuple.
  4. The privileged root or key-derivation capability MUST NOT be exposed to the application.
  5. 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: false MUST 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:

  1. The encoding is exactly 33 bytes represented by 66 lowercase hexadecimal characters.
  2. The first byte is 02 or 03.
  3. The encoded x-coordinate is less than the secp256k1 field prime p.
  4. The encoding decodes to a point satisfying y² = x³ + 7 (mod p).
  5. 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: true behavior 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.

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants