Delivery-date: Sat, 21 Dec 2024 09:04:52 -0800 Received: from mail-yb1-f184.google.com ([209.85.219.184]) by mail.fairlystable.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (Exim 4.94.2) (envelope-from ) id 1tP2uJ-00046A-9S for bitcoindev@gnusha.org; Sat, 21 Dec 2024 09:04:52 -0800 Received: by mail-yb1-f184.google.com with SMTP id 3f1490d57ef6-e3a109984a5sf3623277276.3 for ; Sat, 21 Dec 2024 09:04:50 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=googlegroups.com; s=20230601; t=1734800685; x=1735405485; darn=gnusha.org; h=list-unsubscribe:list-subscribe:list-archive:list-help:list-post :list-id:mailing-list:precedence:x-original-sender:mime-version :subject:message-id:to:from:date:sender:from:to:cc:subject:date :message-id:reply-to; bh=ssGMpP9ylQlEa1TettXV8Um68IDDwpV8BOJScTeFSck=; b=rm6i89L65Z2krMQeAX++1JOIr4iVLquc8x5RxGwHEAVH+FnDdvR60QWdXFJ3IvEV2M AE9WPlCMCQ72F1uyJ8TPDM3StJyekoPu0PwXNJVsHTvAkfWSyNRc72/jKTnSp0+QgNk9 r/lxOuGZmOgJIQqE974/hitfaRjYYezrslLAYcGASTfIKHymiL+mbIYZDmOTQmjSEeCA ArIHdAhMfyALPtfWgn9PSEUA0iZ0f6rXhcurN3Zh9f7MikU1332+1ns3Pzyv1VjQTDz0 Zu5vP2RpJSA56UMxL0bl5WtyvOdSAUGHcGbxF79dLh2JSHOmL8PZJXvZmfwkljioJBMA QZFQ== DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20230601; t=1734800685; x=1735405485; darn=gnusha.org; h=list-unsubscribe:list-subscribe:list-archive:list-help:list-post :list-id:mailing-list:precedence:x-original-sender:mime-version :subject:message-id:to:from:date:from:to:cc:subject:date:message-id :reply-to; bh=ssGMpP9ylQlEa1TettXV8Um68IDDwpV8BOJScTeFSck=; b=FokCA+w+fQ0+9XW+DAUoXrtI3sskKgIWeAqofFshzlnKEPDENFuzQCzxi93A1GDp88 pAiXg/2NgPjf4ZTyBm3mKUBc9eATe224ZMW+IfuFR8JYsczesRyM7AZG/+IDuZn/NY4f roehoOEJt9mhpLJLGveXGAWm3mVBN0aAZzdEFSpJbISb23lxvt1hGQSsKEYutcttdFNM jT11X8eFCh3J7O/x38hX9QiQH8svPFWBcjENKwTE6Uixe5Au5KsFE8DuF/0ZD9SpuG9M O4X1Ht1X+Ml0aBZMUmQeI+jsnIY+RN8tRJdeBw5gZtFMdK92j3SIDFQoVpO3F0k+Fth/ VTdQ== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1734800685; x=1735405485; h=list-unsubscribe:list-subscribe:list-archive:list-help:list-post :list-id:mailing-list:precedence:x-original-sender:mime-version :subject:message-id:to:from:date:x-beenthere:x-gm-message-state :sender:from:to:cc:subject:date:message-id:reply-to; bh=ssGMpP9ylQlEa1TettXV8Um68IDDwpV8BOJScTeFSck=; b=XP0G+0fPZAzL1VL9deOujQEaZUHLiX6i3GwPNfNd1z4vyixoGnUiAbfT+RzefRcwXJ zYUfTQEV/qdtELpTzAdnJnwrGLoQWm1+PMzvSQ3t7beRzZw+SBTheKDoglSZ6t8zW0SF 5kZ0gq1lK90pRcDVmXcGduxj4sTz7uLZXUvg/XIhgouM+vJtkpnDlLQZ+vSHTHxUnn4I 9iq9BwoieavjxhvgCvI/mQIqgX6Cyigwgu4wcAA9q9BiWjLejp+V6PtfjLYEfhHQOWlC la5mbVuk2SVvW7WQ5u6Sb7W6k/Uku6ljJ9XM7lWsIRupFoLX8yILHCVwCD1+zM0Aznzx AKhg== Sender: bitcoindev@googlegroups.com X-Forwarded-Encrypted: i=1; AJvYcCXqp/yRrdj+ZmtHXoytXvGQ9wRL503ncXD1L0khjoWNTGD7FFGaGwecYuphCiLVyugEpF6EkfrylAuO@gnusha.org X-Gm-Message-State: AOJu0Yxzh6zHthA2VeeKoMFuA4f1UdOcQCLEkOj0xeAVoFYPdCM2vvz3 X/sfi2wBjT+AasFuF0uDxMhe+KVXxad8HFe3ADkPZs7E7sLzbyY0 X-Google-Smtp-Source: AGHT+IFnJT1YyH0aVyUjUShk5tVPOoXuE3DNOdXCzb9xOYdMHBc+i6QyDSqjFO/AxSfD30BvFlajxw== X-Received: by 2002:a05:6902:120a:b0:e4e:551e:fce9 with SMTP id 3f1490d57ef6-e538c3fca98mr5666460276.47.1734800684535; Sat, 21 Dec 2024 09:04:44 -0800 (PST) X-BeenThere: bitcoindev@googlegroups.com Received: by 2002:a25:d6d4:0:b0:e38:8f56:e85 with SMTP id 3f1490d57ef6-e537601e984ls467459276.1.-pod-prod-09-us; Sat, 21 Dec 2024 09:04:41 -0800 (PST) X-Received: by 2002:a05:690c:490c:b0:6e2:43ea:552 with SMTP id 00721157ae682-6f3f8110ed5mr59755097b3.16.1734800681678; Sat, 21 Dec 2024 09:04:41 -0800 (PST) Received: by 2002:a81:ad03:0:b0:6ef:892f:89f3 with SMTP id 00721157ae682-6f3f57ea50dms7b3; Sat, 21 Dec 2024 08:58:46 -0800 (PST) X-Received: by 2002:a05:690c:88c:b0:6e3:37a7:8a98 with SMTP id 00721157ae682-6f3f8114d78mr65683427b3.14.1734800325232; Sat, 21 Dec 2024 08:58:45 -0800 (PST) Date: Sat, 21 Dec 2024 08:58:44 -0800 (PST) From: /dev /fd0 To: Bitcoin Development Mailing List Message-Id: <27b19012-20da-46a7-8a84-f90e0070aa77n@googlegroups.com> Subject: [bitcoindev] Censorship and Privacy in Chaumian ecash implementations MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="----=_Part_246520_1172322693.1734800324988" X-Original-Sender: alicexbtong@gmail.com Precedence: list Mailing-list: list bitcoindev@googlegroups.com; contact bitcoindev+owners@googlegroups.com List-ID: X-Google-Group-Id: 786775582512 List-Post: , List-Help: , List-Archive: , List-Unsubscribe: , X-Spam-Score: -0.5 (/) ------=_Part_246520_1172322693.1734800324988 Content-Type: multipart/alternative; boundary="----=_Part_246521_361007710.1734800324988" ------=_Part_246521_361007710.1734800324988 Content-Type: text/plain; charset="UTF-8" Hi Bitcoin Developers, This post is about a myth and some misleading things shared about ecash. It is possible to censor specific users and none of the ecash implementation are censorship resistant. # Censorship Methods in Cashu There are 2 ways to censor individual users in cashu: 1. P2PK 2. Authentication ## P2PK Ecash tokens issued by cashu mints can be locked using public keys so that it can only be redeemed by the user who owns the private key for it. This links ecash with specific public keys and most implementation use nostr keys for it. Most users are doxxed on nostr so they can be censored based on their identity. Even if its linked to an anon they can be censored based on their posts. You can find relevant code snippets in [conditions.py][1] if using nutshell for mint: ```python class LedgerSpendingConditions: def _verify_p2pk_spending_conditions(self, proof, secret): if SecretKind(secret.kind) != SecretKind.P2PK: return True p2pk_secret = P2PKSecret.from_secret(secret) pubkeys = [p2pk_secret.data] + p2pk_secret.tags.get_tag_all("pubkeys") if p2pk_secret.locktime and p2pk_secret.locktime < time.time(): refund_pubkeys = p2pk_secret.tags.get_tag_all("refund") if not refund_pubkeys: return True return self._verify_secret_signatures( proof, refund_pubkeys, proof.p2pksigs, 1 ) return self._verify_secret_signatures( proof, pubkeys, proof.p2pksigs, p2pk_secret.n_sigs ) def _verify_htlc_spending_conditions(self, proof, secret): if SecretKind(secret.kind) != SecretKind.HTLC: return True htlc_secret = HTLCSecret.from_secret(secret) if htlc_secret.locktime and htlc_secret.locktime < time.time(): refund_pubkeys = htlc_secret.tags.get_tag_all("refund") if refund_pubkeys: return self._verify_secret_signatures( proof, refund_pubkeys, proof.p2pksigs, 1 ) return True assert proof.htlcpreimage, TransactionError("no HTLC preimage provided") if not hashlib.sha256(bytes.fromhex(proof.htlcpreimage)).digest() == bytes.fromhex(htlc_secret.data): raise TransactionError("HTLC preimage does not match.") hashlock_pubkeys = htlc_secret.tags.get_tag_all("pubkeys") if not hashlock_pubkeys: return True return self._verify_secret_signatures( proof, hashlock_pubkeys, proof.htlcsigs or [], htlc_secret.n_sigs ) def _verify_secret_signatures(self, proof, pubkeys, signatures, n_sigs_required=1): assert len(set(pubkeys)) == len(pubkeys), "pubkeys must be unique." if not signatures: raise TransactionError("no signatures in proof.") if len(set(signatures)) != len(signatures): raise TransactionError("signatures must be unique.") n_sigs_required = n_sigs_required or 1 assert n_sigs_required > 0, "n_sigs must be positive." assert len(signatures) >= n_sigs_required, f"not enough signatures provided: {len(signatures)} < {n_sigs_required}." n_valid_sigs_per_output = 0 for input_sig in signatures: for pubkey in pubkeys: if verify_schnorr_signature( message=proof.secret.encode("utf-8"), pubkey=PublicKey(bytes.fromhex(pubkey), raw=True), signature=bytes.fromhex(input_sig), ): n_valid_sigs_per_output += 1 assert n_valid_sigs_per_output, "no valid signature provided for input." assert n_valid_sigs_per_output >= n_sigs_required, f"signature threshold not met. {n_valid_sigs_per_output} < {n_sigs_required}." return True def _verify_input_spending_conditions(self, proof): try: secret = Secret.deserialize(proof.secret) except Exception: return True if SecretKind(secret.kind) == SecretKind.P2PK: return self._verify_p2pk_spending_conditions(proof, secret) if SecretKind(secret.kind) == SecretKind.HTLC: return self._verify_htlc_spending_conditions(proof, secret) return True def _verify_output_p2pk_spending_conditions(self, proofs, outputs): try: secrets_generic = [Secret.deserialize(p.secret) for p in proofs] p2pk_secrets = [P2PKSecret.from_secret(secret) for secret in secrets_generic] except Exception: return True if not all([SecretKind(secret.kind) == SecretKind.P2PK for secret in p2pk_secrets]): return True if not all([secret.sigflag == SigFlags.SIG_ALL for secret in p2pk_secrets]): return True pubkeys_per_proof = [ [p2pk_secret.data] + p2pk_secret.tags.get_tag_all("pubkeys") for p2pk_secret in p2pk_secrets ] n_sigs_per_proof = [p2pk_secret.n_sigs for p2pk_secret in p2pk_secrets] for p2pk_secret in p2pk_secrets: if p2pk_secret.locktime and p2pk_secret.locktime < time.time(): refund_pubkeys = p2pk_secret.tags.get_tag_all("refund") if refund_pubkeys: pubkeys_per_proof.append(refund_pubkeys) n_sigs_per_proof.append(1) if not pubkeys_per_proof: return True assert len({tuple(pubs_output) for pubs_output in pubkeys_per_proof}) == 1, "pubkeys in all proofs must match." assert len(set(n_sigs_per_proof)) == 1, "n_sigs in all proofs must match." pubkeys = pubkeys_per_proof[0] n_sigs = n_sigs_per_proof[0] or 1 for output in outputs: p2pksigs = output.p2pksigs assert p2pksigs, "no signatures in output." assert len(set(p2pksigs)) == len(p2pksigs), "duplicate signatures in output." n_valid_sigs_per_output = 0 for sig in p2pksigs: for pubkey in pubkeys: if verify_schnorr_signature( message=bytes.fromhex(output.B_), pubkey=PublicKey(bytes.fromhex(pubkey), raw=True), signature=bytes.fromhex(sig), ): n_valid_sigs_per_output += 1 assert n_valid_sigs_per_output, "no valid signature provided for output." assert n_valid_sigs_per_output >= n_sigs, f"signature threshold not met. {n_valid_sigs_per_output} < {n_sigs}." return True def _verify_output_spending_conditions(self, proofs, outputs): return self._verify_output_p2pk_spending_conditions(proofs, outputs) ``` ## Authentication Mints can enforce authentication at some point and do KYC for mint, melt, swap etc. Users who refuse to KYC will not be able to use or redeem their ecash tokens. Some of the KYCed users can be censored based on their identity. This would also affect privacy. gandlaf21 agrees this is possible however it is still marketed as censorship resistant. There was some discussion about it in a pull request and supertestnet also shared his thoughts: https://github.com/bitcoinlayers/bitcoinlayers/pull/164 This whole debate started in May 2024 when cashu's twitter account [announced][2] that they are considering adding an authentication in the protocol as it is requested by regulated entities. The authentication mechanism is shared in this [pull request][3] which links each user with linkingkey and it will compromise privacy: ``` POST https://bob.com/v1/auth Post Data: { action:"mint", k1:"8278e1a48e61c261916791dabb6af760488e4f01932e11fe7054f59754e3de6e" signature:c568f78e4b234a5f7d8c3b2a679e48d1234567890abcdef linkingKey:7345786068584cd33000582ba87a9ddf77db5377c67910ab59d7e9a5f44 } Response: HTTP/1.1 200 OK { "access_token": "9876543210fedcba", "token_type": "Bearer", "expires_in": 3600 } ``` This pull request was closed last week and a new authentication mechanism is proposed: https://github.com/cashubtc/nuts/pull/198 It uses clear and blind auth but users can still be censored with KYC based on their identity. You can understand the details from this [comment][4]. ## Conclusion The authentication mechanisms shared above are not the only way mints can restrict users as there is nothing in the protocol that stops mints from using a custom authentication. Introducing KYC in protocol is against freedom and privacy. These custodial solutions might end up being another compliant ecash implementation like [GNU Taler][5]. This would also make it easier for government agencies to target other mints that do not comply. [1]: https://github.com/cashubtc/nutshell/blob/main/cashu/mint/conditions.py [2]: https://x.com/CashuBTC/status/1791001643019809146 [3]: https://github.com/cashubtc/nuts/pull/106 [4]: https://github.com/cashubtc/nuts/pull/198#issuecomment-2508706328 [5]: https://taler.net/en/index.html /dev/fd0 floppy disk guy -- You received this message because you are subscribed to the Google Groups "Bitcoin Development Mailing List" group. To unsubscribe from this group and stop receiving emails from it, send an email to bitcoindev+unsubscribe@googlegroups.com. To view this discussion visit https://groups.google.com/d/msgid/bitcoindev/27b19012-20da-46a7-8a84-f90e0070aa77n%40googlegroups.com. ------=_Part_246521_361007710.1734800324988 Content-Type: text/html; charset="UTF-8" Content-Transfer-Encoding: quoted-printable Hi Bitcoin Developers,

