diff --git a/pkg/frost/retry/retry_test.go b/pkg/frost/retry/retry_test.go deleted file mode 100644 index 5e0a16dbcd..0000000000 --- a/pkg/frost/retry/retry_test.go +++ /dev/null @@ -1,290 +0,0 @@ -package retry - -import ( - "fmt" - "math/rand" - "reflect" - "strings" - "testing" - - "github.com/keep-network/keep-core/pkg/chain" -) - -type groupMemberRandomizer func( - []chain.Address, - int64, - uint, - uint, -) ([]chain.Address, error) - -func TestEvaluateRetryParticipantsForSigning_100DifferentOperators(t *testing.T) { - groupMembers := make([]chain.Address, 100) - for i := 0; i < 100; i++ { - groupMembers[i] = chain.Address(fmt.Sprintf("Operator-%d", i)) - } - assertInvariants(t, EvaluateRetryParticipantsForSigning, groupMembers, int64(123), 0, 51) -} - -func TestEvaluateRetryParticipantsForSigning_FewOperators(t *testing.T) { - groupMembers := make([]chain.Address, 100) - for i := 0; i < 100; i++ { - groupMembers[i] = chain.Address(fmt.Sprintf("Operator-%d", i%3)) - } - assertInvariants(t, EvaluateRetryParticipantsForSigning, groupMembers, int64(456), 0, 51) -} - -func TestEvaluateRetryParticipantsForSigning_NotEnoughOperators(t *testing.T) { - groupMembers := make([]chain.Address, 50) - for i := 0; i < 50; i++ { - groupMembers[i] = chain.Address(fmt.Sprintf("Operator-%d", i)) - } - _, err := EvaluateRetryParticipantsForSigning(groupMembers, int64(123), 0, 51) - expectation := "asked for too many seats" - if err == nil { - t.Fatalf( - "unexpected error\nexpected: [%s]\nactual: [%v]", - fmt.Sprintf("%s...", expectation), - nil, - ) - } - if !strings.HasPrefix(err.Error(), expectation) { - t.Fatalf( - "unexpected error\nexpected: [%s]\nactual: [%s]", - fmt.Sprintf("%s...", expectation), - err.Error(), - ) - } -} - -func TestEvaluateRetryParticipantsForKeyGeneration_100DifferentOperators(t *testing.T) { - groupMembers := make([]chain.Address, 100) - for i := 0; i < 100; i++ { - groupMembers[i] = chain.Address(fmt.Sprintf("Operator-%d", i)) - } - assertInvariants(t, EvaluateRetryParticipantsForKeyGeneration, groupMembers, int64(123), 0, 90) -} - -func TestEvaluateRetryParticipantsForKeyGeneration_FewOperators(t *testing.T) { - groupMembers := make([]chain.Address, 100) - for i := 0; i < 100; i++ { - groupMembers[i] = chain.Address(fmt.Sprintf("Operator-%d", i%20)) - } - // There are 20 unique operators, and any 3 of them can be excluded while - // still being above the lower bound of 80 since each operator controls 5 - // seats. Thus, there are 20 single exclusions, 20 choose 2 = 190 pairs, and - // 20 choose 3 = 1140 triplets for a total of 20 + 190 + 1140 = 1350 total - // exclusions. - - // Single exclusion - assertInvariants(t, EvaluateRetryParticipantsForKeyGeneration, groupMembers, int64(456), 15, 80) - - // Pair Exclusion - assertInvariants(t, EvaluateRetryParticipantsForKeyGeneration, groupMembers, int64(456), 170, 80) - - // Triplet Exclusion - assertInvariants(t, EvaluateRetryParticipantsForKeyGeneration, groupMembers, int64(456), 1000, 80) - - // Too many! - _, err := EvaluateRetryParticipantsForKeyGeneration(groupMembers, int64(456), 1350, 80) - expectation := "the retry count [1350] was too large to handle; tried every single, pair, and triplet, but still needed [0] more retries" - if err.Error() != expectation { - t.Errorf( - "unexpected error\nexpected: [%s]\nactual: [%s]", - expectation, - err.Error(), - ) - } -} - -func TestEvaluateRetryParticipantsForKeyGeneration_NotEnoughOperators(t *testing.T) { - groupMembers := make([]chain.Address, 50) - for i := 0; i < 50; i++ { - groupMembers[i] = chain.Address(fmt.Sprintf("Operator-%d", i)) - } - _, err := EvaluateRetryParticipantsForKeyGeneration(groupMembers, int64(123), 0, 90) - expectation := "asked for too many seats" - if err == nil { - t.Fatalf( - "unexpected error\nexpected: [%s]\nactual: [%v]", - fmt.Sprintf("%s...", expectation), - nil, - ) - } - if !strings.HasPrefix(err.Error(), expectation) { - t.Fatalf( - "unexpected error\nexpected: [%s]\nactual: [%s]", - fmt.Sprintf("%s...", expectation), - err.Error(), - ) - } -} - -func TestExcludeOperatorTriplets_UsesThirdOperatorSeatCount(t *testing.T) { - groupMembers := []chain.Address{ - "A", "A", "A", - "B", - "C", "C", "C", - } - - operatorToSeatCount := calculateSeatCount(groupMembers) - operators := []chain.Address{"A", "B", "C"} - - // #nosec G404 (insecure random number source (rand)) - // Deterministic RNG is sufficient for deterministic unit tests. - rng := rand.New(rand.NewSource(1)) - - usedOperators, skippedTries, ok := excludeOperatorTriplets( - rng, - groupMembers, - 0, - operatorToSeatCount, - operators, - 2, - ) - - if ok { - t.Fatalf( - "expected no eligible triplet exclusions, got operators: [%v]", - usedOperators, - ) - } - - if skippedTries != 0 { - t.Fatalf( - "expected zero skipped tries when no triplet is eligible, got: [%d]", - skippedTries, - ) - } -} - -func isSubset( - t *testing.T, - groupMemberRandomizer groupMemberRandomizer, - groupMembers []chain.Address, - seed int64, - retryCount uint, - retryParticipantsCount uint, -) { - subset, err := groupMemberRandomizer(groupMembers, seed, retryCount, retryParticipantsCount) - if err != nil { - t.Fatalf("unexpected error: [%s]", err) - } - memberMap := make(map[chain.Address]struct{}) - for _, operator := range groupMembers { - memberMap[operator] = struct{}{} - } - for _, operator := range subset { - if _, ok := memberMap[operator]; !ok { - t.Errorf("Subset member [%s] is not in the operator group.", operator) - } - } -} - -func isStable( - t *testing.T, - groupMemberRandomizer groupMemberRandomizer, - groupMembers []chain.Address, - seed int64, - retryCount uint, - retryParticipantsCount uint, -) { - subset, err := groupMemberRandomizer(groupMembers, seed, retryCount, retryParticipantsCount) - if err != nil { - t.Fatalf("unexpected error: [%s]", err) - } - for i := 0; i < 30; i++ { - newSubset, err := groupMemberRandomizer(groupMembers, seed, retryCount, retryParticipantsCount) - if err != nil { - t.Fatalf("unexpected error: [%s]", err) - } - if ok := reflect.DeepEqual(subset, newSubset); !ok { - t.Errorf( - "The subsets changed\nexpected: [%v]\nactual: [%v]", - subset, - newSubset, - ) - } - } -} - -func isLargeEnough( - t *testing.T, - groupMemberRandomizer groupMemberRandomizer, - groupMembers []chain.Address, - seed int64, - retryCount uint, - retryParticipantsCount uint, -) { - subset, err := groupMemberRandomizer(groupMembers, seed, retryCount, retryParticipantsCount) - if err != nil { - t.Fatalf("unexpected error: [%s]", err) - } - if len(subset) < int(retryParticipantsCount) { - t.Errorf( - "Subset isn't large enough\nexpected: [%d+]\nactual: [%d]", - retryParticipantsCount, - len(subset), - ) - } -} - -// They don't all have to be different, but they shouldn't all be the same! -func affectedBySeed( - t *testing.T, - groupMemberRandomizer groupMemberRandomizer, - groupMembers []chain.Address, - originalSeed int64, - retryCount uint, - retryParticipantsCount uint, -) { - allTheSame := true - subset, err := groupMemberRandomizer(groupMembers, originalSeed, retryCount, retryParticipantsCount) - if err != nil { - t.Fatalf("unexpected error: [%s]", err) - } - for seed := int64(0); seed < 30 && allTheSame; seed++ { - newSubset, _ := groupMemberRandomizer(groupMembers, seed, retryCount, retryParticipantsCount) - allTheSame = allTheSame && reflect.DeepEqual(subset, newSubset) - } - if allTheSame { - t.Error("The seed did not affect the subset generation. All subsets were the same.") - } -} - -// They don't all have to be different, but they shouldn't all be the same! -func affectedByRetryCount( - t *testing.T, - groupMemberRandomizer groupMemberRandomizer, - groupMembers []chain.Address, - seed int64, - originalRetryCount uint, - retryParticipantsCount uint, -) { - allTheSame := true - subset, err := groupMemberRandomizer(groupMembers, seed, originalRetryCount, retryParticipantsCount) - if err != nil { - t.Fatalf("unexpected error: [%s]", err) - } - for retryCount := uint(1); retryCount < 30 && allTheSame; retryCount++ { - newSubset, _ := groupMemberRandomizer(groupMembers, seed, retryCount, retryParticipantsCount) - allTheSame = allTheSame && reflect.DeepEqual(subset, newSubset) - } - if allTheSame { - t.Error("The seed did not affect the subset generation. All subsets were the same.") - } -} - -func assertInvariants( - t *testing.T, - groupMemberRandomizer groupMemberRandomizer, - groupMembers []chain.Address, - seed int64, - retryCount uint, - retryParticipantsCount uint, -) { - isSubset(t, groupMemberRandomizer, groupMembers, seed, retryCount, retryParticipantsCount) - isStable(t, groupMemberRandomizer, groupMembers, seed, retryCount, retryParticipantsCount) - isLargeEnough(t, groupMemberRandomizer, groupMembers, seed, retryCount, retryParticipantsCount) - affectedBySeed(t, groupMemberRandomizer, groupMembers, seed, retryCount, retryParticipantsCount) - affectedByRetryCount(t, groupMemberRandomizer, groupMembers, seed, retryCount, retryParticipantsCount) -} diff --git a/pkg/frost/roast/signing_retry_adapter.go b/pkg/frost/roast/signing_retry_adapter.go index b65a4cfd06..491f5b9e5a 100644 --- a/pkg/frost/roast/signing_retry_adapter.go +++ b/pkg/frost/roast/signing_retry_adapter.go @@ -98,7 +98,7 @@ type SigningRetryAdapter[T any] struct { } // EvaluateRetryParticipantsForSigning matches the shape of the -// legacy helper in pkg/frost/retry so call sites can adopt the +// legacy helper in pkg/protocol/retry so call sites can adopt the // adapter without changing their function-call surface. The legacy // signature's parameters (groupMembers, seed, retryCount, // retryParticipantsCount) are ignored: the AttemptContext bound to diff --git a/pkg/frost/retry/retry.go b/pkg/protocol/retry/retry.go similarity index 100% rename from pkg/frost/retry/retry.go rename to pkg/protocol/retry/retry.go diff --git a/pkg/tecdsa/retry/retry_test.go b/pkg/protocol/retry/retry_test.go similarity index 100% rename from pkg/tecdsa/retry/retry_test.go rename to pkg/protocol/retry/retry_test.go diff --git a/pkg/tbtc/dkg_loop.go b/pkg/tbtc/dkg_loop.go index bcd02e02a9..b5b8c7b1cb 100644 --- a/pkg/tbtc/dkg_loop.go +++ b/pkg/tbtc/dkg_loop.go @@ -11,8 +11,8 @@ import ( "github.com/ipfs/go-log/v2" "github.com/keep-network/keep-core/pkg/chain" "github.com/keep-network/keep-core/pkg/protocol/group" + "github.com/keep-network/keep-core/pkg/protocol/retry" "github.com/keep-network/keep-core/pkg/tecdsa/dkg" - "github.com/keep-network/keep-core/pkg/tecdsa/retry" "golang.org/x/exp/slices" ) diff --git a/pkg/tbtc/signing_loop_legacy_selector.go b/pkg/tbtc/signing_loop_legacy_selector.go index f118ae569b..94cd7c6a1e 100644 --- a/pkg/tbtc/signing_loop_legacy_selector.go +++ b/pkg/tbtc/signing_loop_legacy_selector.go @@ -6,12 +6,12 @@ import ( "sort" "github.com/keep-network/keep-core/pkg/chain" - "github.com/keep-network/keep-core/pkg/frost/retry" "github.com/keep-network/keep-core/pkg/protocol/group" + "github.com/keep-network/keep-core/pkg/protocol/retry" ) // legacySigningParticipantSelector is the pre-RFC-21 implementation: -// it calls the pseudo-random retry shuffle in pkg/frost/retry and maps +// it calls the pseudo-random retry shuffle in pkg/protocol/retry and maps // the resulting qualified operators back to the included member // indices. // diff --git a/pkg/tbtc/signing_loop_roast_dispatcher.go b/pkg/tbtc/signing_loop_roast_dispatcher.go index 9bf9731886..b1b54b6f18 100644 --- a/pkg/tbtc/signing_loop_roast_dispatcher.go +++ b/pkg/tbtc/signing_loop_roast_dispatcher.go @@ -7,7 +7,7 @@ import ( // signingParticipantSelector picks the set of members included in a // signing attempt. The legacy implementation is the pseudo-random -// retry shuffle in pkg/frost/retry; the RFC-21 Phase-6 migration +// retry shuffle in pkg/protocol/retry; the RFC-21 Phase-6 migration // introduces this interface so an alternate ROAST-driven // implementation can be installed behind the frost_roast_retry build // tag without touching the call site. diff --git a/pkg/tecdsa/retry/retry.go b/pkg/tecdsa/retry/retry.go deleted file mode 100644 index 798d3bed30..0000000000 --- a/pkg/tecdsa/retry/retry.go +++ /dev/null @@ -1,341 +0,0 @@ -package retry - -import ( - "fmt" - "math/rand" - "sort" - - "github.com/keep-network/keep-core/pkg/chain" -) - -type byAddress []chain.Address - -func (ba byAddress) Len() int { return len(ba) } -func (ba byAddress) Swap(i, j int) { ba[i], ba[j] = ba[j], ba[i] } -func (ba byAddress) Less(i, j int) bool { return ba[i] < ba[j] } - -func calculateSeatCount(groupMembers []chain.Address) map[chain.Address]uint { - operatorToSeatCount := make(map[chain.Address]uint) - for _, operator := range groupMembers { - operatorToSeatCount[operator]++ - } - return operatorToSeatCount -} - -// EvaluateRetryParticipantsForSigning takes in a slice of `groupMembers` and -// returns a subslice of those same members of length >= -// `retryParticipantsCount` randomly according to the provided `seed` and -// `retryCount`. -// -// This function is intended to be called during a signing protocol after a -// signing event has failed but *not* due to inactivity. Assuming that some of -// the `groupMembers` are sending corrupted information, either on purpose or -// accidentally, we keep trying to find a subset of `groupMembers` that is as -// small as possible, yet still larger than `retryParticipantsCount`. -// -// The `seed` param needs to vary on a per-message basis but must be the same -// seed between all operators for each invocation. This can be the hash of the -// message since cryptographically secure randomness isn't important. -// -// The `retryCount` denotes the number of the given retry, so that should be -// incremented after each attempt while the `seed` stays consistent on a -// per-message basis. -func EvaluateRetryParticipantsForSigning( - groupMembers []chain.Address, - seed int64, - retryCount uint, - retryParticipantsCount uint, -) ([]chain.Address, error) { - if int(retryParticipantsCount) > len(groupMembers) { - return nil, fmt.Errorf( - "asked for too many seats; [%d] seats were requested, but there are only [%d] available", - retryParticipantsCount, - len(groupMembers), - ) - } - operatorToSeatCount := calculateSeatCount(groupMembers) - - // #nosec G404 (insecure random number source (rand)) - // Shuffling operators for retries does not require secure randomness. - rng := rand.New(rand.NewSource(seed + int64(retryCount))) - - operators := make([]chain.Address, len(operatorToSeatCount)) - i := 0 - for operator := range operatorToSeatCount { - operators[i] = operator - i++ - } - sort.Sort(byAddress(operators)) - rng.Shuffle(len(operators), func(i, j int) { - operators[i], operators[j] = operators[j], operators[i] - }) - - seatCount := uint(0) - acceptedOperators := make(map[chain.Address]bool) - for j := 0; seatCount < retryParticipantsCount; j++ { - operator := operators[j] - seatCount += operatorToSeatCount[operator] - acceptedOperators[operator] = true - } - - var seats []chain.Address - for _, operator := range groupMembers { - if acceptedOperators[operator] { - seats = append(seats, operator) - } - } - return seats, nil -} - -// EvaluateRetryParticipantsForKeyGeneration takes in a slice of `groupMembers` -// and returns a subslice of those same members of length >= -// `retryParticipantsCount` randomly according to the provided `seed` and -// `retryCount`. -// -// This function is intended to be called during key generation after a failure -// *not* due to inactivity. Assuming that some of the `groupMembers` are -// sending corrupted information, either on purpose or accidentally, we keep -// trying to find a subset of `groupMembers` that is as large as possible by -// first excluding single operators, then pairs of operators, then triplets of -// operators. We use the `seed` param to generate randomness to shuffle the -// singles/pairs/triplets of operators to exclude and then use the `retryCount` -// param to select which single/pair/triplet to exclude. -// -// The `seed` param needs to vary on a per-message basis but must be the same -// seed between all operators for each invocation. This can be the hash of the -// message since cryptographically secure randomness isn't important. -// -// The `retryCount` denotes the number of the given retry, so that should be -// incremented after each attempt while the `seed` stays consistent on a -// per-message basis. -func EvaluateRetryParticipantsForKeyGeneration( - groupMembers []chain.Address, - seed int64, - retryCount uint, - retryParticipantsCount uint, -) ([]chain.Address, error) { - remainingTries := retryCount - if int(retryParticipantsCount) > len(groupMembers) { - return nil, fmt.Errorf( - "asked for too many seats; [%d] seats were requested, "+ - "but there are only [%d] available", - retryParticipantsCount, - len(groupMembers), - ) - } - operatorToSeatCount := calculateSeatCount(groupMembers) - // #nosec G404 (insecure random number source (rand)) - // Shuffling operators for retries does not require secure randomness. Unlike - // EvaluateRetryParticipantsForSigning above, we only want to use the seed as - // a source of randomness. The `retryCount` is used to select which operators - // to exclude after we shuffle them. - rng := rand.New(rand.NewSource(seed)) - - operators := make([]chain.Address, 0, len(operatorToSeatCount)) - for operator := range operatorToSeatCount { - // Only include the operators that have few enough seats such that if they - // were excluded we still have at least `retryParticipantsCount` seats. - if len(groupMembers)-int(operatorToSeatCount[operator]) >= int(retryParticipantsCount) { - operators = append(operators, operator) - } - } - sort.Sort(byAddress(operators)) - - usedOperators, tries, ok := excludeSingleOperator( - rng, - groupMembers, - int(remainingTries), - operatorToSeatCount, - operators, - ) - if ok { - return usedOperators, nil - } else { - remainingTries -= uint(tries) - } - - usedOperators, tries, ok = excludeOperatorPairs( - rng, - groupMembers, - int(remainingTries), - operatorToSeatCount, - operators, - int(retryParticipantsCount), - ) - if ok { - return usedOperators, nil - } else { - remainingTries -= uint(tries) - } - - usedOperators, tries, ok = excludeOperatorTriplets( - rng, - groupMembers, - int(remainingTries), - operatorToSeatCount, - operators, - int(retryParticipantsCount), - ) - if ok { - return usedOperators, nil - } else { - remainingTries -= uint(tries) - return nil, fmt.Errorf( - "the retry count [%d] was too large to handle; "+ - "tried every single, pair, and triplet, but still needed [%d] more retries", - retryCount, - remainingTries, - ) - } -} - -// excludeSingleOperator randomly excludes all of an operator's seats from a -// given `groupMembers`. It needs a pre-seeded random generator `rng`, and an -// `index`, which is expected to be inferred from a `retryCount`. -// -// It does this by shuffling a list of eligible-for-exclusion operators -// according to `rng`, selecting the operator according to `index`, and then -// filtering that operator out of `groupMembers`. -// -// In the case that `index` is larger than the number of eligible operators, it -// skips shuffling and returns the number of eligible operators, which is -// useful for determining the index of the operator pair to ignore. -func excludeSingleOperator( - rng *rand.Rand, - groupMembers []chain.Address, - index int, - operatorToSeatCount map[chain.Address]uint, - operators []chain.Address, -) ([]chain.Address, int, bool) { - if index < len(operators) { - rng.Shuffle(len(operators), func(i, j int) { - operators[i], operators[j] = operators[j], operators[i] - }) - removedOperator := operators[index] - usedOperators := make([]chain.Address, 0, len(groupMembers)) - for _, operator := range groupMembers { - if operator != removedOperator { - usedOperators = append(usedOperators, operator) - } - } - return usedOperators, 0, true - } else { - return nil, len(operators), false - } -} - -// excludeOperatorPairs randomly excludes all of a pair of operator's seats from a -// given `groupMembers`. It needs a pre-seeded random generator `rng`, and an -// `index`, which is expected to be inferred from a `retryCount`. -// -// It does this by shuffling a list of eligable-for-exclusion operators -// according to `rng`, selecting the operator according to `index`, and then -// filtering that operator pair out of `groupMembers`. -// -// In the case that `index` is larger than the number of eligible operator -// pairs, it skips shuffling and returns the number of eligible operators -// pairs, which is useful for determining the index of the operator triplet to -// ignore. -func excludeOperatorPairs( - rng *rand.Rand, - groupMembers []chain.Address, - index int, - operatorToSeatCount map[chain.Address]uint, - operators []chain.Address, - retryParticipantsCount int, -) ([]chain.Address, int, bool) { - pairIndexes := make([][2]int, 0, len(operators)*len(operators)) - for i := 0; i < len(operators)-1; i++ { - for j := i + 1; j < len(operators); j++ { - leftOperator := operators[i] - rightOperator := operators[j] - - // Only include the operators pairs that have few enough seats such that - // if they were excluded we still have at least `retryParticipantsCount` - // seats. - count := len(groupMembers) - - int(operatorToSeatCount[leftOperator]) - - int(operatorToSeatCount[rightOperator]) - if count >= int(retryParticipantsCount) { - pairIndexes = append(pairIndexes, [2]int{i, j}) - } - } - } - if index < len(pairIndexes) { - rng.Shuffle(len(pairIndexes), func(i, j int) { - pairIndexes[i], pairIndexes[j] = pairIndexes[j], pairIndexes[i] - }) - pair := pairIndexes[index] - leftOperator := operators[pair[0]] - rightOperator := operators[pair[1]] - usedOperators := make([]chain.Address, 0, len(groupMembers)) - for _, operator := range groupMembers { - if operator != leftOperator && operator != rightOperator { - usedOperators = append(usedOperators, operator) - } - } - return usedOperators, 0, true - } else { - return nil, len(pairIndexes), false - } -} - -// excludeOperatorTriplets randomly excludes all of a triplet of operator's seats from a -// given `groupMembers`. It needs a pre-seeded random generator `rng`, and an -// `index`, which is expected to be inferred from a `retryCount`. -// -// It does this by shuffling a list of eligable-for-exclusion operators -// according to `rng`, selecting the operator according to `index`, and then -// filtering that operator triplet out of `groupMembers`. -// -// In the case that `index` is larger than the number of eligible operator -// triplets, it skips shuffling and returns the number of eligible operators -// triplets, which is useful for logging errors. -func excludeOperatorTriplets( - rng *rand.Rand, - groupMembers []chain.Address, - index int, - operatorToSeatCount map[chain.Address]uint, - operators []chain.Address, - retryParticipantsCount int, -) ([]chain.Address, int, bool) { - tripletIndexes := make([][3]int, 0, len(operators)*len(operators)*len(operators)) - for i := 0; i < len(operators)-2; i++ { - for j := i + 1; j < len(operators)-1; j++ { - for k := j + 1; k < len(operators); k++ { - leftOperator := operators[i] - middleOperator := operators[j] - rightOperator := operators[k] - - // Only include the operators triples that have few enough seats such - // that if they were excluded we still have at least - // `retryParticipantsCount` seats. - count := len(groupMembers) - - int(operatorToSeatCount[leftOperator]) - - int(operatorToSeatCount[middleOperator]) - - int(operatorToSeatCount[rightOperator]) - if count >= int(retryParticipantsCount) { - tripletIndexes = append(tripletIndexes, [3]int{i, j, k}) - } - } - } - } - if index < len(tripletIndexes) { - rng.Shuffle(len(tripletIndexes), func(i, j int) { - tripletIndexes[i], tripletIndexes[j] = tripletIndexes[j], tripletIndexes[i] - }) - triplet := tripletIndexes[index] - leftOperator := operators[triplet[0]] - middleOperator := operators[triplet[1]] - rightOperator := operators[triplet[2]] - usedOperators := make([]chain.Address, 0, len(groupMembers)) - for _, operator := range groupMembers { - if operator != leftOperator && operator != middleOperator && operator != rightOperator { - usedOperators = append(usedOperators, operator) - } - } - return usedOperators, 0, true - } else { - return nil, len(tripletIndexes), false - } -}