lnwire: New API for feature vectors.

This is a rewrite of feature vectors in lnwire. This has a few
benefits:

1) a simpler interface
2) separate structs for a plain set of feature bits and a feature
vector with associated feature names
their respective feature sets
3) loosened requirements that bits MUST be assigned in pairs
4) fix endianness of encoding/decoding
This commit is contained in:
Jim Posen 2017-10-11 11:25:37 -07:00 committed by Olaoluwa Osuntokun
parent b4273d1eaa
commit 1633ab180f
2 changed files with 384 additions and 347 deletions

View File

@ -2,49 +2,24 @@ package lnwire
import (
"encoding/binary"
"fmt"
"io"
"math"
"github.com/go-errors/errors"
)
// featureFlag represent the status of the feature optional/required and needed
// to allow future incompatible changes, or backward compatible changes.
type featureFlag uint8
// String returns the string representation for the featureFlag.
func (f featureFlag) String() string {
switch f {
case OptionalFlag:
return "optional"
case RequiredFlag:
return "required"
default:
return "<unknown>"
}
}
// featureName represent the name of the feature and needed in order to have
// the compile errors if we specify wrong feature name.
type featureName string
// FeatureBit represents a feature that can be enabled in either a local or
// global feature vector at a specific bit position. Feature bits follow the
// "it's OK to be odd" rule, where features at even bit positions must be known
// to a node receiving them from a peer while odd bits do not. In accordance,
// feature bits are usually assigned in pairs, first being assigned an odd bit
// position which may later be changed to the preceding even position once
// knowledge of the feature becomes required on the network.
type FeatureBit uint16
const (
// OptionalFlag represent the feature which we already have but it
// isn't required yet, and if remote peer doesn't have this feature we
// may turn it off without disconnecting with peer.
OptionalFlag featureFlag = 2 // 0b10
// RequiredFlag represent the features which is required for proper
// peer interaction, we disconnect with peer if it doesn't have this
// particular feature.
RequiredFlag featureFlag = 1 // 0b01
// flagMask is a mask which is needed to extract feature flag value.
flagMask = 3 // 0b11
// flagBitsSize represent the size of the feature flag in bits. For
// more information read the init message specification.
flagBitsSize = 2
// InitialRoutingSync is a local feature bit meaning that the receiving node
// should send a complete dump of routing information when a new connection
// is established.
InitialRoutingSync FeatureBit = 3
// maxAllowedSize is a maximum allowed size of feature vector.
//
@ -61,61 +36,62 @@ const (
maxAllowedSize = 32781
)
// Feature represent the feature which is used on stage of initialization of
// feature vector. Initial feature flags might be changed dynamically later.
type Feature struct {
Name featureName
Flag featureFlag
// LocalFeatures is a mapping of known connection-local feature bits to a
// descriptive name. All known local feature bits must be assigned a name in
// this mapping. Local features are those which are only sent to the peer and
// not advertised to the entire network. A full description of these feature
// bits is provided in the BOLT-09 specification.
var LocalFeatures = map[FeatureBit]string{
InitialRoutingSync: "initial-routing-sync",
}
// FeatureVector represents the global/local feature vector. With this
// structure you may set/get the feature by name and compare feature vector
// with remote one.
type FeatureVector struct {
// featuresMap is the map which stores the correspondence between
// feature name and its index within feature vector. Index within
// feature vector and actual binary position of feature are different
// things)
featuresMap map[featureName]int // name -> index
// GlobalFeatures is a mapping of known global feature bits to a descriptive
// name. All known global feature bits must be assigned a name in this mapping.
// Global features are those which are advertised to the entire network. A full
// description of these feature bits is provided in the BOLT-09 specification.
var GlobalFeatures map[FeatureBit]string
// flags is the map which stores the correspondence between feature
// index and its flag.
flags map[int]featureFlag // index -> flag
// RawFeatureVector represents a set of feature bits as defined in BOLT-09.
// A RawFeatureVector itself just stores a set of bit flags but can be used to
// construct a FeatureVector which binds meaning to each bit. Feature vectors
//can be serialized and deserialized to/from a byte representation that is
// transmitted in Lightning network messages.
type RawFeatureVector struct {
features map[FeatureBit]bool
}
// NewFeatureVector creates new instance of feature vector.
func NewFeatureVector(features []Feature) *FeatureVector {
featuresMap := make(map[featureName]int)
flags := make(map[int]featureFlag)
for index, feature := range features {
featuresMap[feature.Name] = index
flags[index] = feature.Flag
}
return &FeatureVector{
featuresMap: featuresMap,
flags: flags,
// NewRawFeatureVector creates a feature vector with all of the feature bits
// given as arguments enabled.
func NewRawFeatureVector(bits ...FeatureBit) *RawFeatureVector {
fv := &RawFeatureVector{features: make(map[FeatureBit]bool)}
for _, bit := range bits {
fv.Set(bit)
}
return fv
}
// SetFeatureFlag assign flag to the feature.
func (f *FeatureVector) SetFeatureFlag(name featureName, flag featureFlag) error {
position, ok := f.featuresMap[name]
if !ok {
return errors.Errorf("can't find feature with name: %v", name)
}
f.flags[position] = flag
return nil
// IsSet returns whether a particular feature bit is enabled in the vector.
func (fv *RawFeatureVector) IsSet(feature FeatureBit) bool {
return fv.features[feature]
}
// serializedSize returns the number of bytes which is needed to represent
// feature vector in byte format.
func (f *FeatureVector) serializedSize() uint16 {
// Find the largest index in f.flags
// Set marks a feature as enabled in the vector.
func (fv *RawFeatureVector) Set(feature FeatureBit) {
fv.features[feature] = true
}
// Unset marks a feature as disabled in the vector.
func (fv *RawFeatureVector) Unset(feature FeatureBit) {
delete(fv.features, feature)
}
// SerializeSize returns the number of bytes needed to represent feature vector
// in byte format.
func (fv *RawFeatureVector) SerializeSize() int {
// Find the largest feature bit index
max := -1
for index := range f.flags {
for feature := range fv.features {
index := int(feature)
if index > max {
max = index
}
@ -123,181 +99,134 @@ func (f *FeatureVector) serializedSize() uint16 {
if max == -1 {
return 0
}
// We calculate length via the largest index in f.flags so as to not
// get an index out of bounds in Encode's setFlag function.
return uint16(math.Ceil(float64(flagBitsSize*(max+1)) / 8))
// We calculate byte-length via the largest bit index
return max/8 + 1
}
// NewFeatureVectorFromReader decodes the feature vector from binary
// representation and creates the instance of it. Every feature decoded as 2
// bits where odd bit determine whether the feature is "optional" and even bit
// told us whether the feature is "required". The even/odd semantic allows
// future incompatible changes, or backward compatible changes. Bits generally
// assigned in pairs, so that optional features can later become compulsory.
func NewFeatureVectorFromReader(r io.Reader) (*FeatureVector, error) {
f := &FeatureVector{
flags: make(map[int]featureFlag),
}
getFlag := func(data []byte, position int) featureFlag {
byteNumber := uint(position / 8)
bitNumber := uint(position % 8)
return featureFlag((data[byteNumber] >> bitNumber) & flagMask)
}
// Read the length of the feature vector.
var l [2]byte
if _, err := io.ReadFull(r, l[:]); err != nil {
return nil, err
}
length := binary.BigEndian.Uint16(l[:])
// Read the feature vector data.
data := make([]byte, length)
if _, err := io.ReadFull(r, data[:]); err != nil {
return nil, err
}
// Initialize feature vector.
bitsNumber := len(data) * 8
for position := 0; position <= bitsNumber-flagBitsSize; position += flagBitsSize {
flag := getFlag(data, position)
switch flag {
case OptionalFlag, RequiredFlag:
// Every feature/flag takes 2 bits, so in order to get
// the feature/flag index we should divide position
// on 2.
index := position / flagBitsSize
f.flags[index] = flag
default:
continue
}
}
return f, nil
}
// Encode encodes the features vector into bytes representation, every feature
// encoded as 2 bits where odd bit determine whether the feature is "optional"
// and even bit told us whether the feature is "required". The even/odd
// semantic allows future incompatible changes, or backward compatible changes.
// Bits generally assigned in pairs, so that optional features can later become
// compulsory.
func (f *FeatureVector) Encode(w io.Writer) error {
setFlag := func(data []byte, position int, flag featureFlag) {
byteNumber := uint(position / 8)
bitNumber := uint(position % 8)
data[byteNumber] |= (byte(flag) << bitNumber)
}
// Encode writes the feature vector in byte representation. Every feature
// encoded as a bit, and the bit vector is serialized using the least number of
// bytes. Since the bit vector length is variable, the first two bytes of the
// serialization represent the length.
func (fv *RawFeatureVector) Encode(w io.Writer) error {
// Write length of feature vector.
var l [2]byte
length := f.serializedSize()
binary.BigEndian.PutUint16(l[:], length)
length := fv.SerializeSize()
binary.BigEndian.PutUint16(l[:], uint16(length))
if _, err := w.Write(l[:]); err != nil {
return err
}
// Generate the data and write it.
data := make([]byte, length)
for index, flag := range f.flags {
// Every feature takes 2 bits, so in order to get the feature
// bits position we should multiply index by 2.
position := index * flagBitsSize
setFlag(data, position, flag)
for feature := range fv.features {
byteIndex := int(feature / 8)
bitIndex := feature % 8
data[length-byteIndex-1] |= 1 << bitIndex
}
_, err := w.Write(data)
return err
}
// Compare checks that features are compatible and returns the features which
// were present in both remote and local feature vectors. If remote/local node
// doesn't have the feature and local/remote node require it than such vectors
// are incompatible.
func (f *FeatureVector) Compare(f2 *FeatureVector) (*SharedFeatures, error) {
shared := newSharedFeatures(f.Copy())
// Decode reads the feature vector from its byte representation. Every feature
// encoded as a bit, and the bit vector is serialized using the least number of
// bytes. Since the bit vector length is variable, the first two bytes of the
// serialization represent the length.
func (fv *RawFeatureVector) Decode(r io.Reader) error {
// Read the length of the feature vector.
var l [2]byte
if _, err := io.ReadFull(r, l[:]); err != nil {
return err
}
length := binary.BigEndian.Uint16(l[:])
for index, flag := range f.flags {
if _, exist := f2.flags[index]; !exist {
switch flag {
case RequiredFlag:
return nil, errors.New("Remote node hasn't " +
"locally required feature")
case OptionalFlag:
// If feature is optional and remote side
// haven't it than it might be safely disabled.
delete(shared.flags, index)
continue
}
}
// If feature exists on both sides than such feature might be
// considered as active.
shared.flags[index] = flag
// Read the feature vector data.
data := make([]byte, length)
if _, err := io.ReadFull(r, data); err != nil {
return err
}
for index, flag := range f2.flags {
if _, exist := f.flags[index]; !exist {
switch flag {
case RequiredFlag:
return nil, errors.New("Local node hasn't " +
"locally required feature")
case OptionalFlag:
// If feature is optional and local side
// haven't it than it might be safely disabled.
delete(shared.flags, index)
continue
}
}
// If feature exists on both sides than such feature might be
// considered as active.
shared.flags[index] = flag
}
return shared, nil
}
// Copy generate new distinct instance of the feature vector.
func (f *FeatureVector) Copy() *FeatureVector {
features := make([]Feature, len(f.featuresMap))
for name, index := range f.featuresMap {
features[index] = Feature{
Name: name,
Flag: f.flags[index],
// Set feature bits from parsed data.
bitsNumber := len(data) * 8
for i := 0; i < bitsNumber; i++ {
byteIndex := uint16(i / 8)
bitIndex := uint(i % 8)
if (data[length-byteIndex-1]>>bitIndex)&1 == 1 {
fv.Set(FeatureBit(i))
}
}
return NewFeatureVector(features)
return nil
}
// SharedFeatures is a product of comparison of two features vector which
// consist of features which are present in both local and remote features
// vectors.
type SharedFeatures struct {
*FeatureVector
// FeatureVector represents a set of enabled features. The set stores
// information on enabled flags and metadata about the feature names. A feature
// vector is serializable to a compact byte representation that is included in
// Lightning network messages.
type FeatureVector struct {
*RawFeatureVector
featureNames map[FeatureBit]string
}
// newSharedFeatures creates new shared features instance.
func newSharedFeatures(f *FeatureVector) *SharedFeatures {
return &SharedFeatures{f}
}
// NewFeatureVector constructs a new FeatureVector from a raw feature vector and
// mapping of feature definitions.
func NewFeatureVector(featureVector *RawFeatureVector,
featureNames map[FeatureBit]string) *FeatureVector {
// IsActive checks is feature active or not, it might be disabled during
// comparision with remote feature vector if it was optional and remote peer
// doesn't support it.
func (f *SharedFeatures) IsActive(name featureName) bool {
index, ok := f.featuresMap[name]
if !ok {
// If we even have no such feature in feature map, than it
// can't be active in any circumstances.
return false
return &FeatureVector{
RawFeatureVector: featureVector,
featureNames: featureNames,
}
_, exist := f.flags[index]
return exist
}
// HasFeature returns whether a particular feature is included in the set. The
// feature can be seen as set either if the bit is set directly OR the queried
// bit has the same meaning as its corresponding even/odd bit, which is set
// instead. The second case is because feature bits are generally assigned in
// pairs where both the even and odd position represent the same feature.
func (fv *FeatureVector) HasFeature(feature FeatureBit) bool {
return fv.IsSet(feature) ||
(fv.isFeatureBitPair(feature) && fv.IsSet(feature^1))
}
// UnknownRequiredFeatures returns a list of feature bits set in the vector that
// are unknown and in an even bit position. Feature bits with an even index must
// be known to a node receiving the feature vector in a message.
func (fv *FeatureVector) UnknownRequiredFeatures() []FeatureBit {
var unknown []FeatureBit
for feature := range fv.features {
if feature%2 == 0 && !fv.IsKnown(feature) {
unknown = append(unknown, feature)
}
}
return unknown
}
// Name returns a string identifier for the feature represented by this bit. If
// the bit does not represent a known feature, this returns a string indicating
// as much.
func (fv *FeatureVector) Name(bit FeatureBit) string {
name, known := fv.featureNames[bit]
if !known {
name = "unknown"
}
return fmt.Sprintf("%s(%d)", name, bit)
}
// IsKnown returns whether this feature bit represents a known feature.
func (fv *FeatureVector) IsKnown(bit FeatureBit) bool {
_, known := fv.featureNames[bit]
return known
}
// isFeatureBitPair returns whether this feature bit and its corresponding
// even/odd bit both represent the same feature. This may often be the case as
// bits are generally assigned in pairs, first being assigned an odd bit
// position then being promoted to an even bit position once the network is
// ready.
func (fv *FeatureVector) isFeatureBitPair(bit FeatureBit) bool {
name1, known1 := fv.featureNames[bit]
name2, known2 := fv.featureNames[bit^1]
return known1 && known2 && name1 == name2
}

View File

@ -3,152 +3,260 @@ package lnwire
import (
"bytes"
"reflect"
"sort"
"testing"
"github.com/davecgh/go-spew/spew"
)
// TestFeaturesRemoteRequireError checks that we throw an error if remote peer
// has required feature which we don't support.
func TestFeaturesRemoteRequireError(t *testing.T) {
var testFeatureNames = map[FeatureBit]string{
0: "feature1",
3: "feature2",
4: "feature3",
5: "feature3",
}
func TestFeatureVectorSetUnset(t *testing.T) {
t.Parallel()
const (
first = "first"
second = "second"
)
tests := []struct {
bits []FeatureBit
expectedFeatures []bool
}{
// No features are enabled if no bits are set.
{
bits: nil,
expectedFeatures: []bool{false, false, false, false, false, false, false, false},
},
// Test setting an even bit for an even-only bit feature. The
// corresponding odd bit should not be seen as set.
{
bits: []FeatureBit{0},
expectedFeatures: []bool{true, false, false, false, false, false, false, false},
},
// Test setting an odd bit for an even-only bit feature. The
// corresponding even bit should not be seen as set.
{
bits: []FeatureBit{1},
expectedFeatures: []bool{false, true, false, false, false, false, false, false},
},
// Test setting an even bit for an odd-only bit feature. The bit should
// be seen as set and the odd bit should not.
{
bits: []FeatureBit{2},
expectedFeatures: []bool{false, false, true, false, false, false, false, false},
},
// Test setting an odd bit for an odd-only bit feature. The bit should
// be seen as set and the even bit should not.
{
bits: []FeatureBit{3},
expectedFeatures: []bool{false, false, false, true, false, false, false, false},
},
// Test setting an even bit for even-odd pair feature. Both bits in the
// pair should be seen as set.
{
bits: []FeatureBit{4},
expectedFeatures: []bool{false, false, false, false, true, true, false, false},
},
// Test setting an odd bit for even-odd pair feature. Both bits in the
// pair should be seen as set.
{
bits: []FeatureBit{5},
expectedFeatures: []bool{false, false, false, false, true, true, false, false},
},
// Test setting an even bit for an unknown feature. The bit should be
// seen as set and the odd bit should not.
{
bits: []FeatureBit{6},
expectedFeatures: []bool{false, false, false, false, false, false, true, false},
},
// Test setting an odd bit for an unknown feature. The bit should be
// seen as set and the odd bit should not.
{
bits: []FeatureBit{7},
expectedFeatures: []bool{false, false, false, false, false, false, false, true},
},
}
localFeatures := NewFeatureVector([]Feature{
{first, OptionalFlag},
})
fv := NewFeatureVector(nil, testFeatureNames)
for i, test := range tests {
for _, bit := range test.bits {
fv.Set(bit)
}
remoteFeatures := NewFeatureVector([]Feature{
{first, OptionalFlag},
{second, RequiredFlag},
})
for j, expectedSet := range test.expectedFeatures {
if fv.HasFeature(FeatureBit(j)) != expectedSet {
t.Errorf("Expection failed in case %d, bit %d", i, j)
break
}
}
if _, err := localFeatures.Compare(remoteFeatures); err == nil {
t.Fatal("error wasn't received")
for _, bit := range test.bits {
fv.Unset(bit)
}
}
}
// TestFeaturesLocalRequireError checks that we throw an error if local peer has
// required feature which remote peer don't support.
func TestFeaturesLocalRequireError(t *testing.T) {
func TestFeatureVectorEncodeDecode(t *testing.T) {
t.Parallel()
const (
first = "first"
second = "second"
)
tests := []struct {
bits []FeatureBit
expectedEncoded []byte
}{
{
bits: nil,
expectedEncoded: []byte{0x00, 0x00},
},
{
bits: []FeatureBit{2, 3, 7},
expectedEncoded: []byte{0x00, 0x01, 0x8C},
},
{
bits: []FeatureBit{2, 3, 8},
expectedEncoded: []byte{0x00, 0x02, 0x01, 0x0C},
},
}
localFeatures := NewFeatureVector([]Feature{
{first, OptionalFlag},
{second, RequiredFlag},
})
for i, test := range tests {
fv := NewRawFeatureVector(test.bits...)
remoteFeatures := NewFeatureVector([]Feature{
{first, OptionalFlag},
})
// Test that Encode produces the correct serialization.
buffer := new(bytes.Buffer)
err := fv.Encode(buffer)
if err != nil {
t.Errorf("Failed to encode feature vector in case %d: %v", i, err)
continue
}
if _, err := localFeatures.Compare(remoteFeatures); err == nil {
t.Fatal("error wasn't received")
encoded := buffer.Bytes()
if !bytes.Equal(encoded, test.expectedEncoded) {
t.Errorf("Wrong encoding in case %d: got %v, expected %v",
i, encoded, test.expectedEncoded)
continue
}
// Test that decoding then re-encoding produces the same result.
fv2 := NewRawFeatureVector()
err = fv2.Decode(bytes.NewReader(encoded))
if err != nil {
t.Errorf("Failed to decode feature vector in case %d: %v", i, err)
continue
}
buffer2 := new(bytes.Buffer)
err = fv2.Encode(buffer2)
if err != nil {
t.Errorf("Failed to re-encode feature vector in case %d: %v",
i, err)
continue
}
reencoded := buffer2.Bytes()
if !bytes.Equal(reencoded, test.expectedEncoded) {
t.Errorf("Wrong re-encoding in case %d: got %v, expected %v",
i, reencoded, test.expectedEncoded)
}
}
}
// TestOptionalFeature checks that if remote peer don't have the feature but
// on our side this feature is optional than we mark this feature as disabled.
func TestOptionalFeature(t *testing.T) {
func TestFeatureVectorUnknownFeatures(t *testing.T) {
t.Parallel()
const first = "first"
localFeatures := NewFeatureVector([]Feature{
{first, OptionalFlag},
})
remoteFeatures := NewFeatureVector([]Feature{})
shared, err := localFeatures.Compare(remoteFeatures)
if err != nil {
t.Fatalf("error while feature vector compare: %v", err)
tests := []struct {
bits []FeatureBit
expectedUnknown []FeatureBit
}{
{
bits: nil,
expectedUnknown: nil,
},
// Since bits {0, 3, 4, 5} are known, and only even bits are considered
// required (according to the "it's OK to be odd rule"), that leaves
// {2, 6} as both unknown and required.
{
bits: []FeatureBit{0, 1, 2, 3, 4, 5, 6, 7},
expectedUnknown: []FeatureBit{2, 6},
},
}
if shared.IsActive(first) {
t.Fatal("locally feature was set but remote peer notified us" +
" that it don't have it")
}
for i, test := range tests {
rawVector := NewRawFeatureVector(test.bits...)
fv := NewFeatureVector(rawVector, testFeatureNames)
// A feature with a non-existent name shouldn't be active.
if shared.IsActive("nothere") {
t.Fatal("non-existent feature shouldn't be active")
unknown := fv.UnknownRequiredFeatures()
// Sort to make comparison independent of order
sort.Slice(unknown, func(i, j int) bool {
return unknown[i] < unknown[j]
})
if !reflect.DeepEqual(unknown, test.expectedUnknown) {
t.Errorf("Wrong unknown features in case %d: got %v, expected %v",
i, unknown, test.expectedUnknown)
}
}
}
// TestSetRequireAfterInit checks that we can change the feature flag after
// initialization.
func TestSetRequireAfterInit(t *testing.T) {
func TestFeatureNames(t *testing.T) {
t.Parallel()
const first = "first"
tests := []struct {
bit FeatureBit
expectedName string
expectedKnown bool
}{
{
bit: 0,
expectedName: "feature1(0)",
expectedKnown: true,
},
{
bit: 1,
expectedName: "unknown(1)",
expectedKnown: false,
},
{
bit: 2,
expectedName: "unknown(2)",
expectedKnown: false,
},
{
bit: 3,
expectedName: "feature2(3)",
expectedKnown: true,
},
{
bit: 4,
expectedName: "feature3(4)",
expectedKnown: true,
},
{
bit: 5,
expectedName: "feature3(5)",
expectedKnown: true,
},
{
bit: 6,
expectedName: "unknown(6)",
expectedKnown: false,
},
{
bit: 7,
expectedName: "unknown(7)",
expectedKnown: false,
},
}
localFeatures := NewFeatureVector([]Feature{
{first, OptionalFlag},
})
localFeatures.SetFeatureFlag(first, RequiredFlag)
remoteFeatures := NewFeatureVector([]Feature{})
fv := NewFeatureVector(nil, testFeatureNames)
for _, test := range tests {
name := fv.Name(test.bit)
if name != test.expectedName {
t.Errorf("Name for feature bit %d is incorrect: "+
"expected %s, got %s", test.bit, name, test.expectedName)
}
_, err := localFeatures.Compare(remoteFeatures)
if err == nil {
t.Fatalf("feature was set as required but error wasn't "+
"returned: %v", err)
}
}
// TestDecodeEncodeFeaturesVector checks that feature vector might be
// successfully encoded and decoded.
func TestDecodeEncodeFeaturesVector(t *testing.T) {
t.Parallel()
const first = "first"
f := NewFeatureVector([]Feature{
{first, OptionalFlag},
})
var b bytes.Buffer
if err := f.Encode(&b); err != nil {
t.Fatalf("error while encoding feature vector: %v", err)
}
nf, err := NewFeatureVectorFromReader(&b)
if err != nil {
t.Fatalf("error while decoding feature vector: %v", err)
}
// Assert equality of the two instances.
if !reflect.DeepEqual(f.flags, nf.flags) {
t.Fatalf("encode/decode feature vector don't match %v vs "+
"%v", spew.Sdump(f), spew.Sdump(nf))
}
}
func TestFeatureFlagString(t *testing.T) {
t.Parallel()
if OptionalFlag.String() != "optional" {
t.Fatalf("incorrect string, expected optional got %v",
OptionalFlag.String())
}
if RequiredFlag.String() != "required" {
t.Fatalf("incorrect string, expected required got %v",
OptionalFlag.String())
}
fakeFlag := featureFlag(9)
if fakeFlag.String() != "<unknown>" {
t.Fatalf("incorrect string, expected <unknown> got %v",
fakeFlag.String())
known := fv.IsKnown(test.bit)
if known != test.expectedKnown {
t.Errorf("IsKnown for feature bit %d is incorrect: "+
"expected %v, got %v", test.bit, known, test.expectedKnown)
}
}
}