github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/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).Sort(flow.Canonical[flow.Identity]).ToSkeleton() 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[flow.Identity]) 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[flow.Identity](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)). 151 Sort(flow.Canonical[flow.Identity]) 152 committee := committeeIdentities.NodeIDs() 153 stakingSigners, beaconSigners := sampleSigners(t, committee, numStakingSigners, numRandomBeaconSigners) 154 155 // encode 156 signerIndices, sigTypes, err := signature.EncodeSignerToIndicesAndSigType(committee, stakingSigners, beaconSigners) 157 require.NoError(t, err) 158 159 // decode 160 decSignerIdentites, err := signature.DecodeSignerIndicesToIdentities(committeeIdentities.ToSkeleton(), signerIndices) 161 require.NoError(t, err) 162 decStakingSigners, decBeaconSigners, err := signature.DecodeSigTypeToStakingAndBeaconSigners(decSignerIdentites, sigTypes) 163 require.NoError(t, err) 164 165 // verify; note that there is a slightly different convention between Filter and the decoding logic: 166 // Filter returns nil for an empty list, while the decoding logic returns an instance of an empty slice 167 sigIdentities := committeeIdentities.Filter( 168 filter.Or(filter.HasNodeID[flow.Identity](stakingSigners...), filter.HasNodeID[flow.Identity](beaconSigners...))).ToSkeleton() // signer identities in canonical order 169 if len(stakingSigners)+len(decBeaconSigners) > 0 { 170 require.Equal(t, sigIdentities, decSignerIdentites) 171 } 172 if len(stakingSigners) == 0 { 173 require.Empty(t, decStakingSigners) 174 } else { 175 require.Equal(t, committeeIdentities.Filter(filter.HasNodeID[flow.Identity](stakingSigners...)).ToSkeleton(), decStakingSigners) 176 } 177 if len(decBeaconSigners) == 0 { 178 require.Empty(t, decBeaconSigners) 179 } else { 180 require.Equal(t, committeeIdentities.Filter(filter.HasNodeID[flow.Identity](beaconSigners...)).ToSkeleton(), decBeaconSigners) 181 } 182 }) 183 } 184 185 func Test_ValidPaddingErrIncompatibleBitVectorLength(t *testing.T) { 186 var err error 187 // if bits is multiply of 8, then there is no padding needed, any sig type can be decoded. 188 signers := unittest.IdentityListFixture(16).ToSkeleton() 189 190 // 16 bits needs 2 bytes, provided 2 bytes 191 _, _, err = signature.DecodeSigTypeToStakingAndBeaconSigners(signers, unittest.RandomBytes(2)) 192 require.NoError(t, err) 193 194 // 1 byte less 195 _, _, err = signature.DecodeSigTypeToStakingAndBeaconSigners(signers, []byte{byte(255)}) 196 require.True(t, signature.IsInvalidSigTypesError(err), "API-level error should be InvalidSigTypesError") 197 require.ErrorIs(t, err, signature.ErrIncompatibleBitVectorLength, "low-level error representing the failure should be ErrIncompatibleBitVectorLength") 198 199 // 1 byte more 200 _, _, err = signature.DecodeSigTypeToStakingAndBeaconSigners(signers, []byte{}) 201 require.True(t, signature.IsInvalidSigTypesError(err), "API-level error should be InvalidSigTypesError") 202 require.ErrorIs(t, err, signature.ErrIncompatibleBitVectorLength, "low-level error representing the failure should be ErrIncompatibleBitVectorLength") 203 204 // if bits is not multiply of 8, then padding is needed 205 signers = unittest.IdentityListFixture(15).ToSkeleton() 206 _, _, err = signature.DecodeSigTypeToStakingAndBeaconSigners(signers, []byte{byte(255), byte(254)}) 207 require.NoError(t, err) 208 209 // 1 byte more 210 _, _, err = signature.DecodeSigTypeToStakingAndBeaconSigners(signers, []byte{byte(255), byte(255), byte(254)}) 211 require.True(t, signature.IsInvalidSigTypesError(err), "API-level error should be InvalidSigTypesError") 212 require.ErrorIs(t, err, signature.ErrIncompatibleBitVectorLength, "low-level error representing the failure should be ErrIncompatibleBitVectorLength") 213 214 // 1 byte less 215 _, _, err = signature.DecodeSigTypeToStakingAndBeaconSigners(signers, []byte{byte(254)}) 216 require.True(t, signature.IsInvalidSigTypesError(err), "API-level error should be InvalidSigTypesError") 217 require.ErrorIs(t, err, signature.ErrIncompatibleBitVectorLength, "low-level error representing the failure should be ErrIncompatibleBitVectorLength") 218 219 // if bits is not multiply of 8, 220 // 1 byte more 221 signers = unittest.IdentityListFixture(0).ToSkeleton() 222 _, _, err = signature.DecodeSigTypeToStakingAndBeaconSigners(signers, []byte{byte(255)}) 223 require.True(t, signature.IsInvalidSigTypesError(err), "API-level error should be InvalidSigTypesError") 224 require.ErrorIs(t, err, signature.ErrIncompatibleBitVectorLength, "low-level error representing the failure should be ErrIncompatibleBitVectorLength") 225 226 // 1 byte more 227 signers = unittest.IdentityListFixture(1).ToSkeleton() 228 _, _, err = signature.DecodeSigTypeToStakingAndBeaconSigners(signers, []byte{byte(0), byte(0)}) 229 require.True(t, signature.IsInvalidSigTypesError(err), "API-level error should be InvalidSigTypesError") 230 require.ErrorIs(t, err, signature.ErrIncompatibleBitVectorLength, "low-level error representing the failure should be ErrIncompatibleBitVectorLength") 231 232 // 1 byte less 233 signers = unittest.IdentityListFixture(7).ToSkeleton() 234 _, _, err = signature.DecodeSigTypeToStakingAndBeaconSigners(signers, []byte{}) 235 require.True(t, signature.IsInvalidSigTypesError(err), "API-level error should be InvalidSigTypesError") 236 require.ErrorIs(t, err, signature.ErrIncompatibleBitVectorLength, "low-level error representing the failure should be ErrIncompatibleBitVectorLength") 237 } 238 239 func TestValidPaddingErrIllegallyPaddedBitVector(t *testing.T) { 240 var signers flow.IdentitySkeletonList 241 var err error 242 // if bits is multiply of 8, then there is no padding needed, any sig type can be decoded. 243 for count := 1; count < 8; count++ { 244 signers = unittest.IdentityListFixture(count).ToSkeleton() 245 _, _, err = signature.DecodeSigTypeToStakingAndBeaconSigners(signers, []byte{byte(255)}) // last bit should be 0, but 1 246 require.True(t, signature.IsInvalidSigTypesError(err), "API-level error should be InvalidSigTypesError") 247 require.ErrorIs(t, err, signature.ErrIllegallyPaddedBitVector, "low-level error representing the failure should be ErrIllegallyPaddedBitVector") 248 249 _, _, err = signature.DecodeSigTypeToStakingAndBeaconSigners(signers, []byte{byte(1)}) // last bit should be 0, but 1 250 require.True(t, signature.IsInvalidSigTypesError(err), "API-level error should be InvalidSigTypesError") 251 require.ErrorIs(t, err, signature.ErrIllegallyPaddedBitVector, "low-level error representing the failure should be ErrIllegallyPaddedBitVector") 252 } 253 254 for count := 9; count < 16; count++ { 255 signers = unittest.IdentityListFixture(count).ToSkeleton() 256 _, _, err = signature.DecodeSigTypeToStakingAndBeaconSigners(signers, []byte{byte(255), byte(255)}) // last bit should be 0, but 1 257 require.True(t, signature.IsInvalidSigTypesError(err), "API-level error should be InvalidSigTypesError") 258 require.ErrorIs(t, err, signature.ErrIllegallyPaddedBitVector, "low-level error representing the failure should be ErrIllegallyPaddedBitVector") 259 260 _, _, err = signature.DecodeSigTypeToStakingAndBeaconSigners(signers, []byte{byte(1), byte(1)}) // last bit should be 0, but 1 261 require.True(t, signature.IsInvalidSigTypesError(err), "API-level error should be InvalidSigTypesError") 262 require.ErrorIs(t, err, signature.ErrIllegallyPaddedBitVector, "low-level error representing the failure should be ErrIllegallyPaddedBitVector") 263 } 264 } 265 266 // Test_EncodeSignersToIndices uses fuzzy-testing framework Rapid to test the method EncodeSignersToIndices: 267 // * we generate a set of authorized signer: `identities` 268 // * part of this set is sampled as singers: `signers` 269 // * we encode the set and check that the results conform to the protocol specification 270 func Test_EncodeSignersToIndices(t *testing.T) { 271 rapid.Check(t, func(t *rapid.T) { 272 // select total committee size, number of random beacon signers and number of staking signers 273 committeeSize := rapid.IntRange(1, 272).Draw(t, "committeeSize") 274 numSigners := rapid.IntRange(0, committeeSize).Draw(t, "numSigners") 275 276 // create committee 277 identities := unittest.IdentityListFixture(committeeSize, unittest.WithRole(flow.RoleConsensus)).Sort(flow.Canonical[flow.Identity]) 278 committee := identities.NodeIDs() 279 signers, err := committee.Sample(uint(numSigners)) 280 require.NoError(t, err) 281 282 // encode 283 prefixed, err := signature.EncodeSignersToIndices(committee, signers) 284 require.NoError(t, err) 285 286 signerIndices, err := signature.CompareAndExtract(committee, prefixed) 287 require.NoError(t, err) 288 289 // check verify signer indices 290 correctEncoding(t, signerIndices, committee, signers) 291 }) 292 } 293 294 // Test_DecodeSignerIndicesToIdentifiers uses fuzzy-testing framework Rapid to test the method DecodeSignerIndicesToIdentifiers: 295 // - we generate a set of authorized signer: `identities` 296 // - part of this set is sampled as signers: `signers` 297 // - We encode the set using `EncodeSignersToIndices` (tested before) and then decode it. 298 // Thereby we should recover the original input. Caution, the order might be different, 299 // so we sort both sets. 300 func Test_DecodeSignerIndicesToIdentifiers(t *testing.T) { 301 rapid.Check(t, func(t *rapid.T) { 302 // select total committee size, number of random beacon signers and number of staking signers 303 committeeSize := rapid.IntRange(1, 272).Draw(t, "committeeSize") 304 numSigners := rapid.IntRange(0, committeeSize).Draw(t, "numSigners") 305 306 // create committee 307 identities := unittest.IdentityListFixture(committeeSize, unittest.WithRole(flow.RoleConsensus)).Sort(flow.Canonical[flow.Identity]) 308 committee := identities.NodeIDs() 309 signers, err := committee.Sample(uint(numSigners)) 310 require.NoError(t, err) 311 sort.Sort(signers) 312 313 // encode 314 signerIndices, err := signature.EncodeSignersToIndices(committee, signers) 315 require.NoError(t, err) 316 317 // decode and verify 318 decodedSigners, err := signature.DecodeSignerIndicesToIdentifiers(committee, signerIndices) 319 require.NoError(t, err) 320 sort.Sort(decodedSigners) 321 require.Equal(t, signers, decodedSigners) 322 }) 323 } 324 325 // Test_DecodeSignerIndicesToIdentities uses fuzzy-testing framework Rapid to test the method DecodeSignerIndicesToIdentities: 326 // * we generate a set of authorized signer: `identities` 327 // * part of this set is sampled as singers: `signers` 328 // * We encode the set using `EncodeSignersToIndices` (tested before) and then decode it. 329 // Thereby we should recover the original input. Caution, the order might be different, 330 // so we sort both sets. 331 // Note: this is _almost_ the same test as `Test_DecodeSignerIndicesToIdentifiers`. However, in the other 332 // test, we decode to node IDs; while in this test, we decode to full _Identities_. 333 334 const UpperBoundCommitteeSize = 272 335 336 func Test_DecodeSignerIndicesToIdentities(t *testing.T) { 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[flow.Identity]) 344 fullSigners, err := identities.Sample(uint(numSigners)) 345 require.NoError(t, err) 346 signers := fullSigners.ToSkeleton() 347 348 // encode 349 signerIndices, err := signature.EncodeSignersToIndices(identities.NodeIDs(), signers.NodeIDs()) 350 require.NoError(t, err) 351 352 // decode and verify 353 decodedSigners, err := signature.DecodeSignerIndicesToIdentities(identities.ToSkeleton(), signerIndices) 354 require.NoError(t, err) 355 356 require.Equal(t, signers.Sort(flow.Canonical[flow.IdentitySkeleton]), decodedSigners.Sort(flow.Canonical[flow.IdentitySkeleton])) 357 }) 358 } 359 360 // sampleSigners takes `committee` and samples to _disjoint_ subsets 361 // (`stakingSigners` and `randomBeaconSigners`) with the specified cardinality 362 func sampleSigners( 363 t *rapid.T, 364 committee flow.IdentifierList, 365 numStakingSigners int, 366 numRandomBeaconSigners int, 367 ) (stakingSigners flow.IdentifierList, randomBeaconSigners flow.IdentifierList) { 368 if numStakingSigners+numRandomBeaconSigners > len(committee) { 369 panic(fmt.Sprintf("Cannot sample %d nodes out of a committee is size %d", numStakingSigners+numRandomBeaconSigners, len(committee))) 370 } 371 372 var err error 373 stakingSigners, err = committee.Sample(uint(numStakingSigners)) 374 require.NoError(t, err) 375 remaining := committee.Filter(id.Not(id.In(stakingSigners...))) 376 randomBeaconSigners, err = remaining.Sample(uint(numRandomBeaconSigners)) 377 require.NoError(t, err) 378 return 379 } 380 381 // correctEncoding verifies that the given indices conform to the following specification: 382 // - indices is the _smallest_ possible byte slice that contains at least `len(canonicalIdentifiers)` number of _bits_ 383 // - Let indices[i] denote the ith bit of `indices`. We verify that: 384 // 385 // . ┌ 1 if and only if canonicalIdentifiers[i] is in `subset` 386 // . indices[i] = └ 0 otherwise 387 // 388 // This function can be used to verify signer indices as well as signature type encoding 389 func correctEncoding(t require.TestingT, indices []byte, canonicalIdentifiers flow.IdentifierList, subset flow.IdentifierList) { 390 // verify that indices has correct length 391 numberBits := 8 * len(indices) 392 require.True(t, numberBits >= len(canonicalIdentifiers), "signerIndices has too few bits") 393 require.True(t, numberBits-len(canonicalIdentifiers) < 8, fmt.Sprintf("signerIndices %v is padded with too many %v bits", 394 numberBits, len(canonicalIdentifiers))) 395 396 // convert canonicalIdentifiers to map Identifier -> index 397 m := make(map[flow.Identifier]int) 398 for i, id := range canonicalIdentifiers { 399 m[id] = i 400 } 401 402 // make sure that every member of the subset is represented by a 1 in `indices` 403 for _, id := range subset { 404 bitIndex := m[id] 405 require.True(t, bitutils.ReadBit(indices, bitIndex) == 1) 406 delete(m, id) 407 } 408 409 // as we delete all IDs in subset from m, the remaining ID in `m` should be represented by a 0 in `indices` 410 for id := range m { 411 bitIndex := m[id] 412 require.True(t, bitutils.ReadBit(indices, bitIndex) == 0) 413 } 414 415 // the padded bits should also all be 0: 416 for i := len(canonicalIdentifiers); i < 8*len(indices); i++ { 417 require.True(t, bitutils.ReadBit(indices, i) == 0) 418 } 419 }