Skip to content

integration: prove a 2-of-2 endorsement policy over a real backend - #306

Open
Storm1289 wants to merge 15 commits into
hyperledger:mainfrom
Storm1289:integration/302-two-endorser-policy
Open

integration: prove a 2-of-2 endorsement policy over a real backend#306
Storm1289 wants to merge 15 commits into
hyperledger:mainfrom
Storm1289:integration/302-two-endorser-policy

Conversation

@Storm1289

@Storm1289 Storm1289 commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #302. Related to #21 (Split Gateway and Endorser Services).

End-to-end proof that a 2-of-2 endorsement policy is accepted by the real Fabric-X backend, with each organization's endorser reached over real gRPC. Includes the endorser / gateway.endorsers naming change from review, the Fablo setup moved to the same shape, and two bugs this surfaced in the already-merged split-deployment path.

Files

Added

File Purpose
integration/fabx-2of2.yaml Gateway config, split mode: dials both endorsers, embeds none
integration/fabx-2of2-org1.yaml Standalone org1 endorser (one endorser)
integration/fabx-2of2-org2.yaml Standalone org2 endorser (one endorser)
integration/fablo-org1.yaml Standalone org1 endorser for Fablo
integration/fablo-org2.yaml Standalone org2 endorser for Fablo
integration/split_endorsers.go Shared helpers to serve endorsers over mTLS gRPC and build a split gateway

Modified

File Change
gateway/config/config.go Endorsers [] becomes Endorser *; reject both keys set, or neither
gateway/app/app.go newApp builds one embedded endorser instead of a list
endorser/client/dial.go Apply tls.server-name when dialing (fix 2)
endorser/client/client.go Restore the signed payload from the wire (fix 1)
endorser/server/handlers.go Send the signed payload over the wire (fix 1)
integration/fablo.yaml Now gateway-only, split across the two org files
integration/fabx.yaml, fabx-full.yaml, fablo.hardhat.yaml endorsers: renamed to endorser:
integration/test_helpers.go newSplitFileConfigHarness; build the single embedded endorser
integration/integration_test.go TestFablo uses the split harness; 2-of-2 subtest
integration/endorsement_grpc_test.go Parity test now asserts the signed payload
endorser/client/dial_test.go Hostname-only certificate cases for fix 2
gateway/config/validate_test.go, load_test.go Cover the singular field and the both-keys error
Makefile start-x also creates the basic2of2 namespace
.github/workflows/tests.yml Raise job timeouts; create the artifact dir before writing to it

Config: endorser singular, gateway.endorsers plural

endorsers previously meant two different things. Now endorser (singular, top level) is this process's own embedded endorser, and gateway.endorsers (plural) are remote ones dialed over gRPC. Setting both is rejected. Mixed mode is deliberately not implemented, per review.

Since the field is a single object rather than a list, no config can embed more than one endorser. A file per organization is not the same as embedding several: each org file declares exactly one endorser, and they run as separate endorsers that a gateway dials.

Test setup

Fabric-X - org1 and org2 each run as a standalone endorser behind its own mTLS gRPC server, using that org's real endorser.<org>.example.com material, with a split-deployment gateway dialing both. Drives the real gateway/app.App, the same type fxevm start uses. start-x creates a basic2of2 namespace with AND('Org1MSP.member','Org2MSP.member'), separate from basic so the stricter policy doesn't affect the existing single-org cases.

Fablo - split the same way. Its chaincode declares no endorsement policy, so Fablo commits it with AND('Org1MSP.member','Org2MSP.member') and both orgs must endorse, which is why it embedded two endorsers before and could not stay singular.

fabx.yaml deliberately stays embedded with a single endorser, so the embedded deployment mode keeps end-to-end coverage against a real backend.

newSplitFileConfigHarness keeps each endorser's KVS registered as a block handler and its builder available to the state primer, so tether_token_replay still works, while only the gateway's path to them is gRPC - the part under test.

Fix 1: the signed endorsement payload was dropped over gRPC

peer.ProposalResponse has a top-level Payload - the signed result fabricx.CreateTx assembles the transaction from - which is a different field from Response.Payload (EVM return data). Only the latter crossed the wire, so the top-level one arrived empty: 456 bytes in-process, 0 bytes over gRPC.

