Skip to content

bug(fxconfig/transaction): Merge panics with index out of range when Endorsements count exceeds Namespaces count #238

@Prachi194agrawal

Description

@Prachi194agrawal

Summary

mergeEndorsements() allocates its internal slices using len(txs[0].GetNamespaces()),
but then indexes into those slices using positions from tx.GetEndorsements().
When any transaction carries more endorsement entries than namespace entries,
this causes a runtime panic: index out of range.

Affected File

tools/fxconfig/internal/transaction/merge.go

Root Cause

func mergeEndorsements(txs []*applicationpb.Tx) []*applicationpb.Endorsements {
    numNamespaces := len(txs.GetNamespaces())       // ← allocated from Namespaces count
    merged := make([]*applicationpb.Endorsements, numNamespaces)
    seen   := make([]map[string]struct{}, numNamespaces)
    ...
    for _, tx := range txs {
        for nsIdx, ns := range tx.GetEndorsements() {  // ← iterates Endorsements (different length!)
            ...
            seen[nsIdx][key] = struct{}{}          // ← PANIC if nsIdx >= numNamespaces
            merged[nsIdx].EndorsementsWithIdentity = append(...)
        }
    }
}

validateTransactionsForMerge only validates:

  • Namespace count is the same across all transactions (len(Namespaces) matches)
  • Each endorsement entry is non-empty

It does not validate that len(Endorsements) == len(Namespaces) within each
individual transaction. A transaction with 1 namespace but 2 endorsement slots passes
validation and then panics inside mergeEndorsements.

Steps to Reproduce

Add the following test to tools/fxconfig/internal/transaction/:

// merge_panic_test.go
package transaction

import (
    "testing"

    "github.com/hyperledger/fabric-x-common/api/applicationpb"
    "github.com/hyperledger/fabric-x-common/api/msppb"
)

func TestMergePanicsWhenEndorsementsExceedNamespaces(t *testing.T) {
    tx1 := &applicationpb.Tx{
        Namespaces: []*applicationpb.TxNamespace{
            {NsId: "ns1", NsVersion: 1},
        },
        Endorsements: []*applicationpb.Endorsements{
            {
                EndorsementsWithIdentity: []*applicationpb.EndorsementWithIdentity{
                    {Endorsement: []byte("sig-a1"), Identity: &msppb.Identity{MspId: "Org1MSP"}},
                },
            },
            // Extra endorsement entry — no matching namespace
            {
                EndorsementsWithIdentity: []*applicationpb.EndorsementWithIdentity{
                    {Endorsement: []byte("sig-a2"), Identity: &msppb.Identity{MspId: "Org2MSP"}},
                },
            },
        },
    }

    tx2 := &applicationpb.Tx{
        Namespaces: []*applicationpb.TxNamespace{
            {NsId: "ns1", NsVersion: 1},
        },
        Endorsements: []*applicationpb.Endorsements{
            {
                EndorsementsWithIdentity: []*applicationpb.EndorsementWithIdentity{
                    {Endorsement: []byte("sig-b1"), Identity: &msppb.Identity{MspId: "Org3MSP"}},
                },
            },
            {
                EndorsementsWithIdentity: []*applicationpb.EndorsementWithIdentity{
                    {Endorsement: []byte("sig-b2"), Identity: &msppb.Identity{MspId: "Org4MSP"}},
                },
            },
        },
    }

    defer func() {
        if r := recover(); r == nil {
            t.Fatalf("expected panic due to index out of range, got nil")
        }
    }()

    _, _ = Merge([]*applicationpb.Tx{tx1, tx2})
}

Run:

go test ./tools/fxconfig/internal/transaction -run TestMergePanicsWhenEndorsementsExceedNamespaces -v

Observed Behavior

--- PASS: TestMergePanicsWhenEndorsementsExceedNamespaces (0.00s)
PASS

The test passes (panic is caught by recover()), confirming the runtime panic occurs.

Expected Behavior

Merge() should return a descriptive error instead of panicking. Either:

  1. validateTransactionsForMerge should reject transactions where len(Endorsements) != len(Namespaces), or
  2. mergeEndorsements should guard the index with a bounds check.

Suggested Fix

Add this check inside validateTransactionsForMerge:

for i, tx := range txs {
    if len(tx.GetEndorsements()) != len(tx.GetNamespaces()) {
        return fmt.Errorf(
            "transaction %d: endorsements count (%d) does not match namespaces count (%d)",
            i, len(tx.GetEndorsements()), len(tx.GetNamespaces()),
        )
    }
    // ... existing checks
}

Environment

  • Repo: hyperledger/fabric-x
  • File: tools/fxconfig/internal/transaction/merge.go
  • Function: mergeEndorsements / validateTransactionsForMerge
  • Go version: tested on go1.23+

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions