Skip to content

Commit cd7d1ce

Browse files
committed
implement nip26 delegation.
1 parent 613a843 commit cd7d1ce

File tree

4 files changed

+240
-0
lines changed

4 files changed

+240
-0
lines changed

README.md

+35
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,41 @@ sub.on('event', (event) => {
202202
})
203203
```
204204

205+
### Performing and checking for delegation
206+
207+
```js
208+
import {nip26, getPublicKey, generatePrivateKey} from 'nostr-tools'
209+
210+
// delegator
211+
let sk1 = generatePrivateKey()
212+
let pk1 = getPublicKey(sk1)
213+
214+
// delegatee
215+
let sk2 = generatePrivateKey()
216+
let pk2 = getPublicKey(sk2)
217+
218+
// generate delegation
219+
let delegation = nip26.createDelegation(sk1, {
220+
pubkey: pk2,
221+
kind: 1,
222+
since: Math.round(Date.now() / 1000),
223+
until: Math.round(Date.now() / 1000) + 60 * 60 * 24 * 30 /* 30 days */
224+
})
225+
226+
// the delegatee uses the delegation when building an event
227+
let event = {
228+
pubkey: pk2,
229+
kind: 1,
230+
created_at: Math.round(Date.now() / 1000),
231+
content: 'hello from a delegated key',
232+
tags: [['delegation', delegation.from, delegation.cond, delegation.sig]]
233+
}
234+
235+
// finally any receiver of this event can check for the presence of a valid delegation tag
236+
let delegator = nip26.getDelegator(event)
237+
assert(delegator === pk1) // will be null if there is no delegation tag or if it is invalid
238+
```
239+
205240
Please consult the tests or [the source code](https://github.com/fiatjaf/nostr-tools) for more information that isn't available here.
206241

207242
### Using from the browser (if you don't want to use a bundler)

index.ts

+10
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,13 @@ export * as nip04 from './nip04'
77
export * as nip05 from './nip05'
88
export * as nip06 from './nip06'
99
export * as nip19 from './nip19'
10+
export * as nip26 from './nip26'
11+
12+
// monkey patch secp256k1
13+
import * as secp256k1 from '@noble/secp256k1'
14+
import {hmac} from '@noble/hashes/hmac'
15+
import {sha256} from '@noble/hashes/sha256'
16+
secp256k1.utils.hmacSha256Sync = (key, ...msgs) =>
17+
hmac(sha256, key, secp256k1.utils.concatBytes(...msgs))
18+
secp256k1.utils.sha256Sync = (...msgs) =>
19+
sha256(secp256k1.utils.concatBytes(...msgs))

nip26.test.js

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/* eslint-env jest */
2+
3+
const {nip26, getPublicKey, generatePrivateKey} = require('./lib/nostr.cjs')
4+
5+
test('parse good delegation from NIP', async () => {
6+
expect(
7+
nip26.getDelegator({
8+
id: 'a080fd288b60ac2225ff2e2d815291bd730911e583e177302cc949a15dc2b2dc',
9+
pubkey:
10+
'62903b1ff41559daf9ee98ef1ae67cc52f301bb5ce26d14baba3052f649c3f49',
11+
created_at: 1660896109,
12+
kind: 1,
13+
tags: [
14+
[
15+
'delegation',
16+
'86f0689bd48dcd19c67a19d994f938ee34f251d8c39976290955ff585f2db42e',
17+
'kind=1&created_at>1640995200',
18+
'c33c88ba78ec3c760e49db591ac5f7b129e3887c8af7729795e85a0588007e5ac89b46549232d8f918eefd73e726cb450135314bfda419c030d0b6affe401ec1'
19+
]
20+
],
21+
content: 'Hello world',
22+
sig: 'cd4a3cd20dc61dcbc98324de561a07fd23b3d9702115920c0814b5fb822cc5b7c5bcdaf3fa326d24ed50c5b9c8214d66c75bae34e3a84c25e4d122afccb66eb6'
23+
})
24+
).toEqual('86f0689bd48dcd19c67a19d994f938ee34f251d8c39976290955ff585f2db42e')
25+
})
26+
27+
test('parse bad delegations', async () => {
28+
expect(
29+
nip26.getDelegator({
30+
id: 'a080fd288b60ac2225ff2e2d815291bd730911e583e177302cc949a15dc2b2dc',
31+
pubkey:
32+
'62903b1ff41559daf9ee98ef1ae67cc52f301bb5ce26d14baba3052f649c3f49',
33+
created_at: 1660896109,
34+
kind: 1,
35+
tags: [
36+
[
37+
'delegation',
38+
'86f0689bd48dcd19c67a19d994f938ee34f251d8c39976290955ff585f2db42f',
39+
'kind=1&created_at>1640995200',
40+
'c33c88ba78ec3c760e49db591ac5f7b129e3887c8af7729795e85a0588007e5ac89b46549232d8f918eefd73e726cb450135314bfda419c030d0b6affe401ec1'
41+
]
42+
],
43+
content: 'Hello world',
44+
sig: 'cd4a3cd20dc61dcbc98324de561a07fd23b3d9702115920c0814b5fb822cc5b7c5bcdaf3fa326d24ed50c5b9c8214d66c75bae34e3a84c25e4d122afccb66eb6'
45+
})
46+
).toEqual(null)
47+
48+
expect(
49+
nip26.getDelegator({
50+
id: 'a080fd288b60ac2225ff2e2d815291bd730911e583e177302cc949a15dc2b2dc',
51+
pubkey:
52+
'62903b1ff41559daf9ee98ef1ae67cc52f301bb5ce26d14baba3052f649c3f49',
53+
created_at: 1660896109,
54+
kind: 1,
55+
tags: [
56+
[
57+
'delegation',
58+
'86f0689bd48dcd19c67a19d994f938ee34f251d8c39976290955ff585f2db42e',
59+
'kind=1&created_at>1740995200',
60+
'c33c88ba78ec3c760e49db591ac5f7b129e3887c8af7729795e85a0588007e5ac89b46549232d8f918eefd73e726cb450135314bfda419c030d0b6affe401ec1'
61+
]
62+
],
63+
content: 'Hello world',
64+
sig: 'cd4a3cd20dc61dcbc98324de561a07fd23b3d9702115920c0814b5fb822cc5b7c5bcdaf3fa326d24ed50c5b9c8214d66c75bae34e3a84c25e4d122afccb66eb6'
65+
})
66+
).toEqual(null)
67+
68+
expect(
69+
nip26.getDelegator({
70+
id: 'a080fd288b60ac2225ff2e2d815291bd730911e583e177302cc949a15dc2b2dc',
71+
pubkey:
72+
'62903b1ff41559daf9ee98ef1ae67c152f301bb5ce26d14baba3052f649c3f49',
73+
created_at: 1660896109,
74+
kind: 1,
75+
tags: [
76+
[
77+
'delegation',
78+
'86f0689bd48dcd19c67a19d994f938ee34f251d8c39976290955ff585f2db42e',
79+
'kind=1&created_at>1640995200',
80+
'c33c88ba78ec3c760e49db591ac5f7b129e3887c8af7729795e85a0588007e5ac89b46549232d8f918eefd73e726cb450135314bfda419c030d0b6affe401ec1'
81+
]
82+
],
83+
content: 'Hello world',
84+
sig: 'cd4a3cd20dc61dcbc98324de561a07fd23b3d9702115920c0814b5fb822cc5b7c5bcdaf3fa326d24ed50c5b9c8214d66c75bae34e3a84c25e4d122afccb66eb6'
85+
})
86+
).toEqual(null)
87+
})
88+
89+
test('create and verify delegation', async () => {
90+
let sk1 = generatePrivateKey()
91+
let pk1 = getPublicKey(sk1)
92+
let sk2 = generatePrivateKey()
93+
let pk2 = getPublicKey(sk2)
94+
let delegation = nip26.createDelegation(sk1, {pubkey: pk2, kind: 1})
95+
expect(delegation).toHaveProperty('from', pk1)
96+
expect(delegation).toHaveProperty('to', pk2)
97+
expect(delegation).toHaveProperty('cond', 'kind=1')
98+
99+
let event = {
100+
kind: 1,
101+
tags: [['delegation', delegation.from, delegation.cond, delegation.sig]],
102+
pubkey: pk2
103+
}
104+
expect(nip26.getDelegator(event)).toEqual(pk1)
105+
})

nip26.ts

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import * as secp256k1 from '@noble/secp256k1'
2+
import {sha256} from '@noble/hashes/sha256'
3+
4+
import {Event} from './event'
5+
import {utf8Encoder} from './utils'
6+
import {getPublicKey} from './keys'
7+
8+
export type Parameters = {
9+
pubkey: string // the key to whom the delegation will be given
10+
kind: number | undefined
11+
until: number | undefined // delegation will only be valid until this date
12+
since: number | undefined // delegation will be valid from this date on
13+
}
14+
15+
export type Delegation = {
16+
from: string // the pubkey who signed the delegation
17+
to: string // the pubkey that is allowed to use the delegation
18+
cond: string // the string of conditions as they should be included in the event tag
19+
sig: string
20+
}
21+
22+
export function createDelegation(
23+
privateKey: string,
24+
parameters: Parameters
25+
): Delegation {
26+
let conditions = []
27+
if ((parameters.kind || -1) >= 0) conditions.push(`kind=${parameters.kind}`)
28+
if (parameters.until) conditions.push(`created_at<${parameters.until}`)
29+
if (parameters.since) conditions.push(`created_at>${parameters.since}`)
30+
let cond = conditions.join('&')
31+
32+
if (cond === '')
33+
throw new Error('refusing to create a delegation without any conditions')
34+
35+
let sighash = sha256(
36+
utf8Encoder.encode(`nostr:delegation:${parameters.pubkey}:${cond}`)
37+
)
38+
39+
let sig = secp256k1.utils.bytesToHex(
40+
secp256k1.schnorr.signSync(sighash, privateKey)
41+
)
42+
43+
return {
44+
from: getPublicKey(privateKey),
45+
to: parameters.pubkey,
46+
cond,
47+
sig
48+
}
49+
}
50+
51+
export function getDelegator(event: Event): string | null {
52+
// find delegation tag
53+
let tag = event.tags.find(tag => tag[0] === 'delegation' && tag.length >= 4)
54+
if (!tag) return null
55+
56+
let pubkey = tag[1]
57+
let cond = tag[2]
58+
let sig = tag[3]
59+
60+
// check conditions
61+
let conditions = cond.split('&')
62+
for (let i = 0; i < conditions.length; i++) {
63+
let [key, operator, value] = conditions[i].split(/\b/)
64+
65+
// the supported conditions are just 'kind' and 'created_at' for now
66+
if (key === 'kind' && operator === '=' && event.kind === parseInt(value))
67+
continue
68+
else if (
69+
key === 'created_at' &&
70+
operator === '<' &&
71+
event.created_at < parseInt(value)
72+
)
73+
continue
74+
else if (
75+
key === 'created_at' &&
76+
operator === '>' &&
77+
event.created_at > parseInt(value)
78+
)
79+
continue
80+
else return null // invalid condition
81+
}
82+
83+
// check signature
84+
let sighash = sha256(
85+
utf8Encoder.encode(`nostr:delegation:${event.pubkey}:${cond}`)
86+
)
87+
if (!secp256k1.schnorr.verifySync(sig, sighash, pubkey)) return null
88+
89+
return pubkey
90+
}

0 commit comments

Comments
 (0)