nip77: remove third-party syncing from last commit and expose stuff so it can be implemented elsewhere (nak) directly.

This commit is contained in:
fiatjaf
2025-12-01 18:08:55 -03:00
parent 15dc5b11aa
commit 668d6fc956
3 changed files with 48 additions and 307 deletions

View File

@@ -7,8 +7,8 @@ import (
"fiatjaf.com/nostr" "fiatjaf.com/nostr"
) )
func (n *Negentropy) readTimestamp(reader *bytes.Reader) (nostr.Timestamp, error) { func (br *BoundReader) ReadTimestamp(reader *bytes.Reader) (nostr.Timestamp, error) {
delta, err := readVarInt(reader) delta, err := ReadVarInt(reader)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@@ -16,7 +16,7 @@ func (n *Negentropy) readTimestamp(reader *bytes.Reader) (nostr.Timestamp, error
if delta == 0 { if delta == 0 {
// zeroes are infinite // zeroes are infinite
timestamp := maxTimestamp timestamp := maxTimestamp
n.lastTimestampIn = timestamp br.lastTimestampIn = timestamp
return timestamp, nil return timestamp, nil
} }
@@ -24,21 +24,21 @@ func (n *Negentropy) readTimestamp(reader *bytes.Reader) (nostr.Timestamp, error
delta-- delta--
// we add the previously cached timestamp to get the current // we add the previously cached timestamp to get the current
timestamp := n.lastTimestampIn + nostr.Timestamp(delta) timestamp := br.lastTimestampIn + nostr.Timestamp(delta)
// cache this so we can apply it to the delta next time // cache this so we can apply it to the delta next time
n.lastTimestampIn = timestamp br.lastTimestampIn = timestamp
return timestamp, nil return timestamp, nil
} }
func (n *Negentropy) readBound(reader *bytes.Reader) (Bound, error) { func (br *BoundReader) ReadBound(reader *bytes.Reader) (Bound, error) {
timestamp, err := n.readTimestamp(reader) timestamp, err := br.ReadTimestamp(reader)
if err != nil { if err != nil {
return Bound{}, fmt.Errorf("failed to decode bound timestamp: %w", err) return Bound{}, fmt.Errorf("failed to decode bound timestamp: %w", err)
} }
length, err := readVarInt(reader) length, err := ReadVarInt(reader)
if err != nil { if err != nil {
return Bound{}, fmt.Errorf("failed to decode bound length: %w", err) return Bound{}, fmt.Errorf("failed to decode bound length: %w", err)
} }
@@ -51,28 +51,28 @@ func (n *Negentropy) readBound(reader *bytes.Reader) (Bound, error) {
return Bound{timestamp, pfb}, nil return Bound{timestamp, pfb}, nil
} }
func (n *Negentropy) writeTimestamp(w *bytes.Buffer, timestamp nostr.Timestamp) { func (bw *BoundWriter) WriteTimestamp(w *bytes.Buffer, timestamp nostr.Timestamp) {
if timestamp == maxTimestamp { if timestamp == maxTimestamp {
// zeroes are infinite // zeroes are infinite
n.lastTimestampOut = maxTimestamp // cache this (see below) bw.lastTimestampOut = maxTimestamp // cache this (see below)
writeVarInt(w, 0) WriteVarInt(w, 0)
return return
} }
// we will only encode the difference between this timestamp and the previous // we will only encode the difference between this timestamp and the previous
delta := timestamp - n.lastTimestampOut delta := timestamp - bw.lastTimestampOut
// we cache this here as the next timestamp we encode will be just a delta from this // we cache this here as the next timestamp we encode will be just a delta from this
n.lastTimestampOut = timestamp bw.lastTimestampOut = timestamp
// add 1 to prevent zeroes from being read as infinites // add 1 to prevent zeroes from being read as infinites
writeVarInt(w, int(delta+1)) WriteVarInt(w, int(delta+1))
return return
} }
func (n *Negentropy) writeBound(w *bytes.Buffer, bound Bound) { func (bw *BoundWriter) WriteBound(w *bytes.Buffer, bound Bound) {
n.writeTimestamp(w, bound.Timestamp) bw.WriteTimestamp(w, bound.Timestamp)
writeVarInt(w, len(bound.IDPrefix)) WriteVarInt(w, len(bound.IDPrefix))
w.Write(bound.IDPrefix) w.Write(bound.IDPrefix)
} }
@@ -93,7 +93,7 @@ func getMinimalBound(prev, curr Item) Bound {
return Bound{curr.Timestamp, curr.ID[:(sharedPrefixBytes + 1)]} return Bound{curr.Timestamp, curr.ID[:(sharedPrefixBytes + 1)]}
} }
func readVarInt(reader *bytes.Reader) (int, error) { func ReadVarInt(reader *bytes.Reader) (int, error) {
var res int = 0 var res int = 0
for { for {
@@ -111,7 +111,7 @@ func readVarInt(reader *bytes.Reader) (int, error) {
return res, nil return res, nil
} }
func writeVarInt(w *bytes.Buffer, n int) { func WriteVarInt(w *bytes.Buffer, n int) {
if n == 0 { if n == 0 {
w.WriteByte(0) w.WriteByte(0)
return return

View File

@@ -11,7 +11,7 @@ import (
) )
const ( const (
protocolVersion byte = 0x61 // version 1 ProtocolVersion byte = 0x61 // version 1
maxTimestamp = nostr.Timestamp(math.MaxInt64) maxTimestamp = nostr.Timestamp(math.MaxInt64)
buckets = 16 buckets = 16
) )
@@ -19,17 +19,26 @@ const (
var InfiniteBound = Bound{Timestamp: maxTimestamp} var InfiniteBound = Bound{Timestamp: maxTimestamp}
type Negentropy struct { type Negentropy struct {
storage Storage storage Storage
initialized bool initialized bool
frameSizeLimit int frameSizeLimit int
isClient bool isClient bool
lastTimestampIn nostr.Timestamp
lastTimestampOut nostr.Timestamp BoundWriter
BoundReader
Haves chan nostr.ID Haves chan nostr.ID
HaveNots chan nostr.ID HaveNots chan nostr.ID
} }
type BoundReader struct {
lastTimestampIn nostr.Timestamp
}
type BoundWriter struct {
lastTimestampOut nostr.Timestamp
}
func New(storage Storage, frameSizeLimit int, up, down bool) *Negentropy { func New(storage Storage, frameSizeLimit int, up, down bool) *Negentropy {
if frameSizeLimit == 0 { if frameSizeLimit == 0 {
frameSizeLimit = math.MaxInt frameSizeLimit = math.MaxInt
@@ -68,7 +77,7 @@ func (n *Negentropy) Start() string {
n.isClient = true n.isClient = true
output := bytes.NewBuffer(make([]byte, 0, 1+n.storage.Size()*64)) output := bytes.NewBuffer(make([]byte, 0, 1+n.storage.Size()*64))
output.WriteByte(protocolVersion) output.WriteByte(ProtocolVersion)
n.SplitRange(0, n.storage.Size(), InfiniteBound, output) n.SplitRange(0, n.storage.Size(), InfiniteBound, output)
return nostr.HexEncodeToString(output.Bytes()) return nostr.HexEncodeToString(output.Bytes())
@@ -105,13 +114,13 @@ func (n *Negentropy) reconcileAux(reader *bytes.Reader) ([]byte, error) {
n.lastTimestampIn, n.lastTimestampOut = 0, 0 // reset for each message n.lastTimestampIn, n.lastTimestampOut = 0, 0 // reset for each message
fullOutput := bytes.NewBuffer(make([]byte, 0, 5000)) fullOutput := bytes.NewBuffer(make([]byte, 0, 5000))
fullOutput.WriteByte(protocolVersion) fullOutput.WriteByte(ProtocolVersion)
pv, err := reader.ReadByte() pv, err := reader.ReadByte()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to read pv: %w", err) return nil, fmt.Errorf("failed to read pv: %w", err)
} }
if pv != protocolVersion { if pv != ProtocolVersion {
if n.isClient { if n.isClient {
return nil, fmt.Errorf("unsupported negentropy protocol version %v", pv) return nil, fmt.Errorf("unsupported negentropy protocol version %v", pv)
} }
@@ -133,16 +142,16 @@ func (n *Negentropy) reconcileAux(reader *bytes.Reader) ([]byte, error) {
// end skip range, if necessary, so we can start a new bound that isn't a skip // end skip range, if necessary, so we can start a new bound that isn't a skip
if skipping { if skipping {
skipping = false skipping = false
n.writeBound(partialOutput, prevBound) n.WriteBound(partialOutput, prevBound)
partialOutput.WriteByte(byte(SkipMode)) partialOutput.WriteByte(byte(SkipMode))
} }
} }
currBound, err := n.readBound(reader) currBound, err := n.ReadBound(reader)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to decode bound: %w", err) return nil, fmt.Errorf("failed to decode bound: %w", err)
} }
modeVal, err := readVarInt(reader) modeVal, err := ReadVarInt(reader)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to decode mode: %w", err) return nil, fmt.Errorf("failed to decode mode: %w", err)
} }
@@ -171,7 +180,7 @@ func (n *Negentropy) reconcileAux(reader *bytes.Reader) ([]byte, error) {
} }
case IdListMode: case IdListMode:
numIds, err := readVarInt(reader) numIds, err := ReadVarInt(reader)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to decode number of ids: %w", err) return nil, fmt.Errorf("failed to decode number of ids: %w", err)
} }
@@ -235,9 +244,9 @@ func (n *Negentropy) reconcileAux(reader *bytes.Reader) ([]byte, error) {
responses++ responses++
} }
n.writeBound(partialOutput, endBound) n.WriteBound(partialOutput, endBound)
partialOutput.WriteByte(byte(IdListMode)) partialOutput.WriteByte(byte(IdListMode))
writeVarInt(partialOutput, responses) WriteVarInt(partialOutput, responses)
partialOutput.Write(responseIds) partialOutput.Write(responseIds)
io.Copy(fullOutput, partialOutput) io.Copy(fullOutput, partialOutput)
@@ -251,7 +260,7 @@ func (n *Negentropy) reconcileAux(reader *bytes.Reader) ([]byte, error) {
if n.frameSizeLimit-200 < fullOutput.Len()+partialOutput.Len() { if n.frameSizeLimit-200 < fullOutput.Len()+partialOutput.Len() {
// frame size limit exceeded, handle by encoding a boundary and fingerprint for the remaining range // frame size limit exceeded, handle by encoding a boundary and fingerprint for the remaining range
remainingFingerprint := n.storage.Fingerprint(upper, n.storage.Size()) remainingFingerprint := n.storage.Fingerprint(upper, n.storage.Size())
n.writeBound(fullOutput, InfiniteBound) n.WriteBound(fullOutput, InfiniteBound)
fullOutput.WriteByte(byte(FingerprintMode)) fullOutput.WriteByte(byte(FingerprintMode))
fullOutput.Write(remainingFingerprint[:]) fullOutput.Write(remainingFingerprint[:])
@@ -273,9 +282,9 @@ func (n *Negentropy) SplitRange(lower, upper int, upperBound Bound, output *byte
if numElems < buckets*2 { if numElems < buckets*2 {
// we just send the full ids here // we just send the full ids here
n.writeBound(output, upperBound) n.WriteBound(output, upperBound)
output.WriteByte(byte(IdListMode)) output.WriteByte(byte(IdListMode))
writeVarInt(output, numElems) WriteVarInt(output, numElems)
for _, item := range n.storage.Range(lower, upper) { for _, item := range n.storage.Range(lower, upper) {
output.Write(item.ID[:]) output.Write(item.ID[:])
@@ -311,7 +320,7 @@ func (n *Negentropy) SplitRange(lower, upper int, upperBound Bound, output *byte
nextBound = minBound nextBound = minBound
} }
n.writeBound(output, nextBound) n.WriteBound(output, nextBound)
output.WriteByte(byte(FingerprintMode)) output.WriteByte(byte(FingerprintMode))
output.Write(ourFingerprint[:]) output.Write(ourFingerprint[:])
} }

View File

@@ -1,268 +0,0 @@
package negentropy
import (
"bytes"
"fmt"
"fiatjaf.com/nostr"
)
type ThirdPartyNegentropy struct {
PeerA NegentropyThirdPartyRemote
PeerB NegentropyThirdPartyRemote
Filter nostr.Filter
Deltas chan Delta
}
type Delta struct {
ID nostr.ID
Have NegentropyThirdPartyRemote
HaveNot NegentropyThirdPartyRemote
}
type boundKey string
func (b Bound) key() boundKey {
return boundKey(fmt.Sprintf("%d:%x", b.Timestamp, b.IDPrefix))
}
type NegentropyThirdPartyRemote interface {
SendInitialMessage(filter nostr.Filter, msg string) error
SendMessage(msg string) error
SendClose() error
Receive() (string, error)
}
func NewThirdPartyNegentropy(peerA, peerB NegentropyThirdPartyRemote, filter nostr.Filter) *ThirdPartyNegentropy {
return &ThirdPartyNegentropy{
PeerA: peerA,
PeerB: peerB,
Filter: filter,
Deltas: make(chan Delta, 100),
}
}
func (n *ThirdPartyNegentropy) Start() error {
peerAIds := make(map[nostr.ID]struct{})
peerBIds := make(map[nostr.ID]struct{})
peerASkippedBounds := make(map[boundKey]struct{})
peerBSkippedBounds := make(map[boundKey]struct{})
// send an empty message to A to start things up
initialMsg := createInitialMessage()
err := n.PeerA.SendInitialMessage(n.Filter, initialMsg)
if err != nil {
return err
}
hasSentInitialMessageToB := false
for {
// receive message from A
msgA, err := n.PeerA.Receive()
if err != nil {
return err
}
msgAb, _ := nostr.HexDecodeString(msgA)
if len(msgAb) == 1 {
break
}
msgToB, err := parseMessageBuildNext(
msgA,
peerBSkippedBounds,
func(id nostr.ID) {
if _, exists := peerBIds[id]; exists {
delete(peerBIds, id)
} else {
peerAIds[id] = struct{}{}
}
},
func(boundKey boundKey) {
peerASkippedBounds[boundKey] = struct{}{}
},
)
if err != nil {
return err
}
// emit deltas from B after receiving message from A
for id := range peerBIds {
n.Deltas <- Delta{ID: id, Have: n.PeerB, HaveNot: n.PeerA}
delete(peerBIds, id)
}
if len(msgToB) == 2 {
// exit condition (no more messages to send)
break
}
// send message to B
if hasSentInitialMessageToB {
err = n.PeerB.SendMessage(msgToB)
} else {
err = n.PeerB.SendInitialMessage(n.Filter, msgToB)
hasSentInitialMessageToB = true
}
if err != nil {
return err
}
// receive message from B
msgB, err := n.PeerB.Receive()
if err != nil {
return err
}
msgBb, _ := nostr.HexDecodeString(msgB)
if len(msgBb) == 1 {
break
}
msgToA, err := parseMessageBuildNext(
msgB,
peerASkippedBounds,
func(id nostr.ID) {
if _, exists := peerAIds[id]; exists {
delete(peerAIds, id)
} else {
peerBIds[id] = struct{}{}
}
},
func(boundKey boundKey) {
peerBSkippedBounds[boundKey] = struct{}{}
},
)
if err != nil {
return err
}
// emit deltas from A after receiving message from B
for id := range peerAIds {
n.Deltas <- Delta{ID: id, Have: n.PeerA, HaveNot: n.PeerB}
delete(peerAIds, id)
}
if len(msgToA) == 2 {
// exit condition (no more messages to send)
break
}
// send message to A
err = n.PeerA.SendMessage(msgToA)
if err != nil {
return err
}
}
// emit remaining deltas before exit
for id := range peerAIds {
n.Deltas <- Delta{ID: id, Have: n.PeerA, HaveNot: n.PeerB}
}
for id := range peerBIds {
n.Deltas <- Delta{ID: id, Have: n.PeerB, HaveNot: n.PeerA}
}
n.PeerA.SendClose()
n.PeerB.SendClose()
close(n.Deltas)
return nil
}
func createInitialMessage() string {
output := bytes.NewBuffer(make([]byte, 0, 64))
output.WriteByte(protocolVersion)
writeVarInt(output, 0) // timestamp for infinite
writeVarInt(output, 0) // prefix len
output.WriteByte(byte(IdListMode))
writeVarInt(output, 0) // num ids
return nostr.HexEncodeToString(output.Bytes())
}
func parseMessageBuildNext(
msg string,
skippedBounds map[boundKey]struct{},
idCallback func(id nostr.ID),
skipCallback func(boundKey boundKey),
) (next string, err error) {
msgb, err := nostr.HexDecodeString(msg)
if err != nil {
return "", err
}
dummy := &Negentropy{}
nextMsg := bytes.NewBuffer(make([]byte, 0, len(msgb)))
dummy32BytePlaceholder := [32]byte{}
reader := bytes.NewReader(msgb)
pv, err := reader.ReadByte()
if err != nil {
return "", err
}
if pv != protocolVersion {
return "", fmt.Errorf("unsupported protocol version %v", pv)
}
nextMsg.WriteByte(pv)
for reader.Len() > 0 {
bound, err := dummy.readBound(reader)
if err != nil {
return "", err
}
modeVal, err := readVarInt(reader)
if err != nil {
return "", err
}
mode := Mode(modeVal)
if _, skipped := skippedBounds[bound.key()]; !skipped {
dummy.writeBound(nextMsg, bound)
writeVarInt(nextMsg, modeVal)
}
switch mode {
case SkipMode:
skipCallback(bound.key())
case FingerprintMode:
_, err = reader.Read(dummy32BytePlaceholder[:])
if err != nil {
return "", err
}
if _, skipped := skippedBounds[bound.key()]; !skipped {
nextMsg.Write(dummy32BytePlaceholder[:])
}
case IdListMode:
skipCallback(bound.key())
numIds, err := readVarInt(reader)
if err != nil {
return "", err
}
if _, skipped := skippedBounds[bound.key()]; !skipped {
writeVarInt(nextMsg, numIds)
}
for range numIds {
_, err = reader.Read(dummy32BytePlaceholder[:])
if err != nil {
return "", err
}
idCallback(dummy32BytePlaceholder)
if _, skipped := skippedBounds[bound.key()]; !skipped {
nextMsg.Write(dummy32BytePlaceholder[:])
}
}
default:
return "", fmt.Errorf("unknown mode %v", mode)
}
}
return nostr.HexEncodeToString(nextMsg.Bytes()), nil
}