Compare commits

..

4 Commits

Author SHA1 Message Date
c7eb532443 Update Go build configurations and bump version to v0.21.3
Some checks failed
Go / build (push) Has been cancelled
Go / release (push) Has been cancelled
- Commented out unused build commands for different platforms in the GitHub Actions workflow to streamline the build process.
- Updated version from v0.21.2 to v0.21.3 to reflect recent changes.
2025-11-01 13:17:09 +00:00
e56b3f0083 Refactor event handling and policy script error management
Some checks failed
Go / build (push) Has been cancelled
Go / release (push) Has been cancelled
- Removed redundant log statement in HandleEvent for cleaner output.
- Enhanced policy script handling to check for script existence before execution, improving error handling and fallback logic.
- Updated error messages to provide clearer feedback when policy scripts are missing or fail to start.
- Bumped version to v0.21.2 to reflect these changes.
2025-11-01 12:55:42 +00:00
daniyal
9064b3ab5f Fix deployment script issues (#1)
- Fix Go installation by extracting to /tmp first then moving to final destination
- Return to original directory after Go installation
- Add attempt to install secp256k1 from package manager before building from source
- Add missing automake package for autoreconf
- Fix binary build by running go build after embedded web update

Co-authored-by: mleku <me@mleku.dev>
Reviewed-on: https://git.nostrdev.com/mleku/next.orly.dev/pulls/1
Co-authored-by: daniyal <daniyal@nostrdev.com>
Co-committed-by: daniyal <daniyal@nostrdev.com>
2025-10-30 20:05:22 +00:00
3486d3d4ab added simple websocket test
Some checks failed
Go / build (push) Has been cancelled
Go / release (push) Has been cancelled
- bump to v0.21.1
2025-10-30 19:32:45 +00:00
11 changed files with 411 additions and 40 deletions

View File

@@ -76,10 +76,10 @@ jobs:
# Build for different platforms
GOEXPERIMENT=greenteagc,jsonv2 GOOS=linux GOARCH=amd64 CGO_ENABLED=1 go build -o release-binaries/orly-${VERSION}-linux-amd64 .
GOEXPERIMENT=greenteagc,jsonv2 GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -o release-binaries/orly-${VERSION}-linux-arm64 .
GOEXPERIMENT=greenteagc,jsonv2 GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -o release-binaries/orly-${VERSION}-darwin-amd64 .
GOEXPERIMENT=greenteagc,jsonv2 GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 go build -o release-binaries/orly-${VERSION}-darwin-arm64 .
GOEXPERIMENT=greenteagc,jsonv2 GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -o release-binaries/orly-${VERSION}-windows-amd64.exe .
# GOEXPERIMENT=greenteagc,jsonv2 GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -o release-binaries/orly-${VERSION}-linux-arm64 .
# GOEXPERIMENT=greenteagc,jsonv2 GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -o release-binaries/orly-${VERSION}-darwin-amd64 .
# GOEXPERIMENT=greenteagc,jsonv2 GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 go build -o release-binaries/orly-${VERSION}-darwin-arm64 .
# GOEXPERIMENT=greenteagc,jsonv2 GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -o release-binaries/orly-${VERSION}-windows-amd64.exe .
# Note: Only building orly binary as requested
# Other cmd utilities (aggregator, benchmark, convert, policytest, stresstest) are development tools

View File

@@ -37,7 +37,6 @@ func (l *Listener) HandleEvent(msg []byte) (err error) {
}
}()
log.I.F("HandleEvent: continuing with event processing...")
if len(msg) > 0 {
log.I.F("extra '%s'", msg)
}

View File

