hex_utils.go raw

   1  // Package neo4j provides hex utilities for normalizing pubkeys and event IDs.
   2  //
   3  // The nostr library applies binary optimization to e/p tags, storing 64-character
   4  // hex strings as 33-byte binary (32 bytes + null terminator). This file provides
   5  // utilities to ensure all pubkeys and event IDs stored in Neo4j are in consistent
   6  // lowercase hex format.
   7  package neo4j
   8  
   9  import (
  10  	"strings"
  11  
  12  	"next.orly.dev/pkg/nostr/encoders/hex"
  13  	"next.orly.dev/pkg/nostr/encoders/tag"
  14  )
  15  
  16  // Tag binary encoding constants (matching the nostr library)
  17  const (
  18  	// BinaryEncodedLen is the length of a binary-encoded 32-byte hash with null terminator
  19  	BinaryEncodedLen = 33
  20  	// HexEncodedLen is the length of a hex-encoded 32-byte hash (pubkey or event ID)
  21  	HexEncodedLen = 64
  22  	// HashLen is the raw length of a hash (pubkey/event ID)
  23  	HashLen = 32
  24  )
  25  
  26  // IsBinaryEncoded checks if a value is stored in the nostr library's binary-optimized format
  27  func IsBinaryEncoded(val []byte) bool {
  28  	return len(val) == BinaryEncodedLen && val[HashLen] == 0
  29  }
  30  
  31  // NormalizePubkeyHex ensures a pubkey/event ID is in lowercase hex format.
  32  // It handles:
  33  // - Binary-encoded values (33 bytes with null terminator) -> converts to lowercase hex
  34  // - Raw binary values (32 bytes) -> converts to lowercase hex
  35  // - Uppercase hex strings -> converts to lowercase
  36  // - Already lowercase hex -> returns as-is
  37  //
  38  // This should be used for all pubkeys and event IDs before storing in Neo4j
  39  // to prevent duplicate nodes due to case differences.
  40  func NormalizePubkeyHex(val []byte) string {
  41  	// Handle binary-encoded values from the nostr library (33 bytes with null terminator)
  42  	if IsBinaryEncoded(val) {
  43  		// Convert binary to lowercase hex
  44  		return hex.Enc(val[:HashLen])
  45  	}
  46  
  47  	// Handle raw binary values (32 bytes) - common when passing ev.ID or ev.Pubkey directly
  48  	if len(val) == HashLen {
  49  		// Convert binary to lowercase hex
  50  		return hex.Enc(val)
  51  	}
  52  
  53  	// Handle hex strings (may be uppercase from external sources)
  54  	if len(val) == HexEncodedLen {
  55  		return strings.ToLower(string(val))
  56  	}
  57  
  58  	// For other lengths (possibly prefixes), lowercase the hex
  59  	return strings.ToLower(string(val))
  60  }
  61  
  62  // ExtractPTagValue extracts a pubkey from a p-tag, handling binary encoding.
  63  // Returns lowercase hex string suitable for Neo4j storage.
  64  // Returns empty string if the tag doesn't have a valid value.
  65  func ExtractPTagValue(t *tag.T) string {
  66  	if t == nil || len(t.T) < 2 {
  67  		return ""
  68  	}
  69  
  70  	// Use ValueHex() which properly handles both binary and hex formats
  71  	hexVal := t.ValueHex()
  72  	if len(hexVal) == 0 {
  73  		return ""
  74  	}
  75  
  76  	// Ensure lowercase (ValueHex returns the library's encoding which is lowercase,
  77  	// but we normalize anyway for safety with external data)
  78  	return strings.ToLower(string(hexVal))
  79  }
  80  
  81  // ExtractETagValue extracts an event ID from an e-tag, handling binary encoding.
  82  // Returns lowercase hex string suitable for Neo4j storage.
  83  // Returns empty string if the tag doesn't have a valid value.
  84  func ExtractETagValue(t *tag.T) string {
  85  	if t == nil || len(t.T) < 2 {
  86  		return ""
  87  	}
  88  
  89  	// Use ValueHex() which properly handles both binary and hex formats
  90  	hexVal := t.ValueHex()
  91  	if len(hexVal) == 0 {
  92  		return ""
  93  	}
  94  
  95  	// Ensure lowercase
  96  	return strings.ToLower(string(hexVal))
  97  }
  98  
  99  // IsValidHexPubkey checks if a string is a valid 64-character hex pubkey
 100  func IsValidHexPubkey(s string) bool {
 101  	if len(s) != HexEncodedLen {
 102  		return false
 103  	}
 104  	for _, c := range s {
 105  		if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
 106  			return false
 107  		}
 108  	}
 109  	return true
 110  }
 111