Skip to content

Define V3 binary secret encoding#389

Open
Egge21M wants to merge 4 commits into
cashubtc:mainfrom
Egge21M:v3-secrets
Open

Define V3 binary secret encoding#389
Egge21M wants to merge 4 commits into
cashubtc:mainfrom
Egge21M:v3-secrets

Conversation

@Egge21M

@Egge21M Egge21M commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Description
Adds the general V3 binary secret envelope to NUT-00 and defines
the NUT-10 condition payload encoding for V3 secrets.

Changes:

  • Defines V3 Proof.secret as raw bytes.
  • Adds secret = SECRET_KIND || DATA.
  • Defines 0x00 random secrets and 0x01 condition secrets.
  • Adds the CBOR ConditionMap for NUT-10 spending conditions.
  • Replaces legacy data with pubkeys for P2PK and hash for HTLC.
  • Encodes legacy tags as dedicated integer keys, while allowing
    extension tags as string: any.

Co-authored-by: Rob Woodgate <robwoodgate@users.noreply.github.com>
Co-authored-by: Rob Woodgate <robwoodgate@users.noreply.github.com>
@Egge21M Egge21M marked this pull request as draft June 16, 2026 10:41

@robwoodgate robwoodgate 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.

Great work, Egge - you've captured everything we discussed really well.

I added one suggestion which was a tangential offline conversation.

Comment thread 00.md
| `0x00` | `random` | Random bytes |
| `0x01` | `condition` | CBOR-encoded spending condition data |

For `SECRET_KIND = 0x00`, `DATA` SHOULD be 32 bytes generated by a CSPRNG.

@robwoodgate robwoodgate Jun 16, 2026

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.

I think we were planning to make this change too for v3 secrets.

Suggested change
For `SECRET_KIND = 0x00`, `DATA` SHOULD be 32 bytes generated by a CSPRNG.
For `SECRET_KIND = 0x00`, `DATA` SHOULD be a random 33 byte compressed secp256k1 public key.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Can you elaborate on the usecase? Making a standard random secret 33 bytes would mean it becomes distinguishable from deterministic secrets

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.

This was proposed to allow future possibilities, such as FROST.

Comment thread 10.md
Comment thread 10.md
@robwoodgate

robwoodgate commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator

I just noticed the ConditionMap excludes kind now, I know we discussed the presence of hash as being a definitive marker between P2PK and HTLC, but I wonder if we need to keep kind for future NUT-10 kinds that may cherry-pick only certain items from the ConditionMap and may look like another type to a parser.

Eg: a future NUT-10 type that uses locktime but not pubkeys might be flagged as an invalid P2PK condition, whereas it is in fact valid for that specific custom kind.

@Egge21M Egge21M marked this pull request as ready for review June 29, 2026 08:28
@robwoodgate

robwoodgate commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator

Following offline discussions not to delay BLS for binary secrets,I had my clanker do a deeper review, bringing in #371 as this PR needs to build on the BLS. Here are the findings:


Reviewed this against the current NUT-00/10/11/13/14 text and the BLS v3 keyset work. The envelope and CBOR maps read well. My main suggestion is structural: bind the binary format to its own keyset version byte rather than to "v3 keysets" in place, and don't gate the BLS keyset rollout on it. Then a few field-level gaps to close.

Structural: give binary its own version byte

This PR makes binary mandatory for every v3 keyset ("For V3 keysets, secret is a byte string") but only edits NUT-00 and NUT-10, while the BLS keyset spec keeps secret a string (TokenV4/Proof shapes unchanged, NUT-13 deterministic secret = raw 32-byte HMAC digest). If BLS ships string-secret first and binary later lands on the same version byte, a proof's secret format is no longer a function of its keyset id, and implementations are forced into per-proof string-vs-binary detection plus mixed-format SIG_ALL within one keyset.

Cleaner: let binary be a new keyset version profile (e.g. 03 = BLS + binary), so the version byte stays the sole discriminator. Each byte is a complete profile (id derivation + curve + secret format), dispatched once. No per-proof detection, no mixed-format-within-a-keyset, no need to retrofit binary transport onto already-live keysets. The single rule worth stating in the spec: a given keyset version MUST use exactly one secret format. This also unblocks shipping BLS keysets immediately with the well-understood string format.

Field-level gaps (independent of the above)

1. No transport encoding for the bytes (NUT-00). The secret is defined "before any transport encoding," but nothing downstream carries raw bytes. The mint JSON API (/v1/swap, /v1/melt) has Proof.secret as a JSON string. In the V4 cashuB token the proof secret field s is a CBOR text string (major type 3) and w is a text string; the bytes-as-hex rule in NUT-00 only applies to fields declared bytes (i, c, DLEQ e/s/r), so a raw-byte secret or a CBOR WitnessMap has no defined home without redefining s/w to bstr (or new fields). As written, binary proofs can't round-trip to a mint. Suggest pinning: hex (or base64url) in JSON, and bstr in CBOR.

2. CBOR MUST be canonical (NUT-00 / NUT-10). The exact secret bytes are hashed into Y = hash_to_curve(x), signed, and stored by the mint for double-spend. CBOR admits multiple encodings of one logical map (key order, non-minimal ints, indefinite lengths), so two encoders of the same ConditionMap produce different Y and different spent-state keys. This needs a MUST for deterministic encoding (RFC 8949 §4.2 core-deterministic), for both ConditionMap and WitnessMap.

This is not hypothetical: cashu-ts's current CBOR encoder (used for V4 tokens) is non-canonical and could not even produce these maps as-is. Its map encoder emits keys in object-insertion order with no sort (fails §4.2.1), and it only emits text-string keys, so it cannot encode the integer keys (08) the ConditionMap/WitnessMap require. Token CBOR has gotten away with this because token bytes are never hashed; the ConditionMap lives inside the hashed secret, so it has to be canonical. A spec MUST here prevents silent interop breakage across implementations.

3. NUT-13 deterministic-secret conflict. The BLS keyset work defines a v3 deterministic secret as "the raw 32-byte HMAC digest." This PR makes a v3 secret SECRET_KIND || DATA, so a deterministic random secret should be 0x00 || digest (33 bytes), not the bare digest. These disagree on what a restored secret is. Worth reconciling NUT-13, and stating whether/how a condition secret's nonce is deterministically derived.

4. SIG_ALL over binary secrets (NUT-11). Legacy SIG_ALL concatenates secret || C as strings and requires "same Secret.data and Secret.tags." With byte secrets, the aggregation (byte vs string concat, how raw-byte secrets mix with hex C/B_) and the equality predicate (now a ConditionMap comparison) are undefined.

5. data loses its distinguished first key. Folding the legacy primary data and the pubkeys tag into one pubkeys array with "no special first entry" changes the n-of-m counting (m = 1 + len(pubkeys tag) becomes m = len(pubkeys)) and every NUT-11 rule that references Secret.data. These should be restated in binary terms.

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

Labels

None yet

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

2 participants