waku-org/go-waku

View on GitHub
waku/v2/protocol/rln/nullifier_log.go

Summary

Maintainability
A
0 mins
Test Coverage
D
66%
package rln

import (
    "bytes"
    "context"
    "errors"
    "sync"
    "time"

    "github.com/waku-org/go-waku/waku/v2/utils"
    "github.com/waku-org/go-zerokit-rln/rln"
    "go.uber.org/zap"
)

// NullifierLog is the log of nullifiers and Shamir shares of the past messages grouped per epoch
type NullifierLog struct {
    sync.RWMutex

    log            *zap.Logger
    nullifierLog   map[rln.Nullifier][]rln.ProofMetadata // Might make sense to replace this map by a shrinkable map due to https://github.com/golang/go/issues/20135.
    nullifierQueue []rln.Nullifier
}

// NewNullifierLog creates an instance of NullifierLog
func NewNullifierLog(ctx context.Context, log *zap.Logger) *NullifierLog {
    result := &NullifierLog{
        nullifierLog: make(map[rln.Nullifier][]rln.ProofMetadata),
        log:          log,
    }

    go result.cleanup(ctx)

    return result
}

var errAlreadyExists = errors.New("proof already exists")

// Insert stores a proof in the nullifier log only if it doesnt exist already
func (n *NullifierLog) Insert(proofMD rln.ProofMetadata) error {
    n.Lock()
    defer n.Unlock()

    proofs, ok := n.nullifierLog[proofMD.ExternalNullifier]
    if ok {
        // check if an identical record exists
        for _, p := range proofs {
            if p.Equals(proofMD) {
                // TODO: slashing logic
                return errAlreadyExists
            }
        }
    }

    n.nullifierLog[proofMD.ExternalNullifier] = append(proofs, proofMD)
    n.nullifierQueue = append(n.nullifierQueue, proofMD.ExternalNullifier)
    return nil
}

// HasDuplicate returns true if there is another message in the  `nullifierLog` with the same
// epoch and nullifier as `msg`'s epoch and nullifier but different Shamir secret shares
// otherwise, returns false
func (n *NullifierLog) HasDuplicate(proofMD rln.ProofMetadata) (bool, error) {
    n.RLock()
    defer n.RUnlock()

    proofs, ok := n.nullifierLog[proofMD.ExternalNullifier]
    if !ok {
        // epoch does not exist
        return false, nil
    }

    for _, p := range proofs {
        if p.Equals(proofMD) {
            // there is an identical record, ignore the msg
            return true, nil
        }
    }

    // check for a message with the same nullifier but different secret shares
    matched := false
    for _, it := range proofs {
        if bytes.Equal(it.Nullifier[:], proofMD.Nullifier[:]) && (!bytes.Equal(it.ShareX[:], proofMD.ShareX[:]) || !bytes.Equal(it.ShareY[:], proofMD.ShareY[:])) {
            matched = true
            break
        }
    }

    return matched, nil
}

// cleanup cleans up the log every time there are more than MaxEpochGap epochs stored in it
func (n *NullifierLog) cleanup(ctx context.Context) {
    defer utils.LogOnPanic()
    t := time.NewTicker(1 * time.Minute) // TODO: tune this
    defer t.Stop()

    for {
        select {
        case <-ctx.Done():
            return

        case <-t.C:
            func() {
                n.Lock()
                defer n.Unlock()

                if int64(len(n.nullifierQueue)) < maxEpochGap {
                    return
                }

                n.log.Debug("clearing epochs from the nullifier log", zap.Int64("count", maxEpochGap))

                toDelete := n.nullifierQueue[0:maxEpochGap]
                for _, l := range toDelete {
                    delete(n.nullifierLog, l)
                }
                n.nullifierQueue = n.nullifierQueue[maxEpochGap:]
            }()
        }
    }

}