Skip to content

Commit 60055ec

Browse files
committed
feat: implement TxIdUtils library for transaction ID computation and add tests
1 parent 2a03e4a commit 60055ec

File tree

4 files changed

+254
-16
lines changed

4 files changed

+254
-16
lines changed

src/core/Coordinator.sol

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {TxManagerBase} from "./TxManagerBase.sol";
66
import {TxAuthManagerBase} from "./TxAuthManagerBase.sol";
77
import {ICrossError} from "./ICrossError.sol";
88
import {ICrossEvent} from "./ICrossEvent.sol";
9+
import {TxIdUtils} from "./TxIdUtils.sol";
910

1011
import {
1112
QueryCoordinatorStateRequest,
@@ -15,6 +16,8 @@ import {
1516
import {MsgInitiateTx} from "../proto/cross/core/initiator/Initiator.sol";
1617

1718
abstract contract Coordinator is ICoordinator, TxAuthManagerBase, TxManagerBase, ICrossError, ICrossEvent {
19+
using TxIdUtils for MsgInitiateTx.Data;
20+
1821
function executeTx(MsgInitiateTx.Data calldata msg_) external override {
1922
uint64 rh = msg_.timeout_height.revision_height;
2023
if (rh != 0 && (block.number + 1 > uint256(rh))) {
@@ -25,15 +28,14 @@ abstract contract Coordinator is ICoordinator, TxAuthManagerBase, TxManagerBase,
2528
revert MessageTimeoutTimestamp(block.timestamp, msg_.timeout_timestamp);
2629
}
2730

28-
bytes32 txIDHash = sha256(MsgInitiateTx.encode(msg_));
29-
30-
if (!_isTxRecorded(txIDHash)) revert TxIDNotFound(txIDHash);
31+
bytes32 txID = msg_.computeTxId();
3132

32-
if (!_isCompletedAuth(txIDHash)) revert AuthNotCompleted(txIDHash);
33+
if (!_isTxRecorded(txID)) revert TxIDNotFound(txID);
3334

34-
_runTxIfCompleted(txIDHash, msg_);
35+
if (!_isCompletedAuth(txID)) revert AuthNotCompleted(txID);
36+
_runTxIfCompleted(txID, msg_);
3537

36-
emit TxExecuted(abi.encodePacked(txIDHash), msg.sender);
38+
emit TxExecuted(abi.encodePacked(txID), msg.sender);
3739
}
3840

3941
function coordinatorState(QueryCoordinatorStateRequest.Data calldata req)

src/core/Initiator.sol

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,19 @@ import {TxAuthManagerBase} from "./TxAuthManagerBase.sol";
66
import {TxManagerBase} from "./TxManagerBase.sol";
77
import {ICrossError} from "./ICrossError.sol";
88
import {ICrossEvent} from "./ICrossEvent.sol";
9-
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
9+
import {TxIdUtils} from "./TxIdUtils.sol";
1010

1111
import {MsgInitiateTx, MsgInitiateTxResponse, QuerySelfXCCResponse} from "../proto/cross/core/initiator/Initiator.sol";
1212
import {Account} from "../proto/cross/core/auth/Auth.sol";
1313
import {GoogleProtobufAny} from "@hyperledger-labs/yui-ibc-solidity/contracts/proto/GoogleProtobufAny.sol";
1414
import {ChannelInfo} from "../proto/cross/core/xcc/XCC.sol";
1515

1616
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
17+
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
1718

1819
abstract contract Initiator is IInitiator, TxAuthManagerBase, TxManagerBase, ReentrancyGuard, ICrossError, ICrossEvent {
20+
using TxIdUtils for MsgInitiateTx.Data;
21+
1922
bytes32 public immutable CHAIN_ID_HASH;
2023

2124
constructor() {
@@ -43,27 +46,27 @@ abstract contract Initiator is IInitiator, TxAuthManagerBase, TxManagerBase, Ree
4346
}
4447

4548
// generate txID
46-
bytes32 txIDHash = sha256(MsgInitiateTx.encode(msg_));
47-
bytes memory txID = abi.encodePacked(txIDHash);
48-
if (_isTxRecorded(txIDHash)) revert TxIDAlreadyExists(txIDHash);
49+
bytes32 txID = msg_.computeTxId();
50+
if (_isTxRecorded(txID)) revert TxIDAlreadyExists(txID);
4951

5052
// persist as PENDING
51-
_createTx(txIDHash, msg_);
53+
_createTx(txID, msg_);
5254

5355
// auth init & sign
5456
Account.Data[] memory required = _getRequiredAccounts(msg_);
55-
_initAuthState(txIDHash, required);
56-
bool completed = _sign(txIDHash, msg_.signers);
57+
_initAuthState(txID, required);
58+
bool completed = _sign(txID, msg_.signers);
5759

58-
emit TxInitiated(txID, msg.sender, msg_);
60+
bytes memory txIDBytes = abi.encodePacked(txID);
61+
emit TxInitiated(txIDBytes, msg.sender, msg_);
5962

6063
if (completed) {
6164
return MsgInitiateTxResponse.Data({
62-
txID: txID, status: MsgInitiateTxResponse.InitiateTxStatus.INITIATE_TX_STATUS_VERIFIED
65+
txID: txIDBytes, status: MsgInitiateTxResponse.InitiateTxStatus.INITIATE_TX_STATUS_VERIFIED
6366
});
6467
}
6568
return MsgInitiateTxResponse.Data({
66-
txID: txID, status: MsgInitiateTxResponse.InitiateTxStatus.INITIATE_TX_STATUS_PENDING
69+
txID: txIDBytes, status: MsgInitiateTxResponse.InitiateTxStatus.INITIATE_TX_STATUS_PENDING
6770
});
6871
}
6972

src/core/TxIDUtils.sol

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
pragma solidity ^0.8.20;
3+
4+
import {MsgInitiateTx} from "../proto/cross/core/initiator/Initiator.sol";
5+
6+
library TxIdUtils {
7+
function computeTxId(MsgInitiateTx.Data calldata msg_) internal pure returns (bytes32) {
8+
return sha256(MsgInitiateTx.encode(msg_));
9+
}
10+
}

test/TxIdUtils.t.sol

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// solhint-disable func-name-mixedcase, one-contract-per-file, gas-small-strings
3+
pragma solidity ^0.8.20;
4+
5+
import "forge-std/src/Test.sol";
6+
import "../src/core/TxIdUtils.sol";
7+
import {MsgInitiateTx, ContractTransaction, Link, ReturnValue} from "../src/proto/cross/core/initiator/Initiator.sol";
8+
import {GoogleProtobufAny as Any} from "@hyperledger-labs/yui-ibc-solidity/contracts/proto/GoogleProtobufAny.sol";
9+
import {AuthType, Account as AuthAccount} from "../src/proto/cross/core/auth/Auth.sol";
10+
import {IbcCoreClientV1Height} from "../src/proto/ibc/core/client/v1/client.sol";
11+
import {Tx} from "../src/proto/cross/core/tx/Tx.sol";
12+
13+
contract TxIdUtilsHarness {
14+
using TxIdUtils for MsgInitiateTx.Data;
15+
16+
function exposed_computeTxId(MsgInitiateTx.Data calldata msg_) external pure returns (bytes32) {
17+
return msg_.computeTxId();
18+
}
19+
}
20+
21+
contract TxIdUtilsTest is Test {
22+
TxIdUtilsHarness private harness;
23+
24+
function setUp() public {
25+
harness = new TxIdUtilsHarness();
26+
}
27+
28+
function test_computeTxId_SucceedsWithTypicalMsg() public view {
29+
MsgInitiateTx.Data memory msg_ = _createBaseMsg();
30+
bytes32 id = harness.exposed_computeTxId(msg_);
31+
assertNotEq(id, bytes32(0), "Computed TxID should not be zero");
32+
}
33+
34+
function test_computeTxId_SucceedsWithMinimalMsg() public view {
35+
MsgInitiateTx.Data memory msg_ = MsgInitiateTx.Data({
36+
chain_id: "",
37+
nonce: 0,
38+
commit_protocol: Tx.CommitProtocol.COMMIT_PROTOCOL_UNKNOWN,
39+
contract_transactions: new ContractTransaction.Data[](0),
40+
signers: new AuthAccount.Data[](0),
41+
timeout_height: IbcCoreClientV1Height.Data(0, 0),
42+
timeout_timestamp: 0
43+
});
44+
45+
bytes32 id = harness.exposed_computeTxId(msg_);
46+
assertNotEq(id, bytes32(0), "Minimal message should still produce a valid hash");
47+
}
48+
49+
function test_computeTxId_ReturnsConsistentId() public view {
50+
MsgInitiateTx.Data memory msg1 = _createBaseMsg();
51+
MsgInitiateTx.Data memory msg2 = _createBaseMsg();
52+
assertEq(harness.exposed_computeTxId(msg1), harness.exposed_computeTxId(msg2));
53+
}
54+
55+
function test_computeTxId_ReturnsDifferentIdForDifferentNonce() public view {
56+
MsgInitiateTx.Data memory m1 = _createBaseMsg();
57+
MsgInitiateTx.Data memory m2 = _createBaseMsg();
58+
m2.nonce = m1.nonce + 1;
59+
assertNotEq(harness.exposed_computeTxId(m1), harness.exposed_computeTxId(m2));
60+
}
61+
62+
function test_computeTxId_ReturnsDifferentIdForDifferentChainId() public view {
63+
MsgInitiateTx.Data memory m1 = _createBaseMsg();
64+
MsgInitiateTx.Data memory m2 = _createBaseMsg();
65+
m2.chain_id = "other-chain";
66+
assertNotEq(harness.exposed_computeTxId(m1), harness.exposed_computeTxId(m2));
67+
}
68+
69+
function test_computeTxId_ReturnsDifferentIdForDifferentCommitProtocol() public view {
70+
MsgInitiateTx.Data memory m1 = _createBaseMsg();
71+
MsgInitiateTx.Data memory m2 = _createBaseMsg();
72+
m2.commit_protocol = Tx.CommitProtocol.COMMIT_PROTOCOL_TPC;
73+
assertNotEq(harness.exposed_computeTxId(m1), harness.exposed_computeTxId(m2));
74+
}
75+
76+
function test_computeTxId_ReturnsDifferentIdForDifferentTimeoutHeight_RevisionNumber() public view {
77+
MsgInitiateTx.Data memory m1 = _createBaseMsg();
78+
MsgInitiateTx.Data memory m2 = _createBaseMsg();
79+
m2.timeout_height.revision_number = 1;
80+
assertNotEq(harness.exposed_computeTxId(m1), harness.exposed_computeTxId(m2));
81+
}
82+
83+
function test_computeTxId_ReturnsDifferentIdForDifferentTimeoutHeight_RevisionHeight() public view {
84+
MsgInitiateTx.Data memory m1 = _createBaseMsg();
85+
MsgInitiateTx.Data memory m2 = _createBaseMsg();
86+
m2.timeout_height.revision_height = 100;
87+
assertNotEq(harness.exposed_computeTxId(m1), harness.exposed_computeTxId(m2));
88+
}
89+
90+
function test_computeTxId_ReturnsDifferentIdForDifferentTimeoutTimestamp() public view {
91+
MsgInitiateTx.Data memory m1 = _createBaseMsg();
92+
MsgInitiateTx.Data memory m2 = _createBaseMsg();
93+
m2.timeout_timestamp = 123456789;
94+
assertNotEq(harness.exposed_computeTxId(m1), harness.exposed_computeTxId(m2));
95+
}
96+
97+
function test_computeTxId_ReturnsDifferentIdForDifferentRootSigners() public view {
98+
MsgInitiateTx.Data memory m1 = _createBaseMsg();
99+
MsgInitiateTx.Data memory m2 = _createBaseMsg();
100+
m2.signers = new AuthAccount.Data[](1);
101+
m2.signers[0] = _createAccount("signer1");
102+
assertNotEq(harness.exposed_computeTxId(m1), harness.exposed_computeTxId(m2));
103+
}
104+
105+
function test_computeTxId_ReturnsDifferentIdForDifferentLocalSigners() public view {
106+
MsgInitiateTx.Data memory m1 = _createBaseMsg();
107+
MsgInitiateTx.Data memory m2 = _createBaseMsg();
108+
m2.contract_transactions[0].signers = new AuthAccount.Data[](1);
109+
m2.contract_transactions[0].signers[0] = _createAccount("local-signer");
110+
assertNotEq(harness.exposed_computeTxId(m1), harness.exposed_computeTxId(m2));
111+
}
112+
113+
function test_computeTxId_ReturnsDifferentIdForDifferentTxArrayLength() public view {
114+
MsgInitiateTx.Data memory m1 = _createBaseMsg();
115+
MsgInitiateTx.Data memory m2 = _createBaseMsg();
116+
117+
m2.contract_transactions = new ContractTransaction.Data[](2);
118+
m2.contract_transactions[0] = m1.contract_transactions[0];
119+
m2.contract_transactions[1] = m1.contract_transactions[0];
120+
121+
assertNotEq(harness.exposed_computeTxId(m1), harness.exposed_computeTxId(m2));
122+
}
123+
124+
function test_computeTxId_ReturnsDifferentIdForDifferentCallInfo() public view {
125+
MsgInitiateTx.Data memory m1 = _createBaseMsg();
126+
MsgInitiateTx.Data memory m2 = _createBaseMsg();
127+
m2.contract_transactions[0].call_info = hex"deadbeef";
128+
assertNotEq(harness.exposed_computeTxId(m1), harness.exposed_computeTxId(m2));
129+
}
130+
131+
function test_computeTxId_ReturnsDifferentIdForDifferentChannelAnyValue() public view {
132+
MsgInitiateTx.Data memory m1 = _createBaseMsg();
133+
MsgInitiateTx.Data memory m2 = _createBaseMsg();
134+
m2.contract_transactions[0].cross_chain_channel.value = hex"010203";
135+
assertNotEq(harness.exposed_computeTxId(m1), harness.exposed_computeTxId(m2));
136+
}
137+
138+
function test_computeTxId_ReturnsDifferentIdForDifferentReturnValue() public view {
139+
MsgInitiateTx.Data memory m1 = _createBaseMsg();
140+
MsgInitiateTx.Data memory m2 = _createBaseMsg();
141+
m2.contract_transactions[0].return_value = ReturnValue.Data(hex"ffee");
142+
assertNotEq(harness.exposed_computeTxId(m1), harness.exposed_computeTxId(m2));
143+
}
144+
145+
function test_computeTxId_ReturnsDifferentIdForDifferentLinks() public view {
146+
MsgInitiateTx.Data memory m1 = _createBaseMsg();
147+
MsgInitiateTx.Data memory m2 = _createBaseMsg();
148+
m2.contract_transactions[0].links = new Link.Data[](1);
149+
m2.contract_transactions[0].links[0] = Link.Data(0);
150+
assertNotEq(harness.exposed_computeTxId(m1), harness.exposed_computeTxId(m2));
151+
}
152+
153+
function test_computeTxId_ReturnsDifferentIdForDifferentSignerAuthMode() public view {
154+
MsgInitiateTx.Data memory m1 = _createBaseMsg();
155+
m1.signers = new AuthAccount.Data[](1);
156+
m1.signers[0] = _createAccount("signer1");
157+
158+
MsgInitiateTx.Data memory m2 = _createBaseMsg();
159+
m2.signers = new AuthAccount.Data[](1);
160+
m2.signers[0] = _createAccount("signer1");
161+
m2.signers[0].auth_type.mode = AuthType.AuthMode.AUTH_MODE_EXTENSION;
162+
163+
assertNotEq(harness.exposed_computeTxId(m1), harness.exposed_computeTxId(m2));
164+
}
165+
166+
function test_computeTxId_ReturnsDifferentIdForDifferentSignerAuthOptionTypeUrl() public view {
167+
MsgInitiateTx.Data memory m1 = _createBaseMsg();
168+
m1.signers = new AuthAccount.Data[](1);
169+
m1.signers[0] = _createAccount("signer1");
170+
171+
MsgInitiateTx.Data memory m2 = _createBaseMsg();
172+
m2.signers = new AuthAccount.Data[](1);
173+
m2.signers[0] = _createAccount("signer1");
174+
m2.signers[0].auth_type.option.type_url = "/new.verifier.v1";
175+
176+
assertNotEq(harness.exposed_computeTxId(m1), harness.exposed_computeTxId(m2));
177+
}
178+
179+
function test_computeTxId_ReturnsDifferentIdForDifferentSignerAuthOptionValue() public view {
180+
MsgInitiateTx.Data memory m1 = _createBaseMsg();
181+
m1.signers = new AuthAccount.Data[](1);
182+
m1.signers[0] = _createAccount("signer1");
183+
184+
MsgInitiateTx.Data memory m2 = _createBaseMsg();
185+
m2.signers = new AuthAccount.Data[](1);
186+
m2.signers[0] = _createAccount("signer1");
187+
m2.signers[0].auth_type.option.value = hex"123456";
188+
189+
assertNotEq(harness.exposed_computeTxId(m1), harness.exposed_computeTxId(m2));
190+
}
191+
192+
// --- Helpers ---
193+
194+
function _createBaseMsg() internal pure returns (MsgInitiateTx.Data memory) {
195+
ContractTransaction.Data[] memory txs = new ContractTransaction.Data[](1);
196+
txs[0] = ContractTransaction.Data({
197+
cross_chain_channel: Any.Data("/type.url", hex"00"),
198+
signers: new AuthAccount.Data[](0),
199+
call_info: hex"abcd",
200+
return_value: ReturnValue.Data(""),
201+
links: new Link.Data[](0)
202+
});
203+
204+
return MsgInitiateTx.Data({
205+
chain_id: "test-chain",
206+
nonce: 1,
207+
commit_protocol: Tx.CommitProtocol.COMMIT_PROTOCOL_SIMPLE,
208+
contract_transactions: txs,
209+
signers: new AuthAccount.Data[](0),
210+
timeout_height: IbcCoreClientV1Height.Data(0, 0),
211+
timeout_timestamp: 0
212+
});
213+
}
214+
215+
function _createAccount(string memory id) internal pure returns (AuthAccount.Data memory) {
216+
return AuthAccount.Data({
217+
id: bytes(id),
218+
auth_type: AuthType.Data({
219+
mode: AuthType.AuthMode.AUTH_MODE_LOCAL, option: Any.Data({type_url: "", value: hex""})
220+
})
221+
});
222+
}
223+
}

0 commit comments

Comments
 (0)