This post is about a myth and some mislea= ding things shared about ecash. It is possible to censor specific users and= none of the ecash implementation are censorship resistant.

# Censorship Methods in Cashu

There are 2 ways to ce= nsor individual users in cashu:

1. P2PK
2. Authentication
## P2PK

Ecash tokens issued by cashu mints can be lock= ed using public keys so that it can only be redeemed by the user who owns t= he private key for it. This links ecash with specific public keys and most = implementation use nostr keys for it. Most users are doxxed on nostr so the= y can be censored based on their identity. Even if its linked to an anon th= ey can be censored based on their posts.

You can find relevant c= ode snippets in [conditions.py][1] if using nutshell for mint:

`= ``python
class LedgerSpendingConditions:
=C2=A0 =C2=A0 def _verif= y_p2pk_spending_conditions(self, proof, secret):
=C2=A0 =C2=A0 =C2=A0 = =C2=A0 if SecretKind(secret.kind) !=3D SecretKind.P2PK:
=C2=A0 =C2=A0 = =C2=A0 =C2=A0 =C2=A0 =C2=A0 return True
=C2=A0 =C2=A0 =C2=A0 =C2=A0 p2= pk_secret =3D P2PKSecret.from_secret(secret)
=C2=A0 =C2=A0 =C2=A0 =C2= =A0 pubkeys =3D [p2pk_secret.data] + p2pk_secret.tags.get_tag_all("pubkeys"= )
=C2=A0 =C2=A0 =C2=A0 =C2=A0 if p2pk_secret.locktime and p2pk_secret.= locktime < time.time():
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 r= efund_pubkeys =3D p2pk_secret.tags.get_tag_all("refund")
=C2=A0 =C2=A0= =C2=A0 =C2=A0 =C2=A0 =C2=A0 if not refund_pubkeys:
=C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return True
=C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 return self._verify_secret_signatures(
=C2=A0= =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 proof, refund_pubkeys, pr= oof.p2pksigs, 1
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )
=C2= =A0 =C2=A0 =C2=A0 =C2=A0 return self._verify_secret_signatures(
=C2=A0= =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 proof, pubkeys, proof.p2pksigs, p2pk_se= cret.n_sigs
=C2=A0 =C2=A0 =C2=A0 =C2=A0 )

=C2=A0 =C2=A0 def= _verify_htlc_spending_conditions(self, proof, secret):
=C2=A0 =C2=A0 = =C2=A0 =C2=A0 if SecretKind(secret.kind) !=3D SecretKind.HTLC:
=C2=A0 = =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return True
=C2=A0 =C2=A0 =C2=A0 = =C2=A0 htlc_secret =3D HTLCSecret.from_secret(secret)
=C2=A0 =C2=A0 = =C2=A0 =C2=A0 if htlc_secret.locktime and htlc_secret.locktime < time.ti= me():
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 refund_pubkeys =3D htl= c_secret.tags.get_tag_all("refund")
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0= =C2=A0 if refund_pubkeys:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0 return self._verify_secret_signatures(
=C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 proof, refund_pubkeys,= proof.p2pksigs, 1
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 )
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return True
= =C2=A0 =C2=A0 =C2=A0 =C2=A0 assert proof.htlcpreimage, TransactionError("no= HTLC preimage provided")
=C2=A0 =C2=A0 =C2=A0 =C2=A0 if not hashlib.s= ha256(bytes.fromhex(proof.htlcpreimage)).digest() =3D=3D bytes.fromhex(htlc= _secret.data):
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise Transac= tionError("HTLC preimage does not match.")
=C2=A0 =C2=A0 =C2=A0 =C2=A0= hashlock_pubkeys =3D htlc_secret.tags.get_tag_all("pubkeys")
=C2=A0 = =C2=A0 =C2=A0 =C2=A0 if not hashlock_pubkeys:
=C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 return True
=C2=A0 =C2=A0 =C2=A0 =C2=A0 return self.= _verify_secret_signatures(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 p= roof, hashlock_pubkeys, proof.htlcsigs or [], htlc_secret.n_sigs
=C2= =A0 =C2=A0 =C2=A0 =C2=A0 )

=C2=A0 =C2=A0 def _verify_secret_sign= atures(self, proof, pubkeys, signatures, n_sigs_required=3D1):
=C2=A0 = =C2=A0 =C2=A0 =C2=A0 assert len(set(pubkeys)) =3D=3D len(pubkeys), "pubkeys= must be unique."
=C2=A0 =C2=A0 =C2=A0 =C2=A0 if not signatures:
= =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise TransactionError("no signat= ures in proof.")
=C2=A0 =C2=A0 =C2=A0 =C2=A0 if len(set(signatures)) != =3D len(signatures):
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise T= ransactionError("signatures must be unique.")
=C2=A0 =C2=A0 =C2=A0 =C2= =A0 n_sigs_required =3D n_sigs_required or 1
=C2=A0 =C2=A0 =C2=A0 =C2= =A0 assert n_sigs_required > 0, "n_sigs must be positive."
=C2=A0 = =C2=A0 =C2=A0 =C2=A0 assert len(signatures) >=3D n_sigs_required, f"not = enough signatures provided: {len(signatures)} < {n_sigs_required}."
=C2=A0 =C2=A0 =C2=A0 =C2=A0 n_valid_sigs_per_output =3D 0
=C2=A0 =C2= =A0 =C2=A0 =C2=A0 for input_sig in signatures:
=C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0 =C2=A0 for pubkey in pubkeys:
=C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if verify_schnorr_signature(
=C2=A0 = =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 message=3Dpr= oof.secret.encode("utf-8"),
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0 =C2=A0 =C2=A0 pubkey=3DPublicKey(bytes.fromhex(pubkey), raw= =3DTrue),
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 signature=3Dbytes.fromhex(input_sig),
=C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 ):
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 n_valid_sigs_per_output +=3D 1
= =C2=A0 =C2=A0 =C2=A0 =C2=A0 assert n_valid_sigs_per_output, "no valid signa= ture provided for input."
=C2=A0 =C2=A0 =C2=A0 =C2=A0 assert n_valid_s= igs_per_output >=3D n_sigs_required, f"signature threshold not met. {n_v= alid_sigs_per_output} < {n_sigs_required}."
=C2=A0 =C2=A0 =C2=A0 = =C2=A0 return True

=C2=A0 =C2=A0 def _verify_input_spending_cond= itions(self, proof):
=C2=A0 =C2=A0 =C2=A0 =C2=A0 try:
=C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 secret =3D Secret.deserialize(proof.secret)=
=C2=A0 =C2=A0 =C2=A0 =C2=A0 except Exception:
=C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 return True
=C2=A0 =C2=A0 =C2=A0 =C2=A0 if Se= cretKind(secret.kind) =3D=3D SecretKind.P2PK:
=C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 return self._verify_p2pk_spending_conditions(proof, secre= t)
=C2=A0 =C2=A0 =C2=A0 =C2=A0 if SecretKind(secret.kind) =3D=3D Secre= tKind.HTLC:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return self._ver= ify_htlc_spending_conditions(proof, secret)
=C2=A0 =C2=A0 =C2=A0 =C2= =A0 return True

=C2=A0 =C2=A0 def _verify_output_p2pk_spending_c= onditions(self, proofs, outputs):
=C2=A0 =C2=A0 =C2=A0 =C2=A0 try:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 secrets_generic =3D [Secret.des= erialize(p.secret) for p in proofs]
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0= =C2=A0 p2pk_secrets =3D [P2PKSecret.from_secret(secret) for secret in secr= ets_generic]
=C2=A0 =C2=A0 =C2=A0 =C2=A0 except Exception:
=C2=A0= =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return True
=C2=A0 =C2=A0 =C2=A0 = =C2=A0 if not all([SecretKind(secret.kind) =3D=3D SecretKind.P2PK for secre= t in p2pk_secrets]):
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return = True
=C2=A0 =C2=A0 =C2=A0 =C2=A0 if not all([secret.sigflag =3D=3D Sig= Flags.SIG_ALL for secret in p2pk_secrets]):
=C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 return True
=C2=A0 =C2=A0 =C2=A0 =C2=A0 pubkeys_per_= proof =3D [
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 [p2pk_secret.dat= a] + p2pk_secret.tags.get_tag_all("pubkeys")
=C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 for p2pk_secret in p2pk_secrets
=C2=A0 =C2=A0 =C2=A0= =C2=A0 ]
=C2=A0 =C2=A0 =C2=A0 =C2=A0 n_sigs_per_proof =3D [p2pk_secre= t.n_sigs for p2pk_secret in p2pk_secrets]
=C2=A0 =C2=A0 =C2=A0 =C2=A0 = for p2pk_secret in p2pk_secrets:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 if p2pk_secret.locktime and p2pk_secret.locktime < time.time():=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 refund_pubkeys = =3D p2pk_secret.tags.get_tag_all("refund")
=C2=A0 =C2=A0 =C2=A0 =C2=A0= =C2=A0 =C2=A0 =C2=A0 =C2=A0 if refund_pubkeys:
=C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 pubkeys_per_proof.append(r= efund_pubkeys)
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0= =C2=A0 =C2=A0 n_sigs_per_proof.append(1)
=C2=A0 =C2=A0 =C2=A0 =C2=A0 = if not pubkeys_per_proof:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 re= turn True
=C2=A0 =C2=A0 =C2=A0 =C2=A0 assert len({tuple(pubs_output) f= or pubs_output in pubkeys_per_proof}) =3D=3D 1, "pubkeys in all proofs must= match."
=C2=A0 =C2=A0 =C2=A0 =C2=A0 assert len(set(n_sigs_per_proof))= =3D=3D 1, "n_sigs in all proofs must match."
=C2=A0 =C2=A0 =C2=A0 =C2= =A0 pubkeys =3D pubkeys_per_proof[0]
=C2=A0 =C2=A0 =C2=A0 =C2=A0 n_sig= s =3D n_sigs_per_proof[0] or 1
=C2=A0 =C2=A0 =C2=A0 =C2=A0 for output = in outputs:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 p2pksigs =3D out= put.p2pksigs
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 assert p2pksigs= , "no signatures in output."
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0= assert len(set(p2pksigs)) =3D=3D len(p2pksigs), "duplicate signatures in o= utput."
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 n_valid_sigs_per_out= put =3D 0
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 for sig in p2pksig= s:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 for pubkey = in pubkeys:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0 if verify_schnorr_signature(
=C2=A0 =C2=A0 =C2=A0 =C2=A0= =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 message=3Dbytes.fr= omhex(output.B_),
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 pubkey=3DPublicKey(bytes.fromhex(pubkey), r= aw=3DTrue),
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0 =C2=A0 =C2=A0 signature=3Dbytes.fromhex(sig),
=C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 ):
=C2=A0 = =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 n_valid_sigs_per_output +=3D 1
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 assert n_valid_sigs_per_output, "no valid signature provided for out= put."
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 assert n_valid_sigs_pe= r_output >=3D n_sigs, f"signature threshold not met. {n_valid_sigs_per_o= utput} < {n_sigs}."
=C2=A0 =C2=A0 =C2=A0 =C2=A0 return True
=C2=A0 =C2=A0 def _verify_output_spending_conditions(self, proofs, outp= uts):
=C2=A0 =C2=A0 =C2=A0 =C2=A0 return self._verify_output_p2pk_spen= ding_conditions(proofs, outputs)
```

