hyperledger/fabric · critical

proposal hash does not match

Error message

proposal hash does not match

What it means

For each action, the validator recomputes the proposal hash (pHash) from the chaincode proposal payload and compares it with the ProposalHash carried in the ProposalResponsePayload. A mismatch means the endorsement does not correspond to the proposal bytes in the transaction, so the action is invalid.

Source

Thrown at core/common/validation/msgvalidation.go:240

		// extract the proposal response payload
		prp, err := protoutil.UnmarshalProposalResponsePayload(ccActionPayload.Action.ProposalResponsePayload)
		if err != nil {
			return err
		}

		// build the original header by stitching together
		// the common ChannelHeader and the per-action SignatureHeader
		hdrOrig := &common.Header{ChannelHeader: hdr.ChannelHeader, SignatureHeader: act.Header}

		// compute proposalHash
		pHash, err := protoutil.GetProposalHash2(hdrOrig, ccActionPayload.ChaincodeProposalPayload)
		if err != nil {
			return err
		}

		// ensure that the proposal hash matches
		if !bytes.Equal(pHash, prp.ProposalHash) {
			return errors.New("proposal hash does not match")
		}
	}

	return nil
}

// ValidateTransaction checks that the transaction envelope is properly formed
func ValidateTransaction(e *common.Envelope, cryptoProvider bccsp.BCCSP) (*common.Payload, pb.TxValidationCode) {
	putilsLogger.Debugf("ValidateTransactionEnvelope starts for envelope %p", e)

	// check for nil argument
	if e == nil {
		putilsLogger.Errorf("Error: nil envelope")
		return nil, pb.TxValidationCode_NIL_ENVELOPE
	}

	// get the payload from the envelope
	payload, err := protoutil.UnmarshalPayload(e.Payload)

View on GitHub (pinned to 2736b63f8f)

Solutions

  1. Use the original proposal payload bytes (not re-marshalled ones) when building the transaction so the hash matches
  2. Ensure the signed proposal submitted for endorsement is the same object used to build the final transaction
  3. Check for any middleware/SDK that re-encodes the proposal; disable re-marshalling
  4. If mixing endorsements, confirm all responses came from the same signed proposal

Example fix

// before
respBytes, _ := proto.Marshal(&proposal) // re-marshalled, may differ
action, _ := utils.CreateTxEndorsement(respBytes, ...)
// after
action, _ := utils.CreateTxEndorsement(originalProposalBytes, ...) // bytes as signed
Defensive patterns

Strategy: validation

Validate before calling

prp, err := protoutil.UnmarshalProposalResponsePayload(act.Payload)
if err != nil { return err }
if !bytes.Equal(prp.ProposalHash, computedProposalHash) { return errors.New("proposal hash mismatch before submit") }

Type guard

func proposalHashMatches(act *common.TransactionAction, proposal []byte) bool {
	prp, err := protoutil.UnmarshalProposalResponsePayload(act.Payload)
	if err != nil { return false }
	h, err := utils.GetProposalHash1(nil, proposal, nil) // per your SDK version
	return err == nil && bytes.Equal(h, prp.ProposalHash)
}

Try / catch

if err := ValidateTransaction(env, policy); err != nil {
	if strings.Contains(err.Error(), "proposal hash does not match") {
		// re-endorse: the endorsement doesn't match the proposal bytes
	}
	return err
}

Prevention

When it happens

Trigger: ValidateTransaction where prp.ProposalHash differs from the hash recomputed from ccInspection/chaincode proposal payload bytes — e.g. the proposal was modified after endorsement or a response from a different proposal was used.

Common situations: Client mixes proposal responses from different invocations or channels; bytes are altered by a proxy/SDK between endorsement and tx creation; hash computed over a differently-encoded payload (field ordering/extra fields); SDK version changes changing serialization.

Related errors


AI-assisted analysis of hyperledger/fabric@2736b63f8f (2026-09-04). Data as JSON: /api/errors/e32c09e1844a28fa. Report an issue: GitHub.