blossom works fully correctly
This commit is contained in:
114
cmd/blossomtest/README.md
Normal file
114
cmd/blossomtest/README.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# Blossom Test Tool
|
||||
|
||||
A simple command-line tool to test the Blossom blob storage service by performing upload, fetch, and delete operations.
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
# From the repository root
|
||||
CGO_ENABLED=0 go build -o cmd/blossomtest/blossomtest ./cmd/blossomtest
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Basic usage with auto-generated key
|
||||
./cmd/blossomtest/blossomtest
|
||||
|
||||
# Specify relay URL
|
||||
./cmd/blossomtest/blossomtest -url http://localhost:3334
|
||||
|
||||
# Use a specific Nostr key (nsec format)
|
||||
./cmd/blossomtest/blossomtest -nsec nsec1...
|
||||
|
||||
# Test with larger blob
|
||||
./cmd/blossomtest/blossomtest -size 10240
|
||||
|
||||
# Verbose output to see HTTP requests and auth events
|
||||
./cmd/blossomtest/blossomtest -v
|
||||
|
||||
# Test anonymous uploads (for open relays)
|
||||
./cmd/blossomtest/blossomtest -no-auth
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
- `-url` - Relay base URL (default: `http://localhost:3334`)
|
||||
- `-nsec` - Nostr private key in nsec format (generates new key if not provided)
|
||||
- `-size` - Size of test blob in bytes (default: 1024)
|
||||
- `-v` - Verbose output showing HTTP requests and authentication events
|
||||
- `-no-auth` - Skip authentication and test anonymous uploads (useful for open relays)
|
||||
|
||||
## What It Tests
|
||||
|
||||
The tool performs the following operations in sequence:
|
||||
|
||||
1. **Upload** - Uploads random test data to the Blossom server
|
||||
- Creates a Blossom authorization event (kind 24242)
|
||||
- Sends a PUT request to `/blossom/upload`
|
||||
- Verifies the returned descriptor
|
||||
|
||||
2. **Fetch** - Retrieves the uploaded blob
|
||||
- Sends a GET request to `/blossom/<sha256>`
|
||||
- Verifies the data matches what was uploaded
|
||||
|
||||
3. **Delete** - Removes the blob from the server
|
||||
- Creates another authorization event for deletion
|
||||
- Sends a DELETE request to `/blossom/<sha256>`
|
||||
|
||||
4. **Verify** - Confirms deletion was successful
|
||||
- Attempts to fetch the blob again
|
||||
- Expects a 404 Not Found response
|
||||
|
||||
## Example Output
|
||||
|
||||
```
|
||||
🌸 Blossom Test Tool
|
||||
===================
|
||||
|
||||
ℹ️ No key provided, generated new keypair
|
||||
Using identity: npub1...
|
||||
Relay URL: http://localhost:3334
|
||||
|
||||
📦 Generated 1024 bytes of random data
|
||||
SHA256: a1b2c3d4...
|
||||
|
||||
📤 Step 1: Uploading blob...
|
||||
✅ Upload successful!
|
||||
URL: http://localhost:3334/blossom/a1b2c3d4...
|
||||
SHA256: a1b2c3d4...
|
||||
Size: 1024 bytes
|
||||
|
||||
📥 Step 2: Fetching blob...
|
||||
✅ Fetch successful! Retrieved 1024 bytes
|
||||
✅ Data verification passed - hashes match!
|
||||
|
||||
🗑️ Step 3: Deleting blob...
|
||||
✅ Delete successful!
|
||||
|
||||
🔍 Step 4: Verifying deletion...
|
||||
✅ Blob successfully deleted - returns 404 as expected
|
||||
|
||||
🎉 All tests passed! Blossom service is working correctly.
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- A running ORLY relay with Blossom enabled
|
||||
- The relay must be using the Badger backend (Blossom is not available with DGraph)
|
||||
- Network connectivity to the relay
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**"connection refused"**
|
||||
- Make sure your relay is running
|
||||
- Check the URL is correct (default: `http://localhost:3334`)
|
||||
|
||||
**"unauthorized" or "403 Forbidden"**
|
||||
- Check your relay's ACL settings
|
||||
- If using `ORLY_AUTH_TO_WRITE=true`, make sure authentication is working
|
||||
- Try adding your test key to `ORLY_ADMINS` if using follows mode
|
||||
|
||||
**"blossom server not initialized"**
|
||||
- Blossom only works with the Badger backend
|
||||
- Check `ORLY_DB_TYPE` is set to `badger` or not set (defaults to badger)
|
||||
384
cmd/blossomtest/main.go
Normal file
384
cmd/blossomtest/main.go
Normal file
@@ -0,0 +1,384 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.mleku.dev/mleku/nostr/crypto/ec/schnorr"
|
||||
"git.mleku.dev/mleku/nostr/crypto/ec/secp256k1"
|
||||
"git.mleku.dev/mleku/nostr/encoders/bech32encoding"
|
||||
"git.mleku.dev/mleku/nostr/interfaces/signer/p8k"
|
||||
"github.com/minio/sha256-simd"
|
||||
)
|
||||
|
||||
const (
|
||||
// BlossomAuthKind is the Nostr event kind for Blossom authorization (BUD-01)
|
||||
BlossomAuthKind = 24242
|
||||
)
|
||||
|
||||
var (
|
||||
relayURL = flag.String("url", "http://localhost:3334", "Relay base URL")
|
||||
nsec = flag.String("nsec", "", "Nostr private key (nsec format). If empty, generates a new key")
|
||||
blobSize = flag.Int("size", 1024, "Size of test blob in bytes")
|
||||
verbose = flag.Bool("v", false, "Verbose output")
|
||||
noAuth = flag.Bool("no-auth", false, "Skip authentication (test anonymous uploads)")
|
||||
)
|
||||
|
||||
// BlossomDescriptor represents a blob descriptor returned by the server
|
||||
type BlossomDescriptor struct {
|
||||
URL string `json:"url"`
|
||||
SHA256 string `json:"sha256"`
|
||||
Size int64 `json:"size"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Uploaded int64 `json:"uploaded"`
|
||||
PublicKey string `json:"public_key,omitempty"`
|
||||
Tags [][]string `json:"tags,omitempty"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
fmt.Println("🌸 Blossom Test Tool")
|
||||
fmt.Println("===================\n")
|
||||
|
||||
// Get or generate keypair (only if auth is enabled)
|
||||
var sec, pub []byte
|
||||
var err error
|
||||
|
||||
if !*noAuth {
|
||||
sec, pub, err = getKeypair()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error getting keypair: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
pubkey, _ := schnorr.ParsePubKey(pub)
|
||||
npubBytes, _ := bech32encoding.PublicKeyToNpub(pubkey)
|
||||
fmt.Printf("Using identity: %s\n", string(npubBytes))
|
||||
} else {
|
||||
fmt.Printf("Testing anonymous uploads (no authentication)\n")
|
||||
}
|
||||
fmt.Printf("Relay URL: %s\n\n", *relayURL)
|
||||
|
||||
// Generate random test data
|
||||
testData := make([]byte, *blobSize)
|
||||
if _, err := rand.Read(testData); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error generating test data: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Calculate SHA256
|
||||
hash := sha256.Sum256(testData)
|
||||
hashHex := hex.EncodeToString(hash[:])
|
||||
|
||||
fmt.Printf("📦 Generated %d bytes of random data\n", *blobSize)
|
||||
fmt.Printf(" SHA256: %s\n\n", hashHex)
|
||||
|
||||
// Step 1: Upload blob
|
||||
fmt.Println("📤 Step 1: Uploading blob...")
|
||||
descriptor, err := uploadBlob(sec, pub, testData)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ Upload failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("✅ Upload successful!\n")
|
||||
fmt.Printf(" URL: %s\n", descriptor.URL)
|
||||
fmt.Printf(" SHA256: %s\n", descriptor.SHA256)
|
||||
fmt.Printf(" Size: %d bytes\n\n", descriptor.Size)
|
||||
|
||||
// Step 2: Fetch blob
|
||||
fmt.Println("📥 Step 2: Fetching blob...")
|
||||
fetchedData, err := fetchBlob(hashHex)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ Fetch failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("✅ Fetch successful! Retrieved %d bytes\n", len(fetchedData))
|
||||
|
||||
// Verify data matches
|
||||
if !bytes.Equal(testData, fetchedData) {
|
||||
fmt.Fprintf(os.Stderr, "❌ Data mismatch! Retrieved data doesn't match uploaded data\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("✅ Data verification passed - hashes match!\n\n")
|
||||
|
||||
// Step 3: Delete blob
|
||||
fmt.Println("🗑️ Step 3: Deleting blob...")
|
||||
if err := deleteBlob(sec, pub, hashHex); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ Delete failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("✅ Delete successful!\n\n")
|
||||
|
||||
// Step 4: Verify deletion
|
||||
fmt.Println("🔍 Step 4: Verifying deletion...")
|
||||
if err := verifyDeleted(hashHex); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ Verification failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("✅ Blob successfully deleted - returns 404 as expected\n\n")
|
||||
|
||||
fmt.Println("🎉 All tests passed! Blossom service is working correctly.")
|
||||
}
|
||||
|
||||
func getKeypair() (sec, pub []byte, err error) {
|
||||
if *nsec != "" {
|
||||
// Decode provided nsec
|
||||
var secKey *secp256k1.SecretKey
|
||||
secKey, err = bech32encoding.NsecToSecretKey(*nsec)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("invalid nsec: %w", err)
|
||||
}
|
||||
sec = secKey.Serialize()
|
||||
} else {
|
||||
// Generate new keypair
|
||||
sec = make([]byte, 32)
|
||||
if _, err := rand.Read(sec); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to generate key: %w", err)
|
||||
}
|
||||
fmt.Println("ℹ️ No key provided, generated new keypair")
|
||||
}
|
||||
|
||||
// Derive public key using p8k signer
|
||||
var signer *p8k.Signer
|
||||
if signer, err = p8k.New(); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to create signer: %w", err)
|
||||
}
|
||||
if err = signer.InitSec(sec); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to initialize signer: %w", err)
|
||||
}
|
||||
pub = signer.Pub()
|
||||
|
||||
return sec, pub, nil
|
||||
}
|
||||
|
||||
// createAuthEvent creates a Blossom authorization event (kind 24242)
|
||||
func createAuthEvent(sec, pub []byte, action, hash string) (string, error) {
|
||||
now := time.Now().Unix()
|
||||
|
||||
// Build tags based on action
|
||||
tags := [][]string{
|
||||
{"t", action},
|
||||
}
|
||||
|
||||
// Add x tag for DELETE and GET actions
|
||||
if hash != "" && (action == "delete" || action == "get") {
|
||||
tags = append(tags, []string{"x", hash})
|
||||
}
|
||||
|
||||
// All Blossom auth events require expiration tag (BUD-01)
|
||||
expiry := now + 300 // Event expires in 5 minutes
|
||||
tags = append(tags, []string{"expiration", fmt.Sprintf("%d", expiry)})
|
||||
|
||||
pubkeyHex := hex.EncodeToString(pub)
|
||||
|
||||
// Create event ID
|
||||
eventJSON, err := json.Marshal([]interface{}{
|
||||
0,
|
||||
pubkeyHex,
|
||||
now,
|
||||
BlossomAuthKind,
|
||||
tags,
|
||||
"",
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal event for ID: %w", err)
|
||||
}
|
||||
|
||||
eventHash := sha256.Sum256(eventJSON)
|
||||
eventID := hex.EncodeToString(eventHash[:])
|
||||
|
||||
// Sign the event using p8k signer
|
||||
signer, err := p8k.New()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create signer: %w", err)
|
||||
}
|
||||
if err = signer.InitSec(sec); err != nil {
|
||||
return "", fmt.Errorf("failed to initialize signer: %w", err)
|
||||
}
|
||||
|
||||
sig, err := signer.Sign(eventHash[:])
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to sign event: %w", err)
|
||||
}
|
||||
sigHex := hex.EncodeToString(sig)
|
||||
|
||||
// Create event JSON (signed)
|
||||
event := map[string]interface{}{
|
||||
"id": eventID,
|
||||
"pubkey": pubkeyHex,
|
||||
"created_at": now,
|
||||
"kind": BlossomAuthKind,
|
||||
"tags": tags,
|
||||
"content": "",
|
||||
"sig": sigHex,
|
||||
}
|
||||
|
||||
// Marshal to JSON for Authorization header
|
||||
authJSON, err := json.Marshal(event)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal auth event: %w", err)
|
||||
}
|
||||
|
||||
if *verbose {
|
||||
fmt.Printf(" Auth event: %s\n", string(authJSON))
|
||||
}
|
||||
|
||||
return string(authJSON), nil
|
||||
}
|
||||
|
||||
func uploadBlob(sec, pub, data []byte) (*BlossomDescriptor, error) {
|
||||
// Create request
|
||||
url := strings.TrimSuffix(*relayURL, "/") + "/blossom/upload"
|
||||
req, err := http.NewRequest(http.MethodPut, url, bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Set headers
|
||||
req.Header.Set("Content-Type", "application/octet-stream")
|
||||
|
||||
// Add authorization if not disabled
|
||||
if !*noAuth && sec != nil && pub != nil {
|
||||
authEvent, err := createAuthEvent(sec, pub, "upload", "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Base64-encode the auth event as per BUD-01
|
||||
authEventB64 := base64.StdEncoding.EncodeToString([]byte(authEvent))
|
||||
req.Header.Set("Authorization", "Nostr "+authEventB64)
|
||||
}
|
||||
|
||||
if *verbose {
|
||||
fmt.Printf(" PUT %s\n", url)
|
||||
fmt.Printf(" Content-Length: %d\n", len(data))
|
||||
}
|
||||
|
||||
// Send request
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Read response
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
|
||||
reason := resp.Header.Get("X-Reason")
|
||||
if reason == "" {
|
||||
reason = string(body)
|
||||
}
|
||||
return nil, fmt.Errorf("server returned %d: %s", resp.StatusCode, reason)
|
||||
}
|
||||
|
||||
// Parse descriptor
|
||||
var descriptor BlossomDescriptor
|
||||
if err := json.Unmarshal(body, &descriptor); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse response: %w (body: %s)", err, string(body))
|
||||
}
|
||||
|
||||
return &descriptor, nil
|
||||
}
|
||||
|
||||
func fetchBlob(hash string) ([]byte, error) {
|
||||
url := strings.TrimSuffix(*relayURL, "/") + "/blossom/" + hash
|
||||
|
||||
if *verbose {
|
||||
fmt.Printf(" GET %s\n", url)
|
||||
}
|
||||
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("server returned %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
func deleteBlob(sec, pub []byte, hash string) error {
|
||||
// Create request
|
||||
url := strings.TrimSuffix(*relayURL, "/") + "/blossom/" + hash
|
||||
req, err := http.NewRequest(http.MethodDelete, url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Add authorization if not disabled
|
||||
if !*noAuth && sec != nil && pub != nil {
|
||||
authEvent, err := createAuthEvent(sec, pub, "delete", hash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Base64-encode the auth event as per BUD-01
|
||||
authEventB64 := base64.StdEncoding.EncodeToString([]byte(authEvent))
|
||||
req.Header.Set("Authorization", "Nostr "+authEventB64)
|
||||
}
|
||||
|
||||
if *verbose {
|
||||
fmt.Printf(" DELETE %s\n", url)
|
||||
}
|
||||
|
||||
// Send request
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
reason := resp.Header.Get("X-Reason")
|
||||
if reason == "" {
|
||||
reason = string(body)
|
||||
}
|
||||
return fmt.Errorf("server returned %d: %s", resp.StatusCode, reason)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func verifyDeleted(hash string) error {
|
||||
url := strings.TrimSuffix(*relayURL, "/") + "/blossom/" + hash
|
||||
|
||||
if *verbose {
|
||||
fmt.Printf(" GET %s (expecting 404)\n", url)
|
||||
}
|
||||
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return fmt.Errorf("blob still exists (expected 404, got 200)")
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusNotFound {
|
||||
return fmt.Errorf("unexpected status code: %d (expected 404)", resp.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user