Compare commits

...

35 Commits

Author SHA1 Message Date
0544159d4b added fetch of metadata and relay lists, and private flag to stop spider fetching 2025-07-20 11:42:39 +01:00
65e1dd6183 fully implemented spider to fetch follows of owners, their follows, and the owner mute list 2025-07-20 09:42:06 +01:00
0e83a56025 completed first level of owners followed pubkeys 2025-07-19 19:37:30 +01:00
93d6871488 Standardize comment format for parameters and return values in socketapi package
- Updated `pkg/protocol/socketapi/socketapi.go` with "# Expected Behaviour" section using British spelling
- Enhanced `pkg/protocol/socketapi/handleEvent.go` parameter documentation with context type annotations
- Standardized return value formatting with consistent bracket placement
- Improved error message phrasing to use "can't" instead of "cannot" for consistency
- Added detailed implementation notes for event deletion validation logic
2025-07-19 14:45:55 +01:00
681cdb3a64 Standardize parameter/return value comments in protocol/socketapi package
- Updated `pkg/protocol/socketapi/handleClose.go` to use "# Parameters" and "# Return Values" for consistent comment formatting
- Updated `pkg/protocol/socketapi/handleReq.go` with standardized parameter and return value documentation
- Modified `pkg/interfaces/relay/interface.go` to align comment style with parameter/return value sections
- Standardized comments in `pkg/protocol/socketapi/pinger.go` using "# Parameters" format
- Improved comment structure in `pkg/protocol/socketapi/socketapi.go` for parameter documentation
- Updated `pkg/protocol/socketapi/handleEvent.go` with consistent return value comment formatting
2025-07-19 14:19:54 +01:00
901b4ff16a Enhance function and configuration documentation with detailed comments
- Updated `pkg/app/main.go` with comprehensive parameter descriptions, return value explanations, and "Expected behaviour" sections for `AcceptFilter` and `AcceptReq` functions
- Improved `pkg/app/config/config.go` comments using consistent terminology (British spelling) and added detailed implementation notes for configuration methods
- Standardized function documentation format across both files with aligned parameter/return value descriptions
2025-07-19 13:32:37 +01:00
6d34664cf8 Update README bullet point wording for clarity
- **readme.adoc**:
  - Adjusted phrasing in feature description from "other bits" to "bits" for precision
  - Maintained consistent line wrapping in technical documentation sections
2025-07-19 12:56:44 +01:00
dac6a30625 Improve readability by splitting long lines in README
- **readme.adoc**:
  - Split long bullet points into shorter lines for improved formatting and readability
  - Adjusted line breaks in technical descriptions (e.g., CGO configuration, database storage location)
  - Ensured consistent line wrapping across documentation sections
2025-07-19 12:55:55 +01:00
5959d5dc7e Enhance documentation for configuration functions
- **pkg/app/config/config.go**
  - Added detailed comments explaining configuration struct purpose, environment loading behavior, and error handling in `New`
  - Expanded function documentation with parameter descriptions, return value explanations, and expected behavior details for all exported methods
  - Improved docstring formatting consistency across the file
  - Clarified .env file handling logic in multiple functions
  - Added implementation notes for key-value operations and sorting behavior
2025-07-19 12:30:38 +01:00
1d07875652 Merge remote-tracking branch 'origin/main' 2025-07-19 11:27:17 +01:00
8ec0c49ecd Add version control infrastructure
- **.gitignore**:
  - Added `!version` to include version files in git tracking.
- **pkg/version/version**:
  - Created new file with initial version `v0.1.1`.
2025-07-19 11:27:06 +01:00
525df97679 Refactor and document Publish method in server-publish.go
### Modified Files
- **pkg/app/relay/server-publish.go**
  - Added comprehensive function documentation for `Publish`.
  - Clarified parameters, return values, and expected behavior.
  - Updated comments to improve readability and understanding of the method's functionality.
2025-07-17 23:55:02 +01:00
e2ad580c65 Refactor and document ServiceURL in auth.go
### Modified Files
- **pkg/app/relay/auth.go**
  - Updated comments to clarify function purpose, parameters, return values, and expected behavior.
  - Enhanced documentation to include details on protocol determination and URL construction.
2025-07-17 23:31:59 +01:00
ba287ee644 Refactor and document ServiceURL in auth.go
- **pkg/app/relay/auth.go**
  - Updated comments to clarify function purpose, parameters, return values, and expected behavior.
  - Enhanced documentation to include details on protocol determination and URL construction.
2025-07-17 23:17:10 +01:00
c391b9db46 Add detailed documentation for ServiceURL function in auth.go
pkg/app/relay/auth.go
  - Added comprehensive function documentation for ServiceURL
  - Clarified parameters, return values, and expected behavior
  - Included implementation details for protocol determination
  - Specified construction logic for WebSocket URL
2025-07-17 23:12:17 +01:00
59f246d304 Refactor accept-req.go and accept-event.go with detailed documentation
### Modified Files
- **pkg/app/relay/accept-req.go**
  - Added comprehensive function documentation for `AcceptReq`.
  - Clarified parameters, return values, and expected behavior.

- **pkg/app/relay/accept-event.go**
  - Added comprehensive function documentation for `AcceptEvent`.
  - Clarified parameters, return values, and expected behavior.
2025-07-17 22:44:44 +01:00
eaed2294bc Refactor addEvent.go with detailed documentation
### Modified Files
- **pkg/app/relay/addEvent.go**
  - Added comprehensive function documentation for `AddEvent`.
  - Clarified parameters, return values, and expected behavior.
2025-07-17 22:39:33 +01:00
ae0d4f5b68 ### Commit Message
Update .gitignore and refactor main.go, pkg/app/main.go

### Modified Files
- **.gitignore**
  - Added `.idea/material_theme_project_new.xml` and `.idea/orly.iml` to be ignored.

- **main.go**
  - Moved `os` import to the top.
  - Adjusted the order of imports for better readability.
  - Commented out the unused `profile` import.

- **pkg/app/main.go**
  - Added detailed comments for types and methods in the `Relay` struct.
  - Reorganized method descriptions for clarity.
  - Ensured consistent documentation style throughout the file.
2025-07-17 22:26:08 +01:00
4ee09ada17 Relocate tests/generate.go under pkg/tests/
- **tests/generate.go**:
  - Moved file to `pkg/tests/` directory for better organization.
2025-07-17 14:02:48 +01:00
e91c591a6f Update readme with note on forthcoming HTTP API details
- **readme.adoc**:
  - Added a note indicating that the HTTP API is not included yet but is planned for future release.
2025-07-17 14:00:38 +01:00
2323545d4b Update readme with clarification on features and repository policy
- **readme.adoc**:
  - Added "(todo: this is mostly built and designed but not currently available)" to a listed feature for better transparency on availability.
  - Clarified that the branch name is `main` instead of `dev` under repository policy.
  - Noted that "Simplified Nostr" is not currently implemented and marked it as "coming soon".
2025-07-17 13:59:45 +01:00
1d18425677 fixed missing bits
- **readme.adoc**:
  - Updated contact information with nostr profile for better user engagement.
2025-07-17 13:30:34 +01:00
fc68bcf3cb moved everything into pkg/ 2025-07-17 13:18:55 +01:00
affd6c1ebc Update readme to replace placeholder image link and remove duplicate image
- **readme.adoc**:
  - Replaced `= orly.lol` with `image:./orly.png[orly.dev]` for better branding.
  - Removed duplicate `image:./orly.png[orly.png]` line to clean up content.
2025-07-17 13:11:28 +01:00
6e103c454d Remove unused fields from Server struct and update readme contact info
- **app/relay/server.go**:
  - Removed unused `Admins` and `Owners` fields from the `Server` struct.
  - Removed unused `signer` package import.

- **readme.adoc**:
  - Updated contact information for the "zap me" section.
2025-07-17 13:10:27 +01:00
db3f98b8cb Refactor ServeHTTP logic and minor documentation updates
- **app/relay/server.go**:
  - Reorganized `ServeHTTP` method to prioritize WebSocket upgrade checks.
  - Streamlined logic for handling relay info requests.

- **readme.adoc**:
  - Fixed punctuation inconsistencies in feature descriptions.
  - Clarified Go version requirements, specifying 1.24 as the minimum.
2025-07-17 13:07:17 +01:00
43404d6a07 Remove unused disconnect method and integrate configuration into server
- **app/relay/disconnect.go**:
  - Deleted unused `disconnect` method that was no longer referenced in code.

- **app/relay/server.go**:
  - Integrated server configuration (`config.C`) directly into the `Server` struct.
  - Refactored `authRequired` and `publicReadable` fields to derive from the configuration.

- **app/relay/server-impl.go**:
  - Removed references to the obsolete `disconnect` method.
  - Updated methods to retrieve authentication and readability settings from configuration.

- **app/relay/handleRelayinfo.go**:
  - Adjusted `handleRelayinfo` to use `config.C.AuthRequired` for showing relay limitations.

- **app/config/config.go**:
  - Added default values for the `SpiderSeeds` field for consistent initialization.
  - Logged configuration details upon loading for better debugging visibility.

- **interfaces/server/server.go**:
  - Removed `Disconnect` method from `server.I`, as it was no longer needed.

- **main.go**:
  - Modified `relay.ServerParams` to pass the configuration as a pointer (`config.C`).
  - Simplified initialization by removing redundant fields replaced by configuration integration.

- **protocol/socketapi/handleEvent.go**:
  - Standardized and clarified comments for parameter descriptions.
2025-07-17 11:22:53 +01:00
49bdf3f5d7 Refactor HandleEvent response handling and enhance configuration defaults
- **protocol/socketapi/handleEvent.go**:
  - Replaced `sendResponse` function with `Ok` utility for cleaner error handling.
  - Consolidated and standardized response handling across event processing flow.
  - Improved readability by simplifying response logic in deletion scenarios.

- **app/config/config.go**:
  - Added default values for `SpiderSeeds` and `Owners` in configuration.
  - Updated fallback logic for `State`, `Config`, and `DataDir` to support `~` expansion.

- **protocol/socketapi/publisher.go**:
  - Minor formatting adjustment for better log output readability.

- **.gitignore**:
  - Ignored `.idea/codeStyles/codeStyleConfig.xml` for better IDE environment handling.
2025-07-17 09:53:02 +01:00
a2449e24ae add acceptreq/acceptevent and minimal implementation
- now rejects events without auth if ORLY_AUTH_REQUIRED=true

- now rejects requests without auth if ORLY_PUBLIC_READABLE=false and ORLY_AUTH_REQUIRED=true

- renamed /app/realy to app/relay to be more descriptive

- cleaned up some noisy logging

- fixed a bug in teh new atomic.Bytes that didn't check for nil (untyped) as the Value
2025-07-16 15:10:22 +01:00
5c129e078e Refactor server interface, add public-readable support, and update versioning
- **app/realy/handleWebsocket.go**:
  - Modified `Server` struct initialization to use `server.I` instead of `server.S`.

- **protocol/socketapi/handleEvent.go**:
  - Updated `HandleEvent` method to accept `server.I` for improved interface consistency.

- **app/realy/server-impl.go**:
  - Added the `PublicReadable` method to the `Server` struct.
  - Replaced `server.S` references with `server.I` to align with updated interface.

- **protocol/relayinfo/types.go**:
  - Fixed typo in error message from "realy" to "relay".

- **protocol/socketapi/handleMessage.go**:
  - Replaced `server.S` references with `server.I` in event handling methods.

- **protocol/socketapi/handleClose.go**:
  - Updated `HandleClose` method to use `server.I`.

- **protocol/socketapi/handleReq.go**:
  - Updated `HandleReq` method signature to accept `server.I`.

- **app/realy/server.go**:
  - Introduced `publicReadable` field in the `Server` struct.
  - Updated `NewServer` to handle `PublicReadable` parameter from `ServerParams`.

- **protocol/socketapi/socketapi.go**:
  - Changed `server.S` to `server.I` across the WebSocket logic for consistency.

- **version/version**:
  - Downgraded version file from `v1.14.3` to `v0.1.1`.

- **version/version.go**:
  - Corrected `URL` constant from `https://orly` to `https://orly.dev`.

- **interfaces/server/server.go**:
  - Renamed interface `S` to `I`.
  - Added `PublicReadable` to the renamed `I` interface.

- **main.go**:
  - Introduced logging of `PublicReadable` support when starting the server.
  - Updated imports and initialization to align with added features.

- **app/config/config.go**:
  - Added `PublicReadable` flag in configuration struct and environment settings.

- **protocol/socketapi/pinger.go**:
  - Modified `Pinger` method to use `server.I`.

- **protocol/socketapi/handleAuth.go**:
  - Adjusted `HandleAuth` logic to utilize `server.I`.

- **app/resources.go**:
  - Introduced a new initialization flow reflecting updated server settings.
2025-07-16 14:15:06 +01:00
b28acc0c29 Add authentication support and enhance protocol handling
- **protocol/socketapi/challenge.go**:
  - Removed unused `GetListener` function and its associated logic.

- **protocol/socketapi/handleEvent.go**:
  - Refactored `env.Id` references to `env.E.Id` to standardize access to event identifiers.

- **app/realy/server-impl.go**:
  - Added `AuthRequired` method to expose server-level authentication configuration.

- **app/realy/handleRelayinfo.go**:
  - Included `Authentication` in supported protocols.
  - Updated relay information to expose `AuthRequired` status in `Limitation`.

- **encoders/envelopes/authenvelope/authenvelope.go**:
  - Introduced `Id` method for `Response` type.

- **app/realy/server-publish.go**:
  - Refined event saving logic by replacing `chk.E` with a direct `err` check.

- **app/realy/server.go**:
  - Added `authRequired` field in `Server` struct and integrated it into relevant methods.

- **protocol/socketapi/socketapi.go**:
  - Introduced authentication handling in WebSocket listeners if `AuthRequired` is enabled.

- **protocol/socketapi/handleAuth.go**:
  - Implemented authentication response validation against challenges and server URLs.
  - Supported logging of successes and failures.

- **app/config/config.go**:
  - Added `AuthRequired` flag to configuration structure and environment variables.

- **encoders/envelopes/eventenvelope/eventenvelope.go**:
  - Added `Id` method for `Submission` and `Result` types.

- **interfaces/server/server.go**:
  - Added `AuthRequired` and `ServiceURL` methods to the interface.

- **protocol/ws/listener.go**:
  - Added `authRequested` field and methods to track authentication requests.

- **protocol/socketapi/ok.go**:
  - Introduced `Ok` utility functions for standard responses indicating reasons and types of errors.

- **app/realy/auth.go**:
  - Added `ServiceURL` method to determine the relay's URL for authentication responses.

- **encoders/reason/reason.go**:
  - Defined reusable reason templates for responses such as `auth-required` and `error`.

- **protocol/socketapi/ws.go**:
  - Adjusted `NewListener` to handle `authRequired` flag and conditionally generate challenges.
2025-07-16 12:53:52 +01:00
71b699c5c5 Add authentication challenge handling in WebSocket listener
- **protocol/ws/listener.go**:
  - Added new fields in `Listener` to support authentication: `authedPubkey`, `isAuthed`, and `challenge`.
  - Integrated `auth.GenerateChallenge` to generate a challenge during `NewListener`.
  - Introduced new methods: `IsAuthed`, `SetAuthed`, `AuthedPubkey`, `SetAuthedPubkey`, `Challenge`, and `SetChallenge`.

- **protocol/auth/nip42.go**:
  - Refined `GenerateChallenge` to produce a 16-byte base64 challenge string using URL-safe encoding.
  - Removed commented-out logging to clean up code.

- **app/resources.go**:
  - Simplified resource monitoring by removing memory statistics logging.
2025-07-16 11:11:08 +01:00
8164330f29 Add atomic.Bytes implementation with JSON support and unit tests
- **utils/atomic/bytes.go**:
  - Introduced `Bytes` type as an atomic wrapper for `[]byte`.
  - Added methods `Load`, `Store`, `MarshalJSON`, and `UnmarshalJSON`.
  - Ensured atomicity and data immutability in data access.

- **utils/atomic/bytes_ext.go**:
  - Implemented `MarshalJSON` to encode `[]byte` as base64 for JSON.
  - Implemented `UnmarshalJSON` to decode base64 data into `[]byte`.

- **utils/atomic/bytes_test.go**:
  - Added unit tests for `Bytes` with various scenarios:
    - Initialization and atomic data access.
    - JSON marshaling and unmarshaling with validation.
    - Concurrency stress testing and data integrity validation.
  - Benchmarked concurrent data reads/writes with `BenchmarkBytesParallel`.
2025-07-16 10:31:43 +01:00
1226b1f534 Add named return values across functions for improved readability
- **protocol/ws/client_test.go**:
  - Updated `newWebsocketServer` to use named return value `server`.
  - Changed `anyOriginHandshake` to return named `err`.
  - Modified `mustRelayConnect` to return named `client`.

- **app/realy/server.go**:
  - Updated `Start` to use named return value `err`.
  - Refactored `Router` to return named `router`.

- **protocol/socketapi/publisher.go**:
  - Changed `Type` and `New` functions to use named return values `typeName` and `publisher`.
  - Adjusted `Type` in `S` to return `typeName`.

- **protocol/servemux/serveMux.go**:
  - Updated `NewServeMux` to return named `mux`.

- **database/get-indexes-from-filter.go**:
  - Refactored `isHexString` to return named `isHex`.

- **protocol/ws/connection.go**:
  - Updated functions (`NewConnection`, `WriteMessage`, `ReadMessage`, `Close`) to use named return values for enhanced code clarity (`connection`, `errResult`, `err`).
2025-07-16 08:27:18 +01:00
3aa56ebe66 Remove unused protocol packages and interfaces and clean up code 2025-07-16 07:46:19 +01:00
524 changed files with 5576 additions and 5311 deletions

9
.gitignore vendored
View File

@@ -64,7 +64,7 @@ node_modules/**
!.gitmodules
!*.txt
!*.sum
!version
!pkg/version
!*.service
!*.benc
!*.png
@@ -84,13 +84,13 @@ node_modules/**
!*.xml
!.name
!.gitignore
!version
# ...even if they are in subdirectories
!*/
/blocklist.json
/gui/gui/main.wasm
/gui/gui/index.html
database/testrealy
pkg/database/testrealy
/.idea/workspace.xml
/.idea/dictionaries/project.xml
/.idea/shelf/Add_tombstone_handling__enhance_event_ID_logic__update_imports.xml
@@ -99,3 +99,6 @@ database/testrealy
/.idea/modules.xml
/.idea/orly.dev.iml
/.idea/vcs.xml
/.idea/codeStyles/codeStyleConfig.xml
/.idea/material_theme_project_new.xml
/.idea/orly.iml

View File

