promenade

module
v0.4.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 10, 2026 License: Unlicense

README

= promenade

provider of multisig for events on nostr and destroyer of encryption.

== video explanation

video::https://cdn.azzamo.net/c6e8ea5bb6cf5b31b1da0ad0b40fbbfb1b2c26e611f249a964cc64782f9f8187.mp4[]

https://njump.me/nevent1qqsqqqph7l5mv0fv5hekm0nxx7ty9nszfmyhpjhh84h3srugvxe9e9qpr9mhxue69uhhq7tjv9kkjepwve5kzar2v9nzucm0d5q35amnwvaz7tmrdpex7mnfvdkx2tnyw3hkummw9e3k7mgzyqalp33lewf5vdq847t6te0wvnags0gs0mu72kz8938tn24wlfze6kfp7fz

== implementation details

the signature algorithm is implemented roughly as described in the https://eprint.iacr.org/2023/899.pdf, with big inspiration from the code at https://github.com/LLFourn/secp256kfun/tree/8e6fd712717692d475287f4a964be57c8584f54e/schnorr_fun/src/frost. relative to the paper (but following secp256kfun) this implementations has the following substantial changes:

  - for BIP-340 compatibility, the key dealing algorithm negates the secret key before sharding if its public key's `y` is odd;
  - for BIP-340 compatibility, when creating partial signatures, signers have to compute the group commitment, and if it's `y` is odd then all the public nonces and their own private nonce is negated;
  - for BIP-340 compatibility, then signing challenge is computed with `taggedhash("BIP0340/challenge", group-commitment-x || user-pubkey-x || event_id)`;
  - because it felt appropriate, other parts of the algorithm that would use hashes also use `taggedhash()` with different tags, the code will speak better than I can.

== internal protocol flow

=== key distribution

1. _client_ generates a `user-secret-key` locally -- or the user inputs a preexisting key;
2. _client_ shards the key into as `n` shards, using the `TrustedKeyDeal()` function under `frost/key_dealer.go` or `trustedKeyDeal()` under `js-dealer/shardkey.ts`, it also has to pick a number `m`, which will be the minimum threshold for generating signatures using FROST;
3. if it doesn't have one already, _client_ signs and publishes a `kind:10002` relay list with some inbox relays;
4. _client_ picks a number `n` of signers, identified by their public keys, each of which will receive a shard;
5. _client_ fetches `kind:10002` relays for each of the signers;
6. _client_ picks a _coordinator_ that acts as a relay;
7. _client_ builds a `kind:26428` "shard event" for each _signer_, as follows:

  {
    "kind": 26428,
    "pubkey": "<user-pubkey>",
    "tags": [
      ["p", "<signer-pubkey>"],
      ["coordinator", "<coordinator-url>"],
    ],
    "content": nip44_encrypt("<hex-encoded-secret-key-shard>")
  }

  where `<hex-encoded-secret-key-shard>` is given by the hex-encoded of the concatenation of
    - [encoded-public-shard]: given by:
      - [public-shard-id]: 2-bytes (little-endian)
      - [number-of-vss-commits]: 4-bytes (little-endian)
      - [shard-public-key]: 33-bytes (compressed)
      - <number-of-vss-commits> * [vss-commit]: 33-bytes (compressed) each
    - [shard-secret-key]: 32-bytes (big-endian)
    - [user-pubkey]: 33-bytes (compressed)

8. _client_ builds NIP-13 proof-of-work into that event of at least 20 bits;
9. _client_ sends the signed "shard event" to the each desired _signer_ in their "read" relays as given by their `kind:10002`;
10. _client_ starts listening on their own "read" relays for replies from _signer_;
11. _signer_ receives the event from _client_, checks the proof-of-work, decrypts and validates the `<encoded-secret-key-shard>`, then saves that information locally somehow;
12. _signer_ builds a `kind:26429` "shard ack event", as follows:

  {
    "kind": 26429,
    "pubkey": "<signer-pubkey>",
    "tags": [
      ["e", "<shard-event-id>"],
      ["p", "<user-pubkey>"]
    ]
  }

13. _signer_ fetches `kind:10002` relays for `<user-pubkey>` and sends the "shard ack event" to the "read" relays;
14. upon receiving the "shard ack event" from all the signers, _client_ builds a `kind:16430` "account registration event" as follows:

  {
    "kind": 16430,
    "pubkey": "<user-pubkey>",
    "tags": [
      ["handlersecret", "<random-private-key>"],
      ["h", "<public-key-corresponding-to-handlersecret>"],
      ["threshold", "<m>"],
      ["p", "<signer-pubkey>", "<hex-encoded-public-shard>"] * n,
      ["profile", "<name>", "<secret>", "<restrictions>"] * any
    ]
  }

  in which the `"p"` tag is repeated once for each signer, and "<hex-encoded-public-shard>" is encoded just as above.