@@ -0,0 +1,71 @@
# relay-tester
A command-line tool for testing Nostr relay implementations against the NIP-01 specification and related NIPs.
## Usage
```bash
relay-tester -url <relay-url> [options]
```
## Options
- `-url` (required): Relay websocket URL (e.g., `ws://127.0.0.1:3334` or `wss://relay.example.com`)
- `-test <name>`: Run a specific test by name (default: run all tests)
- `-json`: Output results in JSON format
- `-v`: Verbose output (shows additional info for each test)
- `-list`: List all available tests and exit
## Examples
### Run all tests against a local relay:
```bash
relay-tester -url ws://127.0.0.1:3334
```
### Run all tests with verbose output:
```bash
relay-tester -url ws://127.0.0.1:3334 -v
```
### Run a specific test:
```bash
relay-tester -url ws://127.0.0.1:3334 -test "Publishes basic event"
```
### Output results as JSON:
```bash
relay-tester -url ws://127.0.0.1:3334 -json
```
### List all available tests:
```bash
relay-tester -list
```
## Exit Codes
- `0`: All required tests passed
- `1`: One or more required tests failed, or an error occurred
## Test Categories
The relay-tester runs tests covering:
- **Basic Event Operations**: Publishing, finding by ID/author/kind/tags
- **Filtering**: Time ranges, limits, multiple filters, scrape queries
- **Replaceable Events**: Metadata and contact list replacement
- **Parameterized Replaceable Events**: Addressable events with `d` tags
- **Event Deletion**: Deletion events (NIP-09)
- **Ephemeral Events**: Event handling for ephemeral kinds
- **EOSE Handling**: End of stored events signaling
- **Event Validation**: Signature verification, ID hash verification
- **JSON Compliance**: NIP-01 JSON escape sequences
## Notes
- Tests are run in dependency order (some tests depend on others)
- Required tests must pass for the relay to be considered compliant
- Optional tests may fail without affecting overall compliance
- The tool connects to the relay using WebSocket and runs tests sequentially

160
cmd/relay-tester/main.go Normal file
View File

@@ -0,0 +1,160 @@
package main
import (
"flag"
"fmt"
"os"
"strings"
"lol.mleku.dev/log"
relaytester "next.orly.dev/relay-tester"
)
func main() {
var (
relayURL = flag.String("url", "", "relay websocket URL (required, e.g., ws://127.0.0.1:3334)")
testName = flag.String("test", "", "run specific test by name (default: run all tests)")
jsonOut = flag.Bool("json", false, "output results in JSON format")
verbose = flag.Bool("v", false, "verbose output")
listTests = flag.Bool("list", false, "list all available tests and exit")
)
flag.Parse()
if *listTests {
listAllTests()
return
}
if *relayURL == "" {
log.E.F("required flag: -url (relay websocket URL)")
flag.Usage()
os.Exit(1)
}
// Validate URL format
if !strings.HasPrefix(*relayURL, "ws://") && !strings.HasPrefix(*relayURL, "wss://") {
log.E.F("URL must start with ws:// or wss://")
os.Exit(1)
}
// Create test suite
if *verbose {
log.I.F("Creating test suite for %s...", *relayURL)
}
suite, err := relaytester.NewTestSuite(*relayURL)
if err != nil {
log.E.F("failed to create test suite: %v", err)
os.Exit(1)
}
// Run tests
var results []relaytester.TestResult
if *testName != "" {
if *verbose {
log.I.F("Running test: %s", *testName)
}
result, err := suite.RunTest(*testName)
if err != nil {
log.E.F("failed to run test %s: %v", *testName, err)
os.Exit(1)
}
results = []relaytester.TestResult{result}
} else {
if *verbose {
log.I.F("Running all tests...")
}
if results, err = suite.Run(); err != nil {
log.E.F("failed to run tests: %v", err)
os.Exit(1)
}
}
// Output results
if *jsonOut {
jsonOutput, err := relaytester.FormatJSON(results)
if err != nil {
log.E.F("failed to format JSON: %v", err)
os.Exit(1)
}
fmt.Println(jsonOutput)
} else {
outputResults(results, *verbose)
}
// Check exit code
hasRequiredFailures := false
for _, result := range results {
if result.Required && !result.Pass {
hasRequiredFailures = true
break
}
}
if hasRequiredFailures {
os.Exit(1)
}
}
func outputResults(results []relaytester.TestResult, verbose bool) {
passed := 0
failed := 0
requiredFailed := 0
for _, result := range results {
if result.Pass {
passed++
if verbose {
fmt.Printf("PASS: %s", result.Name)
if result.Info != "" {
fmt.Printf(" - %s", result.Info)
}
fmt.Println()
} else {
fmt.Printf("PASS: %s\n", result.Name)
}
} else {
failed++
if result.Required {
requiredFailed++
fmt.Printf("FAIL (required): %s", result.Name)
} else {
fmt.Printf("FAIL (optional): %s", result.Name)
}
if result.Info != "" {
fmt.Printf(" - %s", result.Info)
}
fmt.Println()
}
}
fmt.Println()
fmt.Println("Test Summary:")
fmt.Printf(" Total: %d\n", len(results))
fmt.Printf(" Passed: %d\n", passed)
fmt.Printf(" Failed: %d\n", failed)
fmt.Printf(" Required Failed: %d\n", requiredFailed)
}
func listAllTests() {
// Create a dummy test suite to get the list of tests
suite, err := relaytester.NewTestSuite("ws://127.0.0.1:0")
if err != nil {
log.E.F("failed to create test suite: %v", err)
os.Exit(1)
}
fmt.Println("Available tests:")
fmt.Println()
testNames := suite.ListTests()
testInfo := suite.GetTestNames()
for _, name := range testNames {
required := ""
if testInfo[name] {
required = " (required)"
}
fmt.Printf(" - %s%s\n", name, required)
}
}

