Personal
nostr-linkr
A contract and SDK for linking control of a Nostr key to an Ethereum address through on-chain BIP-340 verification, demonstrated on Base Sepolia with an unaudited verifier.
nostr-linkr records a link authorized by both a Nostr signing key and an Ethereum address. The proof establishes control of the keys, not a person’s identity. It is a smart contract, a TypeScript SDK, and a specification proposed to the Nostr protocol.
- Parts
- Contract · SDK · example client · NIP proposal
- Proposal
- NIP-XX, On-Chain EVM Identity Linking (open)
- Role
- Author
- Repository
- VincenzoImp/nostr-linkr
The problem
Nostr already has two ways to say “this account is also me somewhere else”, and the specification proposal states the objection to both. NIP-05 verification depends on a DNS-controlled server: whoever runs the domain can change or withdraw the claim. NIP-39 depends on a post on GitHub or Twitter, which the platform can delete and the platform’s owner can decide about.
Both work. Both are mutable, censorable, and require trusting a service that is not a party to the identity being claimed.
What I built
The link is written to a smart contract and requires authorization from both keys.
The Nostr key signs an event containing the Ethereum address. The Ethereum key sends the
transaction that submits it. The contract then does the part that would ordinarily be taken on
trust: it recomputes the NIP-01 event id from the event’s fields, checks it matches the id
presented, and verifies the Schnorr signature against the Nostr public key. Only then is the link
recorded, and it can be looked up in either direction. A pullLinkr call removes it, so the link
is revocable by the address that made it.
Around the contract sit a TypeScript SDK, an example client, and a proposal written up as a NIP and submitted to the specification repository, where it is open.
Hard parts
Implementing Schnorr verification in the contract. Ethereum’s precompiled ecrecover handles ECDSA;
Nostr signs with BIP-340 Schnorr. Same curve, different scheme, and no precompile for it, so the
verification is implemented in Solidity: lifting the x-only public key back to a curve point,
which works because secp256k1’s field prime is congruent to 3 mod 4 and the square root is
therefore a single modular exponentiation, then checking that s·G == R + e·P. It runs entirely
on-chain, so the contract checks the proof before recording the link.
The hand-written verifier has not received a security audit, and the deployed contract is limited
to Base Sepolia. It demonstrates the proof path rather than establishing production security.
Recomputing the event id rather than accepting it. A signature is over the event id, so a contract that trusts a submitted id verifies nothing about the event’s contents. The contract serialises the fields per NIP-01 and derives the id itself, and only compares afterwards.