github.com/onflow/flow-go@v0.33.17/module/signature/signer_indices_test.go (about) 1 package signature_test 2 3 import ( 4 "fmt" 5 "sort" 6 "testing" 7 8 "github.com/stretchr/testify/require" 9 "pgregory.net/rapid" 10 11 "github.com/onflow/flow-go/ledger/common/bitutils" 12 "github.com/onflow/flow-go/model/flow" 13 "github.com/onflow/flow-go/model/flow/filter" 14 "github.com/onflow/flow-go/model/flow/filter/id" 15 "github.com/onflow/flow-go/module/signature" 16 "github.com/onflow/flow-go/utils/unittest" 17 ) 18 19 // TestEncodeDecodeIdentities verifies the two path of encoding -> decoding: 20 // 1. Identifiers --encode--> Indices --decode--> Identifiers 21 // 2. for the decoding step, we offer an optimized convenience function to directly 22 // decode to full identities: Indices --decode--> Identities 23 func TestEncodeDecodeIdentities(t *testing.T) { 24 canonicalIdentities := unittest.IdentityListFixture(20) 25 canonicalIdentifiers := canonicalIdentities.NodeIDs() 26 for s := 0; s < 20; s++ { 27 for e := s; e < 20; e++ { 28 var signers = canonicalIdentities[s:e] 29 30 // encoding 31 indices, err := signature.EncodeSignersToIndices(canonicalIdentities.NodeIDs(), signers.NodeIDs()) 32 require.NoError(t, err) 33 34 // decoding option 1: decode to Identifiers 35 decodedIDs, err := signature.DecodeSignerIndicesToIdentifiers(canonicalIdentifiers, indices) 36 require.NoError(t, err) 37 require.Equal(t, signers.NodeIDs(), decodedIDs) 38 39 // decoding option 2: decode to Identities 40 decodedIdentities, err := signature.DecodeSignerIndicesToIdentities(canonicalIdentities, indices) 41 require.NoError(t, err) 42 require.Equal(t, signers, decodedIdentities) 43 } 44 } 45 } 46 47 func TestEncodeDecodeIdentitiesFail(t *testing.T) { 48 canonicalIdentities := unittest.IdentityListFixture(20) 49 canonicalIdentifiers := canonicalIdentities.NodeIDs() 50 signers := canonicalIdentities[3:19] 51 validIndices, err := signature.EncodeSignersToIndices(canonicalIdentities.NodeIDs(), signers.NodeIDs()) 52 require.NoError(t, err) 53 54 _, err = signature.DecodeSignerIndicesToIdentifiers(canonicalIdentifiers, validIndices) 55 require.NoError(t, err) 56 57 invalidSum := make([]byte, len(validIndices)) 58 copy(invalidSum, validIndices) 59 if invalidSum[0] == byte(0) { 60 invalidSum[0] = byte(1) 61 } else { 62 invalidSum[0] = byte(0) 63 } 64 _, err = signature.DecodeSignerIndicesToIdentifiers(canonicalIdentifiers, invalidSum) 65 require.True(t, signature.IsInvalidSignerIndicesError(err), err) 66 require.ErrorIs(t, err, signature.ErrInvalidChecksum, err) 67 68 incompatibleLength := append(validIndices, byte(0)) 69 _, err = signature.DecodeSignerIndicesToIdentifiers(canonicalIdentifiers, incompatibleLength) 70 require.True(t, signature.IsInvalidSignerIndicesError(err), err) 71 require.False(t, signature.IsInvalidSignerIndicesError(signature.NewInvalidSigTypesErrorf("sdf"))) 72 require.ErrorIs(t, err, signature.ErrIncompatibleBitVectorLength, err) 73 74 illegallyPadded := make([]byte, len(validIndices)) 75 copy(illegallyPadded, validIndices) 76 illegallyPadded[len(illegallyPadded)-1]++ 77 _, err = signature.DecodeSignerIndicesToIdentifiers(canonicalIdentifiers, illegallyPadded) 78 require.True(t, signature.IsInvalidSignerIndicesError(err), err) 79 require.ErrorIs(t, err, signature.ErrIllegallyPaddedBitVector, err) 80 } 81 82 func TestEncodeIdentity(t *testing.T) { 83 only := unittest.IdentifierListFixture(1) 84 indices, err := signature.EncodeSignersToIndices(only, only) 85 require.NoError(t, err) 86 // byte(1,0,0,0,0,0,0,0) 87 require.Equal(t, []byte{byte(1 << 7)}, indices[signature.CheckSumLen:]) 88 } 89 90 // TestEncodeFail verifies that an error is returned in case some signer is not part 91 // of the set of canonicalIdentifiers 92 func TestEncodeFail(t *testing.T) { 93 fullIdentities := unittest.IdentifierListFixture(20) 94 _, err := signature.EncodeSignersToIndices(fullIdentities[1:], fullIdentities[:10]) 95 require.Error(t, err) 96 } 97 98 // Test_EncodeSignerToIndicesAndSigType uses fuzzy-testing framework Rapid to 99 // test the method EncodeSignerToIndicesAndSigType: 100 // * we generate a set of authorized signer: `committeeIdentities` 101 // * part of this set is sampled as staking singers: `stakingSigners` 102 // * another part of `committeeIdentities` is sampled as beacon singers: `beaconSigners` 103 // * we encode the set and check that the results conform to the protocol specification 104 func Test_EncodeSignerToIndicesAndSigType(t *testing.T) { 105 rapid.Check(t, func(t *rapid.T) { 106 // select total committee size, number of random beacon signers and number of staking signers 107 committeeSize := rapid.IntRange(1, 272).Draw(t, "committeeSize") 108 numStakingSigners := rapid.IntRange(0, committeeSize).Draw(t, "numStakingSigners") 109 numRandomBeaconSigners := rapid.IntRange(0, committeeSize-numStakingSigners).Draw(t, "numRandomBeaconSigners") 110 111 // create committee 112 committeeIdentities := unittest.IdentityListFixture(committeeSize, unittest.WithRole(flow.RoleConsensus)).Sort(flow.Canonical) 113 committee := committeeIdentities.NodeIDs() 114 stakingSigners, beaconSigners := sampleSigners(t, committee, numStakingSigners, numRandomBeaconSigners) 115 116 // encode 117 prefixed, sigTypes, err := signature.EncodeSignerToIndicesAndSigType(committee, stakingSigners, beaconSigners) 118 require.NoError(t, err) 119 120 signerIndices, err := signature.CompareAndExtract(committeeIdentities.NodeIDs(), prefixed) 121 require.NoError(t, err) 122 123 // check verify signer indices 124 unorderedSigners := stakingSigners.Union(beaconSigners) // caution, the Union operation potentially changes the ordering 125 correctEncoding(t, signerIndices, committee, unorderedSigners) 126 127 // check sigTypes 128 canSigners := committeeIdentities.Filter(filter.HasNodeID(unorderedSigners...)).NodeIDs() // generates list of signer IDs in canonical order 129 correctEncoding(t, sigTypes, canSigners, beaconSigners) 130 }) 131 } 132 133 // Test_DecodeSigTypeToStakingAndBeaconSigners uses fuzzy-testing framework Rapid to 134 // test the method DecodeSigTypeToStakingAndBeaconSigners: 135 // - we generate a set of authorized signer: `committeeIdentities` 136 // - part of this set is sampled as staking singers: `stakingSigners` 137 // - another part of `committeeIdentities` is sampled as beacon singers: `beaconSigners` 138 // - we encode the set and check that the results conform to the protocol specification 139 // - We encode the set using `EncodeSignerToIndicesAndSigType` (tested before) and then decode it. 140 // Thereby we should recover the original input. Caution, the order might be different, 141 // so we sort both sets. 142 func Test_DecodeSigTypeToStakingAndBeaconSigners(t *testing.T) { 143 rapid.Check(t, func(t *rapid.T) { 144 // select total committee size, number of random beacon signers and number of staking signers 145 committeeSize := rapid.IntRange(1, 272).Draw(t, "committeeSize") 146 numStakingSigners := rapid.IntRange(0, committeeSize).Draw(t, "numStakingSigners") 147 numRandomBeaconSigners := rapid.IntRange(0, committeeSize-numStakingSigners).Draw(t, "numRandomBeaconSigners") 148 149 // create committee 150 committeeIdentities := unittest.IdentityListFixture(committeeSize, unittest.WithRole(flow.RoleConsensus)).Sort(flow.Canonical) 151 committee := committeeIdentities.NodeIDs() 152 stakingSigners, beaconSigners := sampleSigners(t, committee, numStakingSigners, numRandomBeaconSigners) 153 154 // encode 155 signerIndices, sigTypes, err := signature.EncodeSignerToIndicesAndSigType(committee, stakingSigners, beaconSigners) 156 require.NoError(t, err) 157 158 // decode 159 decSignerIdentites, err := signature.DecodeSignerIndicesToIdentities(committeeIdentities, signerIndices) 160 require.NoError(t, err) 161 decStakingSigners, decBeaconSigners, err := signature.DecodeSigTypeToStakingAndBeaconSigners(decSignerIdentites, sigTypes) 162 require.NoError(t, err) 163 164 // verify; note that there is a slightly different convention between Filter and the decoding logic: 165 // Filter returns nil for an empty list, while the decoding logic returns an instance of an empty slice 166 sigIdentities := committeeIdentities.Filter(filter.Or(filter.HasNodeID(stakingSigners...), filter.HasNodeID(beaconSigners...))) // signer identities in canonical order 167 if len(stakingSigners)+len(decBeaconSigners) > 0 { 168 require.Equal(t, sigIdentities, decSignerIdentites) 169 } 170 if len(stakingSigners) == 0 { 171 require.Empty(t, decStakingSigners) 172 } else { 173 require.Equal(t, committeeIdentities.Filter(filter.HasNodeID(stakingSigners...)), decStakingSigners) 174 } 175 if len(decBeaconSigners) == 0 { 176 require.Empty(t, decBeaconSigners) 177 } else { 178 require.Equal(t, committeeIdentities.Filter(filter.HasNodeID(beaconSigners...)), decBeaconSigners) 179 } 180 }) 181 } 182 183 func Test_ValidPaddingErrIncompatibleBitVectorLength(t *testing.T) { 184 var signers flow.IdentityList 185 var err error 186 // if bits is multiply of 8, then there is no padding needed, any sig type can be decoded. 187 signers = unittest.IdentityListFixture(16) 188 189 // 16 bits needs 2 bytes, provided 2 bytes 190 _, _, err = signature.DecodeSigTypeToStakingAndBeaconSigners(signers, unittest.RandomBytes(2)) 191 require.NoError(t, err) 192 193 // 1 byte less 194 _, _, err = signature.DecodeSigTypeToStakingAndBeaconSigners(signers, []byte{byte(255)}) 195 require.True(t, signature.IsInvalidSigTypesError(err), "API-level error should be InvalidSigTypesError") 196 require.ErrorIs(t, err, signature.ErrIncompatibleBitVectorLength, "low-level error representing the failure should be ErrIncompatibleBitVectorLength") 197 198 // 1 byte more 199 _, _, err = signature.DecodeSigTypeToStakingAndBeaconSigners(signers, []byte{}) 200 require.True(t, signature.IsInvalidSigTypesError(err), "API-level error should be InvalidSigTypesError") 201 require.ErrorIs(t, err, signature.ErrIncompatibleBitVectorLength, "low-level error representing the failure should be ErrIncompatibleBitVectorLength") 202 203 // if bits is not multiply of 8, then padding is needed 204 signers = unittest.IdentityListFixture(15) 205 _, _, err = signature.DecodeSigTypeToStakingAndBeaconSigners(signers, []byte{byte(255), byte(254)}) 206 require.NoError(t, err) 207 208 // 1 byte more 209 _, _, err = signature.DecodeSigTypeToStakingAndBeaconSigners(signers, []byte{byte(255), byte(255), byte(254)}) 210 require.True(t, signature.IsInvalidSigTypesError(err), "API-level error should be InvalidSigTypesError") 211 require.ErrorIs(t, err, signature.ErrIncompatibleBitVectorLength, "low-level error representing the failure should be ErrIncompatibleBitVectorLength") 212 213 // 1 byte less 214 _, _, err = signature.DecodeSigTypeToStakingAndBeaconSigners(signers, []byte{byte(254)}) 215 require.True(t, signature.IsInvalidSigTypesError(err), "API-level error should be InvalidSigTypesError") 216 require.ErrorIs(t, err, signature.ErrIncompatibleBitVectorLength, "low-level error representing the failure should be ErrIncompatibleBitVectorLength") 217 218 // if bits is not multiply of 8, 219 // 1 byte more 220 signers = unittest.IdentityListFixture(0) 221 _, _, err = signature.DecodeSigTypeToStakingAndBeaconSigners(signers, []byte{byte(255)}) 222 require.True(t, signature.IsInvalidSigTypesError(err), "API-level error should be InvalidSigTypesError") 223 require.ErrorIs(t, err, signature.ErrIncompatibleBitVectorLength, "low-level error representing the failure should be ErrIncompatibleBitVectorLength") 224 225 // 1 byte more 226 signers = unittest.IdentityListFixture(1) 227 _, _, err = signature.DecodeSigTypeToStakingAndBeaconSigners(signers, []byte{byte(0), byte(0)}) 228 require.True(t, signature.IsInvalidSigTypesError(err), "API-level error should be InvalidSigTypesError") 229 require.ErrorIs(t, err, signature.ErrIncompatibleBitVectorLength, "low-level error representing the failure should be ErrIncompatibleBitVectorLength") 230 231 // 1 byte less 232 signers = unittest.IdentityListFixture(7) 233 _, _, err = signature.DecodeSigTypeToStakingAndBeaconSigners(signers, []byte{}) 234 require.True(t, signature.IsInvalidSigTypesError(err), "API-level error should be InvalidSigTypesError") 235 require.ErrorIs(t, err, signature.ErrIncompatibleBitVectorLength, "low-level error representing the failure should be ErrIncompatibleBitVectorLength") 236 } 237 238 func TestValidPaddingErrIllegallyPaddedBitVector(t *testing.T) { 239 var signers flow.IdentityList 240 var err error 241 // if bits is multiply of 8, then there is no padding needed, any sig type can be decoded. 242 for count := 1; count < 8; count++ { 243 signers = unittest.IdentityListFixture(count) 244 _, _, err = signature.DecodeSigTypeToStakingAndBeaconSigners(signers, []byte{byte(255)}) // last bit should be 0, but 1 245 require.True(t, signature.IsInvalidSigTypesError(err), "API-level error should be InvalidSigTypesError") 246 require.ErrorIs(t, err, signature.ErrIllegallyPaddedBitVector, "low-level error representing the failure should be ErrIllegallyPaddedBitVector") 247 248 _, _, err = signature.DecodeSigTypeToStakingAndBeaconSigners(signers, []byte{byte(1)}) // last bit should be 0, but 1 249 require.True(t, signature.IsInvalidSigTypesError(err), "API-level error should be InvalidSigTypesError") 250 require.ErrorIs(t, err, signature.ErrIllegallyPaddedBitVector, "low-level error representing the failure should be ErrIllegallyPaddedBitVector") 251 } 252 253 for count := 9; count < 16; count++ { 254 signers = unittest.IdentityListFixture(count) 255 _, _, err = signature.DecodeSigTypeToStakingAndBeaconSigners(signers, []byte{byte(255), byte(255)}) // last bit should be 0, but 1 256 require.True(t, signature.IsInvalidSigTypesError(err), "API-level error should be InvalidSigTypesError") 257 require.ErrorIs(t, err, signature.ErrIllegallyPaddedBitVector, "low-level error representing the failure should be ErrIllegallyPaddedBitVector") 258 259 _, _, err = signature.DecodeSigTypeToStakingAndBeaconSigners(signers, []byte{byte(1), byte(1)}) // last bit should be 0, but 1 260 require.True(t, signature.IsInvalidSigTypesError(err), "API-level error should be InvalidSigTypesError") 261 require.ErrorIs(t, err, signature.ErrIllegallyPaddedBitVector, "low-level error representing the failure should be ErrIllegallyPaddedBitVector") 262 } 263 } 264 265 // Test_EncodeSignersToIndices uses fuzzy-testing framework Rapid to test the method EncodeSignersToIndices: 266 // * we generate a set of authorized signer: `identities` 267 // * part of this set is sampled as singers: `signers` 268 // * we encode the set and check that the results conform to the protocol specification 269 func Test_EncodeSignersToIndices(t *testing.T) { 270 rapid.Check(t, func(t *rapid.T) { 271 // select total committee size, number of random beacon signers and number of staking signers 272 committeeSize := rapid.IntRange(1, 272).Draw(t, "committeeSize") 273 numSigners := rapid.IntRange(0, committeeSize).Draw(t, "numSigners") 274 275 // create committee 276 identities := unittest.IdentityListFixture(committeeSize, unittest.WithRole(flow.RoleConsensus)).Sort(flow.Canonical) 277 committee := identities.NodeIDs() 278 signers, err := committee.Sample(uint(numSigners)) 279 require.NoError(t, err) 280 281 // encode 282 prefixed, err := signature.EncodeSignersToIndices(committee, signers) 283 require.NoError(t, err) 284 285 signerIndices, err := signature.CompareAndExtract(committee, prefixed) 286 require.NoError(t, err) 287 288 // check verify signer indices 289 correctEncoding(t, signerIndices, committee, signers) 290 }) 291 } 292 293 // Test_DecodeSignerIndicesToIdentifiers uses fuzzy-testing framework Rapid to test the method DecodeSignerIndicesToIdentifiers: 294 // - we generate a set of authorized signer: `identities` 295 // - part of this set is sampled as signers: `signers` 296 // - We encode the set using `EncodeSignersToIndices` (tested before) and then decode it. 297 // Thereby we should recover the original input. Caution, the order might be different, 298 // so we sort both sets. 299 func Test_DecodeSignerIndicesToIdentifiers(t *testing.T) { 300 rapid.Check(t, func(t *rapid.T) { 301 // select total committee size, number of random beacon signers and number of staking signers 302 committeeSize := rapid.IntRange(1, 272).Draw(t, "committeeSize") 303 numSigners := rapid.IntRange(0, committeeSize).Draw(t, "numSigners") 304 305 // create committee 306 identities := unittest.IdentityListFixture(committeeSize, unittest.WithRole(flow.RoleConsensus)).Sort(flow.Canonical) 307 committee := identities.NodeIDs() 308 signers, err := committee.Sample(uint(numSigners)) 309 require.NoError(t, err) 310 sort.Sort(signers) 311 312 // encode 313 signerIndices, err := signature.EncodeSignersToIndices(committee, signers) 314 require.NoError(t, err) 315 316 // decode and verify 317 decodedSigners, err := signature.DecodeSignerIndicesToIdentifiers(committee, signerIndices) 318 require.NoError(t, err) 319 sort.Sort(decodedSigners) 320 require.Equal(t, signers, decodedSigners) 321 }) 322 } 323 324 // Test_DecodeSignerIndicesToIdentities uses fuzzy-testing framework Rapid to test the method DecodeSignerIndicesToIdentities: 325 // * we generate a set of authorized signer: `identities` 326 // * part of this set is sampled as singers: `signers` 327 // * We encode the set using `EncodeSignersToIndices` (tested before) and then decode it. 328 // Thereby we should recover the original input. Caution, the order might be different, 329 // so we sort both sets. 330 // Note: this is _almost_ the same test as `Test_DecodeSignerIndicesToIdentifiers`. However, in the other 331 // test, we decode to node IDs; while in this test, we decode to full _Identities_. 332 333 const UpperBoundCommitteeSize = 272 334 335 func Test_DecodeSignerIndicesToIdentities(t *testing.T) { 336 337 rapid.Check(t, func(t *rapid.T) { 338 // select total committee size, number of random beacon signers and number of staking signers 339 committeeSize := rapid.IntRange(1, UpperBoundCommitteeSize).Draw(t, "committeeSize") 340 numSigners := rapid.IntRange(0, committeeSize).Draw(t, "numSigners") 341 342 // create committee 343 identities := unittest.IdentityListFixture(committeeSize, unittest.WithRole(flow.RoleConsensus)).Sort(flow.Canonical) 344 signers, err := identities.Sample(uint(numSigners)) 345 require.NoError(t, err) 346 347 // encode 348 signerIndices, err := signature.EncodeSignersToIndices(identities.NodeIDs(), signers.NodeIDs()) 349 require.NoError(t, err) 350 351 // decode and verify 352 decodedSigners, err := signature.DecodeSignerIndicesToIdentities(identities, signerIndices) 353 require.NoError(t, err) 354 require.Equal(t, signers.Sort(flow.Canonical), decodedSigners.Sort(flow.Canonical)) 355 }) 356 } 357 358 // sampleSigners takes `committee` and samples to _disjoint_ subsets 359 // (`stakingSigners` and `randomBeaconSigners`) with the specified cardinality 360 func sampleSigners( 361 t *rapid.T, 362 committee flow.IdentifierList, 363 numStakingSigners int, 364 numRandomBeaconSigners int, 365 ) (stakingSigners flow.IdentifierList, randomBeaconSigners flow.IdentifierList) { 366 if numStakingSigners+numRandomBeaconSigners > len(committee) { 367 panic(fmt.Sprintf("Cannot sample %d nodes out of a committee is size %d", numStakingSigners+numRandomBeaconSigners, len(committee))) 368 } 369 370 var err error 371 stakingSigners, err = committee.Sample(uint(numStakingSigners)) 372 require.NoError(t, err) 373 remaining := committee.Filter(id.Not(id.In(stakingSigners...))) 374 randomBeaconSigners, err = remaining.Sample(uint(numRandomBeaconSigners)) 375 require.NoError(t, err) 376 return 377 } 378 379 // correctEncoding verifies that the given indices conform to the following specification: 380 // - indices is the _smallest_ possible byte slice that contains at least `len(canonicalIdentifiers)` number of _bits_ 381 // - Let indices[i] denote the ith bit of `indices`. We verify that: 382 // 383 // . ┌ 1 if and only if canonicalIdentifiers[i] is in `subset` 384 // . indices[i] = └ 0 otherwise 385 // 386 // This function can be used to verify signer indices as well as signature type encoding 387 func correctEncoding(t require.TestingT, indices []byte, canonicalIdentifiers flow.IdentifierList, subset flow.IdentifierList) { 388 // verify that indices has correct length 389 numberBits := 8 * len(indices) 390 require.True(t, numberBits >= len(canonicalIdentifiers), "signerIndices has too few bits") 391 require.True(t, numberBits-len(canonicalIdentifiers) < 8, fmt.Sprintf("signerIndices %v is padded with too many %v bits", 392 numberBits, len(canonicalIdentifiers))) 393 394 // convert canonicalIdentifiers to map Identifier -> index 395 m := make(map[flow.Identifier]int) 396 for i, id := range canonicalIdentifiers { 397 m[id] = i 398 } 399 400 // make sure that every member of the subset is represented by a 1 in `indices` 401 for _, id := range subset { 402 bitIndex := m[id] 403 require.True(t, bitutils.ReadBit(indices, bitIndex) == 1) 404 delete(m, id) 405 } 406 407 // as we delete all IDs in subset from m, the remaining ID in `m` should be represented by a 0 in `indices` 408 for id := range m { 409 bitIndex := m[id] 410 require.True(t, bitutils.ReadBit(indices, bitIndex) == 0) 411 } 412 413 // the padded bits should also all be 0: 414 for i := len(canonicalIdentifiers); i < 8*len(indices); i++ { 415 require.True(t, bitutils.ReadBit(indices, i) == 0) 416 } 417 }