View File

@@ -10,7 +10,6 @@ import (
"os"
"os/exec"
"path/filepath"
"runtime"
"sync"
"time"
@@ -285,16 +284,18 @@ func (p *P) CheckPolicy(access string, ev *event.E, loggedInPubkey []byte, ipAdd
// Check if script is present and enabled
if rule.Script != "" && p.Manager != nil {
if p.Manager.IsEnabled() {
return p.checkScriptPolicy(access, ev, rule.Script, loggedInPubkey, ipAddress)
}
// Script is configured but policy is disabled - use default policy if rule has no other restrictions
hasOtherRestrictions := len(rule.WriteAllow) > 0 || len(rule.WriteDeny) > 0 || len(rule.ReadAllow) > 0 || len(rule.ReadDeny) > 0 ||
rule.SizeLimit != nil || rule.ContentLimit != nil || len(rule.MustHaveTags) > 0 ||
rule.MaxExpiry != nil || rule.Privileged || rule.RateLimit != nil ||
rule.MaxAgeOfEvent != nil || rule.MaxAgeEventInFuture != nil
if !hasOtherRestrictions {
// No other restrictions, use default policy
return p.getDefaultPolicyAction(), nil
// Check if script file exists before trying to use it
if _, err := os.Stat(p.Manager.GetScriptPath()); err == nil {
// Script exists, try to use it
allowed, err := p.checkScriptPolicy(access, ev, rule.Script, loggedInPubkey, ipAddress)
if err == nil {
// Script ran successfully, return its decision
return allowed, nil
}
// Script failed, fall through to apply other criteria
log.W.F("policy script check failed for kind %d: %v, applying other criteria", ev.Kind, err)
}
// Script doesn't exist or failed, fall through to apply other criteria
}
}
@@ -481,24 +482,14 @@ func (p *P) checkScriptPolicy(access string, ev *event.E, scriptPath string, log
if !p.Manager.IsRunning() {
// Check if script file exists
if _, err := os.Stat(p.Manager.GetScriptPath()); os.IsNotExist(err) {
// Script doesn't exist, this is a fatal error
buf := make([]byte, 1024*1024)
n := runtime.Stack(buf, true)
log.E.F("policy script does not exist at %s", p.Manager.GetScriptPath())
fmt.Fprintf(os.Stderr, "FATAL: Policy script required but not found at %s\n", p.Manager.GetScriptPath())
fmt.Fprintf(os.Stderr, "Stack trace:\n%s\n", buf[:n])
os.Exit(1)
// Script doesn't exist, return error so caller can fall back to other criteria
return false, fmt.Errorf("policy script does not exist at %s", p.Manager.GetScriptPath())
}
// Try to start the policy and wait for it
if err := p.Manager.ensureRunning(); err != nil {
// Startup failed, this is a fatal error
buf := make([]byte, 1024*1024)
n := runtime.Stack(buf, true)
log.E.F("failed to start policy script: %v", err)
fmt.Fprintf(os.Stderr, "FATAL: Failed to start policy script: %v\n", err)
fmt.Fprintf(os.Stderr, "Stack trace:\n%s\n", buf[:n])
os.Exit(1)
// Startup failed, return error so caller can fall back to other criteria
return false, fmt.Errorf("failed to start policy script: %v", err)
}
}

View File

@@ -1 +1 @@
v0.21.0
v0.21.3

View File

@@ -56,6 +56,11 @@ func (c *Client) Close() error {
return c.conn.Close()
}
// URL returns the relay URL.
func (c *Client) URL() string {
return c.url
}
// Send sends a JSON message to the relay.
func (c *Client) Send(msg interface{}) (err error) {
c.mu.Lock()

View File

@@ -291,6 +291,11 @@ func (s *TestSuite) registerTests() {
Required: true,
Func: testSupportsEose,
},
{
Name: "Subscription receives event after ping period",
Required: true,
Func: testSubscriptionReceivesEventAfterPingPeriod,
},
{
Name: "Closes complete subscriptions after EOSE",
Required: false,
@@ -420,6 +425,20 @@ func (s *TestSuite) GetResults() map[string]TestResult {
return s.results
}
// ListTests returns a list of all test names in execution order.
func (s *TestSuite) ListTests() []string {
return s.order
}
// GetTestNames returns all registered test names as a map (name -> required).
func (s *TestSuite) GetTestNames() map[string]bool {
result := make(map[string]bool)
for name, tc := range s.tests {
result[name] = tc.Required
}
return result
}
// FormatJSON formats results as JSON.
func FormatJSON(results []TestResult) (output string, err error) {
var data []byte

View File

@@ -1632,6 +1632,96 @@ func testSupportsEose(client *Client, key1, key2 *KeyPair) (result TestResult) {
}
}
func testSubscriptionReceivesEventAfterPingPeriod(client *Client, key1, key2 *KeyPair) (result TestResult) {
// Create a second client for publishing
publisherClient, err := NewClient(client.URL())
if err != nil {
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create publisher client: %v", err)}
}
defer publisherClient.Close()
// Subscribe to events from key1
filter := map[string]interface{}{
"authors": []string{hex.Enc(key1.Pubkey)},
"kinds": []int{int(kind.TextNote.K)},
}
ch, err := client.Subscribe("test-ping-period", []interface{}{filter})
if err != nil {
return TestResult{Pass: false, Info: fmt.Sprintf("failed to subscribe: %v", err)}
}
defer client.Unsubscribe("test-ping-period")
// Wait for EOSE to ensure subscription is established
eoseTimeout := time.After(3 * time.Second)
gotEose := false
for !gotEose {
select {
case msg, ok := <-ch:
if !ok {
return TestResult{Pass: false, Info: "channel closed before EOSE"}
}
var raw []interface{}
if err = json.Unmarshal(msg, &raw); err != nil {
continue
}
if len(raw) >= 2 {
if typ, ok := raw[0].(string); ok && typ == "EOSE" {
gotEose = true
break
}
}
case <-eoseTimeout:
return TestResult{Pass: false, Info: "timeout waiting for EOSE"}
}
}
// Wait for at least one ping period (30 seconds) to ensure connection is idle
// and has been pinged at least once
pingPeriod := 35 * time.Second // Slightly longer than 30s to ensure at least one ping
time.Sleep(pingPeriod)
// Now publish an event from the publisher client that matches the subscription
ev, err := CreateEvent(key1.Secret, kind.TextNote.K, "event after ping period", nil)
if err != nil {
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)}
}
if err = publisherClient.Publish(ev); err != nil {
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)}
}
accepted, _, err := publisherClient.WaitForOK(ev.ID, 5*time.Second)
if err != nil || !accepted {
return TestResult{Pass: false, Info: "event not accepted"}
}
// Wait for event to come through subscription (should work even after ping period)
eventTimeout := time.After(5 * time.Second)
for {
select {
case msg, ok := <-ch:
if !ok {
return TestResult{Pass: false, Info: "subscription closed"}
}
var raw []interface{}
if err = json.Unmarshal(msg, &raw); err != nil {
continue
}
if len(raw) >= 3 && raw[0] == "EVENT" {
if evData, ok := raw[2].(map[string]interface{}); ok {
evJSON, _ := json.Marshal(evData)
receivedEv := event.New()
if _, err = receivedEv.Unmarshal(evJSON); err == nil {
if string(receivedEv.ID) == string(ev.ID) {
return TestResult{Pass: true}
}
}
}
}
case <-eventTimeout:
return TestResult{Pass: false, Info: "timeout waiting for event after ping period"}
}
}
}
func testClosesCompleteSubscriptionsAfterEose(client *Client, key1, key2 *KeyPair) (result TestResult) {
// Create a filter that fetches a specific event by ID (complete subscription)
fakeID := "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"

View File

@@ -71,6 +71,9 @@ check_go_installation() {
install_go() {
log_info "Installing Go $GO_VERSION..."
# Save original directory
local original_dir=$(pwd)
# Determine architecture
local arch=$(uname -m)
case $arch in
@@ -100,13 +103,17 @@ install_go() {
rm -rf "$GOROOT"
fi
# Extract Go
log_info "Extracting Go to $GOROOT..."
tar -xf "$go_archive"
# Extract Go to a temporary location first, then move to final destination
log_info "Extracting Go..."
tar -xf "$go_archive" -C /tmp
mv /tmp/go "$GOROOT"
# Clean up
rm -f "$go_archive"
# Return to original directory
cd "$original_dir"
log_success "Go $GO_VERSION installed successfully"
}
@@ -167,7 +174,10 @@ build_application() {
log_info "Updating embedded web assets..."
./scripts/update-embedded-web.sh
# The update-embedded-web.sh script should have built the binary
# Build the binary in the current directory
log_info "Building binary in current directory..."
CGO_ENABLED=1 go build -o "$BINARY_NAME"
if [[ -f "./$BINARY_NAME" ]]; then
log_success "ORLY relay built successfully"
else

View File

@@ -1,14 +1,40 @@
#!/usr/bin/env bash
set -e
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
apt -y install build-essential autoconf libtool git wget
cd $SCRIPT_DIR
# Update package lists
apt-get update
# Try to install from package manager first (much faster)
echo "Attempting to install secp256k1 from package manager..."
if apt-get install -y libsecp256k1-dev >/dev/null 2>&1; then
echo "✓ Installed secp256k1 from package manager"
exit 0
fi
# Fall back to building from source if package not available
echo "Package not available in repository, building from source..."
# Install build dependencies
apt-get install -y build-essential autoconf automake libtool git wget pkg-config
cd "$SCRIPT_DIR"
rm -rf secp256k1
# Clone and setup secp256k1
git clone https://github.com/bitcoin-core/secp256k1.git
cd secp256k1
git checkout v0.6.0
# Initialize and update submodules
git submodule init
git submodule update
# Build and install
./autogen.sh
./configure --enable-module-schnorrsig --enable-module-ecdh --prefix=/usr
make -j1
sudo make install
make -j$(nproc)
make install
cd "$SCRIPT_DIR"