## Authentication

Mints can enforce authentication at some point and do KYC for mint, = melt, swap etc. Users who refuse to KYC will not be able to use or redeem t= heir ecash tokens. Some of the KYCed users can be censored based on their i= dentity. This would also affect privacy.

gandlaf21 agrees this i= s possible however it is still marketed as censorship resistant.

There was some discussion about it in a pull request and supertestnet als= o shared his thoughts: https://github.com/bitcoinlayers/bitcoinlayers/pull/= 164

This whole debate started in May 2024 when cashu's twitter a= ccount [announced][2] that they are considering adding an authentication in= the protocol as it is requested by regulated entities.

The auth= entication mechanism is shared in this [pull request][3] which links each u= ser with linkingkey and it will compromise privacy:

```
POS= T https://bob.com/v1/auth

Post Data:
{
=C2=A0 action:= "mint",
=C2=A0 k1:"8278e1a48e61c261916791dabb6af760488e4f01932e11fe70= 54f59754e3de6e"
=C2=A0 signature:c568f78e4b234a5f7d8c3b2a679e48d123456= 7890abcdef
=C2=A0 linkingKey:7345786068584cd33000582ba87a9ddf77db5377c= 67910ab59d7e9a5f44
}

Response:

HTTP/1.1 200 OK
{
=C2=A0 "access_token": "9876543210fedcba",
=C2=A0 "to= ken_type": "Bearer",
=C2=A0 "expires_in": 3600
}
```
This pull request was closed last week and a new authentication mechani= sm is proposed: https://github.com/cashubtc/nuts/pull/198

