Refactor NIP-XX Document and Protocol Implementation for Directory Consensus
- Updated the NIP-XX document to clarify terminology, replacing "attestations" with "acts" for consistency. - Enhanced the protocol by introducing new event kinds: Trust Act (Kind 39101) and Group Tag Act (Kind 39102), with detailed specifications for their structure and usage. - Modified the signature generation process to include the canonical WebSocket URL, ensuring proper binding and verification. - Improved validation mechanisms for identity tags and event replication requests, reinforcing security and integrity within the directory consensus protocol. - Added comprehensive documentation for new event types and their respective validation processes, ensuring clarity for developers and users. - Introduced new helper functions and structures to facilitate the creation and management of directory events and acts.
This commit is contained in:
426
pkg/protocol/directory/helpers.go
Normal file
426
pkg/protocol/directory/helpers.go
Normal file
@@ -0,0 +1,426 @@
|
||||
package directory
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"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/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
|
||||
identitySecKey := secp256k1.SecKeyFromBytes(identityPrivkey)
|
||||
identityPubkey := identitySecKey.PubKey()
|
||||
identityPubkeyBytes := schnorr.SerializePubKey(identityPubkey)
|
||||
|
||||
// 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
|
||||
hash := sha256.Sum256([]byte(message))
|
||||
identitySecKey := secp256k1.SecKeyFromBytes(builder.identityPrivkey)
|
||||
var sig *schnorr.Signature
|
||||
if sig, err = schnorr.Sign(identitySecKey, hash[:]); chk.E(err) {
|
||||
return nil, errorf.E("failed to sign identity tag: %w", err)
|
||||
}
|
||||
signature := sig.Serialize()
|
||||
|
||||
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 TrustLevel("")
|
||||
}
|
||||
|
||||
// CalculateInheritedTrust calculates inherited trust through the web of trust.
|
||||
func (tc *TrustCalculator) CalculateInheritedTrust(
|
||||
fromPubkey, toPubkey string,
|
||||
) TrustLevel {
|
||||
// Direct trust
|
||||
if directTrust := tc.GetTrustLevel(toPubkey); directTrust != "" {
|
||||
return directTrust
|
||||
}
|
||||
|
||||
// Look for inherited trust through intermediate nodes
|
||||
for intermediatePubkey, act := range tc.acts {
|
||||
if act.IsExpired() {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if we trust the intermediate node
|
||||
intermediateLevel := tc.GetTrustLevel(intermediatePubkey)
|
||||
if intermediateLevel == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if intermediate node trusts the target
|
||||
targetLevel := tc.GetTrustLevel(toPubkey)
|
||||
if targetLevel == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Calculate inherited trust level
|
||||
return tc.combinesTrustLevels(intermediateLevel, targetLevel)
|
||||
}
|
||||
|
||||
return TrustLevel("")
|
||||
}
|
||||
|
||||
// combinesTrustLevels combines two trust levels to calculate inherited trust.
|
||||
func (tc *TrustCalculator) combinesTrustLevels(level1, level2 TrustLevel) TrustLevel {
|
||||
// Trust inheritance rules:
|
||||
// high + high = medium
|
||||
// high + medium = low
|
||||
// medium + medium = low
|
||||
// anything else = no trust
|
||||
|
||||
if level1 == TrustLevelHigh && level2 == TrustLevelHigh {
|
||||
return TrustLevelMedium
|
||||
}
|
||||
if (level1 == TrustLevelHigh && level2 == TrustLevelMedium) ||
|
||||
(level1 == TrustLevelMedium && level2 == TrustLevelHigh) {
|
||||
return TrustLevelLow
|
||||
}
|
||||
if level1 == TrustLevelMedium && level2 == TrustLevelMedium {
|
||||
return TrustLevelLow
|
||||
}
|
||||
|
||||
return TrustLevel("")
|
||||
}
|
||||
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user