@@ -1,199 +0,0 @@
// Package config provides a go-simpler.org/env configuration table and helpers
// for working with the list of key/value lists stored in .env files.
package config
import (
"fmt"
"io"
"orly.dev/utils/chk"
env2 "orly.dev/utils/env"
"orly.dev/utils/log"
"orly.dev/utils/lol"
"orly.dev/version"
"os"
"path/filepath"
"reflect"
"sort"
"strings"
"time"
"github.com/adrg/xdg"
"go-simpler.org/env"
"orly.dev/utils/apputil"
)
// C is the configuration for realy relay. These are read from the environment
// if present, or if a .env file is found in ~/.config/realy/ that is read
// instead and overrides anything else.
type C struct {
AppName string `env:"ORLY_APP_NAME" default:"orly"`
Config string `env:"ORLY_CONFIG_DIR" usage:"location for configuration file, which has the name '.env' to make it harder to delete, and is a standard environment KEY=value<newline>... style"`
State string `env:"ORLY_STATE_DATA_DIR" usage:"storage location for state data affected by dynamic interactive interfaces"`
DataDir string `env:"ORLY_DATA_DIR" usage:"storage location for the ratel event store"`
Listen string `env:"ORLY_LISTEN" default:"0.0.0.0" usage:"network listen address"`
DNS string `env:"ORLY_DNS" usage:"external DNS name that points at the relay"`
Port int `env:"ORLY_PORT" default:"3334" usage:"port to listen on"`
LogLevel string `env:"ORLY_LOG_LEVEL" default:"info" usage:"debug level: fatal error warn info debug trace"`
DbLogLevel string `env:"ORLY_DB_LOG_LEVEL" default:"info" usage:"debug level: fatal error warn info debug trace"`
Pprof bool `env:"ORLY_PPROF" default:"false" usage:"enable pprof on 127.0.0.1:6060"`
}
// New creates a new config.C.
func New() (cfg *C, err error) {
cfg = &C{}
if err = env.Load(cfg, &env.Options{SliceSep: ","}); chk.T(err) {
return
}
if cfg.Config == "" {
cfg.Config = filepath.Join(xdg.ConfigHome, cfg.AppName)
}
if cfg.DataDir == "" {
cfg.DataDir = filepath.Join(xdg.DataHome, cfg.AppName)
}
envPath := filepath.Join(cfg.Config, ".env")
if apputil.FileExists(envPath) {
var e env2.Env
if e, err = env2.GetEnv(envPath); chk.T(err) {
return
}
if err = env.Load(
cfg, &env.Options{SliceSep: ",", Source: e},
); chk.E(err) {
return
}
lol.SetLogLevel(cfg.LogLevel)
log.I.F("loaded configuration from %s", envPath)
}
return
}
// HelpRequested returns true if any of the common types of help invocation are
// found as the first command line parameter/flag.
func HelpRequested() (help bool) {
if len(os.Args) > 1 {
switch strings.ToLower(os.Args[1]) {
case "help", "-h", "--h", "-help", "--help", "?":
help = true
}
}
return
}
// GetEnv processes os.Args to detect a request for printing the current
// settings as a list of environment variable key/values.
func GetEnv() (requested bool) {
if len(os.Args) > 1 {
switch strings.ToLower(os.Args[1]) {
case "env":
requested = true
}
}
return
}
// KV is a key/value pair.
type KV struct{ Key, Value string }
// KVSlice is a collection of key/value pairs.
type KVSlice []KV
func (kv KVSlice) Len() int { return len(kv) }
func (kv KVSlice) Less(i, j int) bool { return kv[i].Key < kv[j].Key }
func (kv KVSlice) Swap(i, j int) { kv[i], kv[j] = kv[j], kv[i] }
// Compose merges two KVSlice together, replacing the values of earlier keys
// with same named KV items later in the slice (enabling compositing two
// together as a .env, as well as them being composed as structs.
func (kv KVSlice) Compose(kv2 KVSlice) (out KVSlice) {
// duplicate the initial KVSlice
for _, p := range kv {
out = append(out, p)
}
out:
for i, p := range kv2 {
for j, q := range out {
// if the key is repeated, replace the value
if p.Key == q.Key {
out[j].Value = kv2[i].Value
continue out
}
}
out = append(out, p)
}
return
}
// EnvKV turns a struct with `env` keys (used with go-simpler/env) into a
// standard formatted environment variable key/value pair list, one per line.
// Note you must dereference a pointer type to use this. This allows the
// composition of the config in this file with an extended form with a
// customized variant of realy to produce correct environment variables both
// read and write.
func EnvKV(cfg any) (m KVSlice) {
t := reflect.TypeOf(cfg)
for i := 0; i < t.NumField(); i++ {
k := t.Field(i).Tag.Get("env")
v := reflect.ValueOf(cfg).Field(i).Interface()
var val string
switch v.(type) {
case string:
val = v.(string)
case int, bool, time.Duration:
val = fmt.Sprint(v)
case []string:
arr := v.([]string)
if len(arr) > 0 {
val = strings.Join(arr, ",")
}
}
// this can happen with embedded structs
if k == "" {
continue
}
m = append(m, KV{k, val})
}
return
}
// PrintEnv renders the key/values of a config.C to a provided io.Writer.
func PrintEnv(cfg *C, printer io.Writer) {
kvs := EnvKV(*cfg)
sort.Sort(kvs)
for _, v := range kvs {
_, _ = fmt.Fprintf(printer, "%s=%s\n", v.Key, v.Value)
}
}
// PrintHelp outputs a help text listing the configuration options and default
// values to a provided io.Writer (usually os.Stderr or os.Stdout).
func PrintHelp(cfg *C, printer io.Writer) {
_, _ = fmt.Fprintf(
printer,
"%s %s\n\n", cfg.AppName, version.V,
)
_, _ = fmt.Fprintf(
printer,
"Environment variables that configure %s:\n\n", cfg.AppName,
)
env.Usage(cfg, printer, &env.Options{SliceSep: ","})
_, _ = fmt.Fprintf(
printer,
"\nCLI parameter 'help' also prints this information\n"+
"\n.env file found at the path %s will be automatically "+
"loaded for configuration.\nset these two variables for a custom load path,"+
" this file will be created on first startup.\nenvironment overrides it and "+
"you can also edit the file to set configuration options\n\n"+
"use the parameter 'env' to print out the current configuration to the terminal\n\n"+
"set the environment using\n\n\t%s env > %s/.env\n", os.Args[0],
cfg.Config,
cfg.Config,
)
fmt.Fprintf(printer, "\ncurrent configuration:\n\n")
PrintEnv(cfg, printer)
fmt.Fprintln(printer)
return
}

View File

@@ -1,81 +0,0 @@
// Package app implements the realy nostr relay with a simple follow/mute list authentication scheme and the new HTTP REST based protocol.
package app
import (
"net/http"
"sync"
"orly.dev/app/config"
"orly.dev/encoders/event"
"orly.dev/encoders/filter"
"orly.dev/encoders/filters"
"orly.dev/interfaces/store"
"orly.dev/utils/context"
)
type List map[string]struct{}
type Relay struct {
sync.Mutex
*config.C
Store store.I
}
func (r *Relay) Name() string { return r.C.AppName }
func (r *Relay) Storage() store.I { return r.Store }
func (r *Relay) Init() (err error) {
// for _, src := range r.C.Owners {
// if len(src) < 1 {
// continue
// }
// dst := make([]byte, len(src)/2)
// if _, err = hex.DecBytes(dst, []byte(src)); chk.E(err) {
// if dst, err = bech32encoding.NpubToBytes([]byte(src)); chk.E(err) {
// continue
// }
// }
// r.owners = append(r.owners, dst)
// }
// if len(r.owners) > 0 {
// log.F.C(func() string {
// ownerIds := make([]string, len(r.owners))
// for i, npub := range r.owners {
// ownerIds[i] = hex.Enc(npub)
// }
// owners := strings.Join(ownerIds, ",")
// return fmt.Sprintf("owners %s", owners)
// })
// r.ZeroLists()
// r.CheckOwnerLists(context.Bg())
// }
return nil
}
func (r *Relay) AcceptEvent(
c context.T, evt *event.E, hr *http.Request,
origin string, authedPubkey []byte,
) (accept bool, notice string, afterSave func()) {
accept = true
return
}
func (r *Relay) AcceptFilter(
c context.T, hr *http.Request, f *filter.S,
authedPubkey []byte,
) (allowed *filter.S, ok bool, modified bool) {
allowed = f
ok = true
return
}
func (r *Relay) AcceptReq(
c context.T, hr *http.Request, id []byte,
ff *filters.T, authedPubkey []byte,
) (allowed *filters.T, ok bool, modified bool) {
allowed = ff
ok = true
return
}

View File

@@ -1,70 +0,0 @@
package realy
import (
"errors"
"net/http"
"orly.dev/interfaces/relay"
"orly.dev/utils/normalize"
"strings"
"orly.dev/encoders/event"
"orly.dev/interfaces/store"
"orly.dev/protocol/socketapi"
"orly.dev/utils/context"
)
func (s *Server) addEvent(
c context.T, rl relay.I, ev *event.E,
hr *http.Request, origin string,
authedPubkey []byte,
) (accepted bool, message []byte) {
if ev == nil {
return false, normalize.Invalid.F("empty event")
}
// sto := rl.Storage()
// advancedSaver, _ := sto.(relay.AdvancedSaver)
// don't allow storing event with protected marker as per nip-70 with auth enabled.
// if (s.authRequired || !s.publicReadable) && ev.Tags.ContainsProtectedMarker() {
// if len(authedPubkey) == 0 || !bytes.Equal(ev.Pubkey, authedPubkey) {
// return false,
// []byte(fmt.Sprintf("event with relay marker tag '-' (nip-70 protected event) "+
// "may only be published by matching npub: %0x is not %0x",
// authedPubkey, ev.Pubkey))
// }
// }
if ev.Kind.IsEphemeral() {
} else {
// if advancedSaver != nil {
// advancedSaver.BeforeSave(c, ev)
// }
if saveErr := s.Publish(c, ev); saveErr != nil {
if errors.Is(saveErr, store.ErrDupEvent) {
return false, []byte(saveErr.Error())
}
errmsg := saveErr.Error()
if socketapi.NIP20prefixmatcher.MatchString(errmsg) {
if strings.Contains(errmsg, "tombstone") {
return false, normalize.Error.F("event was deleted, not storing it again")
}
if strings.HasPrefix(errmsg, string(normalize.Blocked)) {
return false, []byte(errmsg)
}
return false, []byte(errmsg)
} else {
return false, []byte(errmsg)
}
}
// if advancedSaver != nil {
// advancedSaver.AfterSave(ev)
// }
}
// var authRequired bool
// if ar, ok := rl.(relay.Authenticator); ok {
// authRequired = ar.AuthRequired()
// }
// notify subscribers
s.listeners.Deliver(ev)
accepted = true
return
}

View File

@@ -1,12 +0,0 @@
package realy
import (
"orly.dev/utils/log"
)
func (s *Server) disconnect() {
for client := range s.clients {
log.I.F("closing client %s", client.RemoteAddr())
client.Close()
}
}

View File

@@ -1,49 +0,0 @@
package realy
import (
"encoding/json"
"net/http"
"orly.dev/interfaces/relay"
"orly.dev/utils/chk"
"orly.dev/utils/log"
"orly.dev/version"
"sort"
"orly.dev/protocol/relayinfo"
)
func (s *Server) handleRelayInfo(w http.ResponseWriter, r *http.Request) {
r.Header.Set("Content-Type", "application/json")
log.I.Ln("handling relay information document")
var info *relayinfo.T
if informationer, ok := s.relay.(relay.Informationer); ok {
info = informationer.GetNIP11InformationDocument()
} else {
supportedNIPs := relayinfo.GetList(
relayinfo.BasicProtocol,
relayinfo.EncryptedDirectMessage,
relayinfo.EventDeletion,
relayinfo.RelayInformationDocument,
relayinfo.GenericTagQueries,
relayinfo.NostrMarketplace,
relayinfo.EventTreatment,
relayinfo.CommandResults,
relayinfo.ParameterizedReplaceableEvents,
relayinfo.ExpirationTimestamp,
relayinfo.ProtectedEvents,
relayinfo.RelayListMetadata,
)
sort.Sort(supportedNIPs)
log.T.Ln("supported NIPs", supportedNIPs)
info = &relayinfo.T{
Name: s.relay.Name(),
Description: version.Description,
Nips: supportedNIPs, Software: version.URL,
Version: version.V,
Limitation: relayinfo.Limits{},
Icon: "https://cdn.satellite.earth/ac9778868fbf23b63c47c769a74e163377e6ea94d3f0f31711931663d035c4f6.png",
}
}
if err := json.NewEncoder(w).Encode(info); chk.E(err) {
}
}

View File

@@ -1,34 +0,0 @@
package helpers
import (
"net/http"
"strings"
)
func GenerateDescription(text string, scopes []string) string {
if len(scopes) == 0 {
return text
}
result := make([]string, 0)
for _, value := range scopes {
result = append(result, "`"+value+"`")
}
return text + "<br/><br/>**Scopes**<br/>" + strings.Join(result, ", ")
}
func GetRemoteFromReq(r *http.Request) (rr string) {
// reverse proxy should populate this field so we see the remote not the proxy
rem := r.Header.Get("X-Forwarded-For")
if rem == "" {
rr = r.RemoteAddr
} else {
splitted := strings.Split(rem, " ")
if len(splitted) == 1 {
rr = splitted[0]
}
if len(splitted) == 2 {
rr = splitted[1]
}
}
return
}

View File

@@ -1,38 +0,0 @@
package interfaces
import (
"net/http"
"orly.dev/app/realy/publish"
"orly.dev/encoders/event"
"orly.dev/interfaces/relay"
"orly.dev/interfaces/store"
"orly.dev/utils/context"
)
type Server interface {
AddEvent(
c context.T, rl relay.I, ev *event.E, hr *http.Request,
origin string, authedPubkey []byte,
) (
accepted bool,
message []byte,
)
Context() context.T
Disconnect()
Publisher() *publish.S
Publish(c context.T, evt *event.E) (err error)
Relay() relay.I
Shutdown()
Storage() store.I
// Options() *options.T
// AcceptEvent(
// c context.T, ev *event.E, hr *http.Request, origin string,
// authedPubkey []byte) (accept bool, notice string, afterSave func())
// AdminAuth(r *http.Request,
// tolerance ...time.Duration) (authed bool, pubkey []byte)
// AuthRequired() bool
// Configuration() store.Configuration
// Owners() [][]byte
// PublicReadable() bool
// SetConfiguration(*store.Configuration)
}

View File

@@ -1,31 +0,0 @@
package realy
import (
"net/http"
"orly.dev/app/realy/interfaces"
"orly.dev/app/realy/publish"
"orly.dev/encoders/event"
"orly.dev/interfaces/relay"
"orly.dev/interfaces/store"
"orly.dev/utils/context"
)
func (s *Server) Storage() store.I { return s.relay.Storage() }
func (s *Server) Relay() relay.I { return s.relay }
func (s *Server) Disconnect() { s.disconnect() }
func (s *Server) AddEvent(
c context.T, rl relay.I, ev *event.E, hr *http.Request, origin string,
authedPubkey []byte,
) (accepted bool, message []byte) {
return s.addEvent(c, rl, ev, hr, origin, authedPubkey)
}
func (s *Server) Publisher() *publish.S { return s.listeners }
func (s *Server) Context() context.T { return s.Ctx }
var _ interfaces.Server = &Server{}

View File

@@ -1,154 +0,0 @@
package realy
import (
_ "embed"
"errors"
"fmt"
"net"
"net/http"
"orly.dev/app/realy/helpers"
"orly.dev/app/realy/options"
"orly.dev/app/realy/publish"
"orly.dev/interfaces/relay"
"orly.dev/utils/chk"
"orly.dev/utils/log"
realy_lol "orly.dev/version"
"strconv"
"sync"
"time"
"github.com/danielgtaylor/huma/v2"
"github.com/fasthttp/websocket"
"github.com/rs/cors"
"orly.dev/interfaces/signer"
"orly.dev/protocol/openapi"
"orly.dev/protocol/socketapi"
"orly.dev/utils/context"
)
type Server struct {
Ctx context.T
Cancel context.F
options *options.T
relay relay.I
clientsMu sync.Mutex
clients map[*websocket.Conn]struct{}
Addr string
mux *openapi.ServeMux
httpServer *http.Server
// authRequired bool
// publicReadable bool
// maxLimit int
// admins []signer.I
// owners [][]byte
listeners *publish.S
huma.API
// ConfigurationMx sync.Mutex
// configuration *store.Configuration
}
type ServerParams struct {
Ctx context.T
Cancel context.F
Rl relay.I
DbPath string
MaxLimit int
Admins []signer.I
Owners [][]byte
PublicReadable bool
}
func NewServer(sp *ServerParams, opts ...options.O) (s *Server, err error) {
op := options.Default()
for _, opt := range opts {
opt(op)
}
if storage := sp.Rl.Storage(); storage != nil {
if err = storage.Init(sp.DbPath); chk.T(err) {
return nil, fmt.Errorf("storage init: %w", err)
}
}
serveMux := openapi.NewServeMux()
s = &Server{
Ctx: sp.Ctx,
Cancel: sp.Cancel,
relay: sp.Rl,
clients: make(map[*websocket.Conn]struct{}),
mux: serveMux,
options: op,
listeners: publish.New(socketapi.New(), openapi.New()),
API: openapi.NewHuma(
serveMux, sp.Rl.Name(), realy_lol.V,
realy_lol.Description,
),
}
// register the http API operations
huma.AutoRegister(s.API, openapi.NewOperations(s))
go func() {
if err := s.relay.Init(); chk.E(err) {
s.Shutdown()
}
}()
return s, nil
}
// ServeHTTP implements the relay's http handler.
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// standard nostr protocol only governs the "root" path of the relay and
// websockets
if r.URL.Path == "/" && r.Header.Get("Accept") == "application/nostr+json" {
s.handleRelayInfo(w, r)
return
}
if r.URL.Path == "/" && r.Header.Get("Upgrade") == "websocket" {
s.handleWebsocket(w, r)
return
}
log.I.F(
"http request: %s from %s", r.URL.String(), helpers.GetRemoteFromReq(r),
)
s.mux.ServeHTTP(w, r)
}
// Start up the relay.
func (s *Server) Start(host string, port int, started ...chan bool) error {
addr := net.JoinHostPort(host, strconv.Itoa(port))
log.I.F("starting relay listener at %s", addr)
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
s.httpServer = &http.Server{
Handler: cors.Default().Handler(s),
Addr: addr,
ReadHeaderTimeout: 7 * time.Second,
IdleTimeout: 28 * time.Second,
}
for _, startedC := range started {
close(startedC)
}
if err = s.httpServer.Serve(ln); errors.Is(err, http.ErrServerClosed) {
} else if err != nil {
}
return nil
}
// Shutdown the relay.
func (s *Server) Shutdown() {
log.I.Ln("shutting down relay")
s.Cancel()
log.W.Ln("closing event store")
chk.E(s.relay.Storage().Close())
log.W.Ln("shutting down relay listener")
chk.E(s.httpServer.Shutdown(s.Ctx))
if f, ok := s.relay.(relay.ShutdownAware); ok {
f.OnShutdown(s.Ctx)
}
}
// Router returns the servemux that handles paths on the HTTP server of the
// relay.
func (s *Server) Router() *http.ServeMux {
return s.mux.ServeMux
}

View File

@@ -1,30 +0,0 @@
package app
import (
"orly.dev/utils/log"
"os"
"runtime"
"time"
"orly.dev/utils/context"
)
func MonitorResources(c context.T) {
tick := time.NewTicker(time.Minute * 15)
log.I.Ln("running process", os.Args[0], os.Getpid())
// memStats := &runtime.MemStats{}
for {
select {
case <-c.Done():
log.D.Ln("shutting down resource monitor")
return
case <-tick.C:
// runtime.ReadMemStats(memStats)
log.D.Ln(
"# goroutines", runtime.NumGoroutine(), "# cgo calls",
runtime.NumCgoCall(),
)
// log.D.S(memStats)
}
}
}

