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:
378
pkg/protocol/directory/trust_act.go
Normal file
378
pkg/protocol/directory/trust_act.go
Normal file
@@ -0,0 +1,378 @@
|
||||
package directory
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"lol.mleku.dev/chk"
|
||||
"lol.mleku.dev/errorf"
|
||||
"next.orly.dev/pkg/encoders/event"
|
||||
"next.orly.dev/pkg/encoders/tag"
|
||||
)
|
||||
|
||||
// TrustAct represents a complete Trust Act event (Kind 39101)
|
||||
// with typed access to its components.
|
||||
type TrustAct struct {
|
||||
Event *event.E
|
||||
TargetPubkey string
|
||||
TrustLevel TrustLevel
|
||||
RelayURL string
|
||||
Expiry *time.Time
|
||||
Reason TrustReason
|
||||
ReplicationKinds []uint16
|
||||
IdentityTag *IdentityTag
|
||||
}
|
||||
|
||||
// IdentityTag represents the I tag with npub identity and proof-of-control.
|
||||
type IdentityTag struct {
|
||||
NPubIdentity string
|
||||
Nonce string
|
||||
Signature string
|
||||
}
|
||||
|
||||
// NewTrustAct creates a new Trust Act event.
|
||||
func NewTrustAct(
|
||||
pubkey []byte,
|
||||
targetPubkey string,
|
||||
trustLevel TrustLevel,
|
||||
relayURL string,
|
||||
expiry *time.Time,
|
||||
reason TrustReason,
|
||||
replicationKinds []uint16,
|
||||
identityTag *IdentityTag,
|
||||
) (ta *TrustAct, err error) {
|
||||
|
||||
// Validate required fields
|
||||
if len(pubkey) != 32 {
|
||||
return nil, errorf.E("pubkey must be 32 bytes")
|
||||
}
|
||||
if targetPubkey == "" {
|
||||
return nil, errorf.E("target pubkey is required")
|
||||
}
|
||||
if len(targetPubkey) != 64 {
|
||||
return nil, errorf.E("target pubkey must be 64 hex characters")
|
||||
}
|
||||
if err = ValidateTrustLevel(string(trustLevel)); chk.E(err) {
|
||||
return
|
||||
}
|
||||
if relayURL == "" {
|
||||
return nil, errorf.E("relay URL is required")
|
||||
}
|
||||
|
||||
// Create base event
|
||||
ev := CreateBaseEvent(pubkey, TrustActKind)
|
||||
|
||||
// Add required tags
|
||||
ev.Tags.Append(tag.NewFromAny(string(PubkeyTag), targetPubkey))
|
||||
ev.Tags.Append(tag.NewFromAny(string(TrustLevelTag), string(trustLevel)))
|
||||
ev.Tags.Append(tag.NewFromAny(string(RelayTag), relayURL))
|
||||
|
||||
// Add optional expiry
|
||||
if expiry != nil {
|
||||
ev.Tags.Append(tag.NewFromAny(string(ExpiryTag), strconv.FormatInt(expiry.Unix(), 10)))
|
||||
}
|
||||
|
||||
// Add reason
|
||||
if reason != "" {
|
||||
ev.Tags.Append(tag.NewFromAny(string(ReasonTag), string(reason)))
|
||||
}
|
||||
|
||||
// Add replication kinds (K tag)
|
||||
if len(replicationKinds) > 0 {
|
||||
var kindStrings []string
|
||||
for _, k := range replicationKinds {
|
||||
kindStrings = append(kindStrings, strconv.FormatUint(uint64(k), 10))
|
||||
}
|
||||
ev.Tags.Append(tag.NewFromAny(string(KTag), strings.Join(kindStrings, ",")))
|
||||
}
|
||||
|
||||
// Add identity tag if provided
|
||||
if identityTag != nil {
|
||||
if err = identityTag.Validate(); chk.E(err) {
|
||||
return
|
||||
}
|
||||
ev.Tags.Append(tag.NewFromAny(string(ITag),
|
||||
identityTag.NPubIdentity,
|
||||
identityTag.Nonce,
|
||||
identityTag.Signature))
|
||||
}
|
||||
|
||||
ta = &TrustAct{
|
||||
Event: ev,
|
||||
TargetPubkey: targetPubkey,
|
||||
TrustLevel: trustLevel,
|
||||
RelayURL: relayURL,
|
||||
Expiry: expiry,
|
||||
Reason: reason,
|
||||
ReplicationKinds: replicationKinds,
|
||||
IdentityTag: identityTag,
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ParseTrustAct parses an event into a TrustAct structure
|
||||
// with validation.
|
||||
func ParseTrustAct(ev *event.E) (ta *TrustAct, err error) {
|
||||
if ev == nil {
|
||||
return nil, errorf.E("event cannot be nil")
|
||||
}
|
||||
|
||||
// Validate event kind
|
||||
if ev.Kind != TrustActKind.K {
|
||||
return nil, errorf.E("invalid event kind: expected %d, got %d",
|
||||
TrustActKind.K, ev.Kind)
|
||||
}
|
||||
|
||||
// Extract required tags
|
||||
pTag := ev.Tags.GetFirst(PubkeyTag)
|
||||
if pTag == nil {
|
||||
return nil, errorf.E("missing p tag")
|
||||
}
|
||||
|
||||
trustLevelTag := ev.Tags.GetFirst(TrustLevelTag)
|
||||
if trustLevelTag == nil {
|
||||
return nil, errorf.E("missing trust_level tag")
|
||||
}
|
||||
|
||||
relayTag := ev.Tags.GetFirst(RelayTag)
|
||||
if relayTag == nil {
|
||||
return nil, errorf.E("missing relay tag")
|
||||
}
|
||||
|
||||
// Validate trust level
|
||||
trustLevel := TrustLevel(trustLevelTag.Value())
|
||||
if err = ValidateTrustLevel(string(trustLevel)); chk.E(err) {
|
||||
return
|
||||
}
|
||||
|
||||
// Parse optional expiry
|
||||
var expiry *time.Time
|
||||
expiryTag := ev.Tags.GetFirst(ExpiryTag)
|
||||
if expiryTag != nil {
|
||||
var expiryUnix int64
|
||||
if expiryUnix, err = strconv.ParseInt(string(expiryTag.Value()), 10, 64); chk.E(err) {
|
||||
return nil, errorf.E("invalid expiry timestamp: %w", err)
|
||||
}
|
||||
expiryTime := time.Unix(expiryUnix, 0)
|
||||
expiry = &expiryTime
|
||||
}
|
||||
|
||||
// Parse optional reason
|
||||
var reason TrustReason
|
||||
reasonTag := ev.Tags.GetFirst(ReasonTag)
|
||||
if reasonTag != nil {
|
||||
reason = TrustReason(reasonTag.Value())
|
||||
}
|
||||
|
||||
// Parse replication kinds (K tag)
|
||||
var replicationKinds []uint16
|
||||
kTag := ev.Tags.GetFirst(KTag)
|
||||
if kTag != nil {
|
||||
kindStrings := strings.Split(string(kTag.Value()), ",")
|
||||
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 in K tag: %s", kindStr)
|
||||
}
|
||||
replicationKinds = append(replicationKinds, uint16(kind))
|
||||
}
|
||||
}
|
||||
|
||||
// Parse identity tag (I tag)
|
||||
var identityTag *IdentityTag
|
||||
iTag := ev.Tags.GetFirst(ITag)
|
||||
if iTag != nil {
|
||||
if identityTag, err = ParseIdentityTag(iTag); chk.E(err) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ta = &TrustAct{
|
||||
Event: ev,
|
||||
TargetPubkey: string(pTag.Value()),
|
||||
TrustLevel: trustLevel,
|
||||
RelayURL: string(relayTag.Value()),
|
||||
Expiry: expiry,
|
||||
Reason: reason,
|
||||
ReplicationKinds: replicationKinds,
|
||||
IdentityTag: identityTag,
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ParseIdentityTag parses an I tag into an IdentityTag structure.
|
||||
func ParseIdentityTag(t *tag.T) (it *IdentityTag, err error) {
|
||||
if t == nil {
|
||||
return nil, errorf.E("tag cannot be nil")
|
||||
}
|
||||
|
||||
if t.Len() < 4 {
|
||||
return nil, errorf.E("I tag must have at least 4 elements")
|
||||
}
|
||||
|
||||
// First element should be "I"
|
||||
if string(t.T[0]) != "I" {
|
||||
return nil, errorf.E("invalid I tag key")
|
||||
}
|
||||
|
||||
it = &IdentityTag{
|
||||
NPubIdentity: string(t.T[1]),
|
||||
Nonce: string(t.T[2]),
|
||||
Signature: string(t.T[3]),
|
||||
}
|
||||
|
||||
if err = it.Validate(); chk.E(err) {
|
||||
return nil, err
|
||||
}
|
||||
return it, nil
|
||||
}
|
||||
|
||||
// Validate performs validation of an IdentityTag.
|
||||
func (it *IdentityTag) Validate() (err error) {
|
||||
if it == nil {
|
||||
return errorf.E("IdentityTag cannot be nil")
|
||||
}
|
||||
|
||||
if it.NPubIdentity == "" {
|
||||
return errorf.E("npub identity is required")
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(it.NPubIdentity, "npub1") {
|
||||
return errorf.E("identity must be npub-encoded")
|
||||
}
|
||||
|
||||
if it.Nonce == "" {
|
||||
return errorf.E("nonce is required")
|
||||
}
|
||||
|
||||
if len(it.Nonce) < 32 { // Minimum 16 bytes hex-encoded
|
||||
return errorf.E("nonce must be at least 16 bytes (32 hex characters)")
|
||||
}
|
||||
|
||||
if it.Signature == "" {
|
||||
return errorf.E("signature is required")
|
||||
}
|
||||
|
||||
if len(it.Signature) != 128 { // 64 bytes hex-encoded
|
||||
return errorf.E("signature must be 64 bytes (128 hex characters)")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate performs comprehensive validation of a TrustAct.
|
||||
func (ta *TrustAct) Validate() (err error) {
|
||||
if ta == nil {
|
||||
return errorf.E("TrustAct cannot be nil")
|
||||
}
|
||||
|
||||
if ta.Event == nil {
|
||||
return errorf.E("event cannot be nil")
|
||||
}
|
||||
|
||||
// Validate event signature
|
||||
if _, err = ta.Event.Verify(); chk.E(err) {
|
||||
return errorf.E("invalid event signature: %w", err)
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if ta.TargetPubkey == "" {
|
||||
return errorf.E("target pubkey is required")
|
||||
}
|
||||
|
||||
if len(ta.TargetPubkey) != 64 {
|
||||
return errorf.E("target pubkey must be 64 hex characters")
|
||||
}
|
||||
|
||||
if err = ValidateTrustLevel(string(ta.TrustLevel)); chk.E(err) {
|
||||
return
|
||||
}
|
||||
|
||||
if ta.RelayURL == "" {
|
||||
return errorf.E("relay URL is required")
|
||||
}
|
||||
|
||||
// Validate expiry if present
|
||||
if ta.Expiry != nil && ta.Expiry.Before(time.Now()) {
|
||||
return errorf.E("trust act has expired")
|
||||
}
|
||||
|
||||
// Validate identity tag if present
|
||||
if ta.IdentityTag != nil {
|
||||
if err = ta.IdentityTag.Validate(); chk.E(err) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsExpired returns true if the trust act has expired.
|
||||
func (ta *TrustAct) IsExpired() bool {
|
||||
return ta.Expiry != nil && ta.Expiry.Before(time.Now())
|
||||
}
|
||||
|
||||
// HasReplicationKind returns true if the act includes the specified
|
||||
// kind for replication.
|
||||
func (ta *TrustAct) HasReplicationKind(kind uint16) bool {
|
||||
for _, k := range ta.ReplicationKinds {
|
||||
if k == kind {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ShouldReplicate returns true if an event of the given kind should be
|
||||
// replicated based on this trust act.
|
||||
func (ta *TrustAct) ShouldReplicate(kind uint16) bool {
|
||||
// Directory events are always replicated
|
||||
if IsDirectoryEventKind(kind) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if kind is in the replication list
|
||||
return ta.HasReplicationKind(kind)
|
||||
}
|
||||
|
||||
// GetTargetPubkey returns the target relay's public key.
|
||||
func (ta *TrustAct) GetTargetPubkey() string {
|
||||
return ta.TargetPubkey
|
||||
}
|
||||
|
||||
// GetTrustLevel returns the trust level.
|
||||
func (ta *TrustAct) GetTrustLevel() TrustLevel {
|
||||
return ta.TrustLevel
|
||||
}
|
||||
|
||||
// GetRelayURL returns the target relay's URL.
|
||||
func (ta *TrustAct) GetRelayURL() string {
|
||||
return ta.RelayURL
|
||||
}
|
||||
|
||||
// GetExpiry returns the expiry time, or nil if no expiry is set.
|
||||
func (ta *TrustAct) GetExpiry() *time.Time {
|
||||
return ta.Expiry
|
||||
}
|
||||
|
||||
// GetReason returns the reason for the trust relationship.
|
||||
func (ta *TrustAct) GetReason() TrustReason {
|
||||
return ta.Reason
|
||||
}
|
||||
|
||||
// GetReplicationKinds returns the list of event kinds to replicate.
|
||||
func (ta *TrustAct) GetReplicationKinds() []uint16 {
|
||||
return ta.ReplicationKinds
|
||||
}
|
||||
|
||||
// GetIdentityTag returns the identity tag, or nil if not present.
|
||||
func (ta *TrustAct) GetIdentityTag() *IdentityTag {
|
||||
return ta.IdentityTag
|
||||
}
|
||||
Reference in New Issue
Block a user