Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for NUT-12 #200

Merged
merged 29 commits into from
Nov 13, 2024
Merged

Support for NUT-12 #200

merged 29 commits into from
Nov 13, 2024

Conversation

lollerfirst
Copy link

@lollerfirst lollerfirst commented Oct 29, 2024

Fixes: #156

Changes

  • added optional dleq and dleqValid fields to Proof. dleq holds a SerializedDLEQ proof, while dleqValid, if present, signals whether it has been correctly verified.
  • CashuWallet.meltProofs and CashuWallet.createSwapPayload strip the dleq field off of each Proof, if there is any. (No privacy leak).
  • CashuWallet.construct_proofs now unpacks and verifies DLEQ proofs returned by the Mint, if any. It will create Proof with:
    • dleqValid == true if the DLEQ is present and verified
    • dleqValid == undefined if DLEQ not present
    • dleqValid == false if DLEQ present and verification failed.
  • added boolean option includeDleq to CashuWallet.send:
    • if includeDleq == true, makes sure the selected send proofs each have a defined dleq field;
    • if includeDleq == false, strip off the dleq field from send;
    • if includeDleq == undefined, do not intervene.
  • added boolean option requireDleq to CashuWallet.receive:
    • if requireDleq == true, verifies each proof's dleq for correctness.
    • if requireDleq == false || requireDleq == undefined, does not verify dleq.
  • added private helper function CashuWallet.requireDLEQ. Separates the logic for requiring valid dleqs from a token from the function receive.
  • added numberToHexPadded64 to the utilities. This function takes in a bigint and returns an a hex string prefixed to 64 characters with '0'.
  • tests

PR Tasks

  • Open PR
  • run npm run test --> no failing unit tests
  • run npm run format

