implement auth, closed and close envelopes

This commit is contained in:
2025-08-30 13:21:06 +01:00
parent 1ba2bb0a9b
commit acee5e3a90
23 changed files with 1186 additions and 44 deletions

View File

@@ -0,0 +1,36 @@
package event
import (
"next.orly.dev/pkg/crypto/sha256"
"next.orly.dev/pkg/encoders/hex"
"next.orly.dev/pkg/encoders/ints"
"next.orly.dev/pkg/encoders/text"
)
// ToCanonical converts the event to the canonical encoding used to derive the
// event ID.
func (ev *E) ToCanonical(dst []byte) (b []byte) {
b = dst
b = append(b, "[0,\""...)
b = hex.EncAppend(b, ev.Pubkey)
b = append(b, "\","...)
b = ints.New(ev.CreatedAt).Marshal(nil)
b = append(b, ',')
b = ints.New(ev.Kind).Marshal(nil)
b = append(b, ',')
b = ev.Tags.Marshal(b)
b = append(b, ',')
b = text.AppendQuote(b, ev.Content, text.NostrEscape)
b = append(b, ']')
return
}
// GetIDBytes returns the raw SHA256 hash of the canonical form of an event.E.
func (ev *E) GetIDBytes() []byte { return Hash(ev.ToCanonical(nil)) }
// Hash is a little helper generate a hash and return a slice instead of an
// array.
func Hash(in []byte) (out []byte) {
h := sha256.Sum256(in)
return h[:]
}

View File