View File

@@ -1,2 +0,0 @@
// Package cmd contains the executable applications of the realy suite.
package cmd

View File

@@ -14,8 +14,14 @@ import (
"net/http"
"net/http/httputil"
"net/url"
"orly.dev/utils/chk"
"orly.dev/utils/log"
"orly.dev/cmd/lerproxy/buf"
"orly.dev/cmd/lerproxy/hsts"
"orly.dev/cmd/lerproxy/reverse"
"orly.dev/cmd/lerproxy/tcpkeepalive"
"orly.dev/cmd/lerproxy/util"
"orly.dev/pkg/utils/chk"
"orly.dev/pkg/utils/context"
"orly.dev/pkg/utils/log"
"os"
"os/signal"
"path/filepath"
@@ -27,13 +33,6 @@ import (
"github.com/alexflint/go-arg"
"golang.org/x/crypto/acme/autocert"
"golang.org/x/sync/errgroup"
"orly.dev/cmd/lerproxy/buf"
"orly.dev/cmd/lerproxy/hsts"
"orly.dev/cmd/lerproxy/reverse"
"orly.dev/cmd/lerproxy/tcpkeepalive"
"orly.dev/cmd/lerproxy/util"
"orly.dev/utils/context"
)
type runArgs struct {

View File

@@ -6,9 +6,8 @@ import (
"net/http"
"net/http/httputil"
"net/url"
"orly.dev/utils/log"
"orly.dev/cmd/lerproxy/util"
"orly.dev/pkg/utils/log"
)
// NewSingleHostReverseProxy is a copy of httputil.NewSingleHostReverseProxy

View File

@@ -4,10 +4,9 @@ package tcpkeepalive
import (
"net"
"orly.dev/utils/chk"
"time"
"orly.dev/cmd/lerproxy/timeout"
"orly.dev/pkg/utils/chk"
"time"
)
// Period can be changed prior to opening a Listener to alter its'

View File

@@ -4,7 +4,7 @@ package timeout
import (
"net"
"orly.dev/utils/chk"
"orly.dev/pkg/utils/chk"
"time"
)

View File

@@ -3,16 +3,15 @@ package main
import (
"encoding/base64"
"fmt"
"orly.dev/crypto/p256k"
"orly.dev/encoders/bech32encoding"
"orly.dev/utils/chk"
"orly.dev/utils/errorf"
"orly.dev/utils/log"
"orly.dev/pkg/crypto/p256k"
"orly.dev/pkg/encoders/bech32encoding"
"orly.dev/pkg/interfaces/signer"
"orly.dev/pkg/protocol/httpauth"
"orly.dev/pkg/utils/chk"
"orly.dev/pkg/utils/errorf"
"orly.dev/pkg/utils/log"
"os"
"time"
"orly.dev/interfaces/signer"
"orly.dev/protocol/httpauth"
)
const secEnv = "NOSTR_SECRET_KEY"

View File

@@ -8,18 +8,17 @@ import (
"io"
"net/http"
"net/url"
"orly.dev/crypto/p256k"
"orly.dev/crypto/sha256"
"orly.dev/encoders/bech32encoding"
"orly.dev/utils/chk"
"orly.dev/utils/errorf"
"orly.dev/utils/log"
realy_lol "orly.dev/version"
"orly.dev/pkg/crypto/p256k"
"orly.dev/pkg/crypto/sha256"
"orly.dev/pkg/encoders/bech32encoding"
"orly.dev/pkg/encoders/hex"
"orly.dev/pkg/interfaces/signer"
"orly.dev/pkg/protocol/httpauth"
"orly.dev/pkg/utils/chk"
"orly.dev/pkg/utils/errorf"
"orly.dev/pkg/utils/log"
realy_lol "orly.dev/pkg/version"
"os"
"orly.dev/encoders/hex"
"orly.dev/interfaces/signer"
"orly.dev/protocol/httpauth"
)
const secEnv = "NOSTR_SECRET_KEY"

View File

@@ -6,13 +6,15 @@ import (
"bytes"
"encoding/hex"
"fmt"
"orly.dev/crypto/ec/bech32"
"orly.dev/crypto/ec/schnorr"
secp256k2 "orly.dev/crypto/ec/secp256k1"
"orly.dev/encoders/bech32encoding"
"orly.dev/utils/chk"
"orly.dev/utils/interrupt"
"orly.dev/utils/log"
"orly.dev/pkg/crypto/ec/bech32"
"orly.dev/pkg/crypto/ec/schnorr"
"orly.dev/pkg/crypto/ec/secp256k1"
"orly.dev/pkg/encoders/bech32encoding"
"orly.dev/pkg/utils/atomic"
"orly.dev/pkg/utils/chk"
"orly.dev/pkg/utils/interrupt"
"orly.dev/pkg/utils/log"
"orly.dev/pkg/utils/qu"
"os"
"runtime"
"strings"
@@ -20,9 +22,6 @@ import (
"time"
"github.com/alexflint/go-arg"
"orly.dev/utils/atomic"
"orly.dev/utils/qu"
)
var prefix = append(bech32encoding.PubHRP, '1')
@@ -34,9 +33,9 @@ const (
)
type Result struct {
sec *secp256k2.SecretKey
sec *secp256k1.SecretKey
npub []byte
pub *secp256k2.PublicKey
pub *secp256k1.PublicKey
}
var args struct {
@@ -219,11 +218,11 @@ out:
// GenKeyPair creates a fresh new key pair using the entropy source used by
// crypto/rand (ie, /dev/random on posix systems).
func GenKeyPair() (
sec *secp256k2.SecretKey,
pub *secp256k2.PublicKey, err error,
sec *secp256k1.SecretKey,
pub *secp256k1.PublicKey, err error,
) {
sec, err = secp256k2.GenerateSecretKey()
sec, err = secp256k1.GenerateSecretKey()
if err != nil {
err = fmt.Errorf("error generating key: %s", err)
return

View File

@@ -1,9 +0,0 @@
package base58_test
import (
"orly.dev/utils/lol"
)
var (
log, chk, errorf = lol.Main.Log, lol.Main.Check, lol.Main.Errorf
)

View File

@@ -0,0 +1,59 @@
Always start documentation comments with the symbol name verbatim, and then use this to start a sentence summarizing the symbol's function
For documentation comments on functions and methods:
- Write a general description in one or two sentences at the top
- use the format `# Header` for headings of sections.
- Follow by a description of the parameters and then return values, with a series of bullet points describing each item, each with an empty line in between.
- Last, describe the expected behaviour of the function or method, keep this with one space apart from the comment start token
For documentation on types, variables and comments, write 1-2 sentences describing how the item is used.
For documentation on package, summarise in up to 3 sentences the functions and purpose of the package
Do not use markdown ** or __ or any similar things in initial words of a bullet point, instead use standard godoc style # prefix for header sections
ALWAYS separate each bullet point with an empty line, and ALWAYS indent them three spaces after the //
NEVER put a colon after the first word of the first line of a document comment
Use British English spelling and Oxford commas
Always break lines before 80 columns, and flow under bullet points two columns right of the bullet point hyphen.
Do not write a section for parameters or return values when there is none
In the `# Expected behavior` section always add an empty line after this title before the description, and don't indent this section as this makes it appear as preformatted monospace.
A good typical example:
// NewServer initializes and returns a new Server instance based on the provided
// ServerParams and optional settings. It sets up storage, initializes the
// relay, and configures necessary components for server operation.
//
// # Parameters
//
// - sp (*ServerParams): The configuration parameters for initializing the
// server.
//
// - opts (...options.O): Optional settings that modify the server's behavior.
//
// # Return Values
//
// - s (*Server): The newly created Server instance.
//
// - err (error): An error if any step fails during initialization.
//
// # Expected Behaviour
//
// - Initializes storage with the provided database path.
//
// - Configures the server's options using the default settings and applies any
// optional settings provided.
//
// - Sets up a ServeMux for handling HTTP requests.
//
// - Initializes the relay, starting its operation in a separate goroutine.

View File

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 70 KiB

View File

@@ -1,7 +0,0 @@
package listener
type I interface {
Write(p []byte) (n int, err error)
Close() error
Remote() string
}

View File

@@ -1,14 +0,0 @@
package publisher
import (
"orly.dev/encoders/event"
"orly.dev/interfaces/typer"
)
type I interface {
typer.T
Deliver(ev *event.E)
Receive(msg typer.T)
}
type Publishers []I

View File

@@ -1,19 +0,0 @@
package server
import (
"net/http"
"orly.dev/encoders/event"
"orly.dev/interfaces/store"
"orly.dev/utils/context"
)
type I interface {
Context() context.T
HandleRelayInfo(
w http.ResponseWriter, r *http.Request,
)
Storage() store.I
AddEvent(
c context.T, ev *event.E, hr *http.Request, remote string,
) (accepted bool, message []byte)
}

37
main.go
View File

@@ -5,26 +5,25 @@ package main
import (
"fmt"
"github.com/pkg/profile"
"net/http"
_ "net/http/pprof"
"orly.dev/app/realy"
"orly.dev/app/realy/options"
"orly.dev/utils/chk"
"orly.dev/utils/interrupt"
"orly.dev/utils/log"
realy_lol "orly.dev/version"
"os"
"orly.dev/app"
"orly.dev/app/config"
"orly.dev/database"
"orly.dev/utils/context"
"orly.dev/utils/lol"
"github.com/pkg/profile"
app2 "orly.dev/pkg/app"
"orly.dev/pkg/app/config"
"orly.dev/pkg/app/relay"
"orly.dev/pkg/app/relay/options"
"orly.dev/pkg/database"
"orly.dev/pkg/utils/chk"
"orly.dev/pkg/utils/context"
"orly.dev/pkg/utils/interrupt"
"orly.dev/pkg/utils/log"
"orly.dev/pkg/utils/lol"
"orly.dev/pkg/version"
)
func main() {
log.I.F("starting realy %s", realy_lol.V)
var err error
var cfg *config.C
if cfg, err = config.New(); chk.T(err) {
@@ -34,6 +33,7 @@ func main() {
config.PrintHelp(cfg, os.Stderr)
os.Exit(0)
}
log.I.F("starting %s %s", cfg.AppName, version.V)
if config.GetEnv() {
config.PrintEnv(cfg, os.Stdout)
os.Exit(0)
@@ -55,18 +55,19 @@ func main() {
if chk.E(err) {
os.Exit(1)
}
r := &app.Relay{C: cfg, Store: storage}
go app.MonitorResources(c)
var server *realy.Server
serverParams := &realy.ServerParams{
r := &app2.Relay{C: cfg, Store: storage}
go app2.MonitorResources(c)
var server *relay.Server
serverParams := &relay.ServerParams{
Ctx: c,
Cancel: cancel,
Rl: r,
DbPath: cfg.DataDir,
MaxLimit: 512, // Default max limit for events
C: cfg,
}
var opts []options.O
if server, err = realy.NewServer(serverParams, opts...); chk.E(err) {
if server, err = relay.NewServer(serverParams, opts...); chk.E(err) {
os.Exit(1)
}
if err != nil {

292
pkg/app/config/config.go Normal file
View File

@@ -0,0 +1,292 @@
// Package config provides a go-simpler.org/env configuration table and helpers
// for working with the list of key/value lists stored in .env files.
package config
import (
"fmt"
"io"
"orly.dev/pkg/utils/apputil"
"orly.dev/pkg/utils/chk"
env2 "orly.dev/pkg/utils/env"
"orly.dev/pkg/utils/log"
"orly.dev/pkg/utils/lol"
"orly.dev/pkg/version"
"os"
"path/filepath"
"reflect"
"sort"
"strings"
"time"
"github.com/adrg/xdg"
"go-simpler.org/env"
)
// C holds application configuration settings loaded from environment variables
// and default values. It defines parameters for app behaviour, storage
// locations, logging, and network settings used across the relay service.
type C struct {
AppName string `env:"ORLY_APP_NAME" default:"orly"`
Config string `env:"ORLY_CONFIG_DIR" usage:"location for configuration file, which has the name '.env' to make it harder to delete, and is a standard environment KEY=value<newline>... style" default:"~/.config/orly"`
State string `env:"ORLY_STATE_DATA_DIR" usage:"storage location for state data affected by dynamic interactive interfaces" default:"~/.local/state/orly"`
DataDir string `env:"ORLY_DATA_DIR" usage:"storage location for the event store" default:"~/.local/cache/orly"`
Listen string `env:"ORLY_LISTEN" default:"0.0.0.0" usage:"network listen address"`
Port int `env:"ORLY_PORT" default:"3334" usage:"port to listen on"`
LogLevel string `env:"ORLY_LOG_LEVEL" default:"info" usage:"debug level: fatal error warn info debug trace"`
DbLogLevel string `env:"ORLY_DB_LOG_LEVEL" default:"info" usage:"debug level: fatal error warn info debug trace"`
Pprof bool `env:"ORLY_PPROF" default:"false" usage:"enable pprof on 127.0.0.1:6060"`
AuthRequired bool `env:"ORLY_AUTH_REQUIRED" default:"false" usage:"require authentication for all requests"`
PublicReadable bool `env:"ORLY_PUBLIC_READABLE" default:"true" usage:"allow public read access to regardless of whether the client is authed"`
SpiderSeeds []string `env:"ORLY_SPIDER_SEEDS" usage:"seeds to use for the spider (relays that are looked up initially to find owner relay lists) (comma separated)" default:"wss://relay.nostr.band/,wss://relay.damus.io/,wss://nostr.wine/,wss://nostr.land/,wss://theforest.nostr1.com/"`
Owners []string `env:"ORLY_OWNERS" usage:"list of users whose follow lists designate whitelisted users who can publish events, and who can read if public readable is false (comma separated)"`
Private bool `env:"ORLY_PRIVATE" usage:"do not spider for user metadata because the relay is private and this would leak relay memberships" default:"false"`
}
// New creates and initializes a new configuration object for the relay
// application
//
// # Return Values
//
// - cfg: A pointer to the initialized configuration struct containing default
// or environment-provided values
//
// - err: An error object that is non-nil if any operation during
// initialization fails
//
// # Expected Behaviour:
//
// Initializes a new configuration instance by loading environment variables and
// checking for a .env file in the default configuration directory. Sets logging
// levels based on configuration values and returns the populated configuration
// or an error if any step fails
func New() (cfg *C, err error) {
cfg = &C{}
if err = env.Load(cfg, &env.Options{SliceSep: ","}); chk.T(err) {
return
}
if cfg.Config == "" || strings.Contains(cfg.State, "~") {
cfg.Config = filepath.Join(xdg.ConfigHome, cfg.AppName)
}
if cfg.DataDir == "" || strings.Contains(cfg.State, "~") {
cfg.DataDir = filepath.Join(xdg.DataHome, cfg.AppName)
}
if cfg.State == "" || strings.Contains(cfg.State, "~") {
cfg.State = filepath.Join(xdg.StateHome, cfg.AppName)
}
envPath := filepath.Join(cfg.Config, ".env")
if apputil.FileExists(envPath) {
var e env2.Env
if e, err = env2.GetEnv(envPath); chk.T(err) {
return
}
if err = env.Load(
cfg, &env.Options{SliceSep: ",", Source: e},
); chk.E(err) {
return
}
lol.SetLogLevel(cfg.LogLevel)
log.I.F("loaded configuration from %s", envPath)
}
log.I.S(cfg)
return
}
// HelpRequested determines if the command line arguments indicate a request for help
//
// # Return Values
//
// - help: A boolean value indicating true if a help flag was detected in the
// command line arguments, false otherwise
//
// # Expected Behaviour
//
// The function checks the first command line argument for common help flags and
// returns true if any of them are present. Returns false if no help flag is found
func HelpRequested() (help bool) {
if len(os.Args) > 1 {
switch strings.ToLower(os.Args[1]) {
case "help", "-h", "--h", "-help", "--help", "?":
help = true
}
}
return
}
// GetEnv checks if the first command line argument is "env" and returns
// whether the environment configuration should be printed.
//
// # Return Values
//
// - requested: A boolean indicating true if the 'env' argument was
// provided, false otherwise.
//
// # Expected Behaviour
//
// The function returns true when the first command line argument is "env"
// (case-insensitive), signalling that the environment configuration should be
// printed. Otherwise, it returns false.
func GetEnv() (requested bool) {
if len(os.Args) > 1 {
switch strings.ToLower(os.Args[1]) {
case "env":
requested = true
}
}
return
}
// KV is a key/value pair.
type KV struct{ Key, Value string }
// KVSlice is a sortable slice of key/value pairs, designed for managing
// configuration data and enabling operations like merging and sorting based on
// keys.
type KVSlice []KV
func (kv KVSlice) Len() int { return len(kv) }
func (kv KVSlice) Less(i, j int) bool { return kv[i].Key < kv[j].Key }
func (kv KVSlice) Swap(i, j int) { kv[i], kv[j] = kv[j], kv[i] }
// Compose merges two KVSlice instances into a new slice where key-value pairs
// from the second slice override any duplicate keys from the first slice.
//
// # Parameters
//
// - kv2: The second KVSlice whose entries will be merged with the receiver.
//
// # Return Values
//
// - out: A new KVSlice containing all entries from both slices, with keys
// from kv2 taking precedence over keys from the receiver.
//
// # Expected Behaviour
//
// The method returns a new KVSlice that combines the contents of the receiver
// and kv2. If any key exists in both slices, the value from kv2 is used. The
// resulting slice remains sorted by keys as per the KVSlice implementation.
func (kv KVSlice) Compose(kv2 KVSlice) (out KVSlice) {
// duplicate the initial KVSlice
for _, p := range kv {
out = append(out, p)
}
out:
for i, p := range kv2 {
for j, q := range out {
// if the key is repeated, replace the value
if p.Key == q.Key {
out[j].Value = kv2[i].Value
continue out
}
}
out = append(out, p)
}
return
}
// EnvKV generates key/value pairs from a configuration object's struct tags
//
// # Parameters
//
// - cfg: A configuration object whose struct fields are processed for env tags
//
// # Return Values
//
// - m: A KVSlice containing key/value pairs derived from the config's env tags
//
// # Expected Behaviour
//
// Processes each field of the config object, extracting values tagged with
// "env" and converting them to strings. Skips fields without an "env" tag.
// Handles various value types including strings, integers, booleans, durations,
// and string slices by joining elements with commas.
func EnvKV(cfg any) (m KVSlice) {
t := reflect.TypeOf(cfg)
for i := 0; i < t.NumField(); i++ {
k := t.Field(i).Tag.Get("env")
v := reflect.ValueOf(cfg).Field(i).Interface()
var val string
switch v.(type) {
case string:
val = v.(string)
case int, bool, time.Duration:
val = fmt.Sprint(v)
case []string:
arr := v.([]string)
if len(arr) > 0 {
val = strings.Join(arr, ",")
}
}
// this can happen with embedded structs
if k == "" {
continue
}
m = append(m, KV{k, val})
}
return
}
// PrintEnv outputs sorted environment key/value pairs from a configuration object
// to the provided writer
//
// # Parameters
//
// - cfg: Pointer to the configuration object containing env tags
//
// - printer: Destination for the output, typically an io.Writer implementation
//
// # Expected Behaviour
//
// Outputs each environment variable derived from the config's struct tags in
// sorted order, formatted as "key=value\n" to the specified writer
func PrintEnv(cfg *C, printer io.Writer) {
kvs := EnvKV(*cfg)
sort.Sort(kvs)
for _, v := range kvs {
_, _ = fmt.Fprintf(printer, "%s=%s\n", v.Key, v.Value)
}
}
// PrintHelp prints help information including application version, environment
// variable configuration, and details about .env file handling to the provided
// writer
//
// # Parameters
//
// - cfg: Configuration object containing app name and config directory path
//
// - printer: Output destination for the help text
//
// # Expected Behaviour
//
// Prints application name and version followed by environment variable
// configuration details, explains .env file behaviour including automatic
// loading and custom path options, and displays current configuration values
// using PrintEnv. Outputs all information to the specified writer
func PrintHelp(cfg *C, printer io.Writer) {
_, _ = fmt.Fprintf(
printer,
"%s %s\n\n", cfg.AppName, version.V,
)
_, _ = fmt.Fprintf(
printer,
"Environment variables that configure %s:\n\n", cfg.AppName,
)
env.Usage(cfg, printer, &env.Options{SliceSep: ","})
_, _ = fmt.Fprintf(
printer,
"\nCLI parameter 'help' also prints this information\n"+
"\n.env file found at the path %s will be automatically "+
"loaded for configuration.\nset these two variables for a custom load path,"+
" this file will be created on first startup.\nenvironment overrides it and "+
"you can also edit the file to set configuration options\n\n"+
"use the parameter 'env' to print out the current configuration to the terminal\n\n"+
"set the environment using\n\n\t%s env > %s/.env\n",
cfg.Config,
os.Args[0],
cfg.Config,
)
fmt.Fprintf(printer, "\ncurrent configuration:\n\n")
PrintEnv(cfg, printer)
fmt.Fprintln(printer)
return
}

170
pkg/app/main.go Normal file
View File

@@ -0,0 +1,170 @@
// Package app implements the orly nostr relay.
package app
import (
"net/http"
"sync"
"orly.dev/pkg/app/config"
"orly.dev/pkg/encoders/event"
"orly.dev/pkg/encoders/filter"
"orly.dev/pkg/encoders/filters"
"orly.dev/pkg/interfaces/store"
"orly.dev/pkg/utils/context"
)
// List represents a set-like structure using a map with empty struct values.
type List map[string]struct{}
// Relay is a struct that represents a relay for Nostr events. It contains a
// configuration and a persistence layer for storing the events. The Relay
// type implements various methods to handle event acceptance, filtering,
// and storage.
type Relay struct {
sync.Mutex
*config.C
Store store.I
}
// Name returns the name of the application represented by this relay.
//
// # Return Values
//
// - string: the name of the application.
//
// # Expected behaviour
//
// This function simply returns the AppName field from the configuration.
func (r *Relay) Name() string { return r.C.AppName }
// Storage represents a persistence layer for Nostr events handled by a relay.
func (r *Relay) Storage() store.I { return r.Store }
// Init initializes and sets up the relay for Nostr events.
//
// #Return Values
//
// - err: an error if any issues occurred during initialization.
//
// #Expected behaviour
//
// This function is responsible for setting up the relay, configuring it,
// and initializing the necessary components to handle Nostr events.
func (r *Relay) Init() (err error) {
return nil
}
// AcceptEvent checks an event and determines whether the event should be
// accepted and if the client has the authority to submit it.
//
// # Parameters
//
// - c - a context.T for signalling if the task has been canceled.
//
// - evt - an *event.E that is being evaluated.
//
// - hr - an *http.Request containing the information about the current
// connection.
//
// - origin - the address of the client.
//
// - authedPubkey - the public key, if authed, of the client for this
// connection.
//
// # Return Values
//
// - accept - returns true if the event is accepted.
//
// - notice - if it is not accepted, a message in the form of
// `machine-readable-prefix: reason for error/blocked/rate-limited/etc`
//
// - afterSave - a closure to run after the event has been stored.
//
// # Expected behaviour
//
// This function checks whether the client has permission to store the event,
// and if they don't, returns false and some kind of error message. If they do,
// the event is forwarded to the database to be stored and indexed.
func (r *Relay) AcceptEvent(
c context.T, evt *event.E, hr *http.Request,
origin string, authedPubkey []byte,
) (accept bool, notice string, afterSave func()) {
accept = true
return
}
// AcceptFilter checks if a filter is allowed based on authentication status and
// relay policies
//
// # Parameters
//
// - c: Context for task cancellation.
//
// - hr: HTTP request containing connection information.
//
// - f: Filter to evaluate for acceptance.
//
// - authedPubkey: Public key of authenticated client, if applicable.
//
// # Return values
//
// - allowed: The filter if permitted; may be modified during processing.
//
// - ok: Boolean indicating whether the filter is accepted.
//
// - modified: Boolean indicating whether the filter was altered during
// evaluation.
//
// # Expected behaviour
//
// The method evaluates whether the provided filter should be allowed based on
// authentication status and relay-specific rules. If permitted, returns the
// filter (possibly modified) and true for ok; otherwise returns nil or false
// for ok accordingly.
func (r *Relay) AcceptFilter(
c context.T, hr *http.Request, f *filter.S,
authedPubkey []byte,
) (allowed *filter.S, ok bool, modified bool) {
allowed = f
ok = true
return
}
// AcceptReq evaluates whether the provided filters are allowed based on
// authentication status and relay policies for an incoming HTTP request.
//
// # Parameters
//
// - c: Context for task cancellation.
//
// - hr: HTTP request containing connection information.
//
// - id: Identifier associated with the request.
//
// - ff: Filters to evaluate for acceptance.
//
// - authedPubkey: Public key of authenticated client, if applicable.
//
// # Return Values
//
// - allowed: The filters if permitted; may be modified during processing.
//
// - ok: Boolean indicating whether the filters are accepted.
//
// - modified: Boolean indicating whether the filters were altered during
// evaluation.
//
// # Expected Behaviour:
//
// The method evaluates whether the provided filters should be allowed based on
// authentication status and relay-specific rules. If permitted, returns the
// filters (possibly modified) and true for ok; otherwise returns nil or false
// for ok accordingly.
func (r *Relay) AcceptReq(
c context.T, hr *http.Request, id []byte,
ff *filters.T, authedPubkey []byte,
) (allowed *filters.T, ok bool, modified bool) {
allowed = ff
ok = true
return
}

View File

@@ -0,0 +1,58 @@
package relay
import (
"bytes"
"net/http"
"orly.dev/pkg/encoders/event"
"orly.dev/pkg/utils/context"
)
// AcceptEvent determines whether an incoming event should be accepted for
// processing based on authentication requirements.
//
// # Parameters
//
// - c: the context of the request
//
// - ev: pointer to the event structure
//
// - hr: HTTP request related to the event (if any)
//
// - authedPubkey: public key of the authenticated user (if any)
//
// - remote: remote address from where the event was received
//
// # Return Values
//
// - accept: boolean indicating whether the event should be accepted
//
// - notice: string providing a message or error notice
//
// - afterSave: function to execute after saving the event (if applicable)
//
// # Expected Behaviour:
//
// - If authentication is required and no public key is provided, reject the
// event.
//
// - Otherwise, accept the event for processing.
func (s *Server) AcceptEvent(
c context.T, ev *event.E, hr *http.Request, authedPubkey []byte,
remote string,
) (accept bool, notice string, afterSave func()) {
// if auth is required and the user is not authed, reject
if s.AuthRequired() && len(authedPubkey) == 0 {
return
}
// check if the authed user is on the lists
list := append(s.OwnersFollowed(), s.FollowedFollows()...)
for _, u := range list {
if bytes.Equal(u, authedPubkey) {
accept = true
break
}
}
// todo: check if event author is on owners' mute lists or block list
return
}

View File

@@ -0,0 +1,237 @@
package relay
import (
"bytes"
"net/http"
"testing"
"orly.dev/pkg/app/config"
"orly.dev/pkg/encoders/event"
"orly.dev/pkg/utils/context"
)
// mockServerForEvent is a simple mock implementation of the Server struct for testing AcceptEvent
type mockServerForEvent struct {
authRequired bool
ownersFollowed [][]byte
followedFollows [][]byte
}
func (m *mockServerForEvent) AuthRequired() bool {
return m.authRequired
}
func (m *mockServerForEvent) OwnersFollowed() [][]byte {
return m.ownersFollowed
}
func (m *mockServerForEvent) FollowedFollows() [][]byte {
return m.followedFollows
}
// AcceptEvent implements the Server.AcceptEvent method for testing
func (m *mockServerForEvent) AcceptEvent(
c context.T, ev *event.E, hr *http.Request, authedPubkey []byte,
remote string,
) (accept bool, notice string, afterSave func()) {
// if auth is required and the user is not authed, reject
if m.AuthRequired() && len(authedPubkey) == 0 {
return
}
// check if the authed user is on the lists
list := append(m.OwnersFollowed(), m.FollowedFollows()...)
for _, u := range list {
if bytes.Equal(u, authedPubkey) {
accept = true
break
}
}
return
}
func TestAcceptEvent(t *testing.T) {
// Create a context and HTTP request for testing
ctx := context.Bg()
req, _ := http.NewRequest("GET", "http://example.com", nil)
// Create a test event
testEvent := &event.E{}
// Test cases
tests := []struct {
name string
server *mockServerForEvent
authedPubkey []byte
expectedAccept bool
}{
{
name: "Auth required, no pubkey",
server: &mockServerForEvent{
authRequired: true,
},
authedPubkey: nil,
expectedAccept: false,
},
{
name: "Auth required, with pubkey, not on lists",
server: &mockServerForEvent{
authRequired: true,
ownersFollowed: [][]byte{
[]byte("followed1"),
[]byte("followed2"),
},
followedFollows: [][]byte{
[]byte("follow1"),
[]byte("follow2"),
},
},
authedPubkey: []byte("test-pubkey"),
expectedAccept: false,
},
{
name: "Auth required, with pubkey, on owners followed list",
server: &mockServerForEvent{
authRequired: true,
ownersFollowed: [][]byte{
[]byte("followed1"),
[]byte("test-pubkey"),
[]byte("followed2"),
},
followedFollows: [][]byte{
[]byte("follow1"),
[]byte("follow2"),
},
},
authedPubkey: []byte("test-pubkey"),
expectedAccept: true,
},
{
name: "Auth required, with pubkey, on followed follows list",
server: &mockServerForEvent{
authRequired: true,
ownersFollowed: [][]byte{
[]byte("followed1"),
[]byte("followed2"),
},
followedFollows: [][]byte{
[]byte("follow1"),
[]byte("test-pubkey"),
[]byte("follow2"),
},
},
authedPubkey: []byte("test-pubkey"),
expectedAccept: true,
},
{
name: "Auth not required, no pubkey, not on lists",
server: &mockServerForEvent{
authRequired: false,
ownersFollowed: [][]byte{
[]byte("followed1"),
[]byte("followed2"),
},
followedFollows: [][]byte{
[]byte("follow1"),
[]byte("follow2"),
},
},
authedPubkey: nil,
expectedAccept: false,
},
{
name: "Auth not required, with pubkey, on lists",
server: &mockServerForEvent{
authRequired: false,
ownersFollowed: [][]byte{
[]byte("followed1"),
[]byte("test-pubkey"),
[]byte("followed2"),
},
followedFollows: [][]byte{
[]byte("follow1"),
[]byte("follow2"),
},
},
authedPubkey: []byte("test-pubkey"),
expectedAccept: true,
},
}
// Run tests
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Use the mock server's AcceptEvent method
accept, notice, afterSave := tt.server.AcceptEvent(ctx, testEvent, req, tt.authedPubkey, "127.0.0.1")
// Check if the acceptance status matches the expected value
if accept != tt.expectedAccept {
t.Errorf("AcceptEvent() accept = %v, want %v", accept, tt.expectedAccept)
}
// Notice should be empty in the current implementation
if notice != "" {
t.Errorf("AcceptEvent() notice = %v, want empty string", notice)
}
// afterSave should be nil in the current implementation
if afterSave != nil {
t.Error("AcceptEvent() afterSave is not nil, but should be nil")
}
})
}
}
// TestAcceptEventWithRealServer tests the AcceptEvent function with a real Server instance
func TestAcceptEventWithRealServer(t *testing.T) {
// Create a context and HTTP request for testing
ctx := context.Bg()
req, _ := http.NewRequest("GET", "http://example.com", nil)
// Create a test event
testEvent := &event.E{}
// Create a Server instance with configuration
s := &Server{
C: &config.C{
AuthRequired: true,
},
Lists: new(Lists),
}
// Test with no authenticated pubkey
accept, notice, afterSave := s.AcceptEvent(ctx, testEvent, req, nil, "127.0.0.1")
if accept {
t.Error("AcceptEvent() accept = true, want false")
}
if notice != "" {
t.Errorf("AcceptEvent() notice = %v, want empty string", notice)
}
if afterSave != nil {
t.Error("AcceptEvent() afterSave is not nil, but should be nil")
}
// Test with authenticated pubkey but not on any list
accept, notice, afterSave = s.AcceptEvent(ctx, testEvent, req, []byte("test-pubkey"), "127.0.0.1")
if accept {
t.Error("AcceptEvent() accept = true, want false")
}
// Add the pubkey to the owners followed list
s.SetOwnersFollowed([][]byte{[]byte("test-pubkey")})
// Test with authenticated pubkey on the owners followed list
accept, notice, afterSave = s.AcceptEvent(ctx, testEvent, req, []byte("test-pubkey"), "127.0.0.1")
if !accept {
t.Error("AcceptEvent() accept = false, want true")
}
// Clear the owners followed list and add the pubkey to the followed follows list
s.SetOwnersFollowed(nil)
s.SetFollowedFollows([][]byte{[]byte("test-pubkey")})
// Test with authenticated pubkey on the followed follows list
accept, notice, afterSave = s.AcceptEvent(ctx, testEvent, req, []byte("test-pubkey"), "127.0.0.1")
if !accept {
t.Error("AcceptEvent() accept = false, want true")
}
}

View File

@@ -0,0 +1,50 @@
package relay
import (
"net/http"
"orly.dev/pkg/encoders/filters"
"orly.dev/pkg/utils/context"
)
// AcceptReq determines whether a request should be accepted based on
// authentication and public readability settings.
//
// # Parameters
//
// - c: context for the request handling
//
// - hr: HTTP request received
//
// - f: filters to apply
//
// - authedPubkey: authenticated public key (if any)
//
// - remote: remote address of the request
//
// # Return Values
//
// - allowed: filters that are allowed after processing
//
// - accept: boolean indicating whether the request should be accepted
//
// - modified: boolean indicating if the request has been modified during
// processing
//
// # Expected Behaviour:
//
// - If authentication is required and there's no authenticated public key,
// reject the request.
//
// - Otherwise, accept the request.
func (s *Server) AcceptReq(
c context.T, hr *http.Request, ff *filters.T,
authedPubkey []byte, remote string,
) (allowed *filters.T, accept bool, modified bool) {
// if auth is required, and not public readable, reject
if s.AuthRequired() && len(authedPubkey) == 0 && !s.PublicReadable() {
return
}
allowed = ff
accept = true
return
}

View File

@@ -0,0 +1,210 @@
package relay
import (
"net/http"
"testing"
"orly.dev/pkg/app/config"
"orly.dev/pkg/encoders/filters"
"orly.dev/pkg/utils/context"
)
// mockServer is a simple mock implementation of the Server struct for testing
type mockServer struct {
authRequired bool
publicReadable bool
ownersPubkeys [][]byte
}
func (m *mockServer) AuthRequired() bool {
return m.authRequired || m.LenOwnersPubkeys() > 0
}
func (m *mockServer) PublicReadable() bool {
return m.publicReadable
}
func (m *mockServer) LenOwnersPubkeys() int {
return len(m.ownersPubkeys)
}
func (m *mockServer) OwnersFollowed() [][]byte {
return nil
}
func (m *mockServer) FollowedFollows() [][]byte {
return nil
}
// AcceptReq implements the Server.AcceptReq method for testing
func (m *mockServer) AcceptReq(
c context.T, hr *http.Request, ff *filters.T,
authedPubkey []byte, remote string,
) (allowed *filters.T, accept bool, modified bool) {
// if auth is required, and not public readable, reject
if m.AuthRequired() && len(authedPubkey) == 0 && !m.PublicReadable() {
return
}
allowed = ff
accept = true
return
}
func TestAcceptReq(t *testing.T) {
// Create a context and HTTP request for testing
ctx := context.Bg()
req, _ := http.NewRequest("GET", "http://example.com", nil)
// Create test filters
testFilters := filters.New()
// Test cases
tests := []struct {
name string
server *mockServer
authedPubkey []byte
expectedAccept bool
}{
{
name: "Auth required, no pubkey, not public readable",
server: &mockServer{
authRequired: true,
publicReadable: false,
},
authedPubkey: nil,
expectedAccept: false,
},
{
name: "Auth required, no pubkey, public readable",
server: &mockServer{
authRequired: true,
publicReadable: true,
},
authedPubkey: nil,
expectedAccept: true,
},
{
name: "Auth required, with pubkey",
server: &mockServer{
authRequired: true,
publicReadable: false,
},
authedPubkey: []byte("test-pubkey"),
expectedAccept: true,
},
{
name: "Auth not required",
server: &mockServer{
authRequired: false,
publicReadable: false,
},
authedPubkey: nil,
expectedAccept: true,
},
{
name: "Auth required due to owner pubkeys, no pubkey, not public readable",
server: &mockServer{
authRequired: false,
publicReadable: false,
ownersPubkeys: [][]byte{[]byte("owner1")},
},
authedPubkey: nil,
expectedAccept: false,
},
{
name: "Auth required due to owner pubkeys, no pubkey, public readable",
server: &mockServer{
authRequired: false,
publicReadable: true,
ownersPubkeys: [][]byte{[]byte("owner1")},
},
authedPubkey: nil,
expectedAccept: true,
},
}
// Run tests
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Use the mock server's AcceptReq method
allowed, accept, modified := tt.server.AcceptReq(ctx, req, testFilters, tt.authedPubkey, "127.0.0.1")
// Check if the acceptance status matches the expected value
if accept != tt.expectedAccept {
t.Errorf("AcceptReq() accept = %v, want %v", accept, tt.expectedAccept)
}
// If the request should be accepted, check that the filters are returned
if tt.expectedAccept {
if allowed == nil {
t.Error("AcceptReq() allowed is nil, but request was accepted")
}
} else {
if allowed != nil {
t.Error("AcceptReq() allowed is not nil, but request was rejected")
}
}
// Modified should be false as the current implementation doesn't modify filters
if modified {
t.Error("AcceptReq() modified = true, want false")
}
})
}
}
// TestAcceptReqWithRealServer tests the AcceptReq function with a real Server instance
func TestAcceptReqWithRealServer(t *testing.T) {
// Create a context and HTTP request for testing
ctx := context.Bg()
req, _ := http.NewRequest("GET", "http://example.com", nil)
// Create test filters
testFilters := filters.New()
// Create a Server instance with configuration
s := &Server{
C: &config.C{
AuthRequired: true,
PublicReadable: false,
},
Lists: new(Lists),
}
// Test with no authenticated pubkey
allowed, accept, modified := s.AcceptReq(ctx, req, testFilters, nil, "127.0.0.1")
if accept {
t.Error("AcceptReq() accept = true, want false")
}
if allowed != nil {
t.Error("AcceptReq() allowed is not nil, but request was rejected")
}
if modified {
t.Error("AcceptReq() modified = true, want false")
}
// Test with authenticated pubkey
allowed, accept, modified = s.AcceptReq(ctx, req, testFilters, []byte("test-pubkey"), "127.0.0.1")
if !accept {
t.Error("AcceptReq() accept = false, want true")
}
if allowed != testFilters {
t.Error("AcceptReq() allowed is not the same as input filters")
}
if modified {
t.Error("AcceptReq() modified = true, want false")
}
// Test with public readable
s.C.PublicReadable = true
allowed, accept, modified = s.AcceptReq(ctx, req, testFilters, nil, "127.0.0.1")
if !accept {
t.Error("AcceptReq() accept = false, want true")
}
if allowed != testFilters {
t.Error("AcceptReq() allowed is not the same as input filters")
}
if modified {
t.Error("AcceptReq() modified = true, want false")
}
}

85
pkg/app/relay/addEvent.go Normal file
View File

@@ -0,0 +1,85 @@
package relay
import (
"errors"
"net/http"
"strings"
"orly.dev/pkg/encoders/event"
"orly.dev/pkg/interfaces/relay"
"orly.dev/pkg/interfaces/store"
"orly.dev/pkg/protocol/socketapi"
"orly.dev/pkg/utils/context"
"orly.dev/pkg/utils/normalize"
)
// AddEvent processes an incoming event, saves it if valid, and delivers it to
// subscribers.
//
// # Parameters
//
// - c: context for request handling
//
// - rl: relay interface
//
// - ev: the event to be added
//
// - hr: HTTP request related to the event (if any)
//
// - origin: origin of the event (if any)
//
// - authedPubkey: public key of the authenticated user (if any)
//
// # Return Values
//
// - accepted: true if the event was successfully processed, false otherwise
//
// - message: additional information or error message related to the
// processing
//
// # Expected Behaviour:
//
// - Validates the incoming event.
//
// - Saves the event using the Publish method if it is not ephemeral.
//
// - Handles duplicate events by returning an appropriate error message.
//
// - Delivers the event to subscribers via the listeners' Deliver method.
//
// - Returns a boolean indicating whether the event was accepted and any
// relevant message.
func (s *Server) AddEvent(
c context.T, rl relay.I, ev *event.E,
hr *http.Request, origin string,
authedPubkey []byte,
) (accepted bool, message []byte) {
if ev == nil {
return false, normalize.Invalid.F("empty event")
}
if ev.Kind.IsEphemeral() {
} else {
if saveErr := s.Publish(c, ev); saveErr != nil {
if errors.Is(saveErr, store.ErrDupEvent) {
return false, []byte(saveErr.Error())
}
errmsg := saveErr.Error()
if socketapi.NIP20prefixmatcher.MatchString(errmsg) {
if strings.Contains(errmsg, "tombstone") {
return false, normalize.Error.F("event was deleted, not storing it again")
}
if strings.HasPrefix(errmsg, string(normalize.Blocked)) {
return false, []byte(errmsg)
}
return false, []byte(errmsg)
} else {
return false, []byte(errmsg)
}
}
}
// notify subscribers
s.listeners.Deliver(ev)
accepted = true
return
}

71
pkg/app/relay/auth.go Normal file
View File

@@ -0,0 +1,71 @@
package relay
import (
"net/http"
"strconv"
"strings"
"orly.dev/pkg/utils/chk"
"orly.dev/pkg/utils/log"
"orly.dev/pkg/utils/lol"
)
// ServiceURL constructs the service URL based on the incoming HTTP request. It
// checks for authentication requirements and determines the protocol (ws or
// wss) based on headers like X-Forwarded-Host, X-Forwarded-Proto, and the host
// itself.
//
// # Parameters
//
// - req: A pointer to an http.Request object representing the incoming request.
//
// # Return Values
//
// - st: A string representing the constructed service URL.
//
// # Expected Behaviour:
//
// - Checks if authentication is required.
//
// - Retrieves the host from X-Forwarded-Host or falls back to req.Host.
//
// - Determines the protocol (ws or wss) based on various conditions including
// headers and host details.
//
// - Returns the constructed URL string.
func (s *Server) ServiceURL(req *http.Request) (st string) {
lol.Tracer("ServiceURL")
defer func() { lol.Tracer("end ServiceURL", st) }()
if !s.AuthRequired() {
log.T.F("auth not required")
return
}
host := req.Header.Get("X-Forwarded-Host")
if host == "" {
host = req.Host
}
proto := req.Header.Get("X-Forwarded-Proto")
if proto == "" {
if host == "localhost" {
proto = "ws"
} else if strings.Contains(host, ":") {
// has a port number
proto = "ws"
} else if _, err := strconv.Atoi(
strings.ReplaceAll(
host, ".",
"",
),
); chk.E(err) {
// it's a naked IP
proto = "ws"
} else {
proto = "wss"
}
} else if proto == "https" {
proto = "wss"
} else if proto == "http" {
proto = "ws"
}
return proto + "://" + host
}

View File

@@ -0,0 +1,65 @@
package relay
import (
"encoding/json"
"net/http"
"orly.dev/pkg/interfaces/relay"
"orly.dev/pkg/protocol/relayinfo"
"orly.dev/pkg/utils/chk"
"orly.dev/pkg/utils/log"
"orly.dev/pkg/version"
"sort"
)
// HandleRelayInfo generates and returns a relay information document in JSON
// format based on the server's configuration and supported NIPs.
//
// # Parameters
//
// - w: HTTP response writer used to send the generated document.
//
// - r: HTTP request object containing incoming client request data.
//
// # Expected Behaviour
//
// The function constructs a relay information document using either the
// Informer interface implementation or predefined server configuration. It
// returns this document as a JSON response to the client.
func (s *Server) HandleRelayInfo(w http.ResponseWriter, r *http.Request) {
r.Header.Set("Content-Type", "application/json")
log.I.Ln("handling relay information document")
var info *relayinfo.T
if informationer, ok := s.relay.(relay.Informer); ok {
info = informationer.GetNIP11InformationDocument()
} else {
supportedNIPs := relayinfo.GetList(
relayinfo.BasicProtocol,
relayinfo.Authentication,
// relayinfo.EncryptedDirectMessage,
relayinfo.EventDeletion,
relayinfo.RelayInformationDocument,
relayinfo.GenericTagQueries,
// relayinfo.NostrMarketplace,
relayinfo.EventTreatment,
// relayinfo.CommandResults,
relayinfo.ParameterizedReplaceableEvents,
// relayinfo.ExpirationTimestamp,
// relayinfo.ProtectedEvents,
// relayinfo.RelayListMetadata,
)
sort.Sort(supportedNIPs)
log.T.Ln("supported NIPs", supportedNIPs)
info = &relayinfo.T{
Name: s.relay.Name(),
Description: version.Description,
Nips: supportedNIPs, Software: version.URL,
Version: version.V,
Limitation: relayinfo.Limits{
AuthRequired: s.C.AuthRequired,
},
Icon: "https://cdn.satellite.earth/ac9778868fbf23b63c47c769a74e163377e6ea94d3f0f31711931663d035c4f6.png",
}
}
if err := json.NewEncoder(w).Encode(info); chk.E(err) {
}
}

View File

@@ -1,12 +1,11 @@
package realy
package relay
import (
"net/http"
"orly.dev/protocol/socketapi"
"orly.dev/pkg/protocol/socketapi"
)
func (s *Server) handleWebsocket(w http.ResponseWriter, r *http.Request) {
a := &socketapi.A{Server: s}
a := &socketapi.A{I: s}
a.Serve(w, r, s)
}

View File

@@ -0,0 +1,101 @@
package helpers
import (
"net/http"
"strings"
)
// GenerateDescription generates a detailed description containing the provided
// text and an optional list of scopes.
//
// # Parameters
//
// - text: A string representing the base description.
//
// - scopes: A slice of strings indicating scopes to be included in the
// description.
//
// # Return Values
//
// - A string combining the base description and a formatted list of
// scopes, if provided.
//
// # Expected behaviour
//
// The function appends a formatted list of scopes to the base description if
// any scopes are provided. If no scopes are provided, it returns the base
// description unchanged. The formatted list of scopes includes each scope
// surrounded by backticks and separated by commas.
func GenerateDescription(text string, scopes []string) string {
if len(scopes) == 0 {
return text
}
result := make([]string, 0)
for _, value := range scopes {
result = append(result, "`"+value+"`")
}
return text + "<br/><br/>**Scopes**<br/>" + strings.Join(result, ", ")
}
// GetRemoteFromReq retrieves the originating IP address of the client from
// an HTTP request, considering standard and non-standard proxy headers.
//
// # Parameters
//
// - r: The HTTP request object containing details of the client and
// routing information.
//
// # Return Values
//
// - rr: A string value representing the IP address of the originating
// remote client.
//
// # Expected behaviour
//
// The function first checks for the standardized "Forwarded" header (RFC 7239)
// to identify the original client IP. If that isn't available, it falls back to
// the "X-Forwarded-For" header. If both headers are absent, it defaults to
// using the request's RemoteAddr.
//
// For the "Forwarded" header, it extracts the client IP from the "for"
// parameter. For the "X-Forwarded-For" header, if it contains one IP, it
// returns that. If it contains two IPs, it returns the second.
func GetRemoteFromReq(r *http.Request) (rr string) {
// First check for the standardized Forwarded header (RFC 7239)
forwarded := r.Header.Get("Forwarded")
if forwarded != "" {
// Parse the Forwarded header which can contain multiple parameters
//
// Format:
//
// Forwarded: by=<identifier>;for=<identifier>;host=<host>;proto=<http|https>
parts := strings.Split(forwarded, ";")
for _, part := range parts {
part = strings.TrimSpace(part)
if strings.HasPrefix(part, "for=") {
// Extract the client IP from the "for" parameter
forValue := strings.TrimPrefix(part, "for=")
// Remove quotes if present
forValue = strings.Trim(forValue, "\"")
// Handle IPv6 addresses which are enclosed in square brackets
forValue = strings.Trim(forValue, "[]")
return forValue
}
}
}
// If the Forwarded header is not available or doesn't contain "for"
// parameter, fall back to X-Forwarded-For
rem := r.Header.Get("X-Forwarded-For")
if rem == "" {
rr = r.RemoteAddr
} else {
splitted := strings.Split(rem, " ")
if len(splitted) == 1 {
rr = splitted[0]
}
if len(splitted) == 2 {
rr = splitted[1]
}
}
return
}

108
pkg/app/relay/lists.go Normal file
View File

@@ -0,0 +1,108 @@
package relay
import (
"sync"
)
// Lists manages lists of pubkeys, followed users, follows, and muted users with
// concurrency safety via a mutex.
//
// This list is designed primarily for owner-follow-list in mind, but with an
// explicit allowlist/blocklist set up, ownersFollowed corresponds to the
// allowed users, and ownersMuted corresponds to the blocked users, and all
// filtering logic will work the same way.
//
// Currently, there is no explicit purpose for the followedFollows list being
// separate from the ownersFollowed list, but there could be reasons for this
// distinction, such as rate limiting applying to the former and not the latter.
type Lists struct {
sync.Mutex
ownersPubkeys [][]byte
ownersFollowed [][]byte
followedFollows [][]byte
ownersMuted [][]byte
}
func (l *Lists) LenOwnersPubkeys() (ll int) {
l.Lock()
defer l.Unlock()
ll = len(l.ownersPubkeys)
return
}
func (l *Lists) OwnersPubkeys() (pks [][]byte) {
l.Lock()
defer l.Unlock()
pks = append(pks, l.ownersPubkeys...)
return
}
func (l *Lists) SetOwnersPubkeys(pks [][]byte) {
l.Lock()
defer l.Unlock()
l.ownersPubkeys = pks
return
}
func (l *Lists) LenOwnersFollowed() (ll int) {
l.Lock()
defer l.Unlock()
ll = len(l.ownersFollowed)
return
}
func (l *Lists) OwnersFollowed() (pks [][]byte) {
l.Lock()
defer l.Unlock()
pks = append(pks, l.ownersFollowed...)
return
}
func (l *Lists) SetOwnersFollowed(pks [][]byte) {
l.Lock()
defer l.Unlock()
l.ownersFollowed = pks
return
}
func (l *Lists) LenFollowedFollows() (ll int) {
l.Lock()
defer l.Unlock()
ll = len(l.followedFollows)
return
}
func (l *Lists) FollowedFollows() (pks [][]byte) {
l.Lock()
defer l.Unlock()
pks = append(pks, l.followedFollows...)
return
}
func (l *Lists) SetFollowedFollows(pks [][]byte) {
l.Lock()
defer l.Unlock()
l.followedFollows = pks
return
}
func (l *Lists) LenOwnersMuted() (ll int) {
l.Lock()
defer l.Unlock()
ll = len(l.ownersMuted)
return
}
func (l *Lists) OwnersMuted() (pks [][]byte) {
l.Lock()
defer l.Unlock()
pks = append(pks, l.ownersMuted...)
return
}
func (l *Lists) SetOwnersMuted(pks [][]byte) {
l.Lock()
defer l.Unlock()
l.ownersMuted = pks
return
}

217
pkg/app/relay/lists_test.go Normal file
View File

@@ -0,0 +1,217 @@
package relay
import (
"bytes"
"testing"
)
func TestLists_OwnersPubkeys(t *testing.T) {
// Create a new Lists instance
l := &Lists{}
// Test with empty list
pks := l.OwnersPubkeys()
if len(pks) != 0 {
t.Errorf("Expected empty list, got %d items", len(pks))
}
// Test with some pubkeys
testPubkeys := [][]byte{
[]byte("pubkey1"),
[]byte("pubkey2"),
[]byte("pubkey3"),
}
l.SetOwnersPubkeys(testPubkeys)
// Verify length
if l.LenOwnersPubkeys() != len(testPubkeys) {
t.Errorf("Expected length %d, got %d", len(testPubkeys), l.LenOwnersPubkeys())
}
// Verify content
pks = l.OwnersPubkeys()
if len(pks) != len(testPubkeys) {
t.Errorf("Expected %d pubkeys, got %d", len(testPubkeys), len(pks))
}
// Verify each pubkey
for i, pk := range pks {
if !bytes.Equal(pk, testPubkeys[i]) {
t.Errorf("Pubkey at index %d doesn't match: expected %s, got %s",
i, testPubkeys[i], pk)
}
}
// Verify that the returned slice is a copy, not a reference
pks[0] = []byte("modified")
newPks := l.OwnersPubkeys()
if bytes.Equal(pks[0], newPks[0]) {
t.Error("Returned slice should be a copy, not a reference")
}
}
func TestLists_OwnersFollowed(t *testing.T) {
// Create a new Lists instance
l := &Lists{}
// Test with empty list
followed := l.OwnersFollowed()
if len(followed) != 0 {
t.Errorf("Expected empty list, got %d items", len(followed))
}
// Test with some pubkeys
testPubkeys := [][]byte{
[]byte("followed1"),
[]byte("followed2"),
[]byte("followed3"),
}
l.SetOwnersFollowed(testPubkeys)
// Verify length
if l.LenOwnersFollowed() != len(testPubkeys) {
t.Errorf("Expected length %d, got %d", len(testPubkeys), l.LenOwnersFollowed())
}
// Verify content
followed = l.OwnersFollowed()
if len(followed) != len(testPubkeys) {
t.Errorf("Expected %d followed, got %d", len(testPubkeys), len(followed))
}
// Verify each pubkey
for i, pk := range followed {
if !bytes.Equal(pk, testPubkeys[i]) {
t.Errorf("Followed at index %d doesn't match: expected %s, got %s",
i, testPubkeys[i], pk)
}
}
}
func TestLists_FollowedFollows(t *testing.T) {
// Create a new Lists instance
l := &Lists{}
// Test with empty list
follows := l.FollowedFollows()
if len(follows) != 0 {
t.Errorf("Expected empty list, got %d items", len(follows))
}
// Test with some pubkeys
testPubkeys := [][]byte{
[]byte("follow1"),
[]byte("follow2"),
[]byte("follow3"),
}
l.SetFollowedFollows(testPubkeys)
// Verify length
if l.LenFollowedFollows() != len(testPubkeys) {
t.Errorf("Expected length %d, got %d", len(testPubkeys), l.LenFollowedFollows())
}
// Verify content
follows = l.FollowedFollows()
if len(follows) != len(testPubkeys) {
t.Errorf("Expected %d follows, got %d", len(testPubkeys), len(follows))
}
// Verify each pubkey
for i, pk := range follows {
if !bytes.Equal(pk, testPubkeys[i]) {
t.Errorf("Follow at index %d doesn't match: expected %s, got %s",
i, testPubkeys[i], pk)
}
}
}
func TestLists_OwnersMuted(t *testing.T) {
// Create a new Lists instance
l := &Lists{}
// Test with empty list
muted := l.OwnersMuted()
if len(muted) != 0 {
t.Errorf("Expected empty list, got %d items", len(muted))
}
// Test with some pubkeys
testPubkeys := [][]byte{
[]byte("muted1"),
[]byte("muted2"),
[]byte("muted3"),
}
l.SetOwnersMuted(testPubkeys)
// Verify length
if l.LenOwnersMuted() != len(testPubkeys) {
t.Errorf("Expected length %d, got %d", len(testPubkeys), l.LenOwnersMuted())
}
// Verify content
muted = l.OwnersMuted()
if len(muted) != len(testPubkeys) {
t.Errorf("Expected %d muted, got %d", len(testPubkeys), len(muted))
}
// Verify each pubkey
for i, pk := range muted {
if !bytes.Equal(pk, testPubkeys[i]) {
t.Errorf("Muted at index %d doesn't match: expected %s, got %s",
i, testPubkeys[i], pk)
}
}
}
func TestLists_ConcurrentAccess(t *testing.T) {
// Create a new Lists instance
l := &Lists{}
// Test concurrent access to the lists
done := make(chan bool)
// Concurrent reads and writes
go func() {
for i := 0; i < 100; i++ {
l.SetOwnersPubkeys([][]byte{[]byte("pubkey1"), []byte("pubkey2")})
l.OwnersPubkeys()
}
done <- true
}()
go func() {
for i := 0; i < 100; i++ {
l.SetOwnersFollowed([][]byte{[]byte("followed1"), []byte("followed2")})
l.OwnersFollowed()
}
done <- true
}()
go func() {
for i := 0; i < 100; i++ {
l.SetFollowedFollows([][]byte{[]byte("follow1"), []byte("follow2")})
l.FollowedFollows()
}
done <- true
}()
go func() {
for i := 0; i < 100; i++ {
l.SetOwnersMuted([][]byte{[]byte("muted1"), []byte("muted2")})
l.OwnersMuted()
}
done <- true
}()
// Wait for all goroutines to complete
for i := 0; i < 4; i++ {
<-done
}
// If we got here without deadlocks or panics, the test passes
}

View File

@@ -1,19 +1,20 @@
// Package options provides some option configurations for the realy relay.
// Package options provides some option configurations for the relay.
//
// None of this package is actually in use, and the skip event function has not been
// implemented. In theory this could be used for something but it currently isn't.
// None of this package is actually in use, and the skip event function has not
// been implemented. In theory, this could be used for something but it currently
// isn't.
package options
import (
"orly.dev/encoders/event"
"orly.dev/pkg/encoders/event"
)
type SkipEventFunc func(*event.E) bool
// T is a collection of options.
type T struct {
// SkipEventFunc is in theory a function to test whether an event should not be sent in
// response to a query.
// SkipEventFunc is in theory a function to test whether an event should not
// be sent in response to a query.
SkipEventFunc
}
@@ -25,7 +26,8 @@ func Default() *T {
return &T{}
}
// WithSkipEventFunc is an options.T generator that adds a function to skip events.
// WithSkipEventFunc is an options.T generator that adds a function to skip
// events.
func WithSkipEventFunc(skipEventFunc func(*event.E) bool) O {
return func(o *T) {
o.SkipEventFunc = skipEventFunc

View File

@@ -4,8 +4,8 @@
package publish
import (
"orly.dev/app/realy/publish/publisher"
"orly.dev/encoders/event"
"orly.dev/pkg/encoders/event"
"orly.dev/pkg/interfaces/publisher"
)
// S is the control structure for the subscription management scheme.

View File

@@ -0,0 +1,23 @@
package relay
import (
"orly.dev/pkg/app/relay/publish"
"orly.dev/pkg/interfaces/relay"
"orly.dev/pkg/interfaces/server"
"orly.dev/pkg/interfaces/store"
"orly.dev/pkg/utils/context"
)
func (s *Server) Storage() store.I { return s.relay.Storage() }
func (s *Server) Relay() relay.I { return s.relay }
func (s *Server) Publisher() *publish.S { return s.listeners }
func (s *Server) Context() context.T { return s.Ctx }
func (s *Server) AuthRequired() bool { return s.C.AuthRequired || s.LenOwnersPubkeys() > 0 }
func (s *Server) PublicReadable() bool { return s.C.PublicReadable }
var _ server.I = &Server{}

View File

@@ -1,34 +1,53 @@
package realy
package relay
import (
"bytes"
"errors"
"fmt"
"orly.dev/encoders/tags"
"orly.dev/utils/chk"
"orly.dev/utils/errorf"
"orly.dev/utils/log"
"orly.dev/utils/normalize"
"orly.dev/encoders/event"
"orly.dev/encoders/filter"
"orly.dev/encoders/kinds"
"orly.dev/encoders/tag"
"orly.dev/interfaces/store"
"orly.dev/utils/context"
"orly.dev/pkg/encoders/event"
"orly.dev/pkg/encoders/filter"
"orly.dev/pkg/encoders/kind"
"orly.dev/pkg/encoders/kinds"
"orly.dev/pkg/encoders/tag"
"orly.dev/pkg/encoders/tags"
"orly.dev/pkg/interfaces/store"
"orly.dev/pkg/utils/chk"
"orly.dev/pkg/utils/context"
"orly.dev/pkg/utils/errorf"
"orly.dev/pkg/utils/log"
"orly.dev/pkg/utils/normalize"
)
// Publish processes and saves an event based on its type and rules.
// It handles replaceable, ephemeral, and parameterized replaceable events.
// Duplicate or conflicting events are managed before saving the new one.
// Publish processes and stores an event in the server's storage. It handles different types of events: ephemeral, replaceable, and parameterized replaceable.
//
// # Parameters
//
// - c (context.Context): The context for the operation.
//
// - evt (*event.E): The event to be published.
//
// # Return Values
//
// - err (error): An error if any step fails during the publishing process.
//
// # Expected Behaviour
//
// - For ephemeral events, the method doesn't store them and returns
// immediately.
//
// - For replaceable events, it first queries for existing similar events,
// deletes older ones, and then stores the new event.
//
// - For parameterized replaceable events, it performs a similar process but
// uses additional tags to identify duplicates.
func (s *Server) Publish(c context.T, evt *event.E) (err error) {
sto := s.relay.Storage()
if evt.Kind.IsEphemeral() {
// do not store ephemeral events
// don't store ephemeral events
return nil
} else if evt.Kind.IsReplaceable() {
// replaceable event, delete before storing
// replaceable event, delete old after storing
var evs []*event.E
f := filter.New()
f.Authors = tag.New(evt.Pubkey)
@@ -48,14 +67,63 @@ func (s *Server) Publish(c context.T, evt *event.E) (err error) {
"maybe replace %s with %s", ev.Serialize(), evt.Serialize(),
)
if ev.CreatedAt.Int() > evt.CreatedAt.Int() {
log.I.S(ev, evt)
return errorf.W(string(normalize.Invalid.F("not replacing newer replaceable event")))
return errorf.W(
string(
normalize.Invalid.F(
"not replacing newer replaceable event",
),
),
)
}
if evt.Kind.Equal(kind.FollowList) {
// if the event is from someone on ownersFollowed or
// followedFollows, for now add to this list so they're
// immediately effective.
var isFollowed bool
ownersFollowed := s.OwnersFollowed()
for _, pk := range ownersFollowed {
if bytes.Equal(evt.Pubkey, pk) {
isFollowed = true
}
}
if isFollowed {
if _, _, err = sto.SaveEvent(
c, evt,
); err != nil && !errors.Is(
err, store.ErrDupEvent,
) {
return
}
// we need to trigger the spider with no fetch
if err = s.Spider(true); chk.E(err) {
err = nil
}
// event has been saved and lists updated.
return
}
}
if evt.Kind.Equal(kind.MuteList) {
// check if this is one of the owners, if so, the mute list
// should be applied immediately.
owners := s.OwnersPubkeys()
for _, pk := range owners {
if bytes.Equal(evt.Pubkey, pk) {
if _, _, err = sto.SaveEvent(
c, evt,
); err != nil && !errors.Is(
err, store.ErrDupEvent,
) {
return
}
// we need to trigger the spider with no fetch
if err = s.Spider(true); chk.E(err) {
err = nil
}
// event has been saved and lists updated.
return
}
}
// not deleting these events because some clients are retarded
// and the query will pull the new one, but a backup can recover
// the data of old ones
if ev.Kind.IsDirectoryEvent() {
del = false
}
// defer the delete until after the save, further down, has
// completed.
@@ -74,8 +142,6 @@ func (s *Server) Publish(c context.T, evt *event.E) (err error) {
)
},
)
// replaceable events we don't tombstone when replacing,
// so if deleted, old versions can be restored
if err = sto.DeleteEvent(c, ev.EventId()); chk.E(err) {
return
}
@@ -156,7 +222,7 @@ func (s *Server) Publish(c context.T, evt *event.E) (err error) {
}
}
}
if _, _, err = sto.SaveEvent(c, evt); chk.E(err) && !errors.Is(
if _, _, err = sto.SaveEvent(c, evt); err != nil && !errors.Is(
err, store.ErrDupEvent,
) {
return

272
pkg/app/relay/server.go Normal file
View File

@@ -0,0 +1,272 @@
package relay
import (
_ "embed"
"errors"
"fmt"
"net"
"net/http"
"strconv"
"time"
"orly.dev/pkg/app/config"
"orly.dev/pkg/app/relay/helpers"
"orly.dev/pkg/app/relay/options"
"orly.dev/pkg/app/relay/publish"
"orly.dev/pkg/interfaces/relay"
"orly.dev/pkg/protocol/servemux"
"orly.dev/pkg/protocol/socketapi"
"orly.dev/pkg/utils/chk"
"orly.dev/pkg/utils/context"
"orly.dev/pkg/utils/log"
"github.com/rs/cors"
)
// Server represents the core structure for running a nostr relay. It
// encapsulates various components such as context, cancel function, options,
// relay interface, address, HTTP server, and configuration settings.
type Server struct {
Ctx context.T
Cancel context.F
options *options.T
relay relay.I
Addr string
mux *servemux.S
httpServer *http.Server
listeners *publish.S
*config.C
*Lists
}
// ServerParams represents the configuration parameters for initializing a
// server. It encapsulates various components such as context, cancel function,
// relay interface, database path, maximum limit, and configuration settings.
type ServerParams struct {
Ctx context.T
Cancel context.F
Rl relay.I
DbPath string
MaxLimit int
*config.C
}
// NewServer initializes and returns a new Server instance based on the provided
// ServerParams and optional settings. It sets up storage, initializes the
// relay, and configures necessary components for server operation.
//
// # Parameters
//
// - sp (*ServerParams): The configuration parameters for initializing the
// server.
//
// - opts (...options.O): Optional settings that modify the server's behavior.
//
// # Return Values
//
// - s (*Server): The newly created Server instance.
//
// - err (error): An error if any step fails during initialization.
//
// # Expected Behaviour
//
// - Initializes storage with the provided database path.
//
// - Configures the server's options using the default settings and applies any
// optional settings provided.
//
// - Sets up a ServeMux for handling HTTP requests.
//
// - Initializes the relay, starting its operation in a separate goroutine.
func NewServer(sp *ServerParams, opts ...options.O) (s *Server, err error) {
op := options.Default()
for _, opt := range opts {
opt(op)
}
if storage := sp.Rl.Storage(); storage != nil {
if err = storage.Init(sp.DbPath); chk.T(err) {
return nil, fmt.Errorf("storage init: %w", err)
}
}
serveMux := servemux.NewServeMux()
s = &Server{
Ctx: sp.Ctx,
Cancel: sp.Cancel,
relay: sp.Rl,
mux: serveMux,
options: op,
listeners: publish.New(socketapi.New()),
C: sp.C,
Lists: new(Lists),
}
go func() {
if err := s.relay.Init(); chk.E(err) {
s.Shutdown()
}
}()
return s, nil
}
// ServeHTTP handles incoming HTTP requests according to the standard Nostr
// protocol. It specifically processes WebSocket upgrades and
// "application/nostr+json" Accept headers.
//
// # Parameters
//
// - w (http.ResponseWriter): The response writer for sending responses.
//
// - r (*http.Request): The request object containing client's details and data.
//
// # Expected Behaviour
//
// - Checks if the request URL path is "/".
//
// - For WebSocket upgrades, calls handleWebsocket method.
//
// - If "Accept" header is "application/nostr+json", calls HandleRelayInfo
// method.
//
// - Logs the HTTP request details for non-standard requests.
//
// - For all other paths, delegates to the internal mux's ServeHTTP method.
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// standard nostr protocol only governs the "root" path of the relay and
// websockets
if r.URL.Path == "/" {
if r.Header.Get("Upgrade") == "websocket" {
s.handleWebsocket(w, r)
return
}
if r.Header.Get("Accept") == "application/nostr+json" {
s.HandleRelayInfo(w, r)
return
}
}
log.I.F(
"http request: %s from %s",
r.URL.String(), helpers.GetRemoteFromReq(r),
)
s.mux.ServeHTTP(w, r)
}
// Start initializes the server by setting up a TCP listener and serving HTTP
// requests.
//
// # Parameters
//
// - host (string): The hostname or IP address to listen on.
//
// - port (int): The port number to bind to.
//
// - started (...chan bool): Optional channels that are closed after the server
// starts successfully.
//
// # Return Values
//
// - err (error): An error if any step fails during the server startup process.
//
// # Expected Behaviour
//
// - Joins the host and port into a full address string.
//
// - Logs the intention to start the relay listener at the specified address.
//
// - Listens for TCP connections on the specified address.
//
// - Configures an HTTP server with CORS middleware, sets timeouts, and binds it
// to the listener.
//
// - If any started channels are provided, closes them upon successful startup.
//
// - Starts serving requests using the configured HTTP server.
func (s *Server) Start(
host string, port int, started ...chan bool,
) (err error) {
if len(s.C.Owners) > 0 {
// start up spider
if err = s.Spider(s.C.Private); chk.E(err) {
// there wasn't any owners, or they couldn't be found on the spider
// seeds.
err = nil
}
}
// start up a spider run to trigger every 30 minutes
ticker := time.NewTicker(30 * time.Minute)
go func() {
for {
select {
case <-ticker.C:
if err = s.Spider(s.C.Private); chk.E(err) {
// there wasn't any owners, or they couldn't be found on the spider
// seeds.
err = nil
}
case <-s.Ctx.Done():
log.I.F("stopping spider ticker")
return
}
}
}()
addr := net.JoinHostPort(host, strconv.Itoa(port))
log.I.F("starting relay listener at %s", addr)
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
s.httpServer = &http.Server{
Handler: cors.Default().Handler(s),
Addr: addr,
ReadHeaderTimeout: 7 * time.Second,
IdleTimeout: 28 * time.Second,
}
for _, startedC := range started {
close(startedC)
}
if err = s.httpServer.Serve(ln); errors.Is(err, http.ErrServerClosed) {
} else if err != nil {
}
return nil
}
// Shutdown gracefully shuts down the server and its components. It ensures that
// all resources are properly released.
//
// # Expected Behaviour
//
// - Logs shutting down message.
//
// - Cancels the context to stop ongoing operations.
//
// - Closes the event store, logging the action and checking for errors.
//
// - Shuts down the HTTP server, logging the action and checking for errors.
//
// - If the relay implements ShutdownAware, it calls OnShutdown with the
// context.
func (s *Server) Shutdown() {
log.I.Ln("shutting down relay")
s.Cancel()
log.W.Ln("closing event store")
chk.E(s.relay.Storage().Close())
if s.httpServer != nil {
log.W.Ln("shutting down relay listener")
chk.E(s.httpServer.Shutdown(s.Ctx))
}
if f, ok := s.relay.(relay.ShutdownAware); ok {
f.OnShutdown(s.Ctx)
}
}
// Router retrieves and returns the HTTP ServeMux associated with the server.
//
// # Return Values
//
// - router (*http.ServeMux): The ServeMux instance used for routing HTTP
// requests.
//
// # Expected Behaviour
//
// - Returns the ServeMux that handles incoming HTTP requests to the server.
func (s *Server) Router() (router *http.ServeMux) {
return s.mux.ServeMux
}

View File

@@ -0,0 +1,120 @@
package relay
import (
"orly.dev/pkg/crypto/ec/schnorr"
"orly.dev/pkg/encoders/event"
"orly.dev/pkg/encoders/filter"
"orly.dev/pkg/encoders/hex"
"orly.dev/pkg/encoders/kind"
"orly.dev/pkg/encoders/kinds"
"orly.dev/pkg/encoders/tag"
"orly.dev/pkg/protocol/ws"
"orly.dev/pkg/utils/chk"
"orly.dev/pkg/utils/context"
"orly.dev/pkg/utils/log"
"sort"
)
func (s *Server) SpiderFetch(
k *kind.T, noFetch bool, pubkeys ...[]byte,
) (pks [][]byte, err error) {
// first search the local database
pkList := tag.New(pubkeys...)
f := &filter.F{
Kinds: kinds.New(k),
Authors: pkList,
}
var evs event.S
if evs, err = s.Storage().QueryEvents(s.Ctx, f); chk.E(err) {
// none were found, so we need to scan the spiders
err = nil
}
if len(evs) < len(pubkeys) && !noFetch {
// we need to search the spider seeds.
// Break up pubkeys into batches of 512
log.I.F("breaking up %d pubkeys into batches of 512", len(pubkeys))
for i := 0; i < len(pubkeys); i += 512 {
end := i + 512
if end > len(pubkeys) {
end = len(pubkeys)
}
batchPubkeys := pubkeys[i:end]
log.I.F(
"processing batch %d to %d of %d pubkeys", i, end, len(pubkeys),
)
batchPkList := tag.New(batchPubkeys...)
batchFilter := &filter.F{
Kinds: kinds.New(k),
Authors: batchPkList,
}
for _, seed := range s.C.SpiderSeeds {
select {
case <-s.Ctx.Done():
return
default:
}
var evss event.S
var cli *ws.Client
if cli, err = ws.RelayConnect(context.Bg(), seed); chk.E(err) {
err = nil
continue
}
if evss, err = cli.QuerySync(
context.Bg(), batchFilter,
); chk.E(err) {
err = nil
continue
}
for _, ev := range evss {
evs = append(evs, ev)
}
}
}
// save the events to the database
for _, ev := range evs {
if _, _, err = s.Storage().SaveEvent(s.Ctx, ev); chk.E(err) {
err = nil
continue
}
}
}
// deduplicate and take the newest
var tmp event.S
evMap := make(map[string]event.S)
for _, ev := range evs {
evMap[ev.PubKeyString()] = append(evMap[ev.PubKeyString()], ev)
}
for _, evm := range evMap {
if len(evm) < 1 {
continue
}
if len(evm) > 1 {
sort.Sort(evm)
}
tmp = append(tmp, evm[0])
}
evs = tmp
// we have all we're going to get now
pkMap := make(map[string]struct{})
for _, ev := range evs {
t := ev.Tags.GetAll(tag.New("p"))
for _, tt := range t.ToSliceOfTags() {
pkh := tt.Value()
if len(pkh) != 2*schnorr.PubKeyBytesLen {
continue
}
pk := make([]byte, schnorr.PubKeyBytesLen)
if _, err = hex.DecBytes(pk, pkh); chk.E(err) {
err = nil
continue
}
pkMap[string(pk)] = struct{}{}
}
}
for pk := range pkMap {
pks = append(pks, []byte(pk))
}
log.I.F("found %d pks", len(pks))
return
}

133
pkg/app/relay/spider.go Normal file
View File

@@ -0,0 +1,133 @@
package relay
import (
"bytes"
"orly.dev/pkg/crypto/ec/bech32"
"orly.dev/pkg/encoders/bech32encoding"
"orly.dev/pkg/encoders/hex"
"orly.dev/pkg/encoders/kind"
"orly.dev/pkg/utils/chk"
"orly.dev/pkg/utils/log"
)
func (s *Server) Spider(noFetch ...bool) (err error) {
var ownersPubkeys [][]byte
for _, v := range s.C.Owners {
var prf []byte
var pk []byte
var bits5 []byte
if prf, bits5, err = bech32.DecodeNoLimit([]byte(v)); chk.D(err) {
// try hex then
if _, err = hex.DecBytes(pk, []byte(v)); chk.E(err) {
log.W.F(
"owner key %s is neither bech32 npub nor hex",
v,
)
continue
}
} else {
if !bytes.Equal(prf, bech32encoding.NpubHRP) {
log.W.F(
"owner key %s is neither bech32 npub nor hex",
v,
)
continue
}
if pk, err = bech32.ConvertBits(bits5, 5, 8, false); chk.E(err) {
continue
}
}
// owners themselves are on the OwnersFollowed list as first level
ownersPubkeys = append(ownersPubkeys, pk)
}
if len(ownersPubkeys) == 0 {
// there is no OwnersPubkeys, so there is nothing to do.
return
}
dontFetch := false
if len(noFetch) > 0 && noFetch[0] {
dontFetch = true
}
log.I.F("getting ownersFollowed")
var ownersFollowed [][]byte
if ownersFollowed, err = s.SpiderFetch(
kind.FollowList, dontFetch, ownersPubkeys...,
); chk.E(err) {
return
}
log.I.F("getting followedFollows")
var followedFollows [][]byte
if followedFollows, err = s.SpiderFetch(
kind.FollowList, dontFetch, ownersFollowed...,
); chk.E(err) {
return
}
log.I.F("getting ownersMuted")
var ownersMuted [][]byte
if ownersMuted, err = s.SpiderFetch(
kind.MuteList, dontFetch, ownersPubkeys...,
); chk.E(err) {
return
}
// remove the ownersFollowed and ownersMuted items from the followedFollows
// list
filteredFollows := make([][]byte, 0, len(followedFollows))
for _, follow := range followedFollows {
found := false
for _, owner := range ownersFollowed {
if bytes.Equal(follow, owner) {
found = true
break
}
}
for _, owner := range ownersMuted {
if bytes.Equal(follow, owner) {
found = true
break
}
}
if !found {
filteredFollows = append(filteredFollows, follow)
}
}
followedFollows = filteredFollows
own := "owner"
if len(ownersPubkeys) > 1 {
own = "owners"
}
fol := "pubkey"
if len(ownersFollowed) > 1 {
fol = "pubkeys"
}
folfol := "pubkey"
if len(followedFollows) > 1 {
folfol = "pubkeys"
}
mut := "pubkey"
if len(ownersMuted) > 1 {
mut = "pubkeys"
}
log.T.F(
"found %d %s with a total of %d followed %s and %d followed's follows %s, and excluding %d owner muted %s",
len(ownersPubkeys), own,
len(ownersFollowed), fol,
len(followedFollows), folfol,
len(ownersMuted), mut,
)
// add the owners
ownersFollowed = append(ownersFollowed, ownersPubkeys...)
s.SetOwnersPubkeys(ownersPubkeys)
s.SetOwnersFollowed(ownersFollowed)
s.SetFollowedFollows(followedFollows)
s.SetOwnersMuted(ownersMuted)
// lastly, update users profile metadata and relay lists in the background
if !dontFetch {
go func() {
everyone := append(ownersFollowed, followedFollows...)
s.SpiderFetch(kind.ProfileMetadata, false, everyone...)
s.SpiderFetch(kind.RelayListMetadata, false, everyone...)
s.SpiderFetch(kind.DMRelaysList, false, everyone...)
}()
}
return
}

View File

@@ -1,16 +1,15 @@
package realy
package relay
import (
"io"
"net/http"
"orly.dev/pkg/encoders/event"
"orly.dev/pkg/encoders/eventid"
"orly.dev/pkg/encoders/filter"
"orly.dev/pkg/interfaces/store"
"orly.dev/pkg/utils/context"
"orly.dev/pkg/utils/units"
"testing"
"orly.dev/encoders/event"
"orly.dev/encoders/eventid"
"orly.dev/encoders/filter"
"orly.dev/interfaces/store"
"orly.dev/utils/context"
"orly.dev/utils/units"
)
func startTestRelay(c context.T, t *testing.T, tr *testRelay) *Server {

39
pkg/app/resources.go Normal file
View File

@@ -0,0 +1,39 @@
package app
import (
"orly.dev/pkg/utils/context"
"orly.dev/pkg/utils/log"
"os"
"runtime"
"time"
)
// MonitorResources periodically logs resource usage metrics such as the number
// of active goroutines and CGO calls at 15-minute intervals, and exits when the
// provided context signals cancellation.
//
// # Parameters
//
// - c: Context used to control the lifecycle of the resource monitoring process.
//
// # Expected behaviour
//
// The function runs indefinitely, logging metrics every 15 minutes until the
// context is cancelled. Upon cancellation, it logs a shutdown message and exits
// gracefully without returning any values.
func MonitorResources(c context.T) {
tick := time.NewTicker(time.Minute * 15)
log.I.Ln("running process", os.Args[0], os.Getpid())
for {
select {
case <-c.Done():
log.D.Ln("shutting down resource monitor")
return
case <-tick.C:
log.D.Ln(
"# goroutines", runtime.NumGoroutine(),
"# cgo calls", runtime.NumCgoCall(),
)
}
}
}

View File

@@ -7,7 +7,7 @@ package base58_test
import (
"bytes"
"encoding/hex"
"orly.dev/crypto/ec/base58"
"orly.dev/pkg/crypto/ec/base58"
"testing"
)

View File

@@ -6,7 +6,7 @@ package base58_test
import (
"bytes"
"orly.dev/crypto/ec/base58"
"orly.dev/pkg/crypto/ec/base58"
"testing"
)

View File

@@ -6,7 +6,7 @@ package base58
import (
"errors"
"orly.dev/crypto/sha256"
"orly.dev/pkg/crypto/sha256"
)
// ErrChecksum indicates that the checksum of a check-encoded string does not verify against

View File

@@ -5,7 +5,7 @@
package base58_test
import (
"orly.dev/crypto/ec/base58"
"orly.dev/pkg/crypto/ec/base58"
"testing"
)

View File

@@ -6,14 +6,14 @@ package base58_test
import (
"fmt"
base59 "orly.dev/crypto/ec/base58"
"orly.dev/pkg/crypto/ec/base58"
)
// This example demonstrates how to decode modified base58 encoded data.
func ExampleDecode() {
// Decode example modified base58 encoded data.
encoded := "25JnwSn7XKfNQ"
decoded := base59.Decode(encoded)
decoded := base58.Decode(encoded)
// Show the decoded data.
fmt.Println("Decoded Data:", string(decoded))
@@ -27,7 +27,7 @@ func ExampleDecode() {
func ExampleEncode() {
// Encode example data with the modified base58 encoding scheme.
data := []byte("Test data")
encoded := base59.Encode(data)
encoded := base58.Encode(data)
// Show the encoded data.
fmt.Println("Encoded Data:", encoded)
@@ -40,7 +40,7 @@ func ExampleEncode() {
func ExampleCheckDecode() {
// Decode an example Base58Check encoded data.
encoded := "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"
decoded, version, err := base59.CheckDecode(encoded)
decoded, version, err := base58.CheckDecode(encoded)
if err != nil {
fmt.Println(err)
return
@@ -60,7 +60,7 @@ func ExampleCheckDecode() {
func ExampleCheckEncode() {
// Encode example data with the Base58Check encoding scheme.
data := []byte("Test data")
encoded := base59.CheckEncode(data, 0)
encoded := base58.CheckEncode(data, 0)
// Show the encoded data.
fmt.Println("Encoded Data:", encoded)

View File

@@ -6,10 +6,9 @@ package btcec
import (
"math/big"
"orly.dev/crypto/ec/secp256k1"
"orly.dev/pkg/crypto/ec/secp256k1"
"orly.dev/pkg/encoders/hex"
"testing"
"orly.dev/encoders/hex"
)
// setHex decodes the passed big-endian hex string into the internal field value

View File

@@ -20,31 +20,31 @@ package btcec
// reverse the transform than to operate in affine coordinates.
import (
secp256k2 "orly.dev/crypto/ec/secp256k1"
"orly.dev/pkg/crypto/ec/secp256k1"
)
// KoblitzCurve provides an implementation for secp256k1 that fits the ECC
// Curve interface from crypto/elliptic.
type KoblitzCurve = secp256k2.KoblitzCurve
type KoblitzCurve = secp256k1.KoblitzCurve
// S256 returns a Curve which implements secp256k1.
func S256() *KoblitzCurve {
return secp256k2.S256()
return secp256k1.S256()
}
// CurveParams contains the parameters for the secp256k1 curve.
type CurveParams = secp256k2.CurveParams
type CurveParams = secp256k1.CurveParams
// Params returns the secp256k1 curve parameters for convenience.
func Params() *CurveParams {
return secp256k2.Params()
return secp256k1.Params()
}
// Generator returns the public key at the Generator Point.
func Generator() *PublicKey {
var (
result JacobianPoint
k secp256k2.ModNScalar
k secp256k1.ModNScalar
)
k.SetInt(1)
ScalarBaseMultNonConst(&k, &result)

View File

@@ -2,7 +2,7 @@ package chaincfg
import (
"fmt"
"orly.dev/crypto/ec/wire"
"orly.dev/pkg/crypto/ec/wire"
"time"
)

View File

@@ -1,19 +1,19 @@
package chaincfg
import (
"orly.dev/crypto/ec/chainhash"
wire2 "orly.dev/crypto/ec/wire"
"orly.dev/pkg/crypto/ec/chainhash"
"orly.dev/pkg/crypto/ec/wire"
"time"
)
var (
// genesisCoinbaseTx is the coinbase transaction for the genesis blocks for
// the main network, regression test network, and test network (version 3).
genesisCoinbaseTx = wire2.MsgTx{
genesisCoinbaseTx = wire.MsgTx{
Version: 1,
TxIn: []*wire2.TxIn{
TxIn: []*wire.TxIn{
{
PreviousOutPoint: wire2.OutPoint{
PreviousOutPoint: wire.OutPoint{
Hash: chainhash.Hash{},
Index: 0xffffffff,
},
@@ -41,7 +41,7 @@ var (
Sequence: 0xffffffff,
},
},
TxOut: []*wire2.TxOut{
TxOut: []*wire.TxOut{
{
Value: 0x12a05f200,
PkScript: []byte{
@@ -92,8 +92,8 @@ var (
// genesisBlock defines
// genesisBlock defines the genesis block of the block chain which serves as the
// public transaction ledger for the main network.
genesisBlock = wire2.MsgBlock{
Header: wire2.BlockHeader{
genesisBlock = wire.MsgBlock{
Header: wire.BlockHeader{
Version: 1,
PrevBlock: chainhash.Hash{}, // 0000000000000000000000000000000000000000000000000000000000000000
MerkleRoot: genesisMerkleRoot, // 4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b
@@ -104,6 +104,6 @@ var (
Bits: 0x1d00ffff, // 486604799 [00000000ffff0000000000000000000000000000000000000000000000000000]
Nonce: 0x7c2bac1d, // 2083236893
},
Transactions: []*wire2.MsgTx{&genesisCoinbaseTx},
Transactions: []*wire.MsgTx{&genesisCoinbaseTx},
}
)

View File

@@ -3,8 +3,8 @@ package chaincfg
import (
"math/big"
"orly.dev/crypto/ec/chainhash"
wire2 "orly.dev/crypto/ec/wire"
"orly.dev/pkg/crypto/ec/chainhash"
"orly.dev/pkg/crypto/ec/wire"
"time"
)
@@ -113,7 +113,7 @@ type Params struct {
Name string
// Net defines the magic bytes used to identify the network.
Net wire2.BitcoinNet
Net wire.BitcoinNet
// DefaultPort defines the default peer-to-peer port for the network.
DefaultPort string
@@ -123,7 +123,7 @@ type Params struct {
DNSSeeds []DNSSeed
// GenesisBlock defines the first block of the chain.
GenesisBlock *wire2.MsgBlock
GenesisBlock *wire.MsgBlock
// GenesisHash is the starting block hash.
GenesisHash *chainhash.Hash
@@ -231,7 +231,7 @@ type Params struct {
// MainNetParams defines the network parameters for the main Bitcoin network.
var MainNetParams = Params{
Name: "mainnet",
Net: wire2.MainNet,
Net: wire.MainNet,
DefaultPort: "8333",
DNSSeeds: []DNSSeed{
{"seed.bitcoin.sipa.be", true},

View File

@@ -8,9 +8,8 @@ package chainhash
import (
"encoding/json"
"fmt"
"orly.dev/crypto/sha256"
"orly.dev/encoders/hex"
"orly.dev/pkg/crypto/sha256"
"orly.dev/pkg/encoders/hex"
)
const (

View File

@@ -6,7 +6,7 @@
package chainhash
import (
"orly.dev/crypto/sha256"
"orly.dev/pkg/crypto/sha256"
)
// HashB calculates hash(b) and returns the resulting bytes.

View File

@@ -5,7 +5,7 @@
package btcec
import (
"orly.dev/crypto/ec/secp256k1"
"orly.dev/pkg/crypto/ec/secp256k1"
)
// GenerateSharedSecret generates a shared secret based on a secret key and a

View File

@@ -5,12 +5,12 @@ package btcec
import (
"fmt"
secp256k2 "orly.dev/crypto/ec/secp256k1"
"orly.dev/pkg/crypto/ec/secp256k1"
)
// JacobianPoint is an element of the group formed by the secp256k1 curve in
// Jacobian projective coordinates and thus represents a point on the curve.
type JacobianPoint = secp256k2.JacobianPoint
type JacobianPoint = secp256k1.JacobianPoint
// infinityPoint is the jacobian representation of the point at infinity.
var infinityPoint JacobianPoint
@@ -18,13 +18,13 @@ var infinityPoint JacobianPoint
// MakeJacobianPoint returns a Jacobian point with the provided X, Y, and Z
// coordinates.
func MakeJacobianPoint(x, y, z *FieldVal) JacobianPoint {
return secp256k2.MakeJacobianPoint(x, y, z)
return secp256k1.MakeJacobianPoint(x, y, z)
}
// AddNonConst adds the passed Jacobian points together and stores the result
// in the provided result param in *non-constant* time.
func AddNonConst(p1, p2, result *JacobianPoint) {
secp256k2.AddNonConst(p1, p2, result)
secp256k1.AddNonConst(p1, p2, result)
}
// DecompressY attempts to calculate the Y coordinate for the given X
@@ -35,7 +35,7 @@ func AddNonConst(p1, p2, result *JacobianPoint) {
// The magnitude of the provided X coordinate field val must be a max of 8 for
// a correct result. The resulting Y field val will have a max magnitude of 2.
func DecompressY(x *FieldVal, odd bool, resultY *FieldVal) bool {
return secp256k2.DecompressY(x, odd, resultY)
return secp256k1.DecompressY(x, odd, resultY)
}
// DoubleNonConst doubles the passed Jacobian point and stores the result in
@@ -44,7 +44,7 @@ func DecompressY(x *FieldVal, odd bool, resultY *FieldVal) bool {
// NOTE: The point must be normalized for this function to return the correct
// result. The resulting point will be normalized.
func DoubleNonConst(p, result *JacobianPoint) {
secp256k2.DoubleNonConst(p, result)
secp256k1.DoubleNonConst(p, result)
}
// ScalarBaseMultNonConst multiplies k*G where G is the base point of the group
@@ -53,7 +53,7 @@ func DoubleNonConst(p, result *JacobianPoint) {
//
// NOTE: The resulting point will be normalized.
func ScalarBaseMultNonConst(k *ModNScalar, result *JacobianPoint) {
secp256k2.ScalarBaseMultNonConst(k, result)
secp256k1.ScalarBaseMultNonConst(k, result)
}
// ScalarMultNonConst multiplies k*P where k is a big endian integer modulo the
@@ -63,7 +63,7 @@ func ScalarBaseMultNonConst(k *ModNScalar, result *JacobianPoint) {
// NOTE: The point must be normalized for this function to return the correct
// result. The resulting point will be normalized.
func ScalarMultNonConst(k *ModNScalar, point, result *JacobianPoint) {
secp256k2.ScalarMultNonConst(k, point, result)
secp256k1.ScalarMultNonConst(k, point, result)
}
// ParseJacobian parses a byte slice point as a secp256k1.Publickey and returns the
@@ -76,12 +76,12 @@ func ParseJacobian(point []byte) (JacobianPoint, error) {
"invalid nonce: invalid length: %v",
len(point),
)
return JacobianPoint{}, makeError(secp256k2.ErrPubKeyInvalidLen, str)
return JacobianPoint{}, makeError(secp256k1.ErrPubKeyInvalidLen, str)
}
if point[0] == 0x00 {
return infinityPoint, nil
}
noncePk, err := secp256k2.ParsePubKey(point)
noncePk, err := secp256k1.ParsePubKey(point)
if err != nil {
return JacobianPoint{}, err
}

View File

@@ -6,22 +6,21 @@
package ecdsa
import (
secp256k2 "orly.dev/crypto/ec/secp256k1"
"orly.dev/pkg/crypto/ec/secp256k1"
"orly.dev/pkg/encoders/hex"
"testing"
"orly.dev/encoders/hex"
)
// hexToModNScalar converts the passed hex string into a ModNScalar and will
// panic if there is an error. This is only provided for the hard-coded
// constants so errors in the source code can be detected. It will only (and
// must only) be called with hard-coded values.
func hexToModNScalar(s string) *secp256k2.ModNScalar {
func hexToModNScalar(s string) *secp256k1.ModNScalar {
b, err := hex.Dec(s)
if err != nil {
panic("invalid hex in source file: " + s)
}
var scalar secp256k2.ModNScalar
var scalar secp256k1.ModNScalar
if overflow := scalar.SetByteSlice(b); overflow {
panic("hex in source file overflows mod N scalar: " + s)
}
@@ -32,12 +31,12 @@ func hexToModNScalar(s string) *secp256k2.ModNScalar {
// if there is an error. This is only provided for the hard-coded constants so
// errors in the source code can be detected. It will only (and must only) be
// called with hard-coded values.
func hexToFieldVal(s string) *secp256k2.FieldVal {
func hexToFieldVal(s string) *secp256k1.FieldVal {
b, err := hex.Dec(s)
if err != nil {
panic("invalid hex in source file: " + s)
}
var f secp256k2.FieldVal
var f secp256k1.FieldVal
if overflow := f.SetByteSlice(b); overflow {
panic("hex in source file overflows mod P: " + s)
}
@@ -49,7 +48,7 @@ func hexToFieldVal(s string) *secp256k2.FieldVal {
func BenchmarkSigVerify(b *testing.B) {
// Randomly generated keypair.
// Secret key: 9e0699c91ca1e3b7e3c9ba71eb71c89890872be97576010fe593fbf3fd57e66d
pubKey := secp256k2.NewPublicKey(
pubKey := secp256k1.NewPublicKey(
hexToFieldVal("d2e670a19c6d753d1a6d8b20bd045df8a08fb162cf508956c31268c6d81ffdab"),
hexToFieldVal("ab65528eefbb8057aa85d597258a3fbd481a24633bc9b47a9aa045c91371de52"),
)
@@ -74,7 +73,7 @@ func BenchmarkSigVerify(b *testing.B) {
func BenchmarkSign(b *testing.B) {
// Randomly generated keypair.
d := hexToModNScalar("9e0699c91ca1e3b7e3c9ba71eb71c89890872be97576010fe593fbf3fd57e66d")
secKey := secp256k2.NewSecretKey(d)
secKey := secp256k1.NewSecretKey(d)
// blake256 of by{0x01, 0x02, 0x03, 0x04}.
msgHash := hexToBytes("c301ba9de5d6053caad9f5eb46523f007702add2c62fa39de03146a36b8026b7")
b.ReportAllocs()
@@ -114,9 +113,9 @@ func BenchmarkNonceRFC6979(b *testing.B) {
msgHash := hexToBytes("c301ba9de5d6053caad9f5eb46523f007702add2c62fa39de03146a36b8026b7")
b.ReportAllocs()
b.ResetTimer()
var noElideNonce *secp256k2.ModNScalar
var noElideNonce *secp256k1.ModNScalar
for i := 0; i < b.N; i++ {
noElideNonce = secp256k2.NonceRFC6979(secKey, msgHash, nil, nil, 0)
noElideNonce = secp256k1.NonceRFC6979(secKey, msgHash, nil, nil, 0)
}
_ = noElideNonce
}
@@ -125,7 +124,7 @@ func BenchmarkNonceRFC6979(b *testing.B) {
// signature for a message.
func BenchmarkSignCompact(b *testing.B) {
d := hexToModNScalar("9e0699c91ca1e3b7e3c9ba71eb71c89890872be97576010fe593fbf3fd57e66d")
secKey := secp256k2.NewSecretKey(d)
secKey := secp256k1.NewSecretKey(d)
// blake256 of by{0x01, 0x02, 0x03, 0x04}.
msgHash := hexToBytes("c301ba9de5d6053caad9f5eb46523f007702add2c62fa39de03146a36b8026b7")
b.ReportAllocs()
@@ -139,7 +138,7 @@ func BenchmarkSignCompact(b *testing.B) {
// given a compact signature and message.
func BenchmarkRecoverCompact(b *testing.B) {
// Secret key: 9e0699c91ca1e3b7e3c9ba71eb71c89890872be97576010fe593fbf3fd57e66d
wantPubKey := secp256k2.NewPublicKey(
wantPubKey := secp256k1.NewPublicKey(
hexToFieldVal("d2e670a19c6d753d1a6d8b20bd045df8a08fb162cf508956c31268c6d81ffdab"),
hexToFieldVal("ab65528eefbb8057aa85d597258a3fbd481a24633bc9b47a9aa045c91371de52"),
)

View File

@@ -7,7 +7,7 @@ package ecdsa
import (
"fmt"
secp256k2 "orly.dev/crypto/ec/secp256k1"
"orly.dev/pkg/crypto/ec/secp256k1"
)
// References:
@@ -27,9 +27,9 @@ var (
// orderAsFieldVal is the order of the secp256k1 curve group stored as a
// field value. It is provided here to avoid the need to create it multiple
// times.
orderAsFieldVal = func() secp256k2.FieldVal {
var f secp256k2.FieldVal
f.SetByteSlice(secp256k2.Params().N.Bytes())
orderAsFieldVal = func() secp256k1.FieldVal {
var f secp256k1.FieldVal
f.SetByteSlice(secp256k1.Params().N.Bytes())
return f
}()
)
@@ -47,12 +47,12 @@ const (
// Signature is a type representing an ECDSA signature.
type Signature struct {
r secp256k2.ModNScalar
s secp256k2.ModNScalar
r secp256k1.ModNScalar
s secp256k1.ModNScalar
}
// NewSignature instantiates a new signature given some r and s values.
func NewSignature(r, s *secp256k2.ModNScalar) *Signature {
func NewSignature(r, s *secp256k1.ModNScalar) *Signature {
return &Signature{*r, *s}
}
@@ -86,7 +86,7 @@ func (sig *Signature) Serialize() []byte {
// order of the group because both S and its negation are valid signatures
// modulo the order, so this forces a consistent choice to reduce signature
// malleability.
sigS := new(secp256k2.ModNScalar).Set(&sig.s)
sigS := new(secp256k1.ModNScalar).Set(&sig.s)
if sigS.IsOverHalfOrder() {
sigS.Negate()
}
@@ -135,27 +135,27 @@ func zeroArray32(b *[32]byte) {
// Note that a bool is not used here because it is not possible in Go to convert
// from a bool to numeric value in constant time and many constant-time
// operations require a numeric value.
func fieldToModNScalar(v *secp256k2.FieldVal) (secp256k2.ModNScalar, uint32) {
func fieldToModNScalar(v *secp256k1.FieldVal) (secp256k1.ModNScalar, uint32) {
var buf [32]byte
v.PutBytes(&buf)
var s secp256k2.ModNScalar
var s secp256k1.ModNScalar
overflow := s.SetBytes(&buf)
zeroArray32(&buf)
return s, overflow
}
// modNScalarToField converts a scalar modulo the group order to a field value.
func modNScalarToField(v *secp256k2.ModNScalar) secp256k2.FieldVal {
func modNScalarToField(v *secp256k1.ModNScalar) secp256k1.FieldVal {
var buf [32]byte
v.PutBytes(&buf)
var fv secp256k2.FieldVal
var fv secp256k1.FieldVal
fv.SetBytes(&buf)
return fv
}
// Verify returns whether the signature is valid for the provided hash
// and secp256k1 public key.
func (sig *Signature) Verify(hash []byte, pubKey *secp256k2.PublicKey) bool {
func (sig *Signature) Verify(hash []byte, pubKey *secp256k1.PublicKey) bool {
// The algorithm for verifying an ECDSA signature is given as algorithm 4.30
// in [GECC].
//
@@ -221,26 +221,26 @@ func (sig *Signature) Verify(hash []byte, pubKey *secp256k2.PublicKey) bool {
// Step 2.
//
// e = H(m)
var e secp256k2.ModNScalar
var e secp256k1.ModNScalar
e.SetByteSlice(hash)
// Step 3.
//
// w = S^-1 mod N
w := new(secp256k2.ModNScalar).InverseValNonConst(&sig.s)
w := new(secp256k1.ModNScalar).InverseValNonConst(&sig.s)
// Step 4.
//
// u1 = e * w mod N
// u2 = R * w mod N
u1 := new(secp256k2.ModNScalar).Mul2(&e, w)
u2 := new(secp256k2.ModNScalar).Mul2(&sig.r, w)
u1 := new(secp256k1.ModNScalar).Mul2(&e, w)
u2 := new(secp256k1.ModNScalar).Mul2(&sig.r, w)
// Step 5.
//
// X = u1G + u2Q
var X, Q, u1G, u2Q secp256k2.JacobianPoint
var X, Q, u1G, u2Q secp256k1.JacobianPoint
pubKey.AsJacobian(&Q)
secp256k2.ScalarBaseMultNonConst(u1, &u1G)
secp256k2.ScalarMultNonConst(u2, &Q, &u2Q)
secp256k2.AddNonConst(&u1G, &u2Q, &X)
secp256k1.ScalarBaseMultNonConst(u1, &u1G)
secp256k1.ScalarMultNonConst(u2, &Q, &u2Q)
secp256k1.AddNonConst(&u1G, &u2Q, &X)
// Step 6.
//
// Fail if X is the point at infinity
@@ -250,12 +250,12 @@ func (sig *Signature) Verify(hash []byte, pubKey *secp256k2.PublicKey) bool {
// Step 7.
//
// z = (X.z)^2 mod P (X.z is the z coordinate of X)
z := new(secp256k2.FieldVal).SquareVal(&X.Z)
z := new(secp256k1.FieldVal).SquareVal(&X.Z)
// Step 8.
//
// Verified if R * z == X.x (mod P)
sigRModP := modNScalarToField(&sig.r)
result := new(secp256k2.FieldVal).Mul2(&sigRModP, z).Normalize()
result := new(secp256k1.FieldVal).Mul2(&sigRModP, z).Normalize()
if result.Equals(&X.X) {
return true
}
@@ -470,7 +470,7 @@ func ParseDERSignature(sig []byte) (*Signature, error) {
// R must be in the range [1, N-1]. Notice the check for the maximum number
// of bytes is required because SetByteSlice truncates as noted in its
// comment so it could otherwise fail to detect the overflow.
var r secp256k2.ModNScalar
var r secp256k1.ModNScalar
if len(rBytes) > 32 {
str := "invalid signature: R is larger than 256 bits"
return nil, signatureError(ErrSigRTooBig, str)
@@ -491,7 +491,7 @@ func ParseDERSignature(sig []byte) (*Signature, error) {
// S must be in the range [1, N-1]. Notice the check for the maximum number
// of bytes is required because SetByteSlice truncates as noted in its
// comment so it could otherwise fail to detect the overflow.
var s secp256k2.ModNScalar
var s secp256k1.ModNScalar
if len(sBytes) > 32 {
str := "invalid signature: S is larger than 256 bits"
return nil, signatureError(ErrSigSTooBig, str)
@@ -519,7 +519,7 @@ func ParseDERSignature(sig []byte) (*Signature, error) {
// signing logic. It differs in that it accepts a nonce to use when signing and
// may not successfully produce a valid signature for the given nonce. It is
// primarily separated for testing purposes.
func sign(secKey, nonce *secp256k2.ModNScalar, hash []byte) (
func sign(secKey, nonce *secp256k1.ModNScalar, hash []byte) (
*Signature, byte,
bool,
) {
@@ -562,8 +562,8 @@ func sign(secKey, nonce *secp256k2.ModNScalar, hash []byte) (
//
// Note that the point must be in affine coordinates.
k := nonce
var kG secp256k2.JacobianPoint
secp256k2.ScalarBaseMultNonConst(k, &kG)
var kG secp256k1.JacobianPoint
secp256k1.ScalarBaseMultNonConst(k, &kG)
kG.ToAffine()
// Step 3.
//
@@ -601,15 +601,15 @@ func sign(secKey, nonce *secp256k2.ModNScalar, hash []byte) (
//
// Note that this actually sets e = H(m) mod N which is correct since
// it is only used in step 5 which itself is mod N.
var e secp256k2.ModNScalar
var e secp256k1.ModNScalar
e.SetByteSlice(hash)
// Step 5 with modification B.
//
// s = k^-1(e + dr) mod N
// Repeat from step 1 if s = 0
// s = -s if s > N/2
kinv := new(secp256k2.ModNScalar).InverseValNonConst(k)
s := new(secp256k2.ModNScalar).Mul2(secKey, &r).Add(&e).Mul(kinv)
kinv := new(secp256k1.ModNScalar).InverseValNonConst(k)
s := new(secp256k1.ModNScalar).Mul2(secKey, &r).Add(&e).Mul(kinv)
if s.IsZero() {
return nil, 0, false
}
@@ -630,7 +630,7 @@ func sign(secKey, nonce *secp256k2.ModNScalar, hash []byte) (
// signRFC6979 generates a deterministic ECDSA signature according to RFC 6979
// and BIP0062 and returns it along with an additional public key recovery code
// for efficiently recovering the public key from the signature.
func signRFC6979(secKey *secp256k2.SecretKey, hash []byte) (
func signRFC6979(secKey *secp256k1.SecretKey, hash []byte) (
*Signature,
byte,
) {
@@ -673,7 +673,7 @@ func signRFC6979(secKey *secp256k2.SecretKey, hash []byte) (
//
// Generate a deterministic nonce in [1, N-1] parameterized by the
// secret key, message being signed, and iteration count.
k := secp256k2.NonceRFC6979(secKeyBytes[:], hash, nil, nil, iteration)
k := secp256k1.NonceRFC6979(secKeyBytes[:], hash, nil, nil, iteration)
// Steps 2-6.
sig, pubKeyRecoveryCode, success := sign(secKeyScalar, k, hash)
k.Zero()
@@ -689,7 +689,7 @@ func signRFC6979(secKey *secp256k2.SecretKey, hash []byte) (
// secret key. The produced signature is deterministic (same message and same
// key yield the same signature) and canonical in accordance with RFC6979 and
// BIP0062.
func Sign(key *secp256k2.SecretKey, hash []byte) *Signature {
func Sign(key *secp256k1.SecretKey, hash []byte) *Signature {
signature, _ := signRFC6979(key, hash)
return signature
}
@@ -730,7 +730,7 @@ const (
// The compact sig recovery code is the value 27 + public key recovery code + 4
// if the compact signature was created with a compressed public key.
func SignCompact(
key *secp256k2.SecretKey, hash []byte,
key *secp256k1.SecretKey, hash []byte,
isCompressedKey bool,
) []byte {
// Create the signature and associated pubkey recovery code and calculate
@@ -753,7 +753,7 @@ func SignCompact(
// the signature matches then the recovered public key will be returned as well
// as a boolean indicating whether or not the original key was compressed.
func RecoverCompact(signature, hash []byte) (
*secp256k2.PublicKey, bool, error,
*secp256k1.PublicKey, bool, error,
) {
// The following is very loosely based on the information and algorithm that
// describes recovering a public key from and ECDSA signature in section
@@ -850,7 +850,7 @@ func RecoverCompact(signature, hash []byte) (
// Parse and validate the R and S signature components.
//
// Fail if r and s are not in [1, N-1].
var r, s secp256k2.ModNScalar
var r, s secp256k1.ModNScalar
if overflow := r.SetByteSlice(signature[1:33]); overflow {
str := "invalid signature: R >= group order"
return nil, false, signatureError(ErrSigRTooBig, str)
@@ -902,40 +902,40 @@ func RecoverCompact(signature, hash []byte) (
// coord originally came from a random point on the curve which means there
// must be a Y coord that satisfies the equation for a valid signature.
oddY := pubKeyRecoveryCode&pubKeyRecoveryCodeOddnessBit != 0
var y secp256k2.FieldVal
if valid := secp256k2.DecompressY(&fieldR, oddY, &y); !valid {
var y secp256k1.FieldVal
if valid := secp256k1.DecompressY(&fieldR, oddY, &y); !valid {
str := "invalid signature: not for a valid curve point"
return nil, false, signatureError(ErrPointNotOnCurve, str)
}
// Step 5.
//
// X = (r, y)
var X secp256k2.JacobianPoint
var X secp256k1.JacobianPoint
X.X.Set(fieldR.Normalize())
X.Y.Set(y.Normalize())
X.Z.SetInt(1)
// Step 6.
//
// e = H(m) mod N
var e secp256k2.ModNScalar
var e secp256k1.ModNScalar
e.SetByteSlice(hash)
// Step 7.
//
// w = r^-1 mod N
w := new(secp256k2.ModNScalar).InverseValNonConst(&r)
w := new(secp256k1.ModNScalar).InverseValNonConst(&r)
// Step 8.
//
// u1 = -(e * w) mod N
// u2 = s * w mod N
u1 := new(secp256k2.ModNScalar).Mul2(&e, w).Negate()
u2 := new(secp256k2.ModNScalar).Mul2(&s, w)
u1 := new(secp256k1.ModNScalar).Mul2(&e, w).Negate()
u2 := new(secp256k1.ModNScalar).Mul2(&s, w)
// Step 9.
//
// Q = u1G + u2X
var Q, u1G, u2X secp256k2.JacobianPoint
secp256k2.ScalarBaseMultNonConst(u1, &u1G)
secp256k2.ScalarMultNonConst(u2, &X, &u2X)
secp256k2.AddNonConst(&u1G, &u2X, &Q)
var Q, u1G, u2X secp256k1.JacobianPoint
secp256k1.ScalarBaseMultNonConst(u1, &u1G)
secp256k1.ScalarMultNonConst(u2, &X, &u2X)
secp256k1.AddNonConst(&u1G, &u2X, &Q)
// Step 10.
//
// Fail if Q is the point at infinity.
@@ -948,6 +948,6 @@ func RecoverCompact(signature, hash []byte) (
}
// Notice that the public key is in affine coordinates.
Q.ToAffine()
pubKey := secp256k2.NewPublicKey(&Q.X, &Q.Y)
pubKey := secp256k1.NewPublicKey(&Q.X, &Q.Y)
return pubKey, wasCompressed, nil
}

View File

@@ -12,12 +12,11 @@ import (
"bytes"
"errors"
"math/rand"
secp256k2 "orly.dev/crypto/ec/secp256k1"
"orly.dev/utils/chk"
"orly.dev/pkg/crypto/ec/secp256k1"
"orly.dev/pkg/encoders/hex"
"orly.dev/pkg/utils/chk"
"testing"
"time"
"orly.dev/encoders/hex"
)
// hexToBytes converts the passed hex string into bytes and will panic if there
@@ -321,8 +320,8 @@ func TestSignatureSerialize(t *testing.T) {
}, {
"zero signature",
&Signature{
r: *new(secp256k2.ModNScalar).SetInt(0),
s: *new(secp256k2.ModNScalar).SetInt(0),
r: *new(secp256k1.ModNScalar).SetInt(0),
s: *new(secp256k1.ModNScalar).SetInt(0),
},
hexToBytes("3006020100020100"),
},
@@ -657,9 +656,9 @@ func TestSignAndVerifyRandom(t *testing.T) {
if _, err := rng.Read(buf[:]); chk.T(err) {
t.Fatalf("failed to read random secret key: %v", err)
}
var secKeyScalar secp256k2.ModNScalar
var secKeyScalar secp256k1.ModNScalar
secKeyScalar.SetBytes(&buf)
secKey := secp256k2.NewSecretKey(&secKeyScalar)
secKey := secp256k1.NewSecretKey(&secKeyScalar)
// Generate a random hash to sign.
var hash [32]byte
if _, err := rng.Read(hash[:]); chk.T(err) {
@@ -798,7 +797,7 @@ func TestVerifyFailures(t *testing.T) {
s := hexToModNScalar(test.s)
sig := NewSignature(r, s)
// Ensure the verification is NOT successful.
pubKey := secp256k2.NewSecretKey(secKey).PubKey()
pubKey := secp256k1.NewSecretKey(secKey).PubKey()
if sig.Verify(hash, pubKey) {
t.Errorf(
"%s: unexpected success for invalid signature: %x",
@@ -1074,9 +1073,9 @@ func TestSignAndRecoverCompactRandom(t *testing.T) {
if _, err := rng.Read(buf[:]); chk.T(err) {
t.Fatalf("failed to read random secret key: %v", err)
}
var secKeyScalar secp256k2.ModNScalar
var secKeyScalar secp256k1.ModNScalar
secKeyScalar.SetBytes(&buf)
secKey := secp256k2.NewSecretKey(&secKeyScalar)
secKey := secp256k1.NewSecretKey(&secKeyScalar)
wantPubKey := secKey.PubKey()
// Generate a random hash to sign.
var hash [32]byte

View File

@@ -1,7 +1,7 @@
package ecdsa_test
import (
"orly.dev/utils/lol"
"orly.dev/pkg/utils/lol"
)
var (

View File

@@ -4,7 +4,7 @@
package btcec
import (
"orly.dev/crypto/ec/secp256k1"
"orly.dev/pkg/crypto/ec/secp256k1"
)
// Error identifies an error related to public key cryptography using a

View File

@@ -1,7 +1,7 @@
package btcec
import (
"orly.dev/crypto/ec/secp256k1"
"orly.dev/pkg/crypto/ec/secp256k1"
)
// FieldVal implements optimized fixed-precision arithmetic over the secp256k1

View File

@@ -7,10 +7,9 @@ package btcec
import (
"math/rand"
"orly.dev/utils/chk"
"orly.dev/pkg/encoders/hex"
"orly.dev/pkg/utils/chk"
"testing"
"orly.dev/encoders/hex"
)
// TestIsZero ensures that checking if a field IsZero works as expected.

View File

@@ -9,9 +9,8 @@
package btcec
import (
"orly.dev/pkg/encoders/hex"
"testing"
"orly.dev/encoders/hex"
)
func FuzzParsePubKey(f *testing.F) {

View File

@@ -4,7 +4,7 @@
package btcec
import (
secp256k2 "orly.dev/crypto/ec/secp256k1"
"orly.dev/pkg/crypto/ec/secp256k1"
)
// ModNScalar implements optimized 256-bit constant-time fixed-precision
@@ -24,7 +24,7 @@ import (
// that should typically be avoided when possible as conversion to big.Ints
// requires allocations, is not constant time, and is slower when working modulo
// the group order.
type ModNScalar = secp256k2.ModNScalar
type ModNScalar = secp256k1.ModNScalar
// NonceRFC6979 generates a nonce deterministically according to RFC 6979 using
// HMAC-SHA256 for the hashing function. It takes a 32-byte hash as an input
@@ -43,7 +43,7 @@ func NonceRFC6979(
extraIterations uint32,
) *ModNScalar {
return secp256k2.NonceRFC6979(
return secp256k1.NonceRFC6979(
privKey, hash, extra, version,
extraIterations,
)

View File

@@ -6,11 +6,10 @@ package musig2
import (
"fmt"
"orly.dev/crypto/ec"
"orly.dev/crypto/ec/schnorr"
"orly.dev/pkg/crypto/ec"
"orly.dev/pkg/crypto/ec/schnorr"
"orly.dev/pkg/encoders/hex"
"testing"
"orly.dev/encoders/hex"
)
var (
@@ -247,7 +246,7 @@ func BenchmarkAggregateNonces(b *testing.B) {
}
}
var testKey *btcec.PublicKey
var testKey *btcec.btcec
// BenchmarkAggregateKeys benchmarks how long it takes to aggregate public
// keys.

Some files were not shown because too many files have changed in this diff Show More