Subject: [bitcoindev] Censorship and Privacy in Chaumian ecash implementations

From: /dev /fd0 <[email-masked]>
Date: Sat, 21 Dec 2024 08:58:44 -0800 (PST)

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 [email-masked].
To view this discussion visit https://groups.google.com/d/msgid/bitcoindev/27b19012-20da-46a7-8a84-f90e0070aa77n%40googlegroups.com.