implement messages and operations for FIND
This commit is contained in:
@@ -17,7 +17,15 @@
|
||||
"Bash(printf:*)",
|
||||
"Bash(websocat:*)",
|
||||
"Bash(go test:*)",
|
||||
"Bash(timeout 180 go test:*)"
|
||||
"Bash(timeout 180 go test:*)",
|
||||
"WebFetch(domain:github.com)",
|
||||
"WebFetch(domain:raw.githubusercontent.com)",
|
||||
"Bash(/tmp/find help)",
|
||||
"Bash(/tmp/find verify-name example.com)",
|
||||
"Skill(golang)",
|
||||
"Bash(/tmp/find verify-name Bitcoin.Nostr)",
|
||||
"Bash(/tmp/find generate-key)",
|
||||
"Bash(git ls-tree:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
||||
4
app/web/dist/bundle.css
vendored
4
app/web/dist/bundle.css
vendored
File diff suppressed because one or more lines are too long
28
app/web/dist/bundle.js
vendored
28
app/web/dist/bundle.js
vendored
File diff suppressed because one or more lines are too long
2
app/web/dist/bundle.js.map
vendored
2
app/web/dist/bundle.js.map
vendored
File diff suppressed because one or more lines are too long
283
cmd/find/main.go
Normal file
283
cmd/find/main.go
Normal file
@@ -0,0 +1,283 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"next.orly.dev/pkg/crypto/keys"
|
||||
"next.orly.dev/pkg/encoders/hex"
|
||||
"next.orly.dev/pkg/find"
|
||||
"next.orly.dev/pkg/interfaces/signer"
|
||||
"next.orly.dev/pkg/interfaces/signer/p8k"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
printUsage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
command := os.Args[1]
|
||||
|
||||
switch command {
|
||||
case "register":
|
||||
handleRegister()
|
||||
case "transfer":
|
||||
handleTransfer()
|
||||
case "verify-name":
|
||||
handleVerifyName()
|
||||
case "generate-key":
|
||||
handleGenerateKey()
|
||||
case "issue-cert":
|
||||
handleIssueCert()
|
||||
case "help":
|
||||
printUsage()
|
||||
default:
|
||||
fmt.Printf("Unknown command: %s\n\n", command)
|
||||
printUsage()
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func printUsage() {
|
||||
fmt.Println("FIND - Free Internet Name Daemon")
|
||||
fmt.Println("Usage: find <command> [options]")
|
||||
fmt.Println()
|
||||
fmt.Println("Commands:")
|
||||
fmt.Println(" register <name> Create a registration proposal for a name")
|
||||
fmt.Println(" transfer <name> <new-owner> Transfer a name to a new owner")
|
||||
fmt.Println(" verify-name <name> Validate a name format")
|
||||
fmt.Println(" generate-key Generate a new key pair")
|
||||
fmt.Println(" issue-cert <name> Issue a certificate for a name")
|
||||
fmt.Println(" help Show this help message")
|
||||
fmt.Println()
|
||||
fmt.Println("Examples:")
|
||||
fmt.Println(" find verify-name example.com")
|
||||
fmt.Println(" find register myname.nostr")
|
||||
fmt.Println(" find generate-key")
|
||||
}
|
||||
|
||||
func handleRegister() {
|
||||
if len(os.Args) < 3 {
|
||||
fmt.Println("Usage: find register <name>")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
name := os.Args[2]
|
||||
|
||||
// Validate the name
|
||||
if err := find.ValidateName(name); err != nil {
|
||||
fmt.Printf("Invalid name: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Generate a key pair for this example
|
||||
// In production, this would load from a secure keystore
|
||||
signer, err := p8k.New()
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to create signer: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := signer.Generate(); err != nil {
|
||||
fmt.Printf("Failed to generate key: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Create registration proposal
|
||||
proposal, err := find.NewRegistrationProposal(name, find.ActionRegister, signer)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to create proposal: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("Registration Proposal Created\n")
|
||||
fmt.Printf("==============================\n")
|
||||
fmt.Printf("Name: %s\n", name)
|
||||
fmt.Printf("Pubkey: %s\n", hex.Enc(signer.Pub()))
|
||||
fmt.Printf("Event ID: %s\n", hex.Enc(proposal.GetIDBytes()))
|
||||
fmt.Printf("Kind: %d\n", proposal.Kind)
|
||||
fmt.Printf("Created At: %s\n", time.Unix(proposal.CreatedAt, 0))
|
||||
fmt.Printf("\nEvent JSON:\n")
|
||||
json := proposal.Marshal(nil)
|
||||
fmt.Println(string(json))
|
||||
}
|
||||
|
||||
func handleTransfer() {
|
||||
if len(os.Args) < 4 {
|
||||
fmt.Println("Usage: find transfer <name> <new-owner-pubkey>")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
name := os.Args[2]
|
||||
newOwnerPubkey := os.Args[3]
|
||||
|
||||
// Validate the name
|
||||
if err := find.ValidateName(name); err != nil {
|
||||
fmt.Printf("Invalid name: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Generate current owner key (in production, load from keystore)
|
||||
currentOwner, err := p8k.New()
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to create current owner signer: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := currentOwner.Generate(); err != nil {
|
||||
fmt.Printf("Failed to generate current owner key: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Authorize the transfer
|
||||
prevSig, timestamp, err := find.AuthorizeTransfer(name, newOwnerPubkey, currentOwner)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to authorize transfer: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("Transfer Authorization Created\n")
|
||||
fmt.Printf("===============================\n")
|
||||
fmt.Printf("Name: %s\n", name)
|
||||
fmt.Printf("Current Owner: %s\n", hex.Enc(currentOwner.Pub()))
|
||||
fmt.Printf("New Owner: %s\n", newOwnerPubkey)
|
||||
fmt.Printf("Timestamp: %s\n", timestamp)
|
||||
fmt.Printf("Signature: %s\n", prevSig)
|
||||
fmt.Printf("\nTo complete the transfer, the new owner must create a proposal with:")
|
||||
fmt.Printf(" prev_owner: %s\n", hex.Enc(currentOwner.Pub()))
|
||||
fmt.Printf(" prev_sig: %s\n", prevSig)
|
||||
}
|
||||
|
||||
func handleVerifyName() {
|
||||
if len(os.Args) < 3 {
|
||||
fmt.Println("Usage: find verify-name <name>")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
name := os.Args[2]
|
||||
|
||||
// Validate the name
|
||||
if err := find.ValidateName(name); err != nil {
|
||||
fmt.Printf("❌ Invalid name: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
normalized := find.NormalizeName(name)
|
||||
isTLD := find.IsTLD(normalized)
|
||||
parent := find.GetParentDomain(normalized)
|
||||
|
||||
fmt.Printf("✓ Valid name\n")
|
||||
fmt.Printf("==============\n")
|
||||
fmt.Printf("Original: %s\n", name)
|
||||
fmt.Printf("Normalized: %s\n", normalized)
|
||||
fmt.Printf("Is TLD: %v\n", isTLD)
|
||||
if parent != "" {
|
||||
fmt.Printf("Parent: %s\n", parent)
|
||||
}
|
||||
}
|
||||
|
||||
func handleGenerateKey() {
|
||||
// Generate a new key pair
|
||||
secKey, err := keys.GenerateSecretKey()
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to generate secret key: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
secKeyHex := hex.Enc(secKey)
|
||||
pubKeyHex, err := keys.GetPublicKeyHex(secKeyHex)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to derive public key: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Println("New Key Pair Generated")
|
||||
fmt.Println("======================")
|
||||
fmt.Printf("Secret Key (keep safe!): %s\n", secKeyHex)
|
||||
fmt.Printf("Public Key: %s\n", pubKeyHex)
|
||||
fmt.Println()
|
||||
fmt.Println("⚠️ IMPORTANT: Store the secret key securely. Anyone with access to it can control your names.")
|
||||
}
|
||||
|
||||
func handleIssueCert() {
|
||||
if len(os.Args) < 3 {
|
||||
fmt.Println("Usage: find issue-cert <name>")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
name := os.Args[2]
|
||||
|
||||
// Validate the name
|
||||
if err := find.ValidateName(name); err != nil {
|
||||
fmt.Printf("Invalid name: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Generate name owner key
|
||||
owner, err := p8k.New()
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to create owner signer: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := owner.Generate(); err != nil {
|
||||
fmt.Printf("Failed to generate owner key: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Generate certificate key (different from name owner)
|
||||
certSigner, err := p8k.New()
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to create cert signer: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := certSigner.Generate(); err != nil {
|
||||
fmt.Printf("Failed to generate cert key: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
certPubkey := hex.Enc(certSigner.Pub())
|
||||
|
||||
// Generate 3 witness signers (in production, these would be separate services)
|
||||
var witnesses []signer.I
|
||||
for i := 0; i < 3; i++ {
|
||||
witness, err := p8k.New()
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to create witness %d: %v\n", i, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := witness.Generate(); err != nil {
|
||||
fmt.Printf("Failed to generate witness %d key: %v\n", i, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
witnesses = append(witnesses, witness)
|
||||
}
|
||||
|
||||
// Issue certificate (90 day validity)
|
||||
cert, err := find.IssueCertificate(name, certPubkey, find.CertificateValidity, owner, witnesses)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to issue certificate: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("Certificate Issued\n")
|
||||
fmt.Printf("==================\n")
|
||||
fmt.Printf("Name: %s\n", cert.Name)
|
||||
fmt.Printf("Cert Pubkey: %s\n", cert.CertPubkey)
|
||||
fmt.Printf("Valid From: %s\n", cert.ValidFrom)
|
||||
fmt.Printf("Valid Until: %s\n", cert.ValidUntil)
|
||||
fmt.Printf("Challenge: %s\n", cert.Challenge)
|
||||
fmt.Printf("Witnesses: %d\n", len(cert.Witnesses))
|
||||
fmt.Printf("Algorithm: %s\n", cert.Algorithm)
|
||||
fmt.Printf("Usage: %s\n", cert.Usage)
|
||||
|
||||
fmt.Printf("\nWitness Pubkeys:\n")
|
||||
for i, w := range cert.Witnesses {
|
||||
fmt.Printf(" %d: %s\n", i+1, w.Pubkey)
|
||||
}
|
||||
}
|
||||
694
docs/go-reference-type-analysis.md
Normal file
694
docs/go-reference-type-analysis.md
Normal file
@@ -0,0 +1,694 @@
|
||||
# Go Reference Type Complexity Analysis and Simplification Proposal
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Go's "reference types" (slices, maps, channels) introduce significant cognitive load and parsing complexity due to their implicit reference semantics that differ from regular value types. This analysis proposes making these types explicitly pointer-based to reduce language complexity, improve safety, and make concurrent programming more predictable.
|
||||
|
||||
## Current State: The Reference Type Problem
|
||||
|
||||
### 1. Slices - The "Fat Pointer" Confusion
|
||||
|
||||
**Current Behavior:**
|
||||
```go
|
||||
// Slice is a struct: {ptr *T, len int, cap int}
|
||||
// Copying a slice copies this struct, NOT the underlying array
|
||||
|
||||
s1 := []int{1, 2, 3}
|
||||
s2 := s1 // Copies the slice header, shares underlying array
|
||||
|
||||
s2[0] = 99 // Modifies shared array - affects s1!
|
||||
s2 = append(s2, 4) // May or may not affect s1 depending on capacity
|
||||
```
|
||||
|
||||
**Problems:**
|
||||
- **Implicit sharing**: Assignment copies reference, not data
|
||||
- **Append confusion**: Sometimes mutates original, sometimes doesn't
|
||||
- **Race conditions**: Multiple goroutines accessing shared slice need explicit locks
|
||||
- **Hidden allocations**: Append may allocate without warning
|
||||
- **Capacity vs length**: Two separate concepts that confuse new users
|
||||
- **Nil vs empty**: `nil` slice vs `[]T{}` behave differently
|
||||
|
||||
**Syntax Complexity:**
|
||||
```go
|
||||
// Multiple ways to create slices
|
||||
var s []int // nil slice
|
||||
s := []int{} // empty slice (non-nil)
|
||||
s := make([]int, 10) // length 10, capacity 10
|
||||
s := make([]int, 10, 20) // length 10, capacity 20
|
||||
s := []int{1, 2, 3} // literal
|
||||
s := arr[:] // from array
|
||||
s := arr[1:3] // subslice
|
||||
s := arr[1:3:5] // subslice with capacity
|
||||
```
|
||||
|
||||
### 2. Maps - The Always-Reference Type
|
||||
|
||||
**Current Behavior:**
|
||||
```go
|
||||
// Map is a pointer to a hash table structure
|
||||
// Assignment ALWAYS copies the pointer
|
||||
|
||||
m1 := make(map[string]int)
|
||||
m2 := m1 // Both point to same map
|
||||
|
||||
m2["key"] = 42 // Modifies shared map - affects m1!
|
||||
|
||||
var m3 map[string]int // nil map - reads panic!
|
||||
m3 = make(map[string]int) // Must initialize before use
|
||||
```
|
||||
|
||||
**Problems:**
|
||||
- **Always reference**: No way to copy a map with simple assignment
|
||||
- **Nil map trap**: Reading from nil map works, writing panics
|
||||
- **No built-in copy**: Must manually iterate to copy
|
||||
- **Concurrent access**: Requires explicit sync.Map or manual locking
|
||||
- **Non-deterministic iteration**: Range order is randomized
|
||||
- **Memory leaks**: Map never shrinks, deleted keys hold memory
|
||||
|
||||
**Syntax Complexity:**
|
||||
```go
|
||||
// Creating maps
|
||||
var m map[K]V // nil map
|
||||
m := map[K]V{} // empty map
|
||||
m := make(map[K]V) // empty map
|
||||
m := make(map[K]V, 100) // with capacity hint
|
||||
m := map[K]V{k1: v1, k2: v2} // literal
|
||||
|
||||
// Checking existence requires two-value form
|
||||
value, ok := m[key] // ok is false if not present
|
||||
value := m[key] // returns zero value if not present
|
||||
```
|
||||
|
||||
### 3. Channels - The Most Complex Reference Type
|
||||
|
||||
**Current Behavior:**
|
||||
```go
|
||||
// Channel is a pointer to a channel structure
|
||||
// Extremely complex semantics
|
||||
|
||||
ch := make(chan int) // unbuffered - blocks on send
|
||||
ch := make(chan int, 10) // buffered - blocks when full
|
||||
|
||||
ch <- 42 // Send (blocks if full/unbuffered)
|
||||
x := <-ch // Receive (blocks if empty)
|
||||
x, ok := <-ch // Receive with closed check
|
||||
|
||||
close(ch) // Close channel
|
||||
// Sending to closed channel: PANIC
|
||||
// Closing closed channel: PANIC
|
||||
// Receiving from closed: returns zero value + ok=false
|
||||
```
|
||||
|
||||
**Problems:**
|
||||
- **Directional types**: `chan T`, `chan<- T`, `<-chan T` add complexity
|
||||
- **Close semantics**: Only sender should close, hard to enforce
|
||||
- **Select complexity**: `select` statement is a mini-language
|
||||
- **Nil channel**: Sending/receiving on nil blocks forever (trap!)
|
||||
- **Buffered vs unbuffered**: Completely different semantics
|
||||
- **No channel copy**: Impossible to copy a channel
|
||||
- **Deadlock detection**: Runtime detection adds complexity
|
||||
|
||||
**Syntax Complexity:**
|
||||
```go
|
||||
// Channel operations
|
||||
ch := make(chan T) // unbuffered
|
||||
ch := make(chan T, N) // buffered
|
||||
ch <- v // send
|
||||
v := <-ch // receive
|
||||
v, ok := <-ch // receive with status
|
||||
close(ch) // close
|
||||
<-ch // receive and discard
|
||||
|
||||
// Directional channels
|
||||
func send(ch chan<- int) {} // send-only
|
||||
func recv(ch <-chan int) {} // receive-only
|
||||
|
||||
// Select statement
|
||||
select {
|
||||
case v := <-ch1:
|
||||
// handle
|
||||
case ch2 <- v:
|
||||
// handle
|
||||
case <-time.After(timeout):
|
||||
// timeout
|
||||
default:
|
||||
// non-blocking
|
||||
}
|
||||
|
||||
// Range over channel
|
||||
for v := range ch {
|
||||
// must be closed by sender or infinite loop
|
||||
}
|
||||
```
|
||||
|
||||
## Complexity Metrics
|
||||
|
||||
### Current Go Reference Types
|
||||
|
||||
| Feature | Syntax Variants | Special Cases | Runtime Behaviors | Total Complexity |
|
||||
|---------|----------------|---------------|-------------------|-----------------|
|
||||
| **Slices** | 8 creation forms | nil vs empty, capacity vs length | append reallocation, sharing semantics | **HIGH** |
|
||||
| **Maps** | 5 creation forms | nil map panic, no shrinking | randomized iteration, no copy | **HIGH** |
|
||||
| **Channels** | 6 operation forms | close rules, directional types | buffered vs unbuffered, select | **VERY HIGH** |
|
||||
|
||||
### Parser Complexity
|
||||
|
||||
Current Go requires parsing:
|
||||
- **8 forms of slice expressions**: `a[:]`, `a[i:]`, `a[:j]`, `a[i:j]`, `a[i:j:k]`, etc.
|
||||
- **3 channel operators**: `<-`, `chan<-`, `<-chan` (context-dependent)
|
||||
- **Select statement**: Unique control flow structure
|
||||
- **Range statement**: 4 different forms for different types
|
||||
- **Make vs new**: Two allocation functions with different semantics
|
||||
|
||||
## Proposed Simplifications
|
||||
|
||||
### Core Principle: Explicit Is Better Than Implicit
|
||||
|
||||
Make all reference types use explicit pointer syntax. This:
|
||||
1. Makes copying behavior obvious
|
||||
2. Eliminates special case handling
|
||||
3. Reduces parser complexity
|
||||
4. Improves concurrent safety
|
||||
5. Unifies type system
|
||||
|
||||
### 1. Explicit Slice Pointers
|
||||
|
||||
**Proposed Syntax:**
|
||||
```go
|
||||
// Slices become explicit pointers to dynamic arrays
|
||||
var s *[]int = nil // explicit nil pointer
|
||||
|
||||
s = &[]int{1, 2, 3} // explicit allocation
|
||||
s2 := &[]int{1, 2, 3} // short form
|
||||
|
||||
// Accessing requires dereference (or auto-deref like methods)
|
||||
(*s)[0] = 42 // explicit dereference
|
||||
s[0] = 42 // auto-deref (like struct methods)
|
||||
|
||||
// Copying requires explicit clone
|
||||
s2 := s.Clone() // explicit copy operation
|
||||
s2 := &[]int(*s) // alternative: copy via literal
|
||||
|
||||
// Appending creates new allocation or mutates
|
||||
s.Append(42) // mutates in place (may reallocate)
|
||||
s2 := s.Clone().Append(42) // copy-on-write pattern
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- **Explicit allocation**: `&[]T{...}` makes heap allocation clear
|
||||
- **No hidden sharing**: Assignment copies pointer, obviously
|
||||
- **Explicit cloning**: Must call `.Clone()` to copy data
|
||||
- **Clear ownership**: Pointer semantics match other types
|
||||
- **Simpler grammar**: Eliminates slice-specific syntax like `make([]T, len, cap)`
|
||||
|
||||
**Eliminate:**
|
||||
- `make([]T, ...)` - replaced by `&[]T{...}` or `&[cap]T{}[:len]`
|
||||
- Multi-index slicing `a[i:j:k]` - too complex, rarely used
|
||||
- Implicit capacity - arrays have size, slices are just `&[]T`
|
||||
|
||||
### 2. Explicit Map Pointers
|
||||
|
||||
**Proposed Syntax:**
|
||||
```go
|
||||
// Maps become explicit pointers to hash tables
|
||||
var m *map[string]int = nil // explicit nil pointer
|
||||
|
||||
m = &map[string]int{} // explicit allocation
|
||||
m := &map[string]int{ // literal initialization
|
||||
"key": 42,
|
||||
}
|
||||
|
||||
// Accessing requires dereference (or auto-deref)
|
||||
(*m)["key"] = 42 // explicit
|
||||
m["key"] = 42 // auto-deref
|
||||
|
||||
// Copying requires explicit clone
|
||||
m2 := m.Clone() // explicit copy operation
|
||||
|
||||
// Nil pointer behavior is consistent
|
||||
if m == nil {
|
||||
m = &map[string]int{}
|
||||
}
|
||||
m["key"] = 42 // no special nil handling
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- **No nil map trap**: Nil pointer is consistently nil
|
||||
- **Explicit cloning**: Must call `.Clone()` to copy
|
||||
- **Unified semantics**: Works like all other pointer types
|
||||
- **Clear ownership**: Pointer passing is obvious
|
||||
|
||||
**Eliminate:**
|
||||
- `make(map[K]V)` - replaced by `&map[K]V{}`
|
||||
- Special nil map read-only behavior
|
||||
- Capacity hints (premature optimization)
|
||||
|
||||
### 3. Simplify or Eliminate Channels
|
||||
|
||||
**Option A: Eliminate Channels Entirely**
|
||||
|
||||
Replace with explicit concurrency primitives:
|
||||
|
||||
```go
|
||||
// Instead of channels, use explicit queues
|
||||
type Queue[T any] struct {
|
||||
items []T
|
||||
mu sync.Mutex
|
||||
cond *sync.Cond
|
||||
}
|
||||
|
||||
func (q *Queue[T]) Send(v T) {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
q.items = append(q.items, v)
|
||||
q.cond.Signal()
|
||||
}
|
||||
|
||||
func (q *Queue[T]) Recv() T {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
for len(q.items) == 0 {
|
||||
q.cond.Wait()
|
||||
}
|
||||
v := q.items[0]
|
||||
q.items = q.items[1:]
|
||||
return v
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- **No special syntax**: Uses standard types and methods
|
||||
- **Explicit locking**: Clear where synchronization happens
|
||||
- **No close semantics**: Just stop sending
|
||||
- **No directional types**: Use interfaces if needed
|
||||
- **Debuggable**: Standard data structures
|
||||
|
||||
**Option B: Explicit Channel Pointers**
|
||||
|
||||
If keeping channels:
|
||||
|
||||
```go
|
||||
// Channels become explicit pointers
|
||||
ch := &chan int{} // unbuffered
|
||||
ch := &chan int{cap: 10} // buffered
|
||||
|
||||
ch.Send(42) // method instead of operator
|
||||
v := ch.Recv() // method instead of operator
|
||||
v, ok := ch.TryRecv() // non-blocking receive
|
||||
ch.Close() // explicit close
|
||||
|
||||
// No directional types - use interfaces
|
||||
type Sender[T] interface { Send(T) }
|
||||
type Receiver[T] interface { Recv() T }
|
||||
```
|
||||
|
||||
**Eliminate:**
|
||||
- `<-` operator entirely (use methods)
|
||||
- `select` statement (use explicit polling or wait groups)
|
||||
- Directional channel types
|
||||
- `make(chan T)` syntax
|
||||
- `range` over channels
|
||||
|
||||
### 4. Unified Allocation
|
||||
|
||||
**Current Go:**
|
||||
```go
|
||||
new(T) // returns *T, zero value
|
||||
make([]T, n) // returns []T (slice)
|
||||
make(map[K]V) // returns map[K]V (map)
|
||||
make(chan T) // returns chan T (channel)
|
||||
```
|
||||
|
||||
**Proposed:**
|
||||
```go
|
||||
new(T) // returns *T, zero value (keep this)
|
||||
&T{} // returns *T, composite literal (keep this)
|
||||
&[]T{} // returns *[]T, slice
|
||||
&[n]T{} // returns *[n]T, array
|
||||
&map[K]V{} // returns *map[K]V, map
|
||||
|
||||
// Eliminate make() entirely
|
||||
```
|
||||
|
||||
### 5. Simplified Type System
|
||||
|
||||
**Before (reference types as special):**
|
||||
```
|
||||
Types:
|
||||
- Value types: int, float, struct, array, pointer
|
||||
- Reference types: slice, map, channel (special semantics)
|
||||
```
|
||||
|
||||
**After (everything is value or pointer):**
|
||||
```
|
||||
Types:
|
||||
- Value types: int, float, struct, [N]T (array)
|
||||
- Pointer types: *T (including *[]T, *map[K]V)
|
||||
```
|
||||
|
||||
## Complexity Reduction Analysis
|
||||
|
||||
### Grammar Simplification
|
||||
|
||||
**Eliminated Syntax:**
|
||||
|
||||
1. **Slice expressions** (8 forms → 1):
|
||||
- ❌ `a[:]`, `a[i:]`, `a[:j]`, `a[i:j]`, `a[i:j:k]`
|
||||
- ✅ `a[i]` (single index only, or use methods like `.Slice(i, j)`)
|
||||
|
||||
2. **Make function** (3 forms → 0):
|
||||
- ❌ `make([]T, len)`, `make([]T, len, cap)`, `make(map[K]V)`, `make(chan T)`
|
||||
- ✅ `&[]T{}`, `&map[K]V{}`
|
||||
|
||||
3. **Channel operators** (3 forms → 0):
|
||||
- ❌ `<-ch`, `ch<-`, `<-chan`, `chan<-`
|
||||
- ✅ `.Send()`, `.Recv()` methods
|
||||
|
||||
4. **Select statement** (1 form → 0):
|
||||
- ❌ `select { case ... }`
|
||||
- ✅ Regular if/switch with polling or wait groups
|
||||
|
||||
5. **Range variants** (4 forms → 2):
|
||||
- ❌ `for v := range ch` (channel)
|
||||
- ❌ `for i, v := range slice` (special case)
|
||||
- ✅ `for i := 0; i < len(slice); i++` (explicit)
|
||||
|
||||
### Semantic Simplification
|
||||
|
||||
**Eliminated Special Cases:**
|
||||
|
||||
1. **Nil map read-only behavior** → Standard nil pointer
|
||||
2. **Append reallocation magic** → Explicit `.Append()` or `.Grow()`
|
||||
3. **Channel close-twice panic** → No special close semantics
|
||||
4. **Slice capacity vs length** → Explicit growth methods
|
||||
5. **Non-deterministic map iteration** → Option to make deterministic
|
||||
|
||||
### Runtime Simplification
|
||||
|
||||
**Eliminated Runtime Features:**
|
||||
|
||||
1. **Deadlock detection** → User responsibility with explicit locks
|
||||
2. **Channel close tracking** → No close needed
|
||||
3. **Select fairness** → No select statement
|
||||
4. **Goroutine channel blocking** → Explicit condition variables
|
||||
|
||||
## Concurrency Safety Improvements
|
||||
|
||||
### Before: Implicit Sharing Causes Races
|
||||
|
||||
```go
|
||||
// Easy to create race conditions
|
||||
s := []int{1, 2, 3}
|
||||
m := map[string]int{"key": 42}
|
||||
|
||||
go func() {
|
||||
s[0] = 99 // RACE: implicit sharing
|
||||
m["key"] = 100 // RACE: implicit sharing
|
||||
}()
|
||||
|
||||
s[1] = 88 // RACE: concurrent access
|
||||
m["key"] = 200 // RACE: concurrent access
|
||||
```
|
||||
|
||||
### After: Explicit Pointers Make Sharing Obvious
|
||||
|
||||
```go
|
||||
// Clear that pointers are shared
|
||||
s := &[]int{1, 2, 3}
|
||||
m := &map[string]int{"key": 42}
|
||||
|
||||
go func() {
|
||||
s[0] = 99 // RACE: obvious pointer sharing
|
||||
m["key"] = 100 // RACE: obvious pointer sharing
|
||||
}()
|
||||
|
||||
// Must explicitly protect
|
||||
var mu sync.Mutex
|
||||
mu.Lock()
|
||||
s[1] = 88
|
||||
mu.Unlock()
|
||||
|
||||
// Or use pass-by-value (copy)
|
||||
s2 := &[]int(*s) // explicit copy
|
||||
go func(local *[]int) {
|
||||
local[0] = 99 // NO RACE: different slice
|
||||
}(s2)
|
||||
```
|
||||
|
||||
### Pattern: Immutable by Default
|
||||
|
||||
```go
|
||||
// Current Go: easy to accidentally mutate
|
||||
func process(s []int) {
|
||||
s[0] = 99 // Mutates caller's slice!
|
||||
}
|
||||
|
||||
// Proposed: explicit mutation
|
||||
func process(s *[]int) {
|
||||
(*s)[0] = 99 // Clear mutation
|
||||
}
|
||||
|
||||
// Or use value semantics
|
||||
func process(s []int) {
|
||||
s[0] = 99 // Only mutates local copy
|
||||
return s
|
||||
}
|
||||
```
|
||||
|
||||
## Migration Path
|
||||
|
||||
### Phase 1: Add Explicit Syntax (Backward Compatible)
|
||||
|
||||
```go
|
||||
// Allow both forms initially
|
||||
s1 := []int{1, 2, 3} // old style
|
||||
s2 := &[]int{1, 2, 3} // new style (same runtime behavior)
|
||||
|
||||
// Add methods to support new style
|
||||
s2.Append(4)
|
||||
s3 := s2.Clone()
|
||||
```
|
||||
|
||||
### Phase 2: Deprecate Implicit Forms
|
||||
|
||||
```go
|
||||
// Warn on old syntax
|
||||
s := make([]int, 10) // WARNING: Use &[]int{} or &[10]int{}
|
||||
ch := make(chan int) // WARNING: Use &chan int{} or Queue[int]
|
||||
ch <- 42 // WARNING: Use ch.Send(42)
|
||||
```
|
||||
|
||||
### Phase 3: Remove Implicit Forms
|
||||
|
||||
```go
|
||||
// Only explicit forms allowed
|
||||
s := &[]int{1, 2, 3} // OK
|
||||
m := &map[K]V{} // OK
|
||||
ch := &chan int{} // OK (or removed entirely)
|
||||
|
||||
make([]int, 10) // ERROR: Use &[]int{} or explicit loop
|
||||
ch <- 42 // ERROR: Use ch.Send(42)
|
||||
```
|
||||
|
||||
## Comparison: Before and After
|
||||
|
||||
### Slice Example
|
||||
|
||||
**Before:**
|
||||
```go
|
||||
func AppendUnique(s []int, v int) []int {
|
||||
for _, existing := range s {
|
||||
if existing == v {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return append(s, v) // May or may not mutate caller's slice!
|
||||
}
|
||||
|
||||
s := []int{1, 2, 3}
|
||||
s = AppendUnique(s, 4) // Must reassign to avoid bugs
|
||||
```
|
||||
|
||||
**After:**
|
||||
```go
|
||||
func AppendUnique(s *[]int, v int) {
|
||||
for _, existing := range *s {
|
||||
if existing == v {
|
||||
return
|
||||
}
|
||||
}
|
||||
s.Append(v) // Always mutates, clear semantics
|
||||
}
|
||||
|
||||
s := &[]int{1, 2, 3}
|
||||
AppendUnique(s, 4) // No reassignment needed
|
||||
```
|
||||
|
||||
### Map Example
|
||||
|
||||
**Before:**
|
||||
```go
|
||||
func Merge(dst, src map[string]int) {
|
||||
for k, v := range src {
|
||||
dst[k] = v // Mutates dst (caller's map)
|
||||
}
|
||||
}
|
||||
|
||||
m1 := map[string]int{"a": 1}
|
||||
m2 := map[string]int{"b": 2}
|
||||
Merge(m1, m2) // m1 is mutated
|
||||
```
|
||||
|
||||
**After:**
|
||||
```go
|
||||
func Merge(dst, src *map[string]int) {
|
||||
for k, v := range *src {
|
||||
(*dst)[k] = v // Clear mutation
|
||||
}
|
||||
}
|
||||
|
||||
m1 := &map[string]int{"a": 1}
|
||||
m2 := &map[string]int{"b": 2}
|
||||
Merge(m1, m2) // Clear that m1 is mutated
|
||||
```
|
||||
|
||||
### Channel Example (Option B: Keep Channels)
|
||||
|
||||
**Before:**
|
||||
```go
|
||||
func Worker(jobs <-chan Job, results chan<- Result) {
|
||||
for job := range jobs {
|
||||
results <- process(job)
|
||||
}
|
||||
}
|
||||
|
||||
jobs := make(chan Job, 10)
|
||||
results := make(chan Result, 10)
|
||||
go Worker(jobs, results)
|
||||
```
|
||||
|
||||
**After:**
|
||||
```go
|
||||
func Worker(jobs Receiver[Job], results Sender[Result]) {
|
||||
for {
|
||||
job, ok := jobs.TryRecv()
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
results.Send(process(job))
|
||||
}
|
||||
}
|
||||
|
||||
jobs := &Queue[Job]{cap: 10}
|
||||
results := &Queue[Result]{cap: 10}
|
||||
go Worker(jobs, results)
|
||||
```
|
||||
|
||||
## Implementation Impact
|
||||
|
||||
### Compiler Changes
|
||||
|
||||
**Simplified:**
|
||||
- ✅ Remove slice expression parsing (8 forms → 1)
|
||||
- ✅ Remove `make()` built-in
|
||||
- ✅ Remove `<-` operator
|
||||
- ✅ Remove `select` statement
|
||||
- ✅ Remove directional channel types
|
||||
- ✅ Unify reference types with pointer types
|
||||
|
||||
**Modified:**
|
||||
- 🔄 Auto-dereference for `*[]T`, `*map[K]V` (like struct methods)
|
||||
- 🔄 Add built-in `.Clone()`, `.Append()`, `.Grow()` methods
|
||||
- 🔄 Array → Slice conversion: `&[N]T{} → *[]T`
|
||||
|
||||
### Runtime Changes
|
||||
|
||||
**Simplified:**
|
||||
- ✅ Remove deadlock detection (no channels)
|
||||
- ✅ Remove select fairness logic
|
||||
- ✅ Remove channel close tracking
|
||||
- ✅ Simpler type reflection (fewer special cases)
|
||||
|
||||
**Preserved:**
|
||||
- ✅ Garbage collection (now simpler with fewer types)
|
||||
- ✅ Goroutine scheduler (unchanged)
|
||||
- ✅ Slice/map internal structure (same layout)
|
||||
|
||||
### Standard Library Changes
|
||||
|
||||
**Packages to Update:**
|
||||
- `sync` - Keep Mutex, RWMutex, WaitGroup; enhance Cond
|
||||
- `container` - Add generic Queue, Stack types
|
||||
- `slices` - Methods become methods on `*[]T`
|
||||
- `maps` - Methods become methods on `*map[K]V`
|
||||
|
||||
**Packages to Remove/Simplify:**
|
||||
- `sync.Map` - No longer needed (use `*map[K]V` with mutex)
|
||||
- Channel-based packages - Rewrite with explicit queues
|
||||
|
||||
## Conclusion
|
||||
|
||||
### Complexity Reduction Summary
|
||||
|
||||
| Metric | Before | After | Reduction |
|
||||
|--------|--------|-------|-----------|
|
||||
| **Reference type forms** | 3 (slice, map, chan) | 0 (all pointers) | **100%** |
|
||||
| **Allocation functions** | 2 (new, make) | 1 (new/&) | **50%** |
|
||||
| **Slice syntax variants** | 8 | 1 | **87.5%** |
|
||||
| **Channel operators** | 3 | 0 | **100%** |
|
||||
| **Special statements** | 2 (select, range-chan) | 0 | **100%** |
|
||||
| **Type system special cases** | 6+ | 0 | **100%** |
|
||||
|
||||
### Benefits
|
||||
|
||||
1. **Simpler Language Definition**
|
||||
- Fewer special types and operators
|
||||
- Unified pointer semantics
|
||||
- Easier to specify and implement
|
||||
|
||||
2. **Easier to Learn**
|
||||
- No hidden reference behavior
|
||||
- Explicit allocation and copying
|
||||
- Consistent with other pointer types
|
||||
|
||||
3. **Safer Concurrent Code**
|
||||
- Obvious when data is shared
|
||||
- Explicit synchronization required
|
||||
- No hidden race conditions
|
||||
|
||||
4. **Better Tooling**
|
||||
- Simpler parser (fewer special cases)
|
||||
- Better static analysis (explicit sharing)
|
||||
- Easier code generation
|
||||
|
||||
5. **Maintained Performance**
|
||||
- Same runtime representation
|
||||
- Same memory layout
|
||||
- Same GC behavior
|
||||
- Potential optimizations preserved
|
||||
|
||||
### Trade-offs
|
||||
|
||||
**Lost:**
|
||||
- Channel select (must use explicit polling)
|
||||
- Syntactic sugar for send/receive (`<-`)
|
||||
- Make function convenience
|
||||
- Slice expression shortcuts
|
||||
|
||||
**Gained:**
|
||||
- Explicit, obvious semantics
|
||||
- Unified type system
|
||||
- Simpler language specification
|
||||
- Better concurrent safety
|
||||
- Easier to parse and analyze
|
||||
|
||||
### Recommendation
|
||||
|
||||
Adopt explicit pointer syntax for all reference types. This change:
|
||||
- Reduces language complexity by ~40% (by eliminating special cases)
|
||||
- Improves safety and predictability
|
||||
- Maintains performance characteristics
|
||||
- Simplifies compiler and tooling implementation
|
||||
- Makes Go easier to learn and use correctly
|
||||
|
||||
The migration path is clear and could be done gradually with deprecation warnings before breaking changes.
|
||||
388
pkg/find/builder.go
Normal file
388
pkg/find/builder.go
Normal file
@@ -0,0 +1,388 @@
|
||||
package find
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"next.orly.dev/pkg/encoders/event"
|
||||
"next.orly.dev/pkg/encoders/tag"
|
||||
"next.orly.dev/pkg/encoders/timestamp"
|
||||
"next.orly.dev/pkg/interfaces/signer"
|
||||
)
|
||||
|
||||
// NewRegistrationProposal creates a new registration proposal event (kind 30100)
|
||||
func NewRegistrationProposal(name, action string, signer signer.I) (*event.E, error) {
|
||||
// Validate and normalize name
|
||||
name = NormalizeName(name)
|
||||
if err := ValidateName(name); err != nil {
|
||||
return nil, fmt.Errorf("invalid name: %w", err)
|
||||
}
|
||||
|
||||
// Validate action
|
||||
if action != ActionRegister && action != ActionTransfer {
|
||||
return nil, fmt.Errorf("invalid action: must be %s or %s", ActionRegister, ActionTransfer)
|
||||
}
|
||||
|
||||
// Create event
|
||||
ev := event.New()
|
||||
ev.Kind = KindRegistrationProposal
|
||||
ev.CreatedAt = timestamp.Now().V
|
||||
ev.Pubkey = signer.Pub()
|
||||
|
||||
// Build tags
|
||||
tags := tag.NewS()
|
||||
tags.Append(tag.NewFromAny("d", name))
|
||||
tags.Append(tag.NewFromAny("action", action))
|
||||
|
||||
// Add expiration tag (5 minutes from now)
|
||||
expiration := time.Now().Add(ProposalExpiry).Unix()
|
||||
tags.Append(tag.NewFromAny("expiration", strconv.FormatInt(expiration, 10)))
|
||||
|
||||
ev.Tags = tags
|
||||
ev.Content = []byte{}
|
||||
|
||||
// Sign the event
|
||||
if err := ev.Sign(signer); err != nil {
|
||||
return nil, fmt.Errorf("failed to sign event: %w", err)
|
||||
}
|
||||
|
||||
return ev, nil
|
||||
}
|
||||
|
||||
// NewRegistrationProposalWithTransfer creates a transfer proposal with previous owner signature
|
||||
func NewRegistrationProposalWithTransfer(name, prevOwner, prevSig string, signer signer.I) (*event.E, error) {
|
||||
// Create base proposal
|
||||
ev, err := NewRegistrationProposal(name, ActionTransfer, signer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Add transfer-specific tags
|
||||
ev.Tags.Append(tag.NewFromAny("prev_owner", prevOwner))
|
||||
ev.Tags.Append(tag.NewFromAny("prev_sig", prevSig))
|
||||
|
||||
// Re-sign after adding tags
|
||||
if err := ev.Sign(signer); err != nil {
|
||||
return nil, fmt.Errorf("failed to sign transfer event: %w", err)
|
||||
}
|
||||
|
||||
return ev, nil
|
||||
}
|
||||
|
||||
// NewAttestation creates a new attestation event (kind 20100)
|
||||
func NewAttestation(proposalID, decision string, weight int, reason, serviceURL string, signer signer.I) (*event.E, error) {
|
||||
// Validate decision
|
||||
if decision != DecisionApprove && decision != DecisionReject && decision != DecisionAbstain {
|
||||
return nil, fmt.Errorf("invalid decision: must be approve, reject, or abstain")
|
||||
}
|
||||
|
||||
// Create event
|
||||
ev := event.New()
|
||||
ev.Kind = KindAttestation
|
||||
ev.CreatedAt = timestamp.Now().V
|
||||
ev.Pubkey = signer.Pub()
|
||||
|
||||
// Build tags
|
||||
tags := tag.NewS()
|
||||
tags.Append(tag.NewFromAny("e", proposalID))
|
||||
tags.Append(tag.NewFromAny("decision", decision))
|
||||
|
||||
if weight > 0 {
|
||||
tags.Append(tag.NewFromAny("weight", strconv.Itoa(weight)))
|
||||
}
|
||||
|
||||
if reason != "" {
|
||||
tags.Append(tag.NewFromAny("reason", reason))
|
||||
}
|
||||
|
||||
if serviceURL != "" {
|
||||
tags.Append(tag.NewFromAny("service", serviceURL))
|
||||
}
|
||||
|
||||
// Add expiration tag (3 minutes from now)
|
||||
expiration := time.Now().Add(AttestationExpiry).Unix()
|
||||
tags.Append(tag.NewFromAny("expiration", strconv.FormatInt(expiration, 10)))
|
||||
|
||||
ev.Tags = tags
|
||||
ev.Content = []byte{}
|
||||
|
||||
// Sign the event
|
||||
if err := ev.Sign(signer); err != nil {
|
||||
return nil, fmt.Errorf("failed to sign attestation: %w", err)
|
||||
}
|
||||
|
||||
return ev, nil
|
||||
}
|
||||
|
||||
// NewTrustGraph creates a new trust graph event (kind 30101)
|
||||
func NewTrustGraph(entries []TrustEntry, signer signer.I) (*event.E, error) {
|
||||
// Validate trust entries
|
||||
for i, entry := range entries {
|
||||
if err := ValidateTrustScore(entry.TrustScore); err != nil {
|
||||
return nil, fmt.Errorf("invalid trust score at index %d: %w", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create event
|
||||
ev := event.New()
|
||||
ev.Kind = KindTrustGraph
|
||||
ev.CreatedAt = timestamp.Now().V
|
||||
ev.Pubkey = signer.Pub()
|
||||
|
||||
// Build tags
|
||||
tags := tag.NewS()
|
||||
tags.Append(tag.NewFromAny("d", "trust-graph"))
|
||||
|
||||
// Add trust entries as p tags
|
||||
for _, entry := range entries {
|
||||
tags.Append(tag.NewFromAny("p", entry.Pubkey, entry.ServiceURL,
|
||||
strconv.FormatFloat(entry.TrustScore, 'f', 2, 64)))
|
||||
}
|
||||
|
||||
// Add expiration tag (30 days from now)
|
||||
expiration := time.Now().Add(TrustGraphExpiry).Unix()
|
||||
tags.Append(tag.NewFromAny("expiration", strconv.FormatInt(expiration, 10)))
|
||||
|
||||
ev.Tags = tags
|
||||
ev.Content = []byte{}
|
||||
|
||||
// Sign the event
|
||||
if err := ev.Sign(signer); err != nil {
|
||||
return nil, fmt.Errorf("failed to sign trust graph: %w", err)
|
||||
}
|
||||
|
||||
return ev, nil
|
||||
}
|
||||
|
||||
// NewNameState creates a new name state event (kind 30102)
|
||||
func NewNameState(name, owner string, registeredAt time.Time, proposalID string,
|
||||
attestations int, confidence float64, signer signer.I) (*event.E, error) {
|
||||
|
||||
// Validate name
|
||||
name = NormalizeName(name)
|
||||
if err := ValidateName(name); err != nil {
|
||||
return nil, fmt.Errorf("invalid name: %w", err)
|
||||
}
|
||||
|
||||
// Create event
|
||||
ev := event.New()
|
||||
ev.Kind = KindNameState
|
||||
ev.CreatedAt = timestamp.Now().V
|
||||
ev.Pubkey = signer.Pub()
|
||||
|
||||
// Build tags
|
||||
tags := tag.NewS()
|
||||
tags.Append(tag.NewFromAny("d", name))
|
||||
tags.Append(tag.NewFromAny("owner", owner))
|
||||
tags.Append(tag.NewFromAny("registered_at", strconv.FormatInt(registeredAt.Unix(), 10)))
|
||||
tags.Append(tag.NewFromAny("proposal", proposalID))
|
||||
tags.Append(tag.NewFromAny("attestations", strconv.Itoa(attestations)))
|
||||
tags.Append(tag.NewFromAny("confidence", strconv.FormatFloat(confidence, 'f', 2, 64)))
|
||||
|
||||
// Add expiration tag (1 year from registration)
|
||||
expiration := registeredAt.Add(NameRegistrationPeriod).Unix()
|
||||
tags.Append(tag.NewFromAny("expiration", strconv.FormatInt(expiration, 10)))
|
||||
|
||||
ev.Tags = tags
|
||||
ev.Content = []byte{}
|
||||
|
||||
// Sign the event
|
||||
if err := ev.Sign(signer); err != nil {
|
||||
return nil, fmt.Errorf("failed to sign name state: %w", err)
|
||||
}
|
||||
|
||||
return ev, nil
|
||||
}
|
||||
|
||||
// NewNameRecord creates a new name record event (kind 30103)
|
||||
func NewNameRecord(name, recordType, value string, ttl int, signer signer.I) (*event.E, error) {
|
||||
// Validate name
|
||||
name = NormalizeName(name)
|
||||
if err := ValidateName(name); err != nil {
|
||||
return nil, fmt.Errorf("invalid name: %w", err)
|
||||
}
|
||||
|
||||
// Validate record value
|
||||
if err := ValidateRecordValue(recordType, value); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create event
|
||||
ev := event.New()
|
||||
ev.Kind = KindNameRecords
|
||||
ev.CreatedAt = timestamp.Now().V
|
||||
ev.Pubkey = signer.Pub()
|
||||
|
||||
// Build tags
|
||||
tags := tag.NewS()
|
||||
tags.Append(tag.NewFromAny("d", fmt.Sprintf("%s:%s", name, recordType)))
|
||||
tags.Append(tag.NewFromAny("name", name))
|
||||
tags.Append(tag.NewFromAny("type", recordType))
|
||||
tags.Append(tag.NewFromAny("value", value))
|
||||
|
||||
if ttl > 0 {
|
||||
tags.Append(tag.NewFromAny("ttl", strconv.Itoa(ttl)))
|
||||
}
|
||||
|
||||
ev.Tags = tags
|
||||
ev.Content = []byte{}
|
||||
|
||||
// Sign the event
|
||||
if err := ev.Sign(signer); err != nil {
|
||||
return nil, fmt.Errorf("failed to sign name record: %w", err)
|
||||
}
|
||||
|
||||
return ev, nil
|
||||
}
|
||||
|
||||
// NewNameRecordWithPriority creates a name record with priority (for MX, SRV)
|
||||
func NewNameRecordWithPriority(name, recordType, value string, ttl, priority int, signer signer.I) (*event.E, error) {
|
||||
// Validate priority
|
||||
if err := ValidatePriority(priority); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create base record
|
||||
ev, err := NewNameRecord(name, recordType, value, ttl, signer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Add priority tag
|
||||
ev.Tags.Append(tag.NewFromAny("priority", strconv.Itoa(priority)))
|
||||
|
||||
// Re-sign
|
||||
if err := ev.Sign(signer); err != nil {
|
||||
return nil, fmt.Errorf("failed to sign record with priority: %w", err)
|
||||
}
|
||||
|
||||
return ev, nil
|
||||
}
|
||||
|
||||
// NewSRVRecord creates an SRV record with all required fields
|
||||
func NewSRVRecord(name, value string, ttl, priority, weight, port int, signer signer.I) (*event.E, error) {
|
||||
// Validate SRV-specific fields
|
||||
if err := ValidatePriority(priority); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := ValidateWeight(weight); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := ValidatePort(port); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create base record
|
||||
ev, err := NewNameRecord(name, RecordTypeSRV, value, ttl, signer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Add SRV-specific tags
|
||||
ev.Tags.Append(tag.NewFromAny("priority", strconv.Itoa(priority)))
|
||||
ev.Tags.Append(tag.NewFromAny("weight", strconv.Itoa(weight)))
|
||||
ev.Tags.Append(tag.NewFromAny("port", strconv.Itoa(port)))
|
||||
|
||||
// Re-sign
|
||||
if err := ev.Sign(signer); err != nil {
|
||||
return nil, fmt.Errorf("failed to sign SRV record: %w", err)
|
||||
}
|
||||
|
||||
return ev, nil
|
||||
}
|
||||
|
||||
// NewCertificate creates a new certificate event (kind 30104)
|
||||
func NewCertificate(name, certPubkey string, validFrom, validUntil time.Time,
|
||||
challenge, challengeProof string, witnesses []WitnessSignature,
|
||||
algorithm, usage string, signer signer.I) (*event.E, error) {
|
||||
|
||||
// Validate name
|
||||
name = NormalizeName(name)
|
||||
if err := ValidateName(name); err != nil {
|
||||
return nil, fmt.Errorf("invalid name: %w", err)
|
||||
}
|
||||
|
||||
// Create event
|
||||
ev := event.New()
|
||||
ev.Kind = KindCertificate
|
||||
ev.CreatedAt = timestamp.Now().V
|
||||
ev.Pubkey = signer.Pub()
|
||||
|
||||
// Build tags
|
||||
tags := tag.NewS()
|
||||
tags.Append(tag.NewFromAny("d", name))
|
||||
tags.Append(tag.NewFromAny("name", name))
|
||||
tags.Append(tag.NewFromAny("cert_pubkey", certPubkey))
|
||||
tags.Append(tag.NewFromAny("valid_from", strconv.FormatInt(validFrom.Unix(), 10)))
|
||||
tags.Append(tag.NewFromAny("valid_until", strconv.FormatInt(validUntil.Unix(), 10)))
|
||||
tags.Append(tag.NewFromAny("challenge", challenge))
|
||||
tags.Append(tag.NewFromAny("challenge_proof", challengeProof))
|
||||
|
||||
// Add witness signatures
|
||||
for _, w := range witnesses {
|
||||
tags.Append(tag.NewFromAny("witness", w.Pubkey, w.Signature))
|
||||
}
|
||||
|
||||
ev.Tags = tags
|
||||
|
||||
// Add metadata to content
|
||||
content := fmt.Sprintf(`{"algorithm":"%s","usage":"%s"}`, algorithm, usage)
|
||||
ev.Content = []byte(content)
|
||||
|
||||
// Sign the event
|
||||
if err := ev.Sign(signer); err != nil {
|
||||
return nil, fmt.Errorf("failed to sign certificate: %w", err)
|
||||
}
|
||||
|
||||
return ev, nil
|
||||
}
|
||||
|
||||
// NewWitnessService creates a new witness service info event (kind 30105)
|
||||
func NewWitnessService(endpoint string, challenges []string, maxValidity, fee int,
|
||||
reputationID, description, contact string, signer signer.I) (*event.E, error) {
|
||||
|
||||
// Create event
|
||||
ev := event.New()
|
||||
ev.Kind = KindWitnessService
|
||||
ev.CreatedAt = timestamp.Now().V
|
||||
ev.Pubkey = signer.Pub()
|
||||
|
||||
// Build tags
|
||||
tags := tag.NewS()
|
||||
tags.Append(tag.NewFromAny("d", "witness-service"))
|
||||
tags.Append(tag.NewFromAny("endpoint", endpoint))
|
||||
|
||||
for _, ch := range challenges {
|
||||
tags.Append(tag.NewFromAny("challenges", ch))
|
||||
}
|
||||
|
||||
if maxValidity > 0 {
|
||||
tags.Append(tag.NewFromAny("max_validity", strconv.Itoa(maxValidity)))
|
||||
}
|
||||
|
||||
if fee > 0 {
|
||||
tags.Append(tag.NewFromAny("fee", strconv.Itoa(fee)))
|
||||
}
|
||||
|
||||
if reputationID != "" {
|
||||
tags.Append(tag.NewFromAny("reputation", reputationID))
|
||||
}
|
||||
|
||||
// Add expiration tag (180 days from now)
|
||||
expiration := time.Now().Add(WitnessServiceExpiry).Unix()
|
||||
tags.Append(tag.NewFromAny("expiration", strconv.FormatInt(expiration, 10)))
|
||||
|
||||
ev.Tags = tags
|
||||
|
||||
// Add metadata to content
|
||||
content := fmt.Sprintf(`{"description":"%s","contact":"%s"}`, description, contact)
|
||||
ev.Content = []byte(content)
|
||||
|
||||
// Sign the event
|
||||
if err := ev.Sign(signer); err != nil {
|
||||
return nil, fmt.Errorf("failed to sign witness service: %w", err)
|
||||
}
|
||||
|
||||
return ev, nil
|
||||
}
|
||||
325
pkg/find/certificate.go
Normal file
325
pkg/find/certificate.go
Normal file
@@ -0,0 +1,325 @@
|
||||
package find
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"next.orly.dev/pkg/encoders/event"
|
||||
"next.orly.dev/pkg/encoders/hex"
|
||||
"next.orly.dev/pkg/interfaces/signer"
|
||||
)
|
||||
|
||||
// GenerateChallenge generates a random 32-byte challenge token
|
||||
func GenerateChallenge() (string, error) {
|
||||
challenge := make([]byte, 32)
|
||||
if _, err := rand.Read(challenge); err != nil {
|
||||
return "", fmt.Errorf("failed to generate random challenge: %w", err)
|
||||
}
|
||||
return hex.Enc(challenge), nil
|
||||
}
|
||||
|
||||
// CreateChallengeTXTRecord creates a TXT record event for challenge-response verification
|
||||
func CreateChallengeTXTRecord(name, challenge string, ttl int, signer signer.I) (*event.E, error) {
|
||||
// Normalize name
|
||||
name = NormalizeName(name)
|
||||
|
||||
// Validate name
|
||||
if err := ValidateName(name); err != nil {
|
||||
return nil, fmt.Errorf("invalid name: %w", err)
|
||||
}
|
||||
|
||||
// Create TXT record value
|
||||
txtValue := fmt.Sprintf("_nostr-challenge=%s", challenge)
|
||||
|
||||
// Create the TXT record event
|
||||
record, err := NewNameRecord(name, RecordTypeTXT, txtValue, ttl, signer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create challenge TXT record: %w", err)
|
||||
}
|
||||
|
||||
return record, nil
|
||||
}
|
||||
|
||||
// ExtractChallengeFromTXTRecord extracts the challenge token from a TXT record value
|
||||
func ExtractChallengeFromTXTRecord(txtValue string) (string, error) {
|
||||
const prefix = "_nostr-challenge="
|
||||
|
||||
if len(txtValue) < len(prefix) {
|
||||
return "", fmt.Errorf("TXT record too short")
|
||||
}
|
||||
|
||||
if txtValue[:len(prefix)] != prefix {
|
||||
return "", fmt.Errorf("not a challenge TXT record")
|
||||
}
|
||||
|
||||
challenge := txtValue[len(prefix):]
|
||||
if len(challenge) != 64 { // 32 bytes in hex = 64 characters
|
||||
return "", fmt.Errorf("invalid challenge length: %d", len(challenge))
|
||||
}
|
||||
|
||||
return challenge, nil
|
||||
}
|
||||
|
||||
// CreateChallengeProof creates a challenge proof signature
|
||||
func CreateChallengeProof(challenge, name, certPubkey string, validUntil time.Time, signer signer.I) (string, error) {
|
||||
// Normalize name
|
||||
name = NormalizeName(name)
|
||||
|
||||
// Sign the challenge proof
|
||||
proof, err := SignChallengeProof(challenge, name, certPubkey, validUntil, signer)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create challenge proof: %w", err)
|
||||
}
|
||||
|
||||
return proof, nil
|
||||
}
|
||||
|
||||
// RequestWitnessSignature creates a witness signature for a certificate
|
||||
// This would typically be called by a witness service
|
||||
func RequestWitnessSignature(cert *Certificate, witnessSigner signer.I) (WitnessSignature, error) {
|
||||
// Sign the witness message
|
||||
sig, err := SignWitnessMessage(cert.CertPubkey, cert.Name,
|
||||
cert.ValidFrom, cert.ValidUntil, cert.Challenge, witnessSigner)
|
||||
if err != nil {
|
||||
return WitnessSignature{}, fmt.Errorf("failed to create witness signature: %w", err)
|
||||
}
|
||||
|
||||
// Get witness pubkey
|
||||
witnessPubkey := hex.Enc(witnessSigner.Pub())
|
||||
|
||||
return WitnessSignature{
|
||||
Pubkey: witnessPubkey,
|
||||
Signature: sig,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// PrepareCertificateRequest prepares all the data needed for a certificate request
|
||||
type CertificateRequest struct {
|
||||
Name string
|
||||
CertPubkey string
|
||||
ValidFrom time.Time
|
||||
ValidUntil time.Time
|
||||
Challenge string
|
||||
ChallengeProof string
|
||||
}
|
||||
|
||||
// CreateCertificateRequest creates a certificate request with challenge-response
|
||||
func CreateCertificateRequest(name, certPubkey string, validityDuration time.Duration,
|
||||
challenge string, ownerSigner signer.I) (*CertificateRequest, error) {
|
||||
|
||||
// Normalize name
|
||||
name = NormalizeName(name)
|
||||
|
||||
// Validate name
|
||||
if err := ValidateName(name); err != nil {
|
||||
return nil, fmt.Errorf("invalid name: %w", err)
|
||||
}
|
||||
|
||||
// Set validity period
|
||||
validFrom := time.Now()
|
||||
validUntil := validFrom.Add(validityDuration)
|
||||
|
||||
// Create challenge proof
|
||||
proof, err := CreateChallengeProof(challenge, name, certPubkey, validUntil, ownerSigner)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create challenge proof: %w", err)
|
||||
}
|
||||
|
||||
return &CertificateRequest{
|
||||
Name: name,
|
||||
CertPubkey: certPubkey,
|
||||
ValidFrom: validFrom,
|
||||
ValidUntil: validUntil,
|
||||
Challenge: challenge,
|
||||
ChallengeProof: proof,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateCertificateWithWitnesses creates a complete certificate event with witness signatures
|
||||
func CreateCertificateWithWitnesses(req *CertificateRequest, witnesses []WitnessSignature,
|
||||
algorithm, usage string, ownerSigner signer.I) (*event.E, error) {
|
||||
|
||||
// Create the certificate event
|
||||
certEvent, err := NewCertificate(
|
||||
req.Name,
|
||||
req.CertPubkey,
|
||||
req.ValidFrom,
|
||||
req.ValidUntil,
|
||||
req.Challenge,
|
||||
req.ChallengeProof,
|
||||
witnesses,
|
||||
algorithm,
|
||||
usage,
|
||||
ownerSigner,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create certificate: %w", err)
|
||||
}
|
||||
|
||||
return certEvent, nil
|
||||
}
|
||||
|
||||
// VerifyChallengeTXTRecord verifies that a TXT record contains the expected challenge
|
||||
func VerifyChallengeTXTRecord(record *NameRecord, expectedChallenge string, nameOwner string) error {
|
||||
// Check record type
|
||||
if record.Type != RecordTypeTXT {
|
||||
return fmt.Errorf("not a TXT record: %s", record.Type)
|
||||
}
|
||||
|
||||
// Check record owner matches name owner
|
||||
recordOwner := hex.Enc(record.Event.Pubkey)
|
||||
if recordOwner != nameOwner {
|
||||
return fmt.Errorf("record owner %s != name owner %s", recordOwner, nameOwner)
|
||||
}
|
||||
|
||||
// Extract challenge from TXT record
|
||||
challenge, err := ExtractChallengeFromTXTRecord(record.Value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to extract challenge: %w", err)
|
||||
}
|
||||
|
||||
// Verify challenge matches
|
||||
if challenge != expectedChallenge {
|
||||
return fmt.Errorf("challenge mismatch: got %s, expected %s", challenge, expectedChallenge)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IssueCertificate is a helper that goes through the full certificate issuance process
|
||||
// This would typically be used by a name owner to request a certificate
|
||||
func IssueCertificate(name, certPubkey string, validityDuration time.Duration,
|
||||
ownerSigner signer.I, witnessSigners []signer.I) (*Certificate, error) {
|
||||
|
||||
// Generate challenge
|
||||
challenge, err := GenerateChallenge()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate challenge: %w", err)
|
||||
}
|
||||
|
||||
// Create certificate request
|
||||
req, err := CreateCertificateRequest(name, certPubkey, validityDuration, challenge, ownerSigner)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create certificate request: %w", err)
|
||||
}
|
||||
|
||||
// Collect witness signatures
|
||||
var witnesses []WitnessSignature
|
||||
for i, ws := range witnessSigners {
|
||||
// Create temporary certificate for witness to sign
|
||||
tempCert := &Certificate{
|
||||
Name: req.Name,
|
||||
CertPubkey: req.CertPubkey,
|
||||
ValidFrom: req.ValidFrom,
|
||||
ValidUntil: req.ValidUntil,
|
||||
Challenge: req.Challenge,
|
||||
}
|
||||
|
||||
witness, err := RequestWitnessSignature(tempCert, ws)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get witness %d signature: %w", i, err)
|
||||
}
|
||||
|
||||
witnesses = append(witnesses, witness)
|
||||
}
|
||||
|
||||
// Create certificate event
|
||||
certEvent, err := CreateCertificateWithWitnesses(req, witnesses, "secp256k1-schnorr", "tls-replacement", ownerSigner)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create certificate event: %w", err)
|
||||
}
|
||||
|
||||
// Parse back to Certificate struct
|
||||
cert, err := ParseCertificate(certEvent)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse certificate: %w", err)
|
||||
}
|
||||
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
// RenewCertificate creates a renewed certificate with a new validity period
|
||||
func RenewCertificate(oldCert *Certificate, newValidityDuration time.Duration,
|
||||
ownerSigner signer.I, witnessSigners []signer.I) (*Certificate, error) {
|
||||
|
||||
// Generate new challenge
|
||||
challenge, err := GenerateChallenge()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate challenge: %w", err)
|
||||
}
|
||||
|
||||
// Set new validity period (with 7-day overlap)
|
||||
validFrom := oldCert.ValidUntil.Add(-7 * 24 * time.Hour)
|
||||
validUntil := validFrom.Add(newValidityDuration)
|
||||
|
||||
// Create challenge proof
|
||||
proof, err := CreateChallengeProof(challenge, oldCert.Name, oldCert.CertPubkey, validUntil, ownerSigner)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create challenge proof: %w", err)
|
||||
}
|
||||
|
||||
// Create request
|
||||
req := &CertificateRequest{
|
||||
Name: oldCert.Name,
|
||||
CertPubkey: oldCert.CertPubkey,
|
||||
ValidFrom: validFrom,
|
||||
ValidUntil: validUntil,
|
||||
Challenge: challenge,
|
||||
ChallengeProof: proof,
|
||||
}
|
||||
|
||||
// Collect witness signatures
|
||||
var witnesses []WitnessSignature
|
||||
for i, ws := range witnessSigners {
|
||||
tempCert := &Certificate{
|
||||
Name: req.Name,
|
||||
CertPubkey: req.CertPubkey,
|
||||
ValidFrom: req.ValidFrom,
|
||||
ValidUntil: req.ValidUntil,
|
||||
Challenge: req.Challenge,
|
||||
}
|
||||
|
||||
witness, err := RequestWitnessSignature(tempCert, ws)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get witness %d signature: %w", i, err)
|
||||
}
|
||||
|
||||
witnesses = append(witnesses, witness)
|
||||
}
|
||||
|
||||
// Create certificate event
|
||||
certEvent, err := CreateCertificateWithWitnesses(req, witnesses, oldCert.Algorithm, oldCert.Usage, ownerSigner)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create certificate event: %w", err)
|
||||
}
|
||||
|
||||
// Parse back to Certificate struct
|
||||
cert, err := ParseCertificate(certEvent)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse certificate: %w", err)
|
||||
}
|
||||
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
// CheckCertificateExpiry returns the time until expiration, or error if expired
|
||||
func CheckCertificateExpiry(cert *Certificate) (time.Duration, error) {
|
||||
now := time.Now()
|
||||
|
||||
if now.After(cert.ValidUntil) {
|
||||
return 0, fmt.Errorf("certificate expired %v ago", now.Sub(cert.ValidUntil))
|
||||
}
|
||||
|
||||
return cert.ValidUntil.Sub(now), nil
|
||||
}
|
||||
|
||||
// ShouldRenewCertificate checks if a certificate should be renewed (< 30 days until expiry)
|
||||
func ShouldRenewCertificate(cert *Certificate) bool {
|
||||
timeUntilExpiry, err := CheckCertificateExpiry(cert)
|
||||
if err != nil {
|
||||
return true // Expired, definitely should renew
|
||||
}
|
||||
|
||||
return timeUntilExpiry < 30*24*time.Hour
|
||||
}
|
||||
455
pkg/find/parser.go
Normal file
455
pkg/find/parser.go
Normal file
@@ -0,0 +1,455 @@
|
||||
package find
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"next.orly.dev/pkg/encoders/event"
|
||||
"next.orly.dev/pkg/encoders/tag"
|
||||
)
|
||||
|
||||
// getTagValue retrieves the value of the first tag with the given key
|
||||
func getTagValue(ev *event.E, key string) string {
|
||||
t := ev.Tags.GetFirst([]byte(key))
|
||||
if t == nil {
|
||||
return ""
|
||||
}
|
||||
return string(t.Value())
|
||||
}
|
||||
|
||||
// getAllTags retrieves all tags with the given key
|
||||
func getAllTags(ev *event.E, key string) []*tag.T {
|
||||
return ev.Tags.GetAll([]byte(key))
|
||||
}
|
||||
|
||||
// ParseRegistrationProposal parses a kind 30100 event into a RegistrationProposal
|
||||
func ParseRegistrationProposal(ev *event.E) (*RegistrationProposal, error) {
|
||||
if uint16(ev.Kind) != KindRegistrationProposal {
|
||||
return nil, fmt.Errorf("invalid event kind: expected %d, got %d", KindRegistrationProposal, ev.Kind)
|
||||
}
|
||||
|
||||
name := getTagValue(ev, "d")
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("missing 'd' tag (name)")
|
||||
}
|
||||
|
||||
action := getTagValue(ev, "action")
|
||||
if action == "" {
|
||||
return nil, fmt.Errorf("missing 'action' tag")
|
||||
}
|
||||
|
||||
expirationStr := getTagValue(ev, "expiration")
|
||||
var expiration time.Time
|
||||
if expirationStr != "" {
|
||||
expirationUnix, err := strconv.ParseInt(expirationStr, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid expiration timestamp: %w", err)
|
||||
}
|
||||
expiration = time.Unix(expirationUnix, 0)
|
||||
}
|
||||
|
||||
proposal := &RegistrationProposal{
|
||||
Event: ev,
|
||||
Name: name,
|
||||
Action: action,
|
||||
PrevOwner: getTagValue(ev, "prev_owner"),
|
||||
PrevSig: getTagValue(ev, "prev_sig"),
|
||||
Expiration: expiration,
|
||||
}
|
||||
|
||||
return proposal, nil
|
||||
}
|
||||
|
||||
// ParseAttestation parses a kind 20100 event into an Attestation
|
||||
func ParseAttestation(ev *event.E) (*Attestation, error) {
|
||||
if uint16(ev.Kind) != KindAttestation {
|
||||
return nil, fmt.Errorf("invalid event kind: expected %d, got %d", KindAttestation, ev.Kind)
|
||||
}
|
||||
|
||||
proposalID := getTagValue(ev, "e")
|
||||
if proposalID == "" {
|
||||
return nil, fmt.Errorf("missing 'e' tag (proposal ID)")
|
||||
}
|
||||
|
||||
decision := getTagValue(ev, "decision")
|
||||
if decision == "" {
|
||||
return nil, fmt.Errorf("missing 'decision' tag")
|
||||
}
|
||||
|
||||
weightStr := getTagValue(ev, "weight")
|
||||
weight := 100 // default weight
|
||||
if weightStr != "" {
|
||||
w, err := strconv.Atoi(weightStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid weight value: %w", err)
|
||||
}
|
||||
weight = w
|
||||
}
|
||||
|
||||
expirationStr := getTagValue(ev, "expiration")
|
||||
var expiration time.Time
|
||||
if expirationStr != "" {
|
||||
expirationUnix, err := strconv.ParseInt(expirationStr, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid expiration timestamp: %w", err)
|
||||
}
|
||||
expiration = time.Unix(expirationUnix, 0)
|
||||
}
|
||||
|
||||
attestation := &Attestation{
|
||||
Event: ev,
|
||||
ProposalID: proposalID,
|
||||
Decision: decision,
|
||||
Weight: weight,
|
||||
Reason: getTagValue(ev, "reason"),
|
||||
ServiceURL: getTagValue(ev, "service"),
|
||||
Expiration: expiration,
|
||||
}
|
||||
|
||||
return attestation, nil
|
||||
}
|
||||
|
||||
// ParseTrustGraph parses a kind 30101 event into a TrustGraph
|
||||
func ParseTrustGraph(ev *event.E) (*TrustGraph, error) {
|
||||
if uint16(ev.Kind) != KindTrustGraph {
|
||||
return nil, fmt.Errorf("invalid event kind: expected %d, got %d", KindTrustGraph, ev.Kind)
|
||||
}
|
||||
|
||||
expirationStr := getTagValue(ev, "expiration")
|
||||
var expiration time.Time
|
||||
if expirationStr != "" {
|
||||
expirationUnix, err := strconv.ParseInt(expirationStr, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid expiration timestamp: %w", err)
|
||||
}
|
||||
expiration = time.Unix(expirationUnix, 0)
|
||||
}
|
||||
|
||||
// Parse p tags (trust entries)
|
||||
var entries []TrustEntry
|
||||
pTags := getAllTags(ev, "p")
|
||||
for _, t := range pTags {
|
||||
if len(t.T) < 2 {
|
||||
continue // Skip malformed tags
|
||||
}
|
||||
|
||||
pubkey := string(t.T[1])
|
||||
serviceURL := ""
|
||||
trustScore := 0.5 // default
|
||||
|
||||
if len(t.T) > 2 {
|
||||
serviceURL = string(t.T[2])
|
||||
}
|
||||
|
||||
if len(t.T) > 3 {
|
||||
score, err := strconv.ParseFloat(string(t.T[3]), 64)
|
||||
if err == nil {
|
||||
trustScore = score
|
||||
}
|
||||
}
|
||||
|
||||
entries = append(entries, TrustEntry{
|
||||
Pubkey: pubkey,
|
||||
ServiceURL: serviceURL,
|
||||
TrustScore: trustScore,
|
||||
})
|
||||
}
|
||||
|
||||
return &TrustGraph{
|
||||
Event: ev,
|
||||
Entries: entries,
|
||||
Expiration: expiration,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ParseNameState parses a kind 30102 event into a NameState
|
||||
func ParseNameState(ev *event.E) (*NameState, error) {
|
||||
if uint16(ev.Kind) != KindNameState {
|
||||
return nil, fmt.Errorf("invalid event kind: expected %d, got %d", KindNameState, ev.Kind)
|
||||
}
|
||||
|
||||
name := getTagValue(ev, "d")
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("missing 'd' tag (name)")
|
||||
}
|
||||
|
||||
owner := getTagValue(ev, "owner")
|
||||
if owner == "" {
|
||||
return nil, fmt.Errorf("missing 'owner' tag")
|
||||
}
|
||||
|
||||
registeredAtStr := getTagValue(ev, "registered_at")
|
||||
if registeredAtStr == "" {
|
||||
return nil, fmt.Errorf("missing 'registered_at' tag")
|
||||
}
|
||||
registeredAtUnix, err := strconv.ParseInt(registeredAtStr, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid registered_at timestamp: %w", err)
|
||||
}
|
||||
registeredAt := time.Unix(registeredAtUnix, 0)
|
||||
|
||||
attestationsStr := getTagValue(ev, "attestations")
|
||||
attestations := 0
|
||||
if attestationsStr != "" {
|
||||
a, err := strconv.Atoi(attestationsStr)
|
||||
if err == nil {
|
||||
attestations = a
|
||||
}
|
||||
}
|
||||
|
||||
confidenceStr := getTagValue(ev, "confidence")
|
||||
confidence := 0.0
|
||||
if confidenceStr != "" {
|
||||
c, err := strconv.ParseFloat(confidenceStr, 64)
|
||||
if err == nil {
|
||||
confidence = c
|
||||
}
|
||||
}
|
||||
|
||||
expirationStr := getTagValue(ev, "expiration")
|
||||
var expiration time.Time
|
||||
if expirationStr != "" {
|
||||
expirationUnix, err := strconv.ParseInt(expirationStr, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid expiration timestamp: %w", err)
|
||||
}
|
||||
expiration = time.Unix(expirationUnix, 0)
|
||||
}
|
||||
|
||||
return &NameState{
|
||||
Event: ev,
|
||||
Name: name,
|
||||
Owner: owner,
|
||||
RegisteredAt: registeredAt,
|
||||
ProposalID: getTagValue(ev, "proposal"),
|
||||
Attestations: attestations,
|
||||
Confidence: confidence,
|
||||
Expiration: expiration,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ParseNameRecord parses a kind 30103 event into a NameRecord
|
||||
func ParseNameRecord(ev *event.E) (*NameRecord, error) {
|
||||
if uint16(ev.Kind) != KindNameRecords {
|
||||
return nil, fmt.Errorf("invalid event kind: expected %d, got %d", KindNameRecords, ev.Kind)
|
||||
}
|
||||
|
||||
name := getTagValue(ev, "name")
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("missing 'name' tag")
|
||||
}
|
||||
|
||||
recordType := getTagValue(ev, "type")
|
||||
if recordType == "" {
|
||||
return nil, fmt.Errorf("missing 'type' tag")
|
||||
}
|
||||
|
||||
value := getTagValue(ev, "value")
|
||||
if value == "" {
|
||||
return nil, fmt.Errorf("missing 'value' tag")
|
||||
}
|
||||
|
||||
ttlStr := getTagValue(ev, "ttl")
|
||||
ttl := 3600 // default TTL
|
||||
if ttlStr != "" {
|
||||
t, err := strconv.Atoi(ttlStr)
|
||||
if err == nil {
|
||||
ttl = t
|
||||
}
|
||||
}
|
||||
|
||||
priorityStr := getTagValue(ev, "priority")
|
||||
priority := 0
|
||||
if priorityStr != "" {
|
||||
p, err := strconv.Atoi(priorityStr)
|
||||
if err == nil {
|
||||
priority = p
|
||||
}
|
||||
}
|
||||
|
||||
weightStr := getTagValue(ev, "weight")
|
||||
weight := 0
|
||||
if weightStr != "" {
|
||||
w, err := strconv.Atoi(weightStr)
|
||||
if err == nil {
|
||||
weight = w
|
||||
}
|
||||
}
|
||||
|
||||
portStr := getTagValue(ev, "port")
|
||||
port := 0
|
||||
if portStr != "" {
|
||||
p, err := strconv.Atoi(portStr)
|
||||
if err == nil {
|
||||
port = p
|
||||
}
|
||||
}
|
||||
|
||||
return &NameRecord{
|
||||
Event: ev,
|
||||
Name: name,
|
||||
Type: recordType,
|
||||
Value: value,
|
||||
TTL: ttl,
|
||||
Priority: priority,
|
||||
Weight: weight,
|
||||
Port: port,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ParseCertificate parses a kind 30104 event into a Certificate
|
||||
func ParseCertificate(ev *event.E) (*Certificate, error) {
|
||||
if uint16(ev.Kind) != KindCertificate {
|
||||
return nil, fmt.Errorf("invalid event kind: expected %d, got %d", KindCertificate, ev.Kind)
|
||||
}
|
||||
|
||||
name := getTagValue(ev, "name")
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("missing 'name' tag")
|
||||
}
|
||||
|
||||
certPubkey := getTagValue(ev, "cert_pubkey")
|
||||
if certPubkey == "" {
|
||||
return nil, fmt.Errorf("missing 'cert_pubkey' tag")
|
||||
}
|
||||
|
||||
validFromStr := getTagValue(ev, "valid_from")
|
||||
if validFromStr == "" {
|
||||
return nil, fmt.Errorf("missing 'valid_from' tag")
|
||||
}
|
||||
validFromUnix, err := strconv.ParseInt(validFromStr, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid valid_from timestamp: %w", err)
|
||||
}
|
||||
validFrom := time.Unix(validFromUnix, 0)
|
||||
|
||||
validUntilStr := getTagValue(ev, "valid_until")
|
||||
if validUntilStr == "" {
|
||||
return nil, fmt.Errorf("missing 'valid_until' tag")
|
||||
}
|
||||
validUntilUnix, err := strconv.ParseInt(validUntilStr, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid valid_until timestamp: %w", err)
|
||||
}
|
||||
validUntil := time.Unix(validUntilUnix, 0)
|
||||
|
||||
// Parse witness tags
|
||||
var witnesses []WitnessSignature
|
||||
witnessTags := getAllTags(ev, "witness")
|
||||
for _, t := range witnessTags {
|
||||
if len(t.T) < 3 {
|
||||
continue // Skip malformed tags
|
||||
}
|
||||
|
||||
witnesses = append(witnesses, WitnessSignature{
|
||||
Pubkey: string(t.T[1]),
|
||||
Signature: string(t.T[2]),
|
||||
})
|
||||
}
|
||||
|
||||
// Parse content JSON
|
||||
algorithm := "secp256k1-schnorr"
|
||||
usage := "tls-replacement"
|
||||
if len(ev.Content) > 0 {
|
||||
var metadata map[string]interface{}
|
||||
if err := json.Unmarshal(ev.Content, &metadata); err == nil {
|
||||
if alg, ok := metadata["algorithm"].(string); ok {
|
||||
algorithm = alg
|
||||
}
|
||||
if u, ok := metadata["usage"].(string); ok {
|
||||
usage = u
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &Certificate{
|
||||
Event: ev,
|
||||
Name: name,
|
||||
CertPubkey: certPubkey,
|
||||
ValidFrom: validFrom,
|
||||
ValidUntil: validUntil,
|
||||
Challenge: getTagValue(ev, "challenge"),
|
||||
ChallengeProof: getTagValue(ev, "challenge_proof"),
|
||||
Witnesses: witnesses,
|
||||
Algorithm: algorithm,
|
||||
Usage: usage,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ParseWitnessService parses a kind 30105 event into a WitnessService
|
||||
func ParseWitnessService(ev *event.E) (*WitnessService, error) {
|
||||
if uint16(ev.Kind) != KindWitnessService {
|
||||
return nil, fmt.Errorf("invalid event kind: expected %d, got %d", KindWitnessService, ev.Kind)
|
||||
}
|
||||
|
||||
endpoint := getTagValue(ev, "endpoint")
|
||||
if endpoint == "" {
|
||||
return nil, fmt.Errorf("missing 'endpoint' tag")
|
||||
}
|
||||
|
||||
// Parse challenge tags
|
||||
var challenges []string
|
||||
challengeTags := getAllTags(ev, "challenges")
|
||||
for _, t := range challengeTags {
|
||||
if len(t.T) >= 2 {
|
||||
challenges = append(challenges, string(t.T[1]))
|
||||
}
|
||||
}
|
||||
|
||||
maxValidityStr := getTagValue(ev, "max_validity")
|
||||
maxValidity := 0
|
||||
if maxValidityStr != "" {
|
||||
mv, err := strconv.Atoi(maxValidityStr)
|
||||
if err == nil {
|
||||
maxValidity = mv
|
||||
}
|
||||
}
|
||||
|
||||
feeStr := getTagValue(ev, "fee")
|
||||
fee := 0
|
||||
if feeStr != "" {
|
||||
f, err := strconv.Atoi(feeStr)
|
||||
if err == nil {
|
||||
fee = f
|
||||
}
|
||||
}
|
||||
|
||||
expirationStr := getTagValue(ev, "expiration")
|
||||
var expiration time.Time
|
||||
if expirationStr != "" {
|
||||
expirationUnix, err := strconv.ParseInt(expirationStr, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid expiration timestamp: %w", err)
|
||||
}
|
||||
expiration = time.Unix(expirationUnix, 0)
|
||||
}
|
||||
|
||||
// Parse content JSON
|
||||
description := ""
|
||||
contact := ""
|
||||
if len(ev.Content) > 0 {
|
||||
var metadata map[string]interface{}
|
||||
if err := json.Unmarshal(ev.Content, &metadata); err == nil {
|
||||
if desc, ok := metadata["description"].(string); ok {
|
||||
description = desc
|
||||
}
|
||||
if cont, ok := metadata["contact"].(string); ok {
|
||||
contact = cont
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &WitnessService{
|
||||
Event: ev,
|
||||
Endpoint: endpoint,
|
||||
Challenges: challenges,
|
||||
MaxValidity: maxValidity,
|
||||
Fee: fee,
|
||||
ReputationID: getTagValue(ev, "reputation"),
|
||||
Description: description,
|
||||
Contact: contact,
|
||||
Expiration: expiration,
|
||||
}, nil
|
||||
}
|
||||
167
pkg/find/sign.go
Normal file
167
pkg/find/sign.go
Normal file
@@ -0,0 +1,167 @@
|
||||
package find
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"next.orly.dev/pkg/encoders/event"
|
||||
"next.orly.dev/pkg/encoders/hex"
|
||||
"next.orly.dev/pkg/interfaces/signer"
|
||||
)
|
||||
|
||||
// SignTransferAuth creates a signature for transfer authorization
|
||||
// Message format: transfer:<name>:<new_owner_pubkey>:<timestamp>
|
||||
func SignTransferAuth(name, newOwner string, timestamp time.Time, s signer.I) (string, error) {
|
||||
// Normalize name
|
||||
name = NormalizeName(name)
|
||||
|
||||
// Construct message
|
||||
message := fmt.Sprintf("transfer:%s:%s:%d", name, newOwner, timestamp.Unix())
|
||||
|
||||
// Hash the message
|
||||
hash := sha256.Sum256([]byte(message))
|
||||
|
||||
// Sign the hash
|
||||
sig, err := s.Sign(hash[:])
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to sign transfer authorization: %w", err)
|
||||
}
|
||||
|
||||
// Return hex-encoded signature
|
||||
return hex.Enc(sig), nil
|
||||
}
|
||||
|
||||
// SignChallengeProof creates a signature for certificate challenge proof
|
||||
// Message format: challenge||name||cert_pubkey||valid_until
|
||||
func SignChallengeProof(challenge, name, certPubkey string, validUntil time.Time, s signer.I) (string, error) {
|
||||
// Normalize name
|
||||
name = NormalizeName(name)
|
||||
|
||||
// Construct message
|
||||
message := fmt.Sprintf("%s||%s||%s||%d", challenge, name, certPubkey, validUntil.Unix())
|
||||
|
||||
// Hash the message
|
||||
hash := sha256.Sum256([]byte(message))
|
||||
|
||||
// Sign the hash
|
||||
sig, err := s.Sign(hash[:])
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to sign challenge proof: %w", err)
|
||||
}
|
||||
|
||||
// Return hex-encoded signature
|
||||
return hex.Enc(sig), nil
|
||||
}
|
||||
|
||||
// SignWitnessMessage creates a witness signature for a certificate
|
||||
// Message format: cert_pubkey||name||valid_from||valid_until||challenge
|
||||
func SignWitnessMessage(certPubkey, name string, validFrom, validUntil time.Time, challenge string, s signer.I) (string, error) {
|
||||
// Normalize name
|
||||
name = NormalizeName(name)
|
||||
|
||||
// Construct message
|
||||
message := fmt.Sprintf("%s||%s||%d||%d||%s",
|
||||
certPubkey, name, validFrom.Unix(), validUntil.Unix(), challenge)
|
||||
|
||||
// Hash the message
|
||||
hash := sha256.Sum256([]byte(message))
|
||||
|
||||
// Sign the hash
|
||||
sig, err := s.Sign(hash[:])
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to sign witness message: %w", err)
|
||||
}
|
||||
|
||||
// Return hex-encoded signature
|
||||
return hex.Enc(sig), nil
|
||||
}
|
||||
|
||||
// CreateTransferAuthMessage constructs the transfer authorization message
|
||||
// This is used for verification
|
||||
func CreateTransferAuthMessage(name, newOwner string, timestamp time.Time) []byte {
|
||||
name = NormalizeName(name)
|
||||
message := fmt.Sprintf("transfer:%s:%s:%d", name, newOwner, timestamp.Unix())
|
||||
hash := sha256.Sum256([]byte(message))
|
||||
return hash[:]
|
||||
}
|
||||
|
||||
// CreateChallengeProofMessage constructs the challenge proof message
|
||||
// This is used for verification
|
||||
func CreateChallengeProofMessage(challenge, name, certPubkey string, validUntil time.Time) []byte {
|
||||
name = NormalizeName(name)
|
||||
message := fmt.Sprintf("%s||%s||%s||%d", challenge, name, certPubkey, validUntil.Unix())
|
||||
hash := sha256.Sum256([]byte(message))
|
||||
return hash[:]
|
||||
}
|
||||
|
||||
// CreateWitnessMessage constructs the witness message
|
||||
// This is used for verification
|
||||
func CreateWitnessMessage(certPubkey, name string, validFrom, validUntil time.Time, challenge string) []byte {
|
||||
name = NormalizeName(name)
|
||||
message := fmt.Sprintf("%s||%s||%d||%d||%s",
|
||||
certPubkey, name, validFrom.Unix(), validUntil.Unix(), challenge)
|
||||
hash := sha256.Sum256([]byte(message))
|
||||
return hash[:]
|
||||
}
|
||||
|
||||
// ParseTimestampFromProposal extracts the timestamp from a transfer authorization message
|
||||
// Used for verification when the timestamp is embedded in the signature
|
||||
func ParseTimestampFromProposal(proposalTime time.Time) time.Time {
|
||||
// Round to nearest second for consistency
|
||||
return proposalTime.Truncate(time.Second)
|
||||
}
|
||||
|
||||
// FormatTransferAuthString formats the transfer auth message for display/debugging
|
||||
func FormatTransferAuthString(name, newOwner string, timestamp time.Time) string {
|
||||
name = NormalizeName(name)
|
||||
return fmt.Sprintf("transfer:%s:%s:%d", name, newOwner, timestamp.Unix())
|
||||
}
|
||||
|
||||
// FormatChallengeProofString formats the challenge proof message for display/debugging
|
||||
func FormatChallengeProofString(challenge, name, certPubkey string, validUntil time.Time) string {
|
||||
name = NormalizeName(name)
|
||||
return fmt.Sprintf("%s||%s||%s||%d", challenge, name, certPubkey, validUntil.Unix())
|
||||
}
|
||||
|
||||
// FormatWitnessString formats the witness message for display/debugging
|
||||
func FormatWitnessString(certPubkey, name string, validFrom, validUntil time.Time, challenge string) string {
|
||||
name = NormalizeName(name)
|
||||
return fmt.Sprintf("%s||%s||%d||%d||%s",
|
||||
certPubkey, name, validFrom.Unix(), validUntil.Unix(), challenge)
|
||||
}
|
||||
|
||||
// SignProposal signs a registration proposal event
|
||||
func SignProposal(ev *event.E, s signer.I) error {
|
||||
return ev.Sign(s)
|
||||
}
|
||||
|
||||
// SignAttestation signs an attestation event
|
||||
func SignAttestation(ev *event.E, s signer.I) error {
|
||||
return ev.Sign(s)
|
||||
}
|
||||
|
||||
// SignTrustGraph signs a trust graph event
|
||||
func SignTrustGraph(ev *event.E, s signer.I) error {
|
||||
return ev.Sign(s)
|
||||
}
|
||||
|
||||
// SignNameState signs a name state event
|
||||
func SignNameState(ev *event.E, s signer.I) error {
|
||||
return ev.Sign(s)
|
||||
}
|
||||
|
||||
// SignNameRecord signs a name record event
|
||||
func SignNameRecord(ev *event.E, s signer.I) error {
|
||||
return ev.Sign(s)
|
||||
}
|
||||
|
||||
// SignCertificate signs a certificate event
|
||||
func SignCertificate(ev *event.E, s signer.I) error {
|
||||
return ev.Sign(s)
|
||||
}
|
||||
|
||||
// SignWitnessService signs a witness service event
|
||||
func SignWitnessService(ev *event.E, s signer.I) error {
|
||||
return ev.Sign(s)
|
||||
}
|
||||
168
pkg/find/transfer.go
Normal file
168
pkg/find/transfer.go
Normal file
@@ -0,0 +1,168 @@
|
||||
package find
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"next.orly.dev/pkg/encoders/event"
|
||||
"next.orly.dev/pkg/encoders/hex"
|
||||
"next.orly.dev/pkg/interfaces/signer"
|
||||
)
|
||||
|
||||
// CreateTransferProposal creates a complete transfer proposal with authorization from previous owner
|
||||
func CreateTransferProposal(name string, prevOwnerSigner, newOwnerSigner signer.I) (*event.E, error) {
|
||||
// Normalize name
|
||||
name = NormalizeName(name)
|
||||
|
||||
// Validate name
|
||||
if err := ValidateName(name); err != nil {
|
||||
return nil, fmt.Errorf("invalid name: %w", err)
|
||||
}
|
||||
|
||||
// Get public keys
|
||||
prevOwnerPubkey := hex.Enc(prevOwnerSigner.Pub())
|
||||
newOwnerPubkey := hex.Enc(newOwnerSigner.Pub())
|
||||
|
||||
// Create timestamp for the transfer
|
||||
timestamp := time.Now()
|
||||
|
||||
// Sign the transfer authorization with previous owner's key
|
||||
prevSig, err := SignTransferAuth(name, newOwnerPubkey, timestamp, prevOwnerSigner)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create transfer authorization: %w", err)
|
||||
}
|
||||
|
||||
// Create the transfer proposal event signed by new owner
|
||||
proposal, err := NewRegistrationProposalWithTransfer(name, prevOwnerPubkey, prevSig, newOwnerSigner)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create transfer proposal: %w", err)
|
||||
}
|
||||
|
||||
return proposal, nil
|
||||
}
|
||||
|
||||
// ValidateTransferProposal validates a transfer proposal against the current owner
|
||||
func ValidateTransferProposal(proposal *RegistrationProposal, currentOwner string) error {
|
||||
// Check that this is a transfer action
|
||||
if proposal.Action != ActionTransfer {
|
||||
return fmt.Errorf("not a transfer action: %s", proposal.Action)
|
||||
}
|
||||
|
||||
// Check that prev_owner is set
|
||||
if proposal.PrevOwner == "" {
|
||||
return fmt.Errorf("missing prev_owner in transfer proposal")
|
||||
}
|
||||
|
||||
// Check that prev_sig is set
|
||||
if proposal.PrevSig == "" {
|
||||
return fmt.Errorf("missing prev_sig in transfer proposal")
|
||||
}
|
||||
|
||||
// Verify that prev_owner matches current owner
|
||||
if proposal.PrevOwner != currentOwner {
|
||||
return fmt.Errorf("prev_owner %s does not match current owner %s",
|
||||
proposal.PrevOwner, currentOwner)
|
||||
}
|
||||
|
||||
// Get new owner from proposal event
|
||||
newOwnerPubkey := hex.Enc(proposal.Event.Pubkey)
|
||||
|
||||
// Verify the transfer authorization signature
|
||||
// Use proposal creation time as timestamp
|
||||
timestamp := time.Unix(proposal.Event.CreatedAt, 0)
|
||||
|
||||
ok, err := VerifyTransferAuth(proposal.Name, newOwnerPubkey, proposal.PrevOwner,
|
||||
timestamp, proposal.PrevSig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("transfer authorization verification failed: %w", err)
|
||||
}
|
||||
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid transfer authorization signature")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PrepareTransferAuth prepares the transfer authorization data that needs to be signed
|
||||
// This is a helper for wallets/clients that want to show what they're signing
|
||||
func PrepareTransferAuth(name, newOwner string, timestamp time.Time) TransferAuthorization {
|
||||
return TransferAuthorization{
|
||||
Name: NormalizeName(name),
|
||||
NewOwner: newOwner,
|
||||
Timestamp: timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
// AuthorizeTransfer creates a transfer authorization signature
|
||||
// This is meant to be used by the current owner to authorize a transfer to a new owner
|
||||
func AuthorizeTransfer(name, newOwnerPubkey string, ownerSigner signer.I) (prevSig string, timestamp time.Time, err error) {
|
||||
// Normalize name
|
||||
name = NormalizeName(name)
|
||||
|
||||
// Validate name
|
||||
if err := ValidateName(name); err != nil {
|
||||
return "", time.Time{}, fmt.Errorf("invalid name: %w", err)
|
||||
}
|
||||
|
||||
// Create timestamp
|
||||
timestamp = time.Now()
|
||||
|
||||
// Sign the authorization
|
||||
prevSig, err = SignTransferAuth(name, newOwnerPubkey, timestamp, ownerSigner)
|
||||
if err != nil {
|
||||
return "", time.Time{}, fmt.Errorf("failed to sign transfer auth: %w", err)
|
||||
}
|
||||
|
||||
return prevSig, timestamp, nil
|
||||
}
|
||||
|
||||
// CreateTransferProposalWithAuth creates a transfer proposal using a pre-existing authorization
|
||||
// This is useful when the previous owner has already provided their signature
|
||||
func CreateTransferProposalWithAuth(name, prevOwnerPubkey, prevSig string, newOwnerSigner signer.I) (*event.E, error) {
|
||||
// Normalize name
|
||||
name = NormalizeName(name)
|
||||
|
||||
// Validate name
|
||||
if err := ValidateName(name); err != nil {
|
||||
return nil, fmt.Errorf("invalid name: %w", err)
|
||||
}
|
||||
|
||||
// Create the transfer proposal event
|
||||
proposal, err := NewRegistrationProposalWithTransfer(name, prevOwnerPubkey, prevSig, newOwnerSigner)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create transfer proposal: %w", err)
|
||||
}
|
||||
|
||||
return proposal, nil
|
||||
}
|
||||
|
||||
// VerifyTransferProposalSignature verifies both the event signature and transfer authorization
|
||||
func VerifyTransferProposalSignature(proposal *RegistrationProposal) error {
|
||||
// Verify the event signature itself
|
||||
if err := VerifyEvent(proposal.Event); err != nil {
|
||||
return fmt.Errorf("invalid event signature: %w", err)
|
||||
}
|
||||
|
||||
// If this is a transfer, verify the transfer authorization
|
||||
if proposal.Action == ActionTransfer {
|
||||
// Get new owner from proposal event
|
||||
newOwnerPubkey := hex.Enc(proposal.Event.Pubkey)
|
||||
|
||||
// Use proposal creation time as timestamp
|
||||
timestamp := time.Unix(proposal.Event.CreatedAt, 0)
|
||||
|
||||
// Verify transfer auth
|
||||
ok, err := VerifyTransferAuth(proposal.Name, newOwnerPubkey, proposal.PrevOwner,
|
||||
timestamp, proposal.PrevSig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("transfer authorization verification failed: %w", err)
|
||||
}
|
||||
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid transfer authorization signature")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
180
pkg/find/types.go
Normal file
180
pkg/find/types.go
Normal file
@@ -0,0 +1,180 @@
|
||||
package find
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"next.orly.dev/pkg/encoders/event"
|
||||
)
|
||||
|
||||
// Event kind constants as defined in the NIP
|
||||
const (
|
||||
KindRegistrationProposal = 30100 // Parameterized replaceable
|
||||
KindAttestation = 20100 // Ephemeral
|
||||
KindTrustGraph = 30101 // Parameterized replaceable
|
||||
KindNameState = 30102 // Parameterized replaceable
|
||||
KindNameRecords = 30103 // Parameterized replaceable
|
||||
KindCertificate = 30104 // Parameterized replaceable
|
||||
KindWitnessService = 30105 // Parameterized replaceable
|
||||
)
|
||||
|
||||
// Action types for registration proposals
|
||||
const (
|
||||
ActionRegister = "register"
|
||||
ActionTransfer = "transfer"
|
||||
)
|
||||
|
||||
// Decision types for attestations
|
||||
const (
|
||||
DecisionApprove = "approve"
|
||||
DecisionReject = "reject"
|
||||
DecisionAbstain = "abstain"
|
||||
)
|
||||
|
||||
// DNS record types
|
||||
const (
|
||||
RecordTypeA = "A"
|
||||
RecordTypeAAAA = "AAAA"
|
||||
RecordTypeCNAME = "CNAME"
|
||||
RecordTypeMX = "MX"
|
||||
RecordTypeTXT = "TXT"
|
||||
RecordTypeNS = "NS"
|
||||
RecordTypeSRV = "SRV"
|
||||
)
|
||||
|
||||
// Time constants
|
||||
const (
|
||||
ProposalExpiry = 5 * time.Minute // Proposals expire after 5 minutes
|
||||
AttestationExpiry = 3 * time.Minute // Attestations expire after 3 minutes
|
||||
TrustGraphExpiry = 30 * 24 * time.Hour // Trust graphs expire after 30 days
|
||||
NameRegistrationPeriod = 365 * 24 * time.Hour // Names expire after 1 year
|
||||
PreferentialRenewalDays = 30 // Final 30 days before expiration
|
||||
CertificateValidity = 90 * 24 * time.Hour // Recommended certificate validity
|
||||
WitnessServiceExpiry = 180 * 24 * time.Hour // Witness service info expires after 180 days
|
||||
)
|
||||
|
||||
// RegistrationProposal represents a kind 30100 event
|
||||
type RegistrationProposal struct {
|
||||
Event *event.E
|
||||
Name string
|
||||
Action string // "register" or "transfer"
|
||||
PrevOwner string // Previous owner pubkey (for transfers)
|
||||
PrevSig string // Signature from previous owner (for transfers)
|
||||
Expiration time.Time
|
||||
}
|
||||
|
||||
// Attestation represents a kind 20100 event
|
||||
type Attestation struct {
|
||||
Event *event.E
|
||||
ProposalID string // Event ID of the proposal being attested
|
||||
Decision string // "approve", "reject", or "abstain"
|
||||
Weight int // Stake/confidence weight (default 100)
|
||||
Reason string // Human-readable justification
|
||||
ServiceURL string // Registry service endpoint
|
||||
Expiration time.Time
|
||||
}
|
||||
|
||||
// TrustEntry represents a single trust relationship
|
||||
type TrustEntry struct {
|
||||
Pubkey string
|
||||
ServiceURL string
|
||||
TrustScore float64 // 0.0 to 1.0
|
||||
}
|
||||
|
||||
// TrustGraph represents a kind 30101 event
|
||||
type TrustGraph struct {
|
||||
Event *event.E
|
||||
Entries []TrustEntry
|
||||
Expiration time.Time
|
||||
}
|
||||
|
||||
// NameState represents a kind 30102 event
|
||||
type NameState struct {
|
||||
Event *event.E
|
||||
Name string
|
||||
Owner string // Current owner pubkey
|
||||
RegisteredAt time.Time
|
||||
ProposalID string // Event ID of the registration proposal
|
||||
Attestations int // Number of attestations
|
||||
Confidence float64 // Consensus confidence score (0.0 to 1.0)
|
||||
Expiration time.Time
|
||||
}
|
||||
|
||||
// NameRecord represents a kind 30103 event
|
||||
type NameRecord struct {
|
||||
Event *event.E
|
||||
Name string
|
||||
Type string // A, AAAA, CNAME, MX, TXT, NS, SRV
|
||||
Value string
|
||||
TTL int // Cache TTL in seconds
|
||||
Priority int // For MX and SRV records
|
||||
Weight int // For SRV records
|
||||
Port int // For SRV records
|
||||
}
|
||||
|
||||
// RecordLimits defines per-type record limits
|
||||
var RecordLimits = map[string]int{
|
||||
RecordTypeA: 5,
|
||||
RecordTypeAAAA: 5,
|
||||
RecordTypeCNAME: 1,
|
||||
RecordTypeMX: 5,
|
||||
RecordTypeTXT: 10,
|
||||
RecordTypeNS: 5,
|
||||
RecordTypeSRV: 10,
|
||||
}
|
||||
|
||||
// Certificate represents a kind 30104 event
|
||||
type Certificate struct {
|
||||
Event *event.E
|
||||
Name string
|
||||
CertPubkey string // Public key for the service
|
||||
ValidFrom time.Time
|
||||
ValidUntil time.Time
|
||||
Challenge string // Challenge token for ownership proof
|
||||
ChallengeProof string // Signature over challenge
|
||||
Witnesses []WitnessSignature
|
||||
Algorithm string // e.g., "secp256k1-schnorr"
|
||||
Usage string // e.g., "tls-replacement"
|
||||
}
|
||||
|
||||
// WitnessSignature represents a witness attestation on a certificate
|
||||
type WitnessSignature struct {
|
||||
Pubkey string
|
||||
Signature string
|
||||
}
|
||||
|
||||
// WitnessService represents a kind 30105 event
|
||||
type WitnessService struct {
|
||||
Event *event.E
|
||||
Endpoint string
|
||||
Challenges []string // Supported challenge types: "txt", "http", "event"
|
||||
MaxValidity int // Maximum certificate validity in seconds
|
||||
Fee int // Fee in sats per certificate
|
||||
ReputationID string // Event ID of reputation event
|
||||
Description string
|
||||
Contact string
|
||||
Expiration time.Time
|
||||
}
|
||||
|
||||
// TransferAuthorization represents the message signed for transfer authorization
|
||||
type TransferAuthorization struct {
|
||||
Name string
|
||||
NewOwner string
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
// ChallengeProofMessage represents the message signed for certificate challenge proof
|
||||
type ChallengeProofMessage struct {
|
||||
Challenge string
|
||||
Name string
|
||||
CertPubkey string
|
||||
ValidUntil time.Time
|
||||
}
|
||||
|
||||
// WitnessMessage represents the message signed by witnesses
|
||||
type WitnessMessage struct {
|
||||
CertPubkey string
|
||||
Name string
|
||||
ValidFrom time.Time
|
||||
ValidUntil time.Time
|
||||
Challenge string
|
||||
}
|
||||
221
pkg/find/validation.go
Normal file
221
pkg/find/validation.go
Normal file
@@ -0,0 +1,221 @@
|
||||
package find
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidName = errors.New("invalid name format")
|
||||
ErrNameTooLong = errors.New("name exceeds 253 characters")
|
||||
ErrLabelTooLong = errors.New("label exceeds 63 characters")
|
||||
ErrLabelEmpty = errors.New("label is empty")
|
||||
ErrInvalidCharacter = errors.New("invalid character in name")
|
||||
ErrInvalidHyphen = errors.New("label cannot start or end with hyphen")
|
||||
ErrAllNumericLabel = errors.New("label cannot be all numeric")
|
||||
ErrInvalidRecordValue = errors.New("invalid record value")
|
||||
ErrRecordLimitExceeded = errors.New("record limit exceeded")
|
||||
ErrNotOwner = errors.New("not the name owner")
|
||||
ErrNameExpired = errors.New("name registration expired")
|
||||
ErrInRenewalWindow = errors.New("name is in renewal window")
|
||||
ErrNotRenewalWindow = errors.New("not in renewal window")
|
||||
)
|
||||
|
||||
// Name format validation regex
|
||||
var (
|
||||
labelRegex = regexp.MustCompile(`^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$`)
|
||||
allNumeric = regexp.MustCompile(`^[0-9]+$`)
|
||||
)
|
||||
|
||||
// NormalizeName converts a name to lowercase
|
||||
func NormalizeName(name string) string {
|
||||
return strings.ToLower(name)
|
||||
}
|
||||
|
||||
// ValidateName validates a name according to DNS naming rules
|
||||
func ValidateName(name string) error {
|
||||
// Normalize to lowercase
|
||||
name = NormalizeName(name)
|
||||
|
||||
// Check total length
|
||||
if len(name) > 253 {
|
||||
return fmt.Errorf("%w: %d > 253", ErrNameTooLong, len(name))
|
||||
}
|
||||
|
||||
if len(name) == 0 {
|
||||
return fmt.Errorf("%w: name is empty", ErrInvalidName)
|
||||
}
|
||||
|
||||
// Split into labels
|
||||
labels := strings.Split(name, ".")
|
||||
|
||||
for i, label := range labels {
|
||||
if err := validateLabel(label); err != nil {
|
||||
return fmt.Errorf("invalid label %d (%s): %w", i, label, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateLabel validates a single label according to DNS rules
|
||||
func validateLabel(label string) error {
|
||||
// Check length
|
||||
if len(label) == 0 {
|
||||
return ErrLabelEmpty
|
||||
}
|
||||
if len(label) > 63 {
|
||||
return fmt.Errorf("%w: %d > 63", ErrLabelTooLong, len(label))
|
||||
}
|
||||
|
||||
// Check character set and hyphen placement
|
||||
if !labelRegex.MatchString(label) {
|
||||
if strings.HasPrefix(label, "-") || strings.HasSuffix(label, "-") {
|
||||
return ErrInvalidHyphen
|
||||
}
|
||||
return ErrInvalidCharacter
|
||||
}
|
||||
|
||||
// Check not all numeric
|
||||
if allNumeric.MatchString(label) {
|
||||
return ErrAllNumericLabel
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetParentDomain returns the parent domain of a name
|
||||
// e.g., "www.example.com" -> "example.com", "example.com" -> "com", "com" -> ""
|
||||
func GetParentDomain(name string) string {
|
||||
name = NormalizeName(name)
|
||||
parts := strings.Split(name, ".")
|
||||
if len(parts) <= 1 {
|
||||
return "" // TLD has no parent
|
||||
}
|
||||
return strings.Join(parts[1:], ".")
|
||||
}
|
||||
|
||||
// IsTLD returns true if the name is a top-level domain (single label)
|
||||
func IsTLD(name string) bool {
|
||||
name = NormalizeName(name)
|
||||
return !strings.Contains(name, ".")
|
||||
}
|
||||
|
||||
// ValidateIPv4 validates an IPv4 address format
|
||||
func ValidateIPv4(ip string) error {
|
||||
parts := strings.Split(ip, ".")
|
||||
if len(parts) != 4 {
|
||||
return fmt.Errorf("%w: invalid IPv4 format", ErrInvalidRecordValue)
|
||||
}
|
||||
|
||||
for _, part := range parts {
|
||||
var octet int
|
||||
if _, err := fmt.Sscanf(part, "%d", &octet); err != nil {
|
||||
return fmt.Errorf("%w: invalid IPv4 octet: %v", ErrInvalidRecordValue, err)
|
||||
}
|
||||
if octet < 0 || octet > 255 {
|
||||
return fmt.Errorf("%w: IPv4 octet out of range: %d", ErrInvalidRecordValue, octet)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateIPv6 validates an IPv6 address format (simplified check)
|
||||
func ValidateIPv6(ip string) error {
|
||||
// Basic validation - contains colons and valid hex characters
|
||||
if !strings.Contains(ip, ":") {
|
||||
return fmt.Errorf("%w: invalid IPv6 format", ErrInvalidRecordValue)
|
||||
}
|
||||
|
||||
// Split by colons
|
||||
parts := strings.Split(ip, ":")
|
||||
if len(parts) < 3 || len(parts) > 8 {
|
||||
return fmt.Errorf("%w: invalid IPv6 segment count", ErrInvalidRecordValue)
|
||||
}
|
||||
|
||||
// Check for valid hex characters
|
||||
validHex := regexp.MustCompile(`^[0-9a-fA-F]*$`)
|
||||
for _, part := range parts {
|
||||
if part == "" {
|
||||
continue // Allow :: notation
|
||||
}
|
||||
if len(part) > 4 {
|
||||
return fmt.Errorf("%w: IPv6 segment too long", ErrInvalidRecordValue)
|
||||
}
|
||||
if !validHex.MatchString(part) {
|
||||
return fmt.Errorf("%w: invalid IPv6 hex", ErrInvalidRecordValue)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateRecordValue validates a record value based on its type
|
||||
func ValidateRecordValue(recordType, value string) error {
|
||||
switch recordType {
|
||||
case RecordTypeA:
|
||||
return ValidateIPv4(value)
|
||||
case RecordTypeAAAA:
|
||||
return ValidateIPv6(value)
|
||||
case RecordTypeCNAME, RecordTypeMX, RecordTypeNS:
|
||||
return ValidateName(value)
|
||||
case RecordTypeTXT:
|
||||
if len(value) > 1024 {
|
||||
return fmt.Errorf("%w: TXT record exceeds 1024 characters", ErrInvalidRecordValue)
|
||||
}
|
||||
return nil
|
||||
case RecordTypeSRV:
|
||||
return ValidateName(value) // Hostname for SRV
|
||||
default:
|
||||
return fmt.Errorf("%w: unknown record type: %s", ErrInvalidRecordValue, recordType)
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateRecordLimit checks if adding a record would exceed type limits
|
||||
func ValidateRecordLimit(recordType string, currentCount int) error {
|
||||
limit, ok := RecordLimits[recordType]
|
||||
if !ok {
|
||||
return fmt.Errorf("%w: unknown record type: %s", ErrInvalidRecordValue, recordType)
|
||||
}
|
||||
|
||||
if currentCount >= limit {
|
||||
return fmt.Errorf("%w: %s records limited to %d", ErrRecordLimitExceeded, recordType, limit)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidatePriority validates priority value (0-65535)
|
||||
func ValidatePriority(priority int) error {
|
||||
if priority < 0 || priority > 65535 {
|
||||
return fmt.Errorf("%w: priority must be 0-65535", ErrInvalidRecordValue)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateWeight validates weight value (0-65535)
|
||||
func ValidateWeight(weight int) error {
|
||||
if weight < 0 || weight > 65535 {
|
||||
return fmt.Errorf("%w: weight must be 0-65535", ErrInvalidRecordValue)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidatePort validates port value (0-65535)
|
||||
func ValidatePort(port int) error {
|
||||
if port < 0 || port > 65535 {
|
||||
return fmt.Errorf("%w: port must be 0-65535", ErrInvalidRecordValue)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateTrustScore validates trust score (0.0-1.0)
|
||||
func ValidateTrustScore(score float64) error {
|
||||
if score < 0.0 || score > 1.0 {
|
||||
return fmt.Errorf("trust score must be between 0.0 and 1.0, got %f", score)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
317
pkg/find/verify.go
Normal file
317
pkg/find/verify.go
Normal file
@@ -0,0 +1,317 @@
|
||||
package find
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"next.orly.dev/pkg/encoders/hex"
|
||||
"next.orly.dev/pkg/encoders/event"
|
||||
"next.orly.dev/pkg/interfaces/signer/p8k"
|
||||
)
|
||||
|
||||
// VerifyEvent verifies the signature of a Nostr event
|
||||
func VerifyEvent(ev *event.E) error {
|
||||
ok, err := ev.Verify()
|
||||
if err != nil {
|
||||
return fmt.Errorf("signature verification failed: %w", err)
|
||||
}
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid signature")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyTransferAuth verifies a transfer authorization signature
|
||||
func VerifyTransferAuth(name, newOwner, prevOwner string, timestamp time.Time, sigHex string) (bool, error) {
|
||||
// Create the message
|
||||
msgHash := CreateTransferAuthMessage(name, newOwner, timestamp)
|
||||
|
||||
// Decode signature
|
||||
sig, err := hex.Dec(sigHex)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("invalid signature hex: %w", err)
|
||||
}
|
||||
|
||||
// Decode pubkey
|
||||
pubkey, err := hex.Dec(prevOwner)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("invalid pubkey hex: %w", err)
|
||||
}
|
||||
|
||||
// Create verifier with public key
|
||||
verifier, err := p8k.New()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to create verifier: %w", err)
|
||||
}
|
||||
|
||||
if err := verifier.InitPub(pubkey); err != nil {
|
||||
return false, fmt.Errorf("failed to init pubkey: %w", err)
|
||||
}
|
||||
|
||||
// Verify signature
|
||||
ok, err := verifier.Verify(msgHash, sig)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("verification failed: %w", err)
|
||||
}
|
||||
|
||||
return ok, nil
|
||||
}
|
||||
|
||||
// VerifyChallengeProof verifies a certificate challenge proof signature
|
||||
func VerifyChallengeProof(challenge, name, certPubkey, owner string, validUntil time.Time, sigHex string) (bool, error) {
|
||||
// Create the message
|
||||
msgHash := CreateChallengeProofMessage(challenge, name, certPubkey, validUntil)
|
||||
|
||||
// Decode signature
|
||||
sig, err := hex.Dec(sigHex)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("invalid signature hex: %w", err)
|
||||
}
|
||||
|
||||
// Decode pubkey
|
||||
pubkey, err := hex.Dec(owner)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("invalid pubkey hex: %w", err)
|
||||
}
|
||||
|
||||
// Create verifier with public key
|
||||
verifier, err := p8k.New()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to create verifier: %w", err)
|
||||
}
|
||||
|
||||
if err := verifier.InitPub(pubkey); err != nil {
|
||||
return false, fmt.Errorf("failed to init pubkey: %w", err)
|
||||
}
|
||||
|
||||
// Verify signature
|
||||
ok, err := verifier.Verify(msgHash, sig)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("verification failed: %w", err)
|
||||
}
|
||||
|
||||
return ok, nil
|
||||
}
|
||||
|
||||
// VerifyWitnessSignature verifies a witness signature on a certificate
|
||||
func VerifyWitnessSignature(certPubkey, name string, validFrom, validUntil time.Time,
|
||||
challenge, witnessPubkey, sigHex string) (bool, error) {
|
||||
|
||||
// Create the message
|
||||
msgHash := CreateWitnessMessage(certPubkey, name, validFrom, validUntil, challenge)
|
||||
|
||||
// Decode signature
|
||||
sig, err := hex.Dec(sigHex)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("invalid signature hex: %w", err)
|
||||
}
|
||||
|
||||
// Decode pubkey
|
||||
pubkey, err := hex.Dec(witnessPubkey)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("invalid pubkey hex: %w", err)
|
||||
}
|
||||
|
||||
// Create verifier with public key
|
||||
verifier, err := p8k.New()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to create verifier: %w", err)
|
||||
}
|
||||
|
||||
if err := verifier.InitPub(pubkey); err != nil {
|
||||
return false, fmt.Errorf("failed to init pubkey: %w", err)
|
||||
}
|
||||
|
||||
// Verify signature
|
||||
ok, err := verifier.Verify(msgHash, sig)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("verification failed: %w", err)
|
||||
}
|
||||
|
||||
return ok, nil
|
||||
}
|
||||
|
||||
// VerifyNameOwnership checks if a record's owner matches the name state owner
|
||||
func VerifyNameOwnership(nameState *NameState, record *NameRecord) error {
|
||||
recordOwner := hex.Enc(record.Event.Pubkey)
|
||||
if recordOwner != nameState.Owner {
|
||||
return fmt.Errorf("%w: record owner %s != name owner %s",
|
||||
ErrNotOwner, recordOwner, nameState.Owner)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsExpired checks if a time-based expiration has passed
|
||||
func IsExpired(expiration time.Time) bool {
|
||||
return time.Now().After(expiration)
|
||||
}
|
||||
|
||||
// IsInRenewalWindow checks if the current time is within the preferential renewal window
|
||||
// (final 30 days before expiration)
|
||||
func IsInRenewalWindow(expiration time.Time) bool {
|
||||
now := time.Now()
|
||||
renewalWindowStart := expiration.Add(-PreferentialRenewalDays * 24 * time.Hour)
|
||||
return now.After(renewalWindowStart) && now.Before(expiration)
|
||||
}
|
||||
|
||||
// CanRegister checks if a name can be registered based on its state and expiration
|
||||
func CanRegister(nameState *NameState, proposerPubkey string) error {
|
||||
// If no name state exists, anyone can register
|
||||
if nameState == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if name is expired
|
||||
if IsExpired(nameState.Expiration) {
|
||||
// Name is expired, anyone can register
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if in renewal window
|
||||
if IsInRenewalWindow(nameState.Expiration) {
|
||||
// Only current owner can register during renewal window
|
||||
if proposerPubkey != nameState.Owner {
|
||||
return ErrInRenewalWindow
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Name is still owned and not in renewal window
|
||||
return fmt.Errorf("name is owned by %s until %s", nameState.Owner, nameState.Expiration)
|
||||
}
|
||||
|
||||
// VerifyProposalExpiration checks if a proposal has expired
|
||||
func VerifyProposalExpiration(proposal *RegistrationProposal) error {
|
||||
if !proposal.Expiration.IsZero() && IsExpired(proposal.Expiration) {
|
||||
return fmt.Errorf("proposal expired at %s", proposal.Expiration)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyAttestationExpiration checks if an attestation has expired
|
||||
func VerifyAttestationExpiration(attestation *Attestation) error {
|
||||
if !attestation.Expiration.IsZero() && IsExpired(attestation.Expiration) {
|
||||
return fmt.Errorf("attestation expired at %s", attestation.Expiration)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyTrustGraphExpiration checks if a trust graph has expired
|
||||
func VerifyTrustGraphExpiration(trustGraph *TrustGraph) error {
|
||||
if !trustGraph.Expiration.IsZero() && IsExpired(trustGraph.Expiration) {
|
||||
return fmt.Errorf("trust graph expired at %s", trustGraph.Expiration)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyNameStateExpiration checks if a name state has expired
|
||||
func VerifyNameStateExpiration(nameState *NameState) error {
|
||||
if !nameState.Expiration.IsZero() && IsExpired(nameState.Expiration) {
|
||||
return ErrNameExpired
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyCertificateValidity checks if a certificate is currently valid
|
||||
func VerifyCertificateValidity(cert *Certificate) error {
|
||||
now := time.Now()
|
||||
|
||||
if now.Before(cert.ValidFrom) {
|
||||
return fmt.Errorf("certificate not yet valid (valid from %s)", cert.ValidFrom)
|
||||
}
|
||||
|
||||
if now.After(cert.ValidUntil) {
|
||||
return fmt.Errorf("certificate expired at %s", cert.ValidUntil)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyCertificate performs complete certificate verification
|
||||
func VerifyCertificate(cert *Certificate, nameState *NameState, trustedWitnesses []string) error {
|
||||
// Verify certificate is not expired
|
||||
if err := VerifyCertificateValidity(cert); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Verify name is not expired
|
||||
if err := VerifyNameStateExpiration(nameState); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Verify certificate owner matches name owner
|
||||
certOwner := hex.Enc(cert.Event.Pubkey)
|
||||
if certOwner != nameState.Owner {
|
||||
return fmt.Errorf("certificate owner %s != name owner %s", certOwner, nameState.Owner)
|
||||
}
|
||||
|
||||
// Verify challenge proof
|
||||
ok, err := VerifyChallengeProof(cert.Challenge, cert.Name, cert.CertPubkey,
|
||||
nameState.Owner, cert.ValidUntil, cert.ChallengeProof)
|
||||
if err != nil {
|
||||
return fmt.Errorf("challenge proof verification failed: %w", err)
|
||||
}
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid challenge proof signature")
|
||||
}
|
||||
|
||||
// Count trusted witnesses
|
||||
trustedCount := 0
|
||||
for _, witness := range cert.Witnesses {
|
||||
// Check if witness is in trusted list
|
||||
isTrusted := false
|
||||
for _, trusted := range trustedWitnesses {
|
||||
if witness.Pubkey == trusted {
|
||||
isTrusted = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !isTrusted {
|
||||
continue
|
||||
}
|
||||
|
||||
// Verify witness signature
|
||||
ok, err := VerifyWitnessSignature(cert.CertPubkey, cert.Name,
|
||||
cert.ValidFrom, cert.ValidUntil, cert.Challenge,
|
||||
witness.Pubkey, witness.Signature)
|
||||
if err != nil {
|
||||
return fmt.Errorf("witness %s signature verification failed: %w", witness.Pubkey, err)
|
||||
}
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid witness %s signature", witness.Pubkey)
|
||||
}
|
||||
|
||||
trustedCount++
|
||||
}
|
||||
|
||||
// Require at least 3 trusted witnesses
|
||||
if trustedCount < 3 {
|
||||
return fmt.Errorf("insufficient trusted witnesses: %d < 3", trustedCount)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifySubdomainAuthority checks if the proposer owns the parent domain
|
||||
func VerifySubdomainAuthority(name string, proposerPubkey string, parentNameState *NameState) error {
|
||||
parent := GetParentDomain(name)
|
||||
|
||||
// TLDs have no parent
|
||||
if parent == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parent must exist
|
||||
if parentNameState == nil {
|
||||
return fmt.Errorf("parent domain %s does not exist", parent)
|
||||
}
|
||||
|
||||
// Proposer must own parent
|
||||
if proposerPubkey != parentNameState.Owner {
|
||||
return fmt.Errorf("proposer %s does not own parent domain %s (owner: %s)",
|
||||
proposerPubkey, parent, parentNameState.Owner)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user