Error: transaction invalidated with status (ENDORSEMENT_POLICY_FAILURE)
On the peer, the matching log line looks something like:
validation of endorsement policy for chaincode basic in tx 12:0 failed: signature set did not satisfy policy
The frustrating part is that the chaincode ran fine. The endorsing peer executed it, signed the result, and the client even got a success response. The transaction was rejected later, at validation, because the set of signatures attached to it didn't satisfy the chaincode's endorsement policy.
Where endorsement fits in the flow
Fabric uses execute-order-validate:
- Execute. The client sends a proposal to one or more endorsing peers. Each simulates the chaincode and signs the resulting read/write set.
- Order. The client bundles the endorsements into a transaction and sends it to the ordering service.
- Validate. Every peer checks each transaction in the block against the endorsement policy and for read conflicts. Invalid transactions stay in the block, marked with a status code, and their writes are not applied.
ENDORSEMENT_POLICY_FAILURE is decided in step 3. That's why the client-side success from step 1 means nothing on its own.
The classic trap: peer chaincode invoke
peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com \
--tls --cafile "$ORDERER_CA" -C mychannel -n basic \
--peerAddresses localhost:7051 --tlsRootCertFiles "$ORG1_PEER_TLS" \
-c '{"function":"CreateAsset","Args":["asset7","blue","5","Tom","300"]}'
This prints Chaincode invoke successful. result: status:200. That only means the Org1 peer endorsed it. If the policy needs Org1 and Org2, the transaction is invalidated at commit and the CLI never tells you, unless you add --waitForEvent. Always use --waitForEvent when testing, and pass one --peerAddresses/--tlsRootCertFiles pair per organization the policy requires:
peer chaincode invoke ... \
--peerAddresses localhost:7051 --tlsRootCertFiles "$ORG1_PEER_TLS" \
--peerAddresses localhost:9051 --tlsRootCertFiles "$ORG2_PEER_TLS" \
--waitForEvent \
-c '{"function":"CreateAsset","Args":["asset7","blue","5","Tom","300"]}'
Find out what the policy actually is
The chaincode endorsement policy is part of the chaincode definition, set when orgs approved and committed it:
--signature-policy "AND('Org1MSP.peer','Org2MSP.peer')"sets an explicit policy.--channel-config-policypoints to a policy in the channel config.- Neither means the default,
/Channel/Application/Endorsement, which in the standardconfigtx.yamlisMAJORITY Endorsement: a majority of the channel's application orgs must endorse.
That default surprises people. With two orgs, a majority is both. With three, it's two of three. Adding an org to the channel can change what "majority" means and start failing transactions that used to pass.
Check the committed definition and its sequence:
peer lifecycle chaincode querycommitted --channelID mychannel --name basic --output json
The policy is in validation_parameter (encoded). If you're unsure what was used, look at the scripts or pipeline that ran approveformyorg.
Common causes
1. Not enough organizations endorsed. You targeted one peer, or your client's peer selection didn't pick peers from every required org. With the Fabric Gateway (Fabric 2.4+ and the fabric-gateway client libraries), the gateway peer uses service discovery to pick endorsers for you. Discovery only finds other orgs' peers if anchor peers are configured for each org on the channel. No anchor peers, no cross-org endorsement.
2. The role in the policy doesn't match the signer. 'Org1MSP.peer' only matches identities classified as peer. That classification comes from Node OUs in the MSP's config.yaml. If Node OUs aren't enabled for that MSP, or the peer's certificate wasn't issued with the peer OU, its signature won't satisfy .peer. Either fix the MSP/certificates or use .member, which matches any valid identity from that org.
3. Private data collection policies. A collection can define its own endorsementPolicy in the collection config. Writes to that collection must satisfy the collection policy, not the chaincode-level one.
4. Key-level (state-based) endorsement. If chaincode calls SetStateValidationParameter on a key, any transaction that writes that key must satisfy that key's policy. The gateway can't always predict these, so name the orgs explicitly:
await contract.submit("TransferAsset", {
arguments: ["asset7", "Org2MSP"],
endorsingOrganizations: ["Org1MSP", "Org2MSP"],
});
In Go, the equivalent is client.WithEndorsingOrganizations("Org1MSP", "Org2MSP").
5. Certificate or MSP problems. A peer whose certificate has expired, was revoked, or was issued by a CA that the channel config doesn't list for that MSP produces a signature that doesn't count.
A related failure: endorsements that don't match
If the endorsing peers return different results, the client can't assemble a valid transaction. The gateway reports that the endorsement responses don't match instead of submitting. The cause is non-deterministic chaincode: reading the system clock, random numbers, iterating a Go map, or calling an external API. Every endorser must produce the identical read/write set from the same input. Pass timestamps and IDs in as arguments, or use GetTxTimestamp().
Get the peer to tell you why
Turn up policy logging on a peer that's rejecting transactions:
FABRIC_LOGGING_SPEC="info:policies,cauthdsl=debug"
You can also change it at runtime through the peer's operations service (PUT /logspec). The debug output shows which principals were evaluated and which signatures failed to match, which usually points straight at cause 1, 2 or 5.
Changing the policy
A policy change is a chaincode definition upgrade: approve with the next sequence number by enough orgs to satisfy the channel's LifecycleEndorsement policy, then commit.
peer lifecycle chaincode approveformyorg -o localhost:7050 --tls --cafile "$ORDERER_CA" \
--channelID mychannel --name basic --version 1.0 --package-id "$PACKAGE_ID" \
--sequence 2 --signature-policy "OR('Org1MSP.peer','Org2MSP.peer')"
peer lifecycle chaincode commit -o localhost:7050 --tls --cafile "$ORDERER_CA" \
--channelID mychannel --name basic --version 1.0 --sequence 2 \
--signature-policy "OR('Org1MSP.peer','Org2MSP.peer')" \
--peerAddresses localhost:7051 --tlsRootCertFiles "$ORG1_PEER_TLS" \
--peerAddresses localhost:9051 --tlsRootCertFiles "$ORG2_PEER_TLS"
Every org must approve the same definition (including the same policy) or the commit fails. Loosening a policy is a governance decision, not a debugging shortcut: an OR policy means any single org can write state on its own.
Checklist
- Confirm validation status:
--waitForEventon the CLI, or the commit status in the Gateway SDK. - Work out the real policy: explicit, channel-config, or the
MAJORITY Endorsementdefault. - Make sure a peer from every required org endorses: anchor peers and discovery for the Gateway, explicit
--peerAddressesfor the CLI. - Check Node OUs and certificates if the policy uses
.peeror.adminroles. - Account for collection-level and key-level policies.
- Enable
policies/cauthdsldebug logging on a validating peer to see which signature failed.
Get the weekly commit
New blockchain deep dives every week.