Files
next.orly.dev/pkg/protocol/directory/helpers.go
mleku e0a95ca1cd
Some checks failed
Go / build (push) Has been cancelled
Go / release (push) Has been cancelled
Refactor signer implementation to use p8k package
- Replaced all instances of p256k1signer with the new p8k.Signer across various modules, including event creation, policy handling, and database interactions.
- Updated related test cases and benchmarks to ensure compatibility with the new signer interface.
- Bumped version to v0.25.0 to reflect these significant changes and improvements in cryptographic operations.
2025-11-04 20:05:19 +00:00

439 lines
12 KiB
Go

package directory
import (
"github.com/minio/sha256-simd"
"encoding/hex"
"fmt"
"strconv"
"strings"
"time"
"lol.mleku.dev/chk"
"lol.mleku.dev/errorf"
"next.orly.dev/pkg/crypto/ec/schnorr"
"next.orly.dev/pkg/crypto/ec/secp256k1"
"next.orly.dev/pkg/interfaces/signer/p8k"
"next.orly.dev/pkg/encoders/bech32encoding"
"next.orly.dev/pkg/encoders/event"
)
// IdentityTagBuilder helps construct identity tags with proper signatures.
type IdentityTagBuilder struct {
identityPrivkey []byte
identityPubkey []byte
npubIdentity string
}
// NewIdentityTagBuilder creates a new identity tag builder with the given
// identity private key.
func NewIdentityTagBuilder(identityPrivkey []byte) (builder *IdentityTagBuilder, err error) {
if len(identityPrivkey) != 32 {
return nil, errorf.E("identity private key must be 32 bytes")
}
// Derive public key from secret key using p8k signer
var signer *p8k.Signer
if signer, err = p8k.New(); chk.E(err) {
return nil, errorf.E("failed to create signer: %w", err)
}
if err = signer.InitSec(identityPrivkey); chk.E(err) {
return nil, errorf.E("failed to initialize signer: %w", err)
}
identityPubkeyBytes := signer.Pub()
// Parse public key for npub encoding
var identityPubkey *secp256k1.PublicKey
if identityPubkey, err = schnorr.ParsePubKey(identityPubkeyBytes); chk.E(err) {
return nil, errorf.E("failed to parse public key: %w", err)
}
// Encode as npub
var npubIdentity []byte
if npubIdentity, err = bech32encoding.PublicKeyToNpub(identityPubkey); chk.E(err) {
return nil, errorf.E("failed to encode npub: %w", err)
}
return &IdentityTagBuilder{
identityPrivkey: identityPrivkey,
identityPubkey: identityPubkeyBytes,
npubIdentity: string(npubIdentity),
}, nil
}
// CreateIdentityTag creates a signed identity tag for the given delegate pubkey.
func (builder *IdentityTagBuilder) CreateIdentityTag(delegatePubkey []byte) (identityTag *IdentityTag, err error) {
if len(delegatePubkey) != 32 {
return nil, errorf.E("delegate pubkey must be 32 bytes")
}
// Generate nonce
var nonceHex string
if nonceHex, err = GenerateNonceHex(16); chk.E(err) {
return nil, errorf.E("failed to generate nonce: %w", err)
}
// Create message: nonce + delegate_pubkey_hex + identity_pubkey_hex
delegatePubkeyHex := hex.EncodeToString(delegatePubkey)
identityPubkeyHex := hex.EncodeToString(builder.identityPubkey)
message := nonceHex + delegatePubkeyHex + identityPubkeyHex
// Hash and sign using p8k signer
hash := sha256.Sum256([]byte(message))
var signer *p8k.Signer
if signer, err = p8k.New(); chk.E(err) {
return nil, errorf.E("failed to create signer: %w", err)
}
if err = signer.InitSec(builder.identityPrivkey); chk.E(err) {
return nil, errorf.E("failed to initialize signer: %w", err)
}
var signature []byte
if signature, err = signer.Sign(hash[:]); chk.E(err) {
return nil, errorf.E("failed to sign identity tag: %w", err)
}
identityTag = &IdentityTag{
NPubIdentity: builder.npubIdentity,
Nonce: nonceHex,
Signature: hex.EncodeToString(signature),
}
return
}
// GetNPubIdentity returns the npub-encoded identity.
func (builder *IdentityTagBuilder) GetNPubIdentity() string {
return builder.npubIdentity
}
// GetIdentityPubkey returns the raw identity public key.
func (builder *IdentityTagBuilder) GetIdentityPubkey() []byte {
return builder.identityPubkey
}
// KeyPoolManager helps manage HD key derivation and advertisement.
type KeyPoolManager struct {
masterSeed []byte
identityIndex uint32
currentIndices map[KeyPurpose]int
}
// NewKeyPoolManager creates a new key pool manager with the given master seed.
func NewKeyPoolManager(masterSeed []byte, identityIndex uint32) *KeyPoolManager {
return &KeyPoolManager{
masterSeed: masterSeed,
identityIndex: identityIndex,
currentIndices: make(map[KeyPurpose]int),
}
}
// GenerateDerivationPath creates a BIP32 derivation path for the given purpose and index.
func (kpm *KeyPoolManager) GenerateDerivationPath(purpose KeyPurpose, index int) string {
var usageIndex int
switch purpose {
case KeyPurposeSigning:
usageIndex = 0
case KeyPurposeEncryption:
usageIndex = 1
case KeyPurposeDelegation:
usageIndex = 2
default:
usageIndex = 0
}
return fmt.Sprintf("m/39103'/1237'/%d'/%d/%d", kpm.identityIndex, usageIndex, index)
}
// GetNextKeyIndex returns the next available key index for the given purpose.
func (kpm *KeyPoolManager) GetNextKeyIndex(purpose KeyPurpose) int {
current := kpm.currentIndices[purpose]
kpm.currentIndices[purpose] = current + 1
return current
}
// SetKeyIndex sets the current key index for the given purpose.
func (kpm *KeyPoolManager) SetKeyIndex(purpose KeyPurpose, index int) {
kpm.currentIndices[purpose] = index
}
// GetCurrentKeyIndex returns the current key index for the given purpose.
func (kpm *KeyPoolManager) GetCurrentKeyIndex(purpose KeyPurpose) int {
return kpm.currentIndices[purpose]
}
// TrustCalculator helps calculate trust scores and inheritance.
type TrustCalculator struct {
acts map[string]*TrustAct
}
// NewTrustCalculator creates a new trust calculator.
func NewTrustCalculator() *TrustCalculator {
return &TrustCalculator{
acts: make(map[string]*TrustAct),
}
}
// AddAct adds a trust act to the calculator.
func (tc *TrustCalculator) AddAct(act *TrustAct) {
key := act.GetTargetPubkey()
tc.acts[key] = act
}
// GetTrustLevel returns the trust level for a given pubkey.
func (tc *TrustCalculator) GetTrustLevel(pubkey string) TrustLevel {
if act, exists := tc.acts[pubkey]; exists {
if !act.IsExpired() {
return act.GetTrustLevel()
}
}
return TrustLevelNone // Return 0 for no trust
}
// CalculateInheritedTrust calculates inherited trust through the web of trust.
// With numeric trust levels, inherited trust is calculated by multiplying
// the trust percentages at each hop, reducing trust over distance.
func (tc *TrustCalculator) CalculateInheritedTrust(
fromPubkey, toPubkey string,
) TrustLevel {
// Direct trust
if directTrust := tc.GetTrustLevel(toPubkey); directTrust > 0 {
return directTrust
}
// Look for inherited trust through intermediate nodes
var maxInheritedTrust TrustLevel = 0
for intermediatePubkey, act := range tc.acts {
if act.IsExpired() {
continue
}
// Check if we trust the intermediate node
intermediateLevel := tc.GetTrustLevel(intermediatePubkey)
if intermediateLevel == 0 {
continue
}
// Check if intermediate node trusts the target
targetLevel := tc.GetTrustLevel(toPubkey)
if targetLevel == 0 {
continue
}
// Calculate inherited trust level (multiply percentages)
inheritedLevel := tc.combinesTrustLevels(intermediateLevel, targetLevel)
if inheritedLevel > maxInheritedTrust {
maxInheritedTrust = inheritedLevel
}
}
return maxInheritedTrust
}
// combinesTrustLevels combines two trust levels to calculate inherited trust.
// With numeric trust levels (0-100), inherited trust is calculated by
// multiplying the two percentages: (level1 * level2) / 100
// This naturally reduces trust over distance.
func (tc *TrustCalculator) combinesTrustLevels(level1, level2 TrustLevel) TrustLevel {
// Multiply percentages: (level1% * level2%) = (level1 * level2) / 100
// Example: 75% trust * 50% trust = 37.5% inherited trust
combined := (uint16(level1) * uint16(level2)) / 100
return TrustLevel(combined)
}
// ReplicationFilter helps determine which events should be replicated.
type ReplicationFilter struct {
trustCalculator *TrustCalculator
acts map[string]*TrustAct
}
// NewReplicationFilter creates a new replication filter.
func NewReplicationFilter(trustCalculator *TrustCalculator) *ReplicationFilter {
return &ReplicationFilter{
trustCalculator: trustCalculator,
acts: make(map[string]*TrustAct),
}
}
// AddTrustAct adds a trust act to the filter.
func (rf *ReplicationFilter) AddTrustAct(act *TrustAct) {
rf.acts[act.GetTargetPubkey()] = act
}
// ShouldReplicate determines if an event should be replicated to a target relay.
func (rf *ReplicationFilter) ShouldReplicate(ev *event.E, targetPubkey string) bool {
act, exists := rf.acts[targetPubkey]
if !exists || act.IsExpired() {
return false
}
return act.ShouldReplicate(ev.Kind)
}
// GetReplicationTargets returns all target relays that should receive an event.
func (rf *ReplicationFilter) GetReplicationTargets(ev *event.E) []string {
var targets []string
for pubkey, act := range rf.acts {
if !act.IsExpired() && act.ShouldReplicate(ev.Kind) {
targets = append(targets, pubkey)
}
}
return targets
}
// EventBatcher helps batch events for efficient replication.
type EventBatcher struct {
maxBatchSize int
batches map[string][]*event.E
}
// NewEventBatcher creates a new event batcher.
func NewEventBatcher(maxBatchSize int) *EventBatcher {
if maxBatchSize <= 0 {
maxBatchSize = 100 // Default batch size
}
return &EventBatcher{
maxBatchSize: maxBatchSize,
batches: make(map[string][]*event.E),
}
}
// AddEvent adds an event to the batch for a target relay.
func (eb *EventBatcher) AddEvent(targetRelay string, ev *event.E) {
eb.batches[targetRelay] = append(eb.batches[targetRelay], ev)
}
// GetBatch returns the current batch for a target relay.
func (eb *EventBatcher) GetBatch(targetRelay string) []*event.E {
return eb.batches[targetRelay]
}
// IsBatchFull returns true if the batch for a target relay is full.
func (eb *EventBatcher) IsBatchFull(targetRelay string) bool {
return len(eb.batches[targetRelay]) >= eb.maxBatchSize
}
// FlushBatch returns and clears the batch for a target relay.
func (eb *EventBatcher) FlushBatch(targetRelay string) []*event.E {
batch := eb.batches[targetRelay]
eb.batches[targetRelay] = nil
return batch
}
// GetAllBatches returns all current batches.
func (eb *EventBatcher) GetAllBatches() map[string][]*event.E {
result := make(map[string][]*event.E)
for relay, batch := range eb.batches {
if len(batch) > 0 {
result[relay] = batch
}
}
return result
}
// FlushAllBatches returns and clears all batches.
func (eb *EventBatcher) FlushAllBatches() map[string][]*event.E {
result := eb.GetAllBatches()
eb.batches = make(map[string][]*event.E)
return result
}
// Utility functions
// ParseKindsList parses a comma-separated list of event kinds.
func ParseKindsList(kindsStr string) (kinds []uint16, err error) {
if kindsStr == "" {
return nil, nil
}
kindStrings := strings.Split(kindsStr, ",")
for _, kindStr := range kindStrings {
kindStr = strings.TrimSpace(kindStr)
if kindStr == "" {
continue
}
var kind uint64
if kind, err = strconv.ParseUint(kindStr, 10, 16); chk.E(err) {
return nil, errorf.E("invalid kind: %s", kindStr)
}
kinds = append(kinds, uint16(kind))
}
return
}
// FormatKindsList formats a list of event kinds as a comma-separated string.
func FormatKindsList(kinds []uint16) string {
if len(kinds) == 0 {
return ""
}
var kindStrings []string
for _, kind := range kinds {
kindStrings = append(kindStrings, strconv.FormatUint(uint64(kind), 10))
}
return strings.Join(kindStrings, ",")
}
// GenerateRequestID generates a unique request ID for replication requests.
func GenerateRequestID() (requestID string, err error) {
// Use timestamp + random nonce for uniqueness
timestamp := time.Now().Unix()
var nonce string
if nonce, err = GenerateNonceHex(8); chk.E(err) {
return
}
requestID = fmt.Sprintf("%d-%s", timestamp, nonce)
return
}
// CreateSuccessResponse creates a successful replication response.
func CreateSuccessResponse(
pubkey []byte,
requestID, sourceRelay string,
eventResults []*EventResult,
) (response *DirectoryEventReplicationResponse, err error) {
return NewDirectoryEventReplicationResponse(
pubkey,
requestID,
ReplicationStatusSuccess,
"",
sourceRelay,
eventResults,
)
}
// CreateErrorResponse creates an error replication response.
func CreateErrorResponse(
pubkey []byte,
requestID, sourceRelay, errorMsg string,
) (response *DirectoryEventReplicationResponse, err error) {
return NewDirectoryEventReplicationResponse(
pubkey,
requestID,
ReplicationStatusError,
errorMsg,
sourceRelay,
nil,
)
}
// CreateEventResult creates an event result for a replication response.
func CreateEventResult(eventID string, success bool, errorMsg string) *EventResult {
status := ReplicationStatusSuccess
if !success {
status = ReplicationStatusError
}
return &EventResult{
EventID: eventID,
Status: status,
Error: errorMsg,
}
}