An empty payload still unmarshals cleanly, so nothing reported an error: the gateway packaged an empty transaction, the orderer accepted it and cut a block, and it was then rejected. Split deployment could endorse and submit but never commit.

Fixed by carrying it in ExecuteResponse.read_write_set, the field docs/design/endorsement-api/01-api-and-proto.md already specifies for this ("kept byte-exact for signing"), so no proto change was needed. TestGRPCEndorsement_Parity had compared only Response.Payload - empty on both sides, so trivially equal - and now asserts the signed payload is non-empty and identical across the boundary.

Fix 2: tls.server-name was ignored when dialing an endorser

ClientConfig.TLS.ServerName exists and both ToPeerConf and ToOrdererConf propagate it, but eclient.Dial dropped it, so certificates were verified against the dial address:

x509: cannot validate certificate for 127.0.0.1 because it doesn't contain any IP SANs

This stayed hidden because the Fabric-X test endorser certs happen to carry IP:127.0.0.1 SANs. Real peer certificates do not - Fablo's are DNS:peer0.orgN.example.com only - so any deployment whose endorser cert is issued for a hostname and reached by IP would fail. Fixed with grpc.WithAuthority, covered by a test that serves a hostname-only certificate and fails with that exact error without the fix.

Verification

Against real backends, not mocks: TestFabricX 11/11 (including the new two_of_two_endorsement_policy) and TestFablo 10/10, both on freshly created networks, re-run after rebasing onto the main merges that brought in #305 and #294. make checks clean and the full -short suite green.

tether_token_replay requires a fresh network in both suites - it primes state and replays a historic mainnet transaction. That behaviour is unchanged here and reproduces identically on main.

Test plan

  • go build ./... / go vet ./... / gofmt -l (clean)
  • make checks
  • go test -short ./...
  • make test-x (11/11)
  • make test-fablo (10/10)

- add a separate basic2of2 namespace/policy to start-x, alongside the
  existing single-org one
- add fabx-2of2.yaml wiring org1 and org2 as endorsers
- add a TestFabricX subtest that commits through the real committer under
  the AND(Org1MSP.member, Org2MSP.member) policy

Signed-off-by: Storm1289 <divakarsharm2934@gmail.com>
@codecov

codecov Bot commented Aug 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 15.88235% with 143 lines in your changes missing coverage. Please review.
✅ Project coverage is 29.26%. Comparing base (7f2cd28) to head (f2b4f39).

Files with missing lines Patch % Lines
integration/split_endorsers.go 0.00% 104 Missing ⚠️
integration/test_helpers.go 28.57% 23 Missing and 2 partials ⚠️
gateway/app/app.go 0.00% 14 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #306      +/-   ##
==========================================
- Coverage   29.56%   29.26%   -0.30%     
==========================================
  Files          79       80       +1     
  Lines       11177    11303     +126     
==========================================
+ Hits         3304     3308       +4     
- Misses       7579     7700     +121     
- Partials      294      295       +1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Storm1289
Storm1289 marked this pull request as ready for review August 7, 2026 13:02
@Storm1289

Copy link
Copy Markdown
Contributor Author

hi @arner, whenever you have sometime please review this pr :)

- oz-hardhat-compat and quick-tests both get more headroom (the full OZ
  suite alone was hitting the old 25m ceiling)
- record-PR-number step now creates its own artifact dir, so it can't fail
  when an earlier step is skipped or the job is cancelled

Signed-off-by: Storm1289 <divakarsharm2934@gmail.com>
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

✅ OZ Hardhat Compatibility — 84.4% passing (5143/6094)

No regressions, no stale entries.

Full breakdown — 5143 passed, 950 failed, 1 skipped (6094 total, 84.4% passing)

By suite

  • access: 354/578 passing (61%)
  • crosschain: 12/15 passing (80%)
  • finance: 8/16 passing (50%)
  • governance: 214/522 passing (41%)
  • metatx: 13/23 passing (57%)
  • proxy: 257/280 passing (92%)
  • token: 1732/1852 passing (94%)
  • utils: 2553/2807 passing (91%)

Expected failures by cause (950)

  • execution reverted: 231
  • max-code-size: 212
  • fixed-timestamp: 68
  • gas-stub: 36
  • hardhat_setBalance: 29
  • Expected transaction to be reverted with custom error 'AccessManagerNotScheduled', but it reverted with custom error 'AccessManagerUnauthorizedAccount': 27
  • execution reverted: arithmetic underflow or overflow: 26
  • eth_signTypedData_v4: 23
  • fixed-block-number: 17
  • insufficient-funds: 16
  • Expected transaction to be reverted without a reason, but it didn't revert: 9
  • Error in "ProposalCreated" event: Error in the 7th argument assertion: expected 1000004 to equal 1786447500.: 8
  • Expected transaction to be reverted: 6
  • eth_getProof: 6
  • expected 10000000 to be at most 30000.: 6
  • Expected transaction to be reverted with custom error 'ERC3009InvalidAuthorizationTime', but it didn't revert: 4
  • Expected transaction to be reverted with custom error 'FailedCall', but it reverted with custom error 'TimelockUnexpectedOperationState': 4
  • Expected transaction to be reverted with custom error 'InvalidAccountNonce', but it reverted with custom error 'ERC3009InvalidAuthorizationTime': 4
  • expected 0 to equal 864000.: 4
  • expected 1000180 to equal 1786447649.: 4
  • Error in "RoleGranted" event: Error in the 4th argument assertion: expected 1000000 to equal 1786447478.: 3
  • expected false to be true: 3
  • hardhat_setStorageAt: 3
  • transaction already pending: 3
  • Error in "RoleGranted" event: Error in the 4th argument assertion: expected 1000000 to equal 1787657078.: 2
  • Error in "RoleGranted" event: Error in the 4th argument assertion: expected 1345600 to equal 1786793078.: 2
  • Expected transaction to be reverted with custom error 'AccessManagerNotScheduled', but it reverted with a different custom error: 2
  • Expected transaction to be reverted with custom error 'FailedCall', but it didn't revert: 2
  • Expected transaction to be reverted with custom error 'GovernorInsufficientProposerVotes', but it reverted with panic code 0x11 (Arithmetic operation overflowed outside of an unchecked block): 2
  • Expected transaction to be reverted with custom error 'GovernorInvalidVoteParams', but it reverted with custom error 'GovernorUnexpectedProposalState': 2
  • Expected transaction to be reverted with custom error 'GovernorInvalidVoteType', but it reverted with custom error 'GovernorUnexpectedProposalState': 2
  • Expected transaction to be reverted with panic code 0x31 (.pop() was called on an empty array), but it reverted with a custom error: 2
  • expected 0 to equal 1.: 2
  • expected 0 to equal 10.: 2
  • expected 0 to equal 1666666666666666666.: 2
  • expected 0 to equal 25000000000000000000.: 2
  • expected 1086400 to equal 1786533899.: 2
  • expected 1345600 to equal 1786793078.: 2
  • expected 1787311471 to equal 1787311470.: 2
  • expected 518400 to equal 172800.: 2
  • expected Result(2) [ 10000n, 0n ] to deeply equal [ 0n, 10000n ]: 2
  • expected Result(2) [ 17n, 9983n ] to deeply equal [ 15n, 9985n ]: 2
  • expected Result(2) [ 42n, 9958n ] to deeply equal [ 40n, 9960n ]: 2
  • expected Result(2) [ 5000n, 5000n ] to deeply equal [ 0n, 10000n ]: 2
  • Clock mismatch in blockNumber mode: expected 0 to equal 1534.: 1
  • Clock mismatch in blockNumber mode: expected 0 to equal 1673.: 1
  • Clock mismatch in timestamp mode: expected 1000000 to equal 1786447494.: 1
  • Error in "GovernorInsufficientProposerVotes" custom error: Error in the 2nd argument assertion: expected 0 to equal 10000000000000000000.: 1
  • Error in "GovernorUnexpectedProposalState" custom error: Error in the 2nd argument assertion: expected 0 to equal 3.: 1
  • Error in "ProposalCreated" event: Error in the 7th argument assertion: expected 1000004 to equal 1786447498.: 1
  • Error in "ProposalCreated" event: Error in the 7th argument assertion: expected 4 to equal 1539.: 1
  • Error in "ProposalCreated" event: Error in the 7th argument assertion: expected 4 to equal 1574.: 1
  • Error in "ProposalCreated" event: Error in the 7th argument assertion: expected 4 to equal 1575.: 1
  • Error in "ProposalCreated" event: Error in the 7th argument assertion: expected 4 to equal 1576.: 1
  • Error in "ProposalCreated" event: Error in the 7th argument assertion: expected 4 to equal 1577.: 1
  • Error in "ProposalCreated" event: Error in the 7th argument assertion: expected 4 to equal 1578.: 1
  • Error in "ProposalCreated" event: Error in the 7th argument assertion: expected 4 to equal 1579.: 1
  • Error in "ProposalCreated" event: Error in the 7th argument assertion: expected 4 to equal 1580.: 1
  • Error in "ProposalCreated" event: Error in the 7th argument assertion: expected 4 to equal 1581.: 1
  • Error in "ProposalCreated" event: Error in the 7th argument assertion: expected 4 to equal 1678.: 1
  • Error in "ProposalCreated" event: Error in the 7th argument assertion: expected 4 to equal 1713.: 1
  • Error in "ProposalCreated" event: Error in the 7th argument assertion: expected 4 to equal 1714.: 1
  • Error in "ProposalCreated" event: Error in the 7th argument assertion: expected 4 to equal 1715.: 1
  • Error in "ProposalCreated" event: Error in the 7th argument assertion: expected 4 to equal 1716.: 1
  • Error in "ProposalCreated" event: Error in the 7th argument assertion: expected 4 to equal 1717.: 1
  • Error in "ProposalCreated" event: Error in the 7th argument assertion: expected 4 to equal 1718.: 1
  • Error in "ProposalCreated" event: Error in the 7th argument assertion: expected 4 to equal 1719.: 1
  • Error in "ProposalCreated" event: Error in the 7th argument assertion: expected 4 to equal 1720.: 1
  • Error in "Transfer" event: Error in the 3rd argument assertion: expected 0 to equal 1666666666666666666.: 1
  • Error in "Transfer" event: Error in the 3rd argument assertion: expected 0 to equal 25000000000000000000.: 1
  • Expected the ether balance of 0x0E801D84Fa97b50751Dbf25036d067dCf18858bF (the 1st address in the list) to change by -1666666666666666666 wei, but it changed by 0 wei
    Expected the ether balance of 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 (the 2nd address in the list) to change by 1666666666666666666 wei, but it changed by 0 wei: 1
  • Expected the ether balance of 0x4c5859f0F772848b2D91F1D83E2Fe57935348029 (the 1st address in the list) to change by -25000000000000000000 wei, but it changed by 0 wei
    Expected the ether balance of 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 (the 2nd address in the list) to change by 25000000000000000000 wei, but it changed by 0 wei: 1
  • Expected transaction NOT to be reverted: 1
  • Expected transaction to be reverted with custom error 'AccessManagerUnauthorizedCall', but it didn't revert: 1
  • Expected transaction to be reverted with custom error 'ERC2771ForwarderExpiredRequest', but it didn't revert: 1
  • Expected transaction to be reverted with custom error 'GovernorExceedRemainingWeight', but it reverted with custom error 'GovernorUnexpectedProposalState': 1
  • Expected transaction to be reverted with custom error 'GovernorUnableToCancel', but it didn't revert: 1
  • Expected transaction to be reverted with custom error 'TimelockUnexecutedPredecessor', but it reverted with custom error 'TimelockUnexpectedOperationState': 1
  • Expected transaction to be reverted with panic code 0x01 (Assertion error), but it reverted with a custom error: 1
  • Invalid block number 20 is not larger than current block number 1543: 1
  • Invalid block number 20 is not larger than current block number 1555: 1
  • Invalid block number 20 is not larger than current block number 1682: 1
  • Invalid block number 20 is not larger than current block number 1694: 1
  • Invalid block number 4 is not larger than current block number 1536: 1
  • Invalid block number 4 is not larger than current block number 1541: 1
  • Invalid block number 4 is not larger than current block number 1542: 1
  • Invalid block number 4 is not larger than current block number 1544: 1
  • Invalid block number 4 is not larger than current block number 1547: 1
  • Invalid block number 4 is not larger than current block number 1548: 1
  • Invalid block number 4 is not larger than current block number 1549: 1
  • Invalid block number 4 is not larger than current block number 1550: 1
  • Invalid block number 4 is not larger than current block number 1551: 1
  • Invalid block number 4 is not larger than current block number 1552: 1
  • Invalid block number 4 is not larger than current block number 1553: 1
  • Invalid block number 4 is not larger than current block number 1554: 1
  • Invalid block number 4 is not larger than current block number 1556: 1
  • Invalid block number 4 is not larger than current block number 1557: 1
  • Invalid block number 4 is not larger than current block number 1559: 1
  • Invalid block number 4 is not larger than current block number 1560: 1
  • Invalid block number 4 is not larger than current block number 1561: 1
  • Invalid block number 4 is not larger than current block number 1562: 1
  • Invalid block number 4 is not larger than current block number 1567: 1
  • Invalid block number 4 is not larger than current block number 1568: 1
  • Invalid block number 4 is not larger than current block number 1569: 1
  • Invalid block number 4 is not larger than current block number 1578: 1
  • Invalid block number 4 is not larger than current block number 1579: 1
  • Invalid block number 4 is not larger than current block number 1580: 1
  • Invalid block number 4 is not larger than current block number 1581: 1
  • Invalid block number 4 is not larger than current block number 1675: 1
  • Invalid block number 4 is not larger than current block number 1680: 1
  • Invalid block number 4 is not larger than current block number 1681: 1
  • Invalid block number 4 is not larger than current block number 1683: 1
  • Invalid block number 4 is not larger than current block number 1686: 1
  • Invalid block number 4 is not larger than current block number 1687: 1
  • Invalid block number 4 is not larger than current block number 1688: 1
  • Invalid block number 4 is not larger than current block number 1689: 1
  • Invalid block number 4 is not larger than current block number 1690: 1
  • Invalid block number 4 is not larger than current block number 1691: 1
  • Invalid block number 4 is not larger than current block number 1692: 1
  • Invalid block number 4 is not larger than current block number 1693: 1
  • Invalid block number 4 is not larger than current block number 1695: 1
  • Invalid block number 4 is not larger than current block number 1696: 1
  • Invalid block number 4 is not larger than current block number 1698: 1
  • Invalid block number 4 is not larger than current block number 1699: 1
  • Invalid block number 4 is not larger than current block number 1700: 1
  • Invalid block number 4 is not larger than current block number 1701: 1
  • Invalid block number 4 is not larger than current block number 1706: 1
  • Invalid block number 4 is not larger than current block number 1707: 1
  • Invalid block number 4 is not larger than current block number 1708: 1
  • Invalid block number 4 is not larger than current block number 1717: 1
  • Invalid block number 4 is not larger than current block number 1718: 1
  • Invalid block number 4 is not larger than current block number 1719: 1
  • Invalid block number 4 is not larger than current block number 1720: 1
  • Invalid block number 4 is not larger than current block number 1804: 1
  • Invalid block number 4 is not larger than current block number 1805: 1
  • Invalid block number 4 is not larger than current block number 1806: 1
  • Invalid block number 4 is not larger than current block number 1807: 1
  • Invalid block number 4 is not larger than current block number 1808: 1
  • Invalid block number 4 is not larger than current block number 1809: 1
  • Invalid block number 4 is not larger than current block number 1810: 1
  • Invalid block number 4 is not larger than current block number 1811: 1
  • Invalid block number 4 is not larger than current block number 1846: 1
  • Invalid block number 4 is not larger than current block number 1876: 1
  • Invalid block number 4 is not larger than current block number 1878: 1
  • Invalid block number 4 is not larger than current block number 1943: 1
  • Invalid block number 4 is not larger than current block number 1945: 1
  • Invalid block number 4 is not larger than current block number 2002: 1
  • Invalid block number 4 is not larger than current block number 2066: 1
  • Invalid block number 4 is not larger than current block number 2067: 1
  • Invalid block number 4 is not larger than current block number 2068: 1
  • Invalid block number 4 is not larger than current block number 2069: 1
  • Invalid block number 4 is not larger than current block number 2128: 1
  • Invalid block number 4 is not larger than current block number 2129: 1
  • Invalid block number 4 is not larger than current block number 2130: 1
  • Invalid block number 4 is not larger than current block number 2131: 1
  • Invalid block number 4 is not larger than current block number 2132: 1
  • Invalid block number 4 is not larger than current block number 2133: 1
  • Invalid block number 4 is not larger than current block number 2134: 1
  • Invalid block number 4 is not larger than current block number 2135: 1
  • Invalid block number 4 is not larger than current block number 2136: 1
  • Invalid block number 4 is not larger than current block number 2137: 1
  • Invalid block number 4 is not larger than current block number 2140: 1
  • Invalid block number 4 is not larger than current block number 2142: 1
  • Invalid block number 4 is not larger than current block number 2147: 1
  • Invalid block number 4 is not larger than current block number 2213: 1
  • Invalid block number 4 is not larger than current block number 2214: 1
  • Invalid block number 4 is not larger than current block number 2215: 1
  • Invalid block number 4 is not larger than current block number 2216: 1
  • Invalid block number 4 is not larger than current block number 2247: 1
  • Invalid block number 4 is not larger than current block number 2248: 1
  • Invalid block number 4 is not larger than current block number 2277: 1
  • Invalid block number 4 is not larger than current block number 2278: 1
  • Invalid block number 5 is not larger than current block number 1566: 1
  • Invalid block number 5 is not larger than current block number 1705: 1
  • Timeout of 40000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves. (/home/runner/work/fabric-x-evm/fabric-x-evm/testdata/openzeppelin-contracts/test/metatx/ERC2771Forwarder.test.js): 1
  • execution reverted: ERC20Votes: block not yet mined: 1
  • expected '0x2c6b7d15c1a6ceee36f239bb947c3a4a464…' to equal '0xa52b7419d8cdc4e1f4fa7df7a0f38d779f8…': 1
  • expected 0 to equal 36000.: 1
  • expected 0 to equal 950400.: 1
  • expected 1000000 to equal 1786447469.: 1
  • expected 1000000 to equal 1786447470.: 1
  • expected 1000004 to equal 1786447508.: 1
  • expected 1259200 to equal 1786706681.: 1
  • expected 1787311471 to equal 0.: 1
  • expected 36000 to equal 4919.: 1
  • expected 4 to equal 1948.: 1
  • expected 4919 to equal 0.: 1
  • expected 9941 to equal 9958.: 1
  • expected 9998000000000000000000 to equal 9998000000000000000010.: 1
  • expected Result(2) [ 10000n, 0n ] to deeply equal [ 9953n, 47n ]: 1
  • expected Result(2) [ 10000n, 10000n ] to deeply equal [ 9962n, 10038n ]: 1
  • expected Result(2) [ 10n, 9990n ] to deeply equal [ 0n, 10000n ]: 1
  • expected Result(2) [ 42n, 9958n ] to deeply equal [ 0n, 10000n ]: 1
  • expected [ 10715, 10716 ] to have a length of 1 but got 2: 1
  • expected [ EventLog{ …(13) }, …(2) ] to have a length of 2 but got 3: 1
  • expected [ false, '0' ] to deeply equal [ true, '0' ]: 1
  • failed to revert Store to block 10465: snapshot for block 10465 not found: 1
  • invalid argument 1: json: cannot unmarshal hex string of odd length into Go value of type common.Hash: 1
  • out of gas: 1
  • transaction execution reverted (action="sendTransaction", data=null, reason=null, invocation=null, revert=null, transaction={ "data": "", "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "to": "0x23228469b3439d81DC64e3523068976201bA08C3" }, receipt={ "_type": "TransactionReceipt", "blobGasPrice": null, "blobGasUsed": null, "blockHash": "0x3bd56aa088072abbc2e214692abe009d878882b999762214b56bd45c974f8e8b", "blockNumber": 10921, "contractAddress": "0x0000000000000000000000000000000000000000", "cumulativeGasUsed": "0", "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "gasPrice": "0", "gasUsed": "0", "hash": "0xc098600b06e977fe743e9471e05833382aaf637b3553acba5d33dc3ae57f6ea3", "index": 0, "logs": [ { "_type": "log", "address": "0x23228469b3439d81DC64e3523068976201bA08C3", "blockHash": "0x3bd56aa088072abbc2e214692abe009d878882b999762214b56bd45c974f8e8b", "blockNumber": 10921, "data": "0x0000000000000000000000000000000000000000000000000000000000000011000000000000000000000000000000000000000000000000000000000000002a", "index": 0, "removed": null, "topics": [ "0x2b42b3c9af4187c86225f201736204c7597312c830cb5c2537dff6ef24b946ae" ], "transactionHash": "0xc098600b06e977fe743e9471e05833382aaf637b3553acba5d33dc3ae57f6ea3", "transactionIndex": 0 } ], "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "root": "0x", "status": 0, "to": "0x23228469b3439d81DC64e3523068976201bA08C3" }, code=CALL_EXCEPTION, version=6.16.0): 1
  • transaction execution reverted (action="sendTransaction", data=null, reason=null, invocation=null, revert=null, transaction={ "data": "", "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "to": "0x9A8Ec3B44ee760b629e204900c86d67414a67e8f" }, receipt={ "_type": "TransactionReceipt", "blobGasPrice": null, "blobGasUsed": null, "blockHash": "0x4048824aa745d3badaf583edf7f86939bfeec675dd434d24da75dd7de4760d7c", "blockNumber": 10706, "contractAddress": "0x0000000000000000000000000000000000000000", "cumulativeGasUsed": "0", "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "gasPrice": "0", "gasUsed": "0", "hash": "0x5d171a449a50292bd6936b4b1204aa0afbdefa13316d490e91374867bb136ff2", "index": 0, "logs": [ ], "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "root": "0x", "status": 0, "to": "0x9A8Ec3B44ee760b629e204900c86d67414a67e8f" }, code=CALL_EXCEPTION, version=6.16.0): 1
  • transaction execution reverted (action="sendTransaction", data=null, reason=null, invocation=null, revert=null, transaction={ "data": "", "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "to": "0x9A8Ec3B44ee760b629e204900c86d67414a67e8f" }, receipt={ "_type": "TransactionReceipt", "blobGasPrice": null, "blobGasUsed": null, "blockHash": "0xe7c8cc0d41cc47fd1542a380c158f5969977fb87025a725399302ce9f6eba286", "blockNumber": 10751, "contractAddress": "0x0000000000000000000000000000000000000000", "cumulativeGasUsed": "0", "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "gasPrice": "0", "gasUsed": "0", "hash": "0x8134aff31f9c1f53bd7792a8522ad1fb11bedd8c81a516cbb6240757b4a55d22", "index": 0, "logs": [ ], "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "root": "0x", "status": 0, "to": "0x9A8Ec3B44ee760b629e204900c86d67414a67e8f" }, code=CALL_EXCEPTION, version=6.16.0): 1

Storm1289 added 3 commits August 7, 2026 17:08
Governor using $ERC20VotesTimestampMock's 'signature does not match
signer' case was never recorded, unlike its five sibling variants with the
same underlying cause. Flagged as a CI regression on an unrelated PR; not
caused by that PR's changes.

Signed-off-by: Storm1289 <divakarsharm2934@gmail.com>
Governor $ERC20VotesTimestampMock's signature-mismatch case failed
locally and in the CI run that flagged it, but passes on this run -
appears flaky/non-deterministic, not related to this PR's changes.

Signed-off-by: Storm1289 <divakarsharm2934@gmail.com>
Governor $ERC20VotesTimestampMock's signature-mismatch case has failed
on two runs (CI, local) and passed on one - flaky rather than deterministic.
Keeping it in the baseline for now; leaving the call on the underlying
flakiness to maintainer review.

Signed-off-by: Storm1289 <divakarsharm2934@gmail.com>
@Storm1289

Storm1289 commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Hi @arner, heads up for the OZ Hardhat baseline gate looks flaky around *TimestampMock Governor tests (ERC20 and ERC721 variants) - same tests flipping pass/fail across runs, likely tied to the documented fixed-timestamp limitation. Not related to this PR (Makefile/CI/integration-test only). Let me know how you'd like to handle it.

@arner

arner commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Thanks @Storm1289 you're right about the flaky tests (though not sure if the timestamp is the issue - that should be consistent). I think it may have to do with how hardhat tests are forced to be sequential (and build on the previous tx result), even when pushed at almost the same time. That would be exactly the kind of issue that the dependency manager should be able to solve!

In the meantime I'm considering to add a flakiness flag to the tests; right now we can't really deal with flapping tests, we expect either failure or success...

@arner

arner commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

It looks good, but these are two embedded endorsers. I was hoping that we can make a test over GRPC, with two instances of an endorser (can be the full application including gateway). It would at least mean two config files; one for each organization. That would make it a step closer to a real, multi-org deployment.

We can still run it in one process (easy to start with the integration tests). At some later point we should also add a sample deployment with several organizations, and if we consider it worthwhile we could test against that too.

What do you think?

- Run org1 and org2 as standalone endorsers behind their own mTLS gRPC
  servers, with a split-deployment gateway dialing both.
- Carry the signed endorsed result in ExecuteResponse.read_write_set;
  without it the packaged tx was empty and rejected after ordering.
- Assert endorsed-payload parity across the gRPC boundary.

Signed-off-by: Storm1289 <divakarsharm2934@gmail.com>
@Storm1289

Storm1289 commented Aug 8, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @arner for the review.
Done two standalone endorsers now, each behind its own mTLS gRPC server, with a split-deployment gateway dialing both. One config file per org, driving the real gateway/app.App. Still one process, as you suggested.

This surfaced a bug in the merged gRPC path: the endorsed payload wasn't crossing the wire, so split deployment could endorse and submit but never commit. Two lines to fix, plus the parity-test assertion that would have caught it. Kept it here since the new test can't pass without it.

Could you add the want-integration-tests label? The 2-of-2 test only runs in the integration job, so without it CI goes green without it executing.

Comment thread integration/fabx-2of2-org2.yaml Outdated
@arner arner added the want-integration-tests We run heavy-duty integration tests label Aug 10, 2026
@Storm1289
Storm1289 requested a review from arner August 10, 2026 09:28
Storm1289 and others added 3 commits August 10, 2026 18:54
- Rename the top-level `endorsers` list to a single `endorser`, so it no
  longer collides with `gateway.endorsers` for remote ones, and reject
  configs that set both.
- Move the Fablo suite to two standalone endorsers over gRPC, since its
  chaincode needs both orgs to endorse.
- Apply tls.server-name when dialing an endorser; without it verification
  used the dial address and rejected hostname-issued certs.

Signed-off-by: Storm1289 <divakarsharm2934@gmail.com>
- Serve TLS with a certificate carrying no IP SANs, as real peer certs
  do, and assert tls.server-name is what gets verified.
- Without it the dial address is verified instead, which is the case
  that silently broke reaching an endorser by IP.

Signed-off-by: Storm1289 <divakarsharm2934@gmail.com>
@Storm1289

Storm1289 commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

Hi @arner, updated the PR description. All three points are in: endorser (singular) for the embedded one, gateway.endorsers for remote, and an error if both are set. Fablo moved to the split setup as well. Doing that surfaced a second bug in the merged gRPC path, described in the description.

@arner arner left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! @ale-linux can you take a look as well? Maybe we can discuss the setup in our call. The "mixed mode" should not be too hard to add and we can consider going for that for at least org1 (so we have only two config files per org). But that would be a separate PR anyway IMO.

Comment thread gateway/config/config.go
Storm1289 and others added 2 commits August 11, 2026 17:02
Reword per review; "yet" reflects that mixed mode is planned separately.

Signed-off-by: Storm1289 <divakarsharm2934@gmail.com>

@ale-linux ale-linux left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great stuff! Just a couple of small improvements

name: Quick Tests
runs-on: ubuntu-latest
timeout-minutes: 15
timeout-minutes: 35

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm - a quick test requiring 35 min... 🤔 can we not have this?

Comment thread Makefile
done; \
[ "$$ok" = 1 ] || { echo "Error: namespace setup failed after 5 attempts"; exit 1; }
@echo "Creating 2-of-2 namespace (retrying until the committer is ready)..."
@ok=0; for attempt in 1 2 3 4 5; do \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This appears a bit brittle - can we do something a bit more resilient?

Signed-off-by: Storm <divakarsharma2934@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

want-integration-tests We run heavy-duty integration tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

integration: prove a 2-of-2 endorsement policy across a distributed deployment

3 participants