{"record":{"id":"d1acae370cf64749","repo":"canopy-network/canopy","slug":"duplicate-bls-public-key","errorCode":null,"errorMessage":"duplicate bls public key","messagePattern":"duplicate bls public key","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"lib/crypto/bls.go","lineNumber":347,"sourceCode":"\t\tfor i := 1; i < len(mpk.PublicKeys); i++ {\n\t\t\tif bytes.Compare(mpk.PublicKeys[i-1], mpk.PublicKeys[i]) >= 0 {\n\t\t\t\treturn nil, errInvalidPK\n\t\t\t}\n\t\t}\n\t}\n\t// Reject unused bits because kyber ignores them during verification.\n\tif remainder := len(mpk.PublicKeys) % 8; remainder != 0 {\n\t\tpaddingMask := byte(0xff << remainder)\n\t\tif mpk.Bitmap[len(mpk.Bitmap)-1]&paddingMask != 0 {\n\t\t\treturn nil, errInvalidPK\n\t\t}\n\t}\n\tvar points []kyber.Point\n\tseen := make(map[string]struct{}, len(mpk.PublicKeys))\n\t// convert to a kyber.point\n\tfor _, key := range mpk.PublicKeys {\n\t\tif _, exists := seen[string(key)]; exists {\n\t\t\treturn nil, errors.New(\"duplicate bls public key\")\n\t\t}\n\t\tseen[string(key)] = struct{}{}\n\t\tpoint, err := BytesToBLS12381Point(key)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tpoints = append(points, point)\n\t}\n\tmask, err := sign.NewMask(newBLSSuite(), points, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err = mask.SetMask(mpk.Bitmap); err != nil {\n\t\treturn nil, err\n\t}\n\tkey := newBLSMultiPublicKey(mask, mpk.Threshold)\n\t// Reject semantically equivalent protobuf encodings that would produce different transaction hashes.\n\tif !bytes.Equal(publicKey, key.Bytes()) {","sourceCodeStart":329,"sourceCodeEnd":365,"githubUrl":"https://github.com/canopy-network/canopy/blob/ee8197d91dd410f6592cb650a94c925ee6dc8bad/lib/crypto/bls.go#L329-L365","documentation":"NewMultiBLSFromPublicKey returns \"duplicate bls public key\" when the decoded MultiPublicKey contains the same public key bytes more than once. Duplicate signers would double-count a single party toward the threshold, so decoding rejects them.","triggerScenarios":"Deserializing a MultiPublicKey whose PublicKeys list contains two identical byte slices; building a key by appending the same signer twice then serializing.","commonSituations":"Hand-rolled key assembly where a signer was added twice; merging signer lists without deduplication; config edits that copy/pasted a key entry.","solutions":["Deduplicate the key list (by raw bytes) before constructing/serializing the MultiPublicKey","Remove the duplicate entry from the serialized key material","Add a pre-check that compares unique key count to threshold"],"exampleFix":"// before\nmpk.PublicKeys = append(mpk.PublicKeys, dupKey) // dupKey already present\n// after\nif !slices.Contains(mpk.PublicKeys, newKey) {\n\tmpk.PublicKeys = append(mpk.PublicKeys, newKey)\n}","handlingStrategy":"validation","validationCode":"seen := map[string]struct{}{}\nfor _, k := range keys {\n\tif _, dup := seen[string(k)]; dup { return errors.New(\"duplicate signer key\") }\n\tseen[string(k)] = struct{}{}\n}\nmpk, err := crypto.NewMultiBLSFromPublicKey(serialized)","typeGuard":null,"tryCatchPattern":"mpk, err := crypto.NewMultiBLSFromPublicKey(publicKey)\nif err != nil {\n\treturn nil, fmt.Errorf(\"multisig key decode: %w\", err)\n}","preventionTips":["Deduplicate signer lists before serialization","Never append a signer without checking existence","Validate unique signer count >= threshold when assembling keys"],"tags":["bls","multisig","validation"],"backgroundTag":"invalid-argument-value","analyzedSha":"ee8197d91dd410f6592cb650a94c925ee6dc8bad","analyzedAt":"2026-09-06T09:30:15.973Z","contentChangedAt":"2026-09-06T09:30:15.973Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}