From 2e478bbeac66ba90e4df802476cbaa71d6198a18 Mon Sep 17 00:00:00 2001 From: ZigBalthazar Date: Mon, 9 Sep 2024 00:25:38 +0330 Subject: [PATCH] feat(types): add event validation and tag marshal --- go.mod | 4 +++ go.sum | 8 ++++++ makefile | 33 ++++++++++++++++++++++ types/event/event.go | 59 +++++++++++++++++++++++++++++++++++++-- types/event/event_test.go | 34 ++++++++++++++++++++++ types/tag.go | 27 ++++++++++++++++++ types/utils.go | 40 ++++++++++++++++++++++++++ 7 files changed, 203 insertions(+), 2 deletions(-) create mode 100644 makefile diff --git a/go.mod b/go.mod index 60876d6..03e7a0a 100644 --- a/go.mod +++ b/go.mod @@ -3,13 +3,17 @@ module github.com/dezh-tech/immortal go 1.22.5 require ( + github.com/btcsuite/btcd/btcec/v2 v2.3.4 github.com/mailru/easyjson v0.7.7 github.com/stretchr/testify v1.9.0 github.com/tidwall/gjson v1.17.3 ) require ( + github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/tidwall/match v1.1.1 // indirect diff --git a/go.sum b/go.sum index f540fdf..fd7869a 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,13 @@ +github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ= +github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= diff --git a/makefile b/makefile new file mode 100644 index 0000000..4d7309d --- /dev/null +++ b/makefile @@ -0,0 +1,33 @@ +PACKAGES=$(shell go list ./... | grep -v 'tests' | grep -v 'grpc/gen') + +ifneq (,$(filter $(OS),Windows_NT MINGW64)) +EXE = .exe +RM = del /q +else +RM = rm -rf +endif + +### Tools needed for development +devtools: + @echo "Installing devtools" + go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + go install mvdan.cc/gofumpt@latest + go install github.com/ethereum/go-ethereum/cmd/abigen@latest + +### Testing +unit_test: + go test $(PACKAGES) + +### Formatting the code +fmt: + gofumpt -l -w . + go mod tidy + +check: + golangci-lint run --timeout=20m0s + +### pre commit +pre-commit: fmt check unit_test + @echo ready to commit... + +.PHONY: build \ No newline at end of file diff --git a/types/event/event.go b/types/event/event.go index 6778ddf..ed4cdcb 100644 --- a/types/event/event.go +++ b/types/event/event.go @@ -1,9 +1,15 @@ package event import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "github.com/dezh-tech/immortal/types" "github.com/dezh-tech/immortal/types/filter" "github.com/mailru/easyjson" + + "github.com/btcsuite/btcd/btcec/v2/schnorr" ) // Event represents an event structure defined on NIP-01. @@ -89,7 +95,56 @@ func (e *Event) Encode() ([]byte, error) { return b, nil } +func (evt *Event) Serialize() []byte { + // the serialization process is just putting everything into a JSON array + // so the order is kept. See NIP-01 + dst := make([]byte, 0) + + // the header portion is easy to serialize + // [0,"pubkey",created_at,kind,[ + dst = append(dst, []byte( + fmt.Sprintf( + "[0,\"%s\",%d,%d,", + evt.PublicKey, + evt.CreatedAt, + evt.Kind, + ))...) + + // tags + dst = types.MarshalTo(evt.Tags, dst) + dst = append(dst, ',') + + // content needs to be escaped in general as it is user generated. + dst = types.EscapeString(dst, evt.Content) + dst = append(dst, ']') + + return dst +} + // IsValid function validats an event Signature and ID. -func (e *Event) IsValid() bool { - return false // TODO::: +func (e *Event) IsValid() (bool, error) { + // read and check pubkey + pk, err := hex.DecodeString(e.PublicKey) + if err != nil { + return false, fmt.Errorf("event pubkey '%s' is invalid hex: %w", e.PublicKey, err) + } + + pubkey, err := schnorr.ParsePubKey(pk) + if err != nil { + return false, fmt.Errorf("event has invalid pubkey '%s': %w", e.PublicKey, err) + } + + // read signature + s, err := hex.DecodeString(e.Signature) + if err != nil { + return false, fmt.Errorf("signature '%s' is invalid hex: %w", e.Signature, err) + } + sig, err := schnorr.ParseSignature(s) + if err != nil { + return false, fmt.Errorf("failed to parse signature: %w", err) + } + + // check signature + hash := sha256.Sum256(e.Serialize()) + return sig.Verify(hash[:], pubkey), nil } diff --git a/types/event/event_test.go b/types/event/event_test.go index 4963d33..7e14394 100644 --- a/types/event/event_test.go +++ b/types/event/event_test.go @@ -3,6 +3,7 @@ package event_test import ( "testing" + "github.com/dezh-tech/immortal/types" "github.com/dezh-tech/immortal/types/event" "github.com/dezh-tech/immortal/types/filter" "github.com/stretchr/testify/assert" @@ -29,6 +30,20 @@ var ( DecodedEvent *event.Event EncodedEvent []byte + + events = []event.Event{ + { + ID: "dc90c95f09947507c1044e8f48bcf6350aa6bff1507dd4acfc755b9239b5c962", + PublicKey: "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", + CreatedAt: 1644271588, + Kind: types.KindTextNote, + Tags: []types.Tag{}, + Content: "now that https://blueskyweb.org/blog/2-7-2022-overview was announced we can stop working on nostr?", + Signature: "230e9d8f0ddaf7eb70b5f7741ccfa37e87a455c9a469282e3464e2052d3192cd63a167e196e381ef9d7e69e9ea43af2443b839974dc85d8aaab9efe1d9296524", + }, + } + + EventValidation bool ) func TestDecode(t *testing.T) { @@ -91,3 +106,22 @@ func TestMatch(t *testing.T) { assert.True(t, e.Match(*f)) } + +func TestValidate(t *testing.T) { + for _, e := range events { + valid, err := e.IsValid() + + assert.NoError(t, err) + assert.True(t, valid) + } +} + +func BenchmarkValidate(b *testing.B) { + var eventValidation bool + for i := 0; i < b.N; i++ { + for _, e := range events { + eventValidation, _ = e.IsValid() + } + } + EventValidation = eventValidation +} diff --git a/types/tag.go b/types/tag.go index c2313c2..c52985b 100644 --- a/types/tag.go +++ b/types/tag.go @@ -1,3 +1,30 @@ package types type Tag []string + +// Marshal Tag. Used for Serialization so string escaping should be as in RFC8259. +func (tag Tag) MarshalTo(dst []byte) []byte { + dst = append(dst, '[') + for i, s := range tag { + if i > 0 { + dst = append(dst, ',') + } + dst = EscapeString(dst, s) + } + dst = append(dst, ']') + return dst +} + +// MarshalTo appends the JSON encoded byte of Tags as [][]string to dst. +// String escaping is as described in RFC8259. +func MarshalTo(tags []Tag, dst []byte) []byte { + dst = append(dst, '[') + for i, tag := range tags { + if i > 0 { + dst = append(dst, ',') + } + dst = tag.MarshalTo(dst) + } + dst = append(dst, ']') + return dst +} diff --git a/types/utils.go b/types/utils.go index eb81193..7c8ba5d 100644 --- a/types/utils.go +++ b/types/utils.go @@ -23,3 +23,43 @@ func ContainsKind(target Kind, arr []Kind) bool { return false } + +// Escaping strings for JSON encoding according to RFC8259. +// Also encloses result in quotation marks "". +func EscapeString(dst []byte, s string) []byte { + dst = append(dst, '"') + for i := 0; i < len(s); i++ { + c := s[i] + switch { + case c == '"': + // quotation mark + dst = append(dst, []byte{'\\', '"'}...) + case c == '\\': + // reverse solidus + dst = append(dst, []byte{'\\', '\\'}...) + case c >= 0x20: + // default, rest below are control chars + dst = append(dst, c) + case c == 0x08: + dst = append(dst, []byte{'\\', 'b'}...) + case c < 0x09: + dst = append(dst, []byte{'\\', 'u', '0', '0', '0', '0' + c}...) + case c == 0x09: + dst = append(dst, []byte{'\\', 't'}...) + case c == 0x0a: + dst = append(dst, []byte{'\\', 'n'}...) + case c == 0x0c: + dst = append(dst, []byte{'\\', 'f'}...) + case c == 0x0d: + dst = append(dst, []byte{'\\', 'r'}...) + case c < 0x10: + dst = append(dst, []byte{'\\', 'u', '0', '0', '0', 0x57 + c}...) + case c < 0x1a: + dst = append(dst, []byte{'\\', 'u', '0', '0', '1', 0x20 + c}...) + case c < 0x20: + dst = append(dst, []byte{'\\', 'u', '0', '0', '1', 0x47 + c}...) + } + } + dst = append(dst, '"') + return dst +}