15. upon receiving the "account registration event", _coordinator_ stores it and keeps it secret;
16. _coordinator_ should now listen for NIP-46 calls directed at its own relay, targeting `<public-key-corresponding-to-handlersecret>`.

=== signing

1. _coordinator_ listens for all NIP-46 events targeting `<public-key-corresponding-to-handlersecret>`;
2. upon receiving a NIP-46 request, _coordinator_ matches it against its `p` tag with a stored `kind:16430`, uses `handlersecret` to decrypt (and later sign and encrypt the response);
3. for `get_public_key` _coordinator_ can just answer immediately;
4. for `sign_event` _coordinator_ then finds out what signers are online and connected, chooses `m` of them and initiates a signing session;
5. _coordinator_ sends a `kind:26430` "configuration event" to each chosen _signer_ in the form:

  {
    "kind": 26430,
    "pubkey": "<coordinator-pubkey>",
    "tags": [
      ["p", "<signer-pubkey>"]
    ],
    "content": "<hex-encoded-configuration-object>"
  }

  where <hex-encoded-configuration-object> is given by the hex-encoded concatenation of
    - [m]: 2-bytes (little-endian)
    - [n]: 2-bytes (little-endian)
    - [number-of-signers]: 2-bytes (little-endian)
    - [user-pubkey]: 33-bytes (compressed)
    - <number-of-signers> * [encoded-public-shard] (as above)

6. upon receiving this, _signer_ generates its local commitments, or a pair of public and private nonces, and sends the public parts to _coordinator_ in a `kind:26431` "commit event", as follows:

  {
    "kind": 26431,
    "pubkey": "<signer-pubkey>",
    "tags": [
      ["e", "<configuration-event-id>"],
      ["p", "<user-pubkey>"]
    ],
    "content": "<hex-encoded-commit>"
  }

  where <hex-encoded-commit> is given by the hex-encoded concatenation of
    - [commit-id]: 8-bytes (little-endian)
    - [signer-id]: 2-bytes (little-endian)
    - [binding-nonce-point]: 33-bytes (compressed)
    - [hiding-nonce-point]: 33-bytes (compressed)

7. upon receiving commits from all signers, _coordinator_ then aggregates the commits into a group commit and sends it back to all the signers:

  {
    "kind": 26432,
    "pubkey": "<coordinator-pubkey>",
    "tags": [
      ["e", "<configuration-event-id>"],
      ["p", "<signer-pubkey>"]
    ],
    "content": "<hex-encoded-group-commit>"
  }

  where <hex-encoded-group-commit> is given by the hex-encoded concatenation of
    - [first-nonce]: 33-bytes (compressed)
    - [second-nonce]: 33-bytes (compressed)

8. then _coordinator_ sends the event that is to be signed to all signers in a `kind:26432` event, in the form:

  {
    "kind": 26433,
    "pubkey": "<coordinator-pubkey>",
    "tags": [
      ["e", "<configuration-event-id>"],
      ["p", "<signer-pubkey>"]
    ],
    "content": "<json-encoded-event-to-be-signed>"
  }

9. finally, each _signer_ groups together all commits and uses these together with their secret nonces and the hash of the event to be signed to produce a `<partial-signature>` and sends that back to _coordinator_ in a `kind:26433` event, as follows:

  {
    "kind": 26434,
    "pubkey": "<signer-pubkey>",
    "tags": [
      ["e", "<configuration-event-id>"],
      ["p", "<user-pubkey>"]
    ],
    "content": "<hex-encoded-partial-signature>"
  }

  where <hex-encoded-partial-signature> is given by the hex-encoded concatenation of:
    - [signer-id]: 2-bytes (little-endian)
    - [partial-signature-scalar]: 32-bytes (big-endian)

10. _coordinator_ assembles all the partial signatures and builds the aggregated signature which can then be put into the event and sent as a response to the `sign_event` NIP-46 request.

== issues

since this implementation uses `github.com/btcsuite/btcd/btcec` and that library doesn't seem to provide constant-time curve operations signers using this may be vulnerable to side-channel attacks by an evil coordinator.

Directories

Path Synopsis
example command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL