diff --git a/solidity/contracts/domains/pente/PentePrivacyGroup.sol b/solidity/contracts/domains/pente/PentePrivacyGroup.sol index 8bb4f47a5..95cb9c840 100644 --- a/solidity/contracts/domains/pente/PentePrivacyGroup.sol +++ b/solidity/contracts/domains/pente/PentePrivacyGroup.sol @@ -29,11 +29,13 @@ contract PentePrivacyGroup is IPente, UUPSUpgradeable, EIP712Upgradeable { mapping(bytes32 => bool) private _unspent; mapping(bytes32 => address) private _approvals; + mapping(bytes32 => bool) private _txids; address _nextImplementation; // Config follows the convention of a 4 byte type selector, followed by ABI encoded bytes bytes4 public constant PenteConfig_V0 = 0x00010000; + error PenteDuplicateTransaction(bytes32 txId); error PenteUnsupportedConfigType(bytes4 configSelector); error PenteDuplicateEndorser(address signer); error PenteInvalidEndorser(address signer); @@ -155,6 +157,13 @@ contract PentePrivacyGroup is IPente, UUPSUpgradeable, EIP712Upgradeable { States calldata states, ExternalCall[] calldata externalCalls ) internal { + + // On-chain enforcement of TXID uniqueness + if (_txids[txId] != false) { + revert PenteDuplicateTransaction(txId); + } + _txids[txId] = true; + // Perform the state transitions for (uint i = 0; i < states.inputs.length; i++) { if (!_unspent[states.inputs[i]]) { diff --git a/solidity/test/domains/pente/PentePrivacyGroup.ts b/solidity/test/domains/pente/PentePrivacyGroup.ts index 3e53efe6c..96a55dadb 100644 --- a/solidity/test/domains/pente/PentePrivacyGroup.ts +++ b/solidity/test/domains/pente/PentePrivacyGroup.ts @@ -129,15 +129,16 @@ describe("PentePrivacyGroup", function () { ); const tx1ID = randBytes32(); + const tx1 = { + inputs: [], + reads: [], + outputs: stateSet1, + info: info1, + }; await expect( privacyGroup.transition( tx1ID, - { - inputs: [], - reads: [], - outputs: stateSet1, - info: info1, - }, + tx1, [], endorsements1 ) @@ -145,6 +146,16 @@ describe("PentePrivacyGroup", function () { .to.emit(privacyGroup, "PenteTransition") .withArgs(tx1ID, [], [], stateSet1, info1); + // Rejects duplicate + await expect( + privacyGroup.transition( + tx1ID, + tx1, + [], + endorsements1 + ) + ).to.be.rejectedWith("PenteDuplicateTransaction"); + const stateSet2 = [randBytes32(), randBytes32(), randBytes32()]; const inputs2 = [stateSet1[1]]; const reads2 = [stateSet1[0], stateSet1[2]];