Skip to content

Commit eace9b4

Browse files
authored
Merge pull request #17 from oasisprotocol/rube/15-evaluate-if-for-proofhelpersts-ethersutilsrlp-may-be-better-suited
feat: implement RLP encoding for mock receipt proofs with ethers
2 parents b74141f + 80ccfcf commit eace9b4

File tree

2 files changed

+147
-70
lines changed

2 files changed

+147
-70
lines changed

contracts/test/helpers/proofHelpers.ts

Lines changed: 11 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { keccak256, AbiCoder, zeroPadValue, toBeHex } from "ethers";
1+
import { keccak256, AbiCoder, zeroPadValue, toBeHex, encodeRlp, getBytes } from "ethers";
22

33
/**
4-
* Helper functions for creating mock receipt proofs
4+
* Helper functions for creating mock receipt proofs.
5+
* Uses ethers v6's native RLP encoding for correctness and maintainability.
56
*/
67

78
// PaymentInitiated event signature
@@ -65,7 +66,8 @@ export function createMockReceiptProof(params: {
6566
}
6667

6768
/**
68-
* Encodes a mock log entry in RLP format
69+
* Encodes a mock log entry in RLP format.
70+
* RLP log structure: [address, topics[], data]
6971
* @param vault Vault address
7072
* @param payer Payer address
7173
* @param recipient Recipient address
@@ -95,78 +97,17 @@ function encodeMockLog(
9597
const data = abiCoder.encode(["uint256", "bytes32"], [amount, paymentId]);
9698

9799
// Encode as RLP: [address, topics[], data]
98-
// For simplicity, we'll use a basic hex encoding that mimics RLP structure
99-
// In a real implementation, you'd use a proper RLP library
100-
101-
// This is a simplified version - in production tests, you'd use actual RLP encoding
102-
const addressRlp = encodeRlpBytes(vault);
103-
const topicsRlp = encodeRlpList(topics.map((t) => encodeRlpBytes(t)));
104-
const dataRlp = encodeRlpBytes(data);
105-
106-
return encodeRlpList([addressRlp, topicsRlp, dataRlp]);
107-
}
108-
109-
/**
110-
* Simple RLP encoding for bytes (simplified - use rlp library for production)
111-
*/
112-
function encodeRlpBytes(data: string): string {
113-
// Remove 0x prefix if present
114-
const hex = data.startsWith("0x") ? data.slice(2) : data;
115-
const bytes = Buffer.from(hex, "hex");
116-
117-
if (bytes.length === 1 && bytes[0] < 0x80) {
118-
return "0x" + hex;
119-
} else if (bytes.length < 56) {
120-
const prefix = (0x80 + bytes.length).toString(16).padStart(2, "0");
121-
return "0x" + prefix + hex;
122-
} else {
123-
const lengthHex = bytes.length.toString(16);
124-
const lengthOfLength = Math.ceil(lengthHex.length / 2);
125-
const prefix = (0xb7 + lengthOfLength).toString(16).padStart(2, "0");
126-
return "0x" + prefix + lengthHex.padStart(lengthOfLength * 2, "0") + hex;
127-
}
100+
// ethers.encodeRlp handles nested arrays and bytes correctly
101+
return encodeRlp([getBytes(vault), topics.map((t) => getBytes(t)), getBytes(data)]);
128102
}
129103

130104
/**
131-
* Simple RLP encoding for lists (simplified - use rlp library for production)
132-
*/
133-
function encodeRlpList(items: string[]): string {
134-
const concatenated = items.map((item) => item.slice(2)).join("");
135-
const totalLength = concatenated.length / 2;
136-
137-
if (totalLength < 56) {
138-
const prefix = (0xc0 + totalLength).toString(16).padStart(2, "0");
139-
return "0x" + prefix + concatenated;
140-
} else {
141-
const lengthHex = totalLength.toString(16);
142-
const lengthOfLength = Math.ceil(lengthHex.length / 2);
143-
const prefix = (0xf7 + lengthOfLength).toString(16).padStart(2, "0");
144-
return "0x" + prefix + lengthHex.padStart(lengthOfLength * 2, "0") + concatenated;
145-
}
146-
}
147-
148-
/**
149-
* Encodes a uint as RLP (simplified)
105+
* Encodes a uint as RLP using ethers native encoding.
106+
* RLP encodes 0 as 0x80 (empty byte string), otherwise as minimal byte representation.
150107
*/
151108
function encodeRlpUint(value: number): string {
152-
if (value === 0) return "0x80";
153-
154-
let hex = value.toString(16);
155-
if (hex.length % 2 !== 0) hex = "0" + hex;
156-
157-
const bytes = Buffer.from(hex, "hex");
158-
159-
if (bytes.length === 1 && bytes[0] < 0x80) {
160-
return "0x" + hex;
161-
} else if (bytes.length < 56) {
162-
const prefix = (0x80 + bytes.length).toString(16).padStart(2, "0");
163-
return "0x" + prefix + hex;
164-
} else {
165-
const lengthHex = bytes.length.toString(16);
166-
const lengthOfLength = Math.ceil(lengthHex.length / 2);
167-
const prefix = (0xb7 + lengthOfLength).toString(16).padStart(2, "0");
168-
return "0x" + prefix + lengthHex.padStart(lengthOfLength * 2, "0") + hex;
169-
}
109+
if (value === 0) return encodeRlp(new Uint8Array(0));
110+
return encodeRlp(getBytes(toBeHex(value)));
170111
}
171112

172113
/**
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import { expect } from "chai";
2+
import { AbiCoder, decodeRlp, hexlify, zeroPadValue } from "ethers";
3+
import {
4+
createMockReceiptProof,
5+
calculatePaymentId,
6+
PAYMENT_INITIATED_TOPIC,
7+
} from "../helpers/proofHelpers";
8+
9+
describe("proofHelpers", function () {
10+
describe("createMockReceiptProof", function () {
11+
const mockParams = {
12+
chainId: 1n,
13+
blockNumber: 100n,
14+
transactionIndex: 5,
15+
logIndex: 0,
16+
vault: "0x1234567890123456789012345678901234567890",
17+
payer: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
18+
recipient: "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
19+
token: "0xcccccccccccccccccccccccccccccccccccccccc",
20+
amount: 1000000n,
21+
paymentId: "0x" + "00".repeat(32),
22+
};
23+
24+
it("should return valid proof structure", function () {
25+
const proof = createMockReceiptProof(mockParams);
26+
27+
expect(proof.chainId).to.equal(mockParams.chainId);
28+
expect(proof.blockNumber).to.equal(mockParams.blockNumber);
29+
expect(proof.blockHeader).to.equal("0x");
30+
expect(proof.ancestry).to.deep.equal([]);
31+
expect(proof.logIndex).to.equal(mockParams.logIndex);
32+
});
33+
34+
it("should encode transactionIndex as RLP", function () {
35+
const proof = createMockReceiptProof(mockParams);
36+
37+
// transactionIndex = 5, which is 0x05, single byte < 0x80
38+
expect(proof.transactionIndex).to.equal("0x05");
39+
});
40+
41+
it("should encode transactionIndex 0 as 0x80", function () {
42+
const proof = createMockReceiptProof({ ...mockParams, transactionIndex: 0 });
43+
expect(proof.transactionIndex).to.equal("0x80");
44+
});
45+
46+
it("should produce RLP-decodable log", function () {
47+
const proof = createMockReceiptProof(mockParams);
48+
49+
// Should be decodable without throwing
50+
const decoded = decodeRlp(proof.log);
51+
expect(Array.isArray(decoded)).to.be.true;
52+
expect(decoded.length).to.equal(3); // [address, topics[], data]
53+
});
54+
55+
it("should encode log address correctly", function () {
56+
const proof = createMockReceiptProof(mockParams);
57+
const decoded = decodeRlp(proof.log) as string[];
58+
59+
// First element is the vault address
60+
expect(hexlify(decoded[0]).toLowerCase()).to.equal(mockParams.vault.toLowerCase());
61+
});
62+
63+
it("should include correct number of topics", function () {
64+
const proof = createMockReceiptProof(mockParams);
65+
const decoded = decodeRlp(proof.log) as unknown[];
66+
67+
// Topics: [event_sig, payer, recipient, token]
68+
expect((decoded[1] as unknown[]).length).to.equal(4);
69+
});
70+
71+
it("should encode topics and data payload correctly", function () {
72+
const proof = createMockReceiptProof(mockParams);
73+
const decoded = decodeRlp(proof.log) as unknown[];
74+
const topics = decoded[1] as unknown[];
75+
76+
expect(hexlify(topics[0]).toLowerCase()).to.equal(PAYMENT_INITIATED_TOPIC.toLowerCase());
77+
expect(hexlify(topics[1]).toLowerCase()).to.equal(
78+
zeroPadValue(mockParams.payer, 32).toLowerCase()
79+
);
80+
expect(hexlify(topics[2]).toLowerCase()).to.equal(
81+
zeroPadValue(mockParams.recipient, 32).toLowerCase()
82+
);
83+
expect(hexlify(topics[3]).toLowerCase()).to.equal(
84+
zeroPadValue(mockParams.token, 32).toLowerCase()
85+
);
86+
87+
const [amount, paymentId] = AbiCoder.defaultAbiCoder().decode(
88+
["uint256", "bytes32"],
89+
decoded[2]
90+
);
91+
expect(amount).to.equal(mockParams.amount);
92+
expect(paymentId).to.equal(mockParams.paymentId);
93+
});
94+
});
95+
96+
describe("calculatePaymentId", function () {
97+
it("should return consistent payment ID", function () {
98+
const params = {
99+
chainId: 1n,
100+
vault: "0x1234567890123456789012345678901234567890",
101+
blockNumber: 100n,
102+
transactionIndex: 5,
103+
logIndex: 0,
104+
};
105+
106+
const id1 = calculatePaymentId(params);
107+
const id2 = calculatePaymentId(params);
108+
109+
expect(id1).to.equal(id2);
110+
expect(id1).to.have.length(66); // 0x + 64 hex chars
111+
});
112+
113+
it("should return different IDs for different inputs", function () {
114+
const params1 = {
115+
chainId: 1n,
116+
vault: "0x1234567890123456789012345678901234567890",
117+
blockNumber: 100n,
118+
transactionIndex: 5,
119+
logIndex: 0,
120+
};
121+
const params2 = { ...params1, blockNumber: 101n };
122+
123+
const id1 = calculatePaymentId(params1);
124+
const id2 = calculatePaymentId(params2);
125+
126+
expect(id1).to.not.equal(id2);
127+
});
128+
});
129+
130+
describe("PAYMENT_INITIATED_TOPIC", function () {
131+
it("should be a valid 32-byte keccak256 hash", function () {
132+
expect(PAYMENT_INITIATED_TOPIC).to.have.length(66); // 0x + 64 hex chars
133+
expect(PAYMENT_INITIATED_TOPIC).to.match(/^0x[a-f0-9]{64}$/i);
134+
});
135+
});
136+
});

0 commit comments

Comments
 (0)