It use= s clear and blind auth but users can still be censored with KYC based on th= eir identity. You can understand the details from this [comment][4].
<= br />## Conclusion

The authentication mechanisms shared above ar= e not the only way mints can restrict users as there is nothing in the prot= ocol that stops mints from using a custom authentication.

Introd= ucing KYC in protocol is against freedom and privacy. These custodial solut= ions might end up being another compliant ecash implementation like [GNU Ta= ler][5]. This would also make it easier for government agencies to target o= ther mints that do not comply.

[1]:=C2=A0https://github.com/cash= ubtc/nutshell/blob/main/cashu/mint/conditions.py
[2]:=C2=A0https://x.c= om/CashuBTC/status/1791001643019809146
[3]:=C2=A0https://github.com/ca= shubtc/nuts/pull/106
[4]:=C2=A0https://github.com/cashubtc/nuts/pull/1= 98#issuecomment-2508706328
[5]:=C2=A0https://taler.net/en/index.html
/dev/fd0
floppy disk guy

--
You received this message because you are subscribed to the Google Groups &= quot;Bitcoin Development Mailing List" group.
To unsubscribe from this group and stop receiving emails from it, send an e= mail to bitcoind= ev+unsubscribe@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/bitcoind= ev/27b19012-20da-46a7-8a84-f90e0070aa77n%40googlegroups.com.
------=_Part_246521_361007710.1734800324988-- ------=_Part_246520_1172322693.1734800324988--