diff --git a/lnwire/features.go b/lnwire/features.go index 726e622a..4b054178 100644 --- a/lnwire/features.go +++ b/lnwire/features.go @@ -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 "" - } -} - -// 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 } diff --git a/lnwire/features_test.go b/lnwire/features_test.go index a5fbb25e..177a5a10 100644 --- a/lnwire/features_test.go +++ b/lnwire/features_test.go @@ -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() != "" { - t.Fatalf("incorrect string, expected 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) + } } }