@@ -94,17 +94,8 @@ func (ev *E) Free() {
ev.b = nil
}
// MarshalJSON marshals an event.E into a JSON byte string.
//
// Call bufpool.PutBytes(b) to return the buffer to the bufpool after use.
//
// WARNING: if json.Marshal is called in the hopes of invoking this function on
// an event, if it has <, > or * in the content or tags they are escaped into
// unicode escapes and break the event ID. Call this function directly in order
// to bypass this issue.
func (ev *E) MarshalJSON() (b []byte, err error) {
b = bufpool.Get()
b = b[:0]
func (ev *E) Marshal(dst []byte) (b []byte) {
b = dst
b = append(b, '{')
b = append(b, '"')
b = append(b, jId...)
@@ -159,10 +150,76 @@ func (ev *E) MarshalJSON() (b []byte, err error) {
return
}
// UnmarshalJSON unmarshalls a JSON string into an event.E.
// MarshalJSON marshals an event.E into a JSON byte string.
//
// Call bufpool.PutBytes(b) to return the buffer to the bufpool after use.
//
// WARNING: if json.Marshal is called in the hopes of invoking this function on
// an event, if it has <, > or * in the content or tags they are escaped into
// unicode escapes and break the event ID. Call this function directly in order
// to bypass this issue.
func (ev *E) MarshalJSON() (b []byte, err error) {
b = bufpool.Get()
b = ev.Marshal(b[:0])
// b = b[:0]
// b = append(b, '{')
// b = append(b, '"')
// b = append(b, jId...)
// b = append(b, `":"`...)
// b = b[:len(b)+2*sha256.Size]
// xhex.Encode(b[len(b)-2*sha256.Size:], ev.ID)
// b = append(b, `","`...)
// b = append(b, jPubkey...)
// b = append(b, `":"`...)
// b = b[:len(b)+2*schnorr.PubKeyBytesLen]
// xhex.Encode(b[len(b)-2*schnorr.PubKeyBytesLen:], ev.Pubkey)
// b = append(b, `","`...)
// b = append(b, jCreatedAt...)
// b = append(b, `":`...)
// b = ints.New(ev.CreatedAt).Marshal(b)
// b = append(b, `,"`...)
// b = append(b, jKind...)
// b = append(b, `":`...)
// b = ints.New(ev.Kind).Marshal(b)
// b = append(b, `,"`...)
// b = append(b, jTags...)
// b = append(b, `":`...)
// if ev.Tags != nil {
// b = ev.Tags.Marshal(b)
// }
// b = append(b, `,"`...)
// b = append(b, jContent...)
// b = append(b, `":"`...)
// // it can happen the slice has insufficient capacity to hold the content AND
// // the signature at this point, because the signature encoder must have
// // sufficient capacity pre-allocated as it does not append to the buffer.
// // unlike every other encoding function up to this point. This also ensures
// // that since the bufpool defaults to 1kb, most events won't have a
// // re-allocation required, but if they do, it will be this next one, and it
// // integrates properly with the buffer pool, reducing GC pressure and
// // avoiding new heap allocations.
// if cap(b) < len(b)+len(ev.Content)+7+256+2 {
// b2 := make([]byte, len(b)+len(ev.Content)*2+7+256+2)
// copy(b2, b)
// b2 = b2[:len(b)]
// // return the old buffer to the pool for reuse.
// bufpool.PutBytes(b)
// b = b2
// }
// b = text.NostrEscape(b, ev.Content)
// b = append(b, `","`...)
// b = append(b, jSig...)
// b = append(b, `":"`...)
// b = b[:len(b)+2*schnorr.SignatureSize]
// xhex.Encode(b[len(b)-2*schnorr.SignatureSize:], ev.Sig)
// b = append(b, `"}`...)
return
}
// Unmarshal unmarshalls a JSON string into an event.E.
//
// Call ev.Free() to return the provided buffer to the bufpool afterwards.
func (ev *E) UnmarshalJSON(b []byte) (err error) {
func (ev *E) Unmarshal(b []byte) (rem []byte, err error) {
key := make([]byte, 0, 9)
for ; len(b) > 0; b = b[1:] {
// Skip whitespace
@@ -337,10 +394,7 @@ BetweenKV:
log.I.F("between kv")
goto eof
AfterClose:
// Skip any trailing whitespace
for len(b) > 0 && isWhitespace(b[0]) {
b = b[1:]
}
rem = b
return
invalid:
err = fmt.Errorf(
@@ -353,6 +407,14 @@ eof:
return
}
// UnmarshalJSON unmarshalls a JSON string into an event.E.
//
// Call ev.Free() to return the provided buffer to the bufpool afterwards.
func (ev *E) UnmarshalJSON(b []byte) (err error) {
_, err = ev.Unmarshal(b)
return
}
// isWhitespace returns true if the byte is a whitespace character (space, tab, newline, carriage return).
func isWhitespace(b byte) bool {
return b == ' ' || b == '\t' || b == '\n' || b == '\r'

View File

@@ -0,0 +1,48 @@
package event
import (
"lol.mleku.dev/chk"
"lol.mleku.dev/errorf"
"lol.mleku.dev/log"
"next.orly.dev/pkg/crypto/p256k"
"next.orly.dev/pkg/interfaces/signer"
"next.orly.dev/pkg/utils"
)
// Sign the event using the signer.I. Uses github.com/bitcoin-core/secp256k1 if
// available for much faster signatures.
//
// Note that this only populates the Pubkey, ID and Sig. The caller must
// set the CreatedAt timestamp as intended.
func (ev *E) Sign(keys signer.I) (err error) {
ev.Pubkey = keys.Pub()
ev.ID = ev.GetIDBytes()
if ev.Sig, err = keys.Sign(ev.ID); chk.E(err) {
return
}
return
}
// Verify an event is signed by the pubkey it contains. Uses
// github.com/bitcoin-core/secp256k1 if available for faster verification.
func (ev *E) Verify() (valid bool, err error) {
keys := p256k.Signer{}
if err = keys.InitPub(ev.Pubkey); chk.E(err) {
return
}
if valid, err = keys.Verify(ev.ID, ev.Sig); chk.T(err) {
// check that this isn't because of a bogus ID
id := ev.GetIDBytes()
if !utils.FastEqual(id, ev.ID) {
log.E.Ln("event ID incorrect")
ev.ID = id
err = nil
if valid, err = keys.Verify(ev.ID, ev.Sig); chk.E(err) {
return
}
err = errorf.W("event ID incorrect but signature is valid on correct ID")
}
return
}
return
}