Comment on lines 1054 to 1061
if (verifyDLEQ) {
if (dleq == undefined) {
throw new Error('DLEQ verification required, but none found');
}
if (!verifyDLEQProof_reblind(secret, dleq, proof.C, A)) {
throw new Error('DLEQ verification failed');
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
if (verifyDLEQ) {
if (dleq == undefined) {
throw new Error('DLEQ verification required, but none found');
}
if (!verifyDLEQProof_reblind(secret, dleq, proof.C, A)) {
throw new Error('DLEQ verification failed');
}
}
if (verifyDLEQ) {
if (dleq == undefined) {
throw new Error('DLEQ verification required, but none found');
}
if (!verifyDLEQProof_reblind(secret, dleq, proof.C, A)) {
throw new Error('DLEQ verification failed');
}
}

It might be dangerous to throw an error here, we might still want to keep the proofs, even if the verification failed. I have no good solution for this case though.

Copy link
Collaborator

Choose a reason for hiding this comment

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

It might be dangerous to throw an error here

Agreed.

What if cashu-ts just constructs the dleq, then its up to the implementer to verify and handle failures?

The other idea I had was have some sort of flag either on the Proof or in the return value of constructProofs that indicates the verification failed, but that gets gross:

/**
 * represents a single Cashu proof.
 */
export type Proof = {
	...

         /**
	 * DLEQ proof
	 */
	dleq?: SerializedDLEQ;
        /**
        * Is the DLEQ proof valid
        */
         dleqValid?: boolan;
};

Copy link
Author

Choose a reason for hiding this comment

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

I really like @gudnuf 's idea

Copy link
Author

Choose a reason for hiding this comment

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

What do you think @callebtc

Copy link
Collaborator

Choose a reason for hiding this comment

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

nostr-tools uses a Symbol to signal verified events: https://github.com/nbd-wtf/nostr-tools/blob/master/core.ts

Copy link
Author

Choose a reason for hiding this comment

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

@Egge21M What changes from a normal boolean flag?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Symbols are a bit "safer" than regular properties. The are not enumerable and have to be accessed very purposefully. Also, because they are not enumerable they will be excluded by default by methods like JSON.stringify

@lollerfirst
Copy link
Author

Unfortunately it seems we need to fix cashu-crypto-ts's package.json again...

@lollerfirst
Copy link
Author

lollerfirst commented Nov 6, 2024

So the integration tests with Nutshell work fine. I might try and other ones better cover cases where DLEQ is not verified correctly.

I couldn't create normal tests (in wallet.test.ts) because the mocked keyset does not provide the corresponding private keys so I can't create the dleq for the mocked proof.

@Egge21M
Copy link
Collaborator

Egge21M commented Nov 7, 2024

Great stuff! LMK when this is ready for review

@Egge21M Egge21M linked an issue Nov 7, 2024 that may be closed by this pull request
@lollerfirst
Copy link
Author

FYI I tried using a symbol for dleqValid before but got some error.

@lollerfirst lollerfirst marked this pull request as ready for review November 7, 2024 11:38
Copy link
Collaborator

@Egge21M Egge21M left a comment

Choose a reason for hiding this comment

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

Great work and a fantastic start! I added some comments, but i think we are mostly there

}
): Promise<SendResponse> {
if (options?.includeDleq ?? false) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why use nullish coalescing here?

src/CashuWallet.ts Show resolved Hide resolved
src/CashuWallet.ts Show resolved Hide resolved
let { keep, send } = await this.swap(amount, sendProofs, options);
keep = keepProofsSelect.concat(keep);

// strip dleq if explicitly told so
Copy link
Collaborator

Choose a reason for hiding this comment

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

does !options?.includeDleq have two meanings in this case? Ignoring proofs without dleqs when true and stripping dleqs when false ? Maybe we should not overload this option to be more clear.

Copy link
Author

Choose a reason for hiding this comment

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

Yea that's what I intended and what made most sense to me.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Okay I see. I think most people will assume that not-passing an optional flag is equal to setting the boolean to false. Diverging from that needs to be documented very clearly I believe. However I would love to get some implementers thoughts on this. @callebtc @gudnuf could you leave your 2 sats on this?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Could you maybe explain the usecase to me? WWhy would someone decide to strip DLEQs when sending to another wallet?

Copy link
Collaborator

@Egge21M Egge21M Nov 12, 2024

Choose a reason for hiding this comment

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

I think it just clicked for me... sorry for all the confusion. includeDleq signals wether or not a token sent should include DLEQs for all included proofs, THEREFORE:

if (options?.includeDleq) -> Filter proofs for proofs with DLEQ, do not strip them
if (!options?.includeDleq) This is not an explicit false, simply NOT an explicit true -> Include all proofs, strip all DLEQs

src/CashuWallet.ts Outdated Show resolved Hide resolved
src/model/BlindedSignature.ts Outdated Show resolved Hide resolved
src/utils.ts Outdated
@@ -292,7 +321,15 @@ export function handleTokens(token: string): Token {
secret: p.s,
C: bytesToHex(p.c),
amount: p.a,
id: bytesToHex(t.i)
id: bytesToHex(t.i),
dleq:
Copy link
Collaborator

Choose a reason for hiding this comment

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

We can avoid dleq:undefined being present on every token without dleqs by doing something like

proofs.push({
    secret: p.s,
    C: bytesToHex(p.c),
    amount: p.a,
    id: bytesToHex(t.i),
    ...(p.d && {
        dleq: {
	    e: bytesToHex(p.d.e),
	    s: bytesToHex(p.d.s),
	    r: bytesToHex(p.d.r)
	} as SerializedDLEQ
    })
});

What do you think?

Copy link
Collaborator

Choose a reason for hiding this comment

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

We could also use an if-statement, as it might be more readable

Copy link
Author

@lollerfirst lollerfirst Nov 8, 2024

Choose a reason for hiding this comment

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

I tried using this elegant solution in the place of almost all other ugly dleq == undefined ? undefined : ... check it out.

* @param token The token subject to the verification
* @param keys The Mint's keyset to be used for verification
*/
private requireDLEQ(token: Token, keys: MintKeys) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I might be missing something here, but it seems like this function does not rely on any of the instance state. Therefore I would like to move it to utils.ts and export it to the public API.

Functions that do not return anything, but instead throw are hard to work with IMHO. Would you not agree that it would be clearer to rename this to hasValidDleq, make it take a SINGLE proof, and make it return true is all pass and false if dleq are either missing or invalid?

We could then use this util to build filters like proofs.filter(p => hasValidDleq(p, keys) or a util to replace requireDleq with a better best-case running time like

if (proofs.some(p => !hasValidDelq(p, keys))) {
    throw new Error("Proofs contain invalid DLEQ")
}

Copy link
Author

Choose a reason for hiding this comment

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

You are right.

Copy link
Author

@lollerfirst lollerfirst Nov 8, 2024

Choose a reason for hiding this comment

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

Doing this right now. Should I let hasValidDleq throw in case there is no matching key from the keyset proof.id for a particular proof amount? Or should I just return false.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think it makes sense to throw in this case. false would indicate an invalid DLEQ, while in this case it's more than just that.

Copy link
Author

@lollerfirst lollerfirst Nov 8, 2024

Choose a reason for hiding this comment

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

added a test for hasValidDleq as well.

let { keep, send } = await this.swap(amount, sendProofs, options);
keep = keepProofsSelect.concat(keep);

// strip dleq if explicitly told so
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could you maybe explain the usecase to me? WWhy would someone decide to strip DLEQs when sending to another wallet?

id: this.id,
amount: this.amount,
C_: this.C_.toHex(true),
dleq:
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we use the spread syntax here too?

Copy link
Author

Choose a reason for hiding this comment

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

Yes. Also I am responding here to your other comment about "why would someone strip the DLEQs when sending to Carol". One might try to get a smaller size cashuB token for example. In general if it's not required by Carol why include it?

Copy link
Collaborator

@Egge21M Egge21M left a comment

Choose a reason for hiding this comment

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

LGTM!

@Egge21M Egge21M merged commit 90dc186 into cashubtc:development Nov 13, 2024
1 check passed
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.

[Feature request] Support for DLEQ proofs
4 participants