status-im/status-go

View on GitHub
waku/v0/status_options.go

Summary

Maintainability
A
1 hr
Test Coverage
C
74%
package v0

import (
    "errors"
    "fmt"
    "io"
    "math"
    "reflect"
    "strings"

    "github.com/ethereum/go-ethereum/rlp"

    "github.com/status-im/status-go/waku/common"
)

// statusOptionKey is a current type used in StatusOptions as a key.
type statusOptionKey string

var (
    defaultMinPoW = math.Float64bits(0.001)
    idxFieldKey   = make(map[int]statusOptionKey)
    keyFieldIdx   = make(map[statusOptionKey]int)
)

// StatusOptions defines additional information shared between peers
// during the handshake.
// There might be more options provided than fields in StatusOptions
// and they should be ignored during deserialization to stay forward compatible.
// In the case of RLP, options should be serialized to an array of tuples
// where the first item is a field name and the second is a RLP-serialized value.
type StatusOptions struct {
    PoWRequirement       *uint64            `rlp:"key=0"` // RLP does not support float64 natively
    BloomFilter          []byte             `rlp:"key=1"`
    LightNodeEnabled     *bool              `rlp:"key=2"`
    ConfirmationsEnabled *bool              `rlp:"key=3"`
    RateLimits           *common.RateLimits `rlp:"key=4"`
    TopicInterest        []common.TopicType `rlp:"key=5"`
}

func StatusOptionsFromHost(host common.WakuHost) StatusOptions {
    opts := StatusOptions{}

    rateLimits := host.PacketRateLimits()
    opts.RateLimits = &rateLimits

    lightNode := host.LightClientMode()
    opts.LightNodeEnabled = &lightNode

    minPoW := host.MinPow()
    opts.SetPoWRequirementFromF(minPoW)

    confirmationsEnabled := host.ConfirmationsEnabled()
    opts.ConfirmationsEnabled = &confirmationsEnabled

    bloomFilterMode := host.BloomFilterMode()
    if bloomFilterMode {
        opts.BloomFilter = host.BloomFilter()
    } else {
        opts.TopicInterest = host.TopicInterest()
    }

    return opts
}

// initFLPKeyFields initialises the values of `idxFieldKey` and `keyFieldIdx`
func initRLPKeyFields() {
    o := StatusOptions{}
    v := reflect.ValueOf(o)

    for i := 0; i < v.NumField(); i++ {
        // skip unexported fields
        if !v.Field(i).CanInterface() {
            continue
        }
        rlpTag := v.Type().Field(i).Tag.Get("rlp")

        // skip fields without rlp field tag
        if rlpTag == "" {
            continue
        }

        keys := strings.Split(rlpTag, "=")

        if len(keys) != 2 || keys[0] != "key" {
            panic("invalid value of \"rlp\" tag, expected \"key=N\" where N is uint")
        }

        // typecast key to be of statusOptionKey type
        keyFieldIdx[statusOptionKey(keys[1])] = i
        idxFieldKey[i] = statusOptionKey(keys[1])
    }
}

// WithDefaults adds the default values for a given peer.
// This are not the host default values, but the default values that ought to
// be used when receiving from an update from a peer.
func (o StatusOptions) WithDefaults() StatusOptions {
    if o.PoWRequirement == nil {
        o.PoWRequirement = &defaultMinPoW
    }

    if o.LightNodeEnabled == nil {
        lightNodeEnabled := false
        o.LightNodeEnabled = &lightNodeEnabled
    }

    if o.ConfirmationsEnabled == nil {
        confirmationsEnabled := false
        o.ConfirmationsEnabled = &confirmationsEnabled
    }

    if o.RateLimits == nil {
        o.RateLimits = &common.RateLimits{}
    }

    if o.BloomFilter == nil {
        o.BloomFilter = common.MakeFullNodeBloom()
    }

    return o
}

func (o StatusOptions) PoWRequirementF() *float64 {
    if o.PoWRequirement == nil {
        return nil
    }
    result := math.Float64frombits(*o.PoWRequirement)
    return &result
}

func (o *StatusOptions) SetPoWRequirementFromF(val float64) {
    requirement := math.Float64bits(val)
    o.PoWRequirement = &requirement
}

func (o StatusOptions) EncodeRLP(w io.Writer) error {
    v := reflect.ValueOf(o)
    var optionsList []interface{}
    for i := 0; i < v.NumField(); i++ {
        field := v.Field(i)
        if !field.IsNil() {
            value := field.Interface()
            key, ok := idxFieldKey[i]
            if !ok {
                continue
            }
            if value != nil {
                optionsList = append(optionsList, []interface{}{key, value})
            }
        }
    }
    return rlp.Encode(w, optionsList)
}

func (o *StatusOptions) DecodeRLP(s *rlp.Stream) error {
    _, err := s.List()
    if err != nil {
        return fmt.Errorf("expected an outer list: %v", err)
    }

    v := reflect.ValueOf(o)

loop:
    for {
        _, err := s.List()
        switch err {
        case nil:
            // continue to decode a key
        case rlp.EOL:
            break loop
        default:
            return fmt.Errorf("expected an inner list: %v", err)
        }
        var key statusOptionKey
        if err := s.Decode(&key); err != nil {
            return fmt.Errorf("invalid key: %v", err)
        }
        // Skip processing if a key does not exist.
        // It might happen when there is a new peer
        // which supports a new option with
        // a higher index.
        idx, ok := keyFieldIdx[key]
        if !ok {
            // Read the rest of the list items and dump peer.
            _, err := s.Raw()
            if err != nil {
                return fmt.Errorf("failed to read the value of key %s: %v", key, err)
            }
            continue
        }
        if err := s.Decode(v.Elem().Field(idx).Addr().Interface()); err != nil {
            return fmt.Errorf("failed to decode an option %s: %v", key, err)
        }
        if err := s.ListEnd(); err != nil {
            return err
        }
    }

    return s.ListEnd()
}

func (o StatusOptions) Validate() error {
    if len(o.TopicInterest) > 10000 {
        return errors.New("topic interest is limited by 1000 items")
    }
    return nil
}