Skip to content

Commit

Permalink
Add support to emit off-chain logs returned in the Suave Transaction (#…
Browse files Browse the repository at this point in the history
…56)

* Add tests

* Looks good

* More cleaning

* Rename modifier

* Commit Suapp contract

* Update forge

* Show suave-geth version on ci

* Do not use assertNotEq

* Forgot something
  • Loading branch information
ferranbt authored Mar 1, 2024
1 parent d6db188 commit aeff1df
Show file tree
Hide file tree
Showing 5 changed files with 279 additions and 1 deletion.
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ jobs:

- name: Run suave
run: |
suave-geth version
suave-geth --suave.dev --suave.eth.external-whitelist='*' &
- name: Install Foundry
Expand Down
2 changes: 1 addition & 1 deletion lib/forge-std
98 changes: 98 additions & 0 deletions src/Logs.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.13;

import "./suavelib/Suave.sol";

library Logs {
struct Log {
address addr;
bytes32[] topics;
bytes data;
}

bytes constant MAGIC_SEQUENCE = hex"543543";

function findStartIndex(bytes memory data) internal pure returns (uint256) {
for (uint256 i = 0; i < data.length; i++) {
if (data[i] == MAGIC_SEQUENCE[0]) {
// Check if current byte matches the first byte of MAGIC_SEQUENCE
bool isMatch = true;
for (uint256 j = 1; j < MAGIC_SEQUENCE.length; j++) {
// Check subsequent bytes
if (i + j >= data.length || data[i + j] != MAGIC_SEQUENCE[j]) {
isMatch = false;
break;
}
}
if (isMatch) {
return i + MAGIC_SEQUENCE.length; // Return the index after the magic sequence
}
}
}
return data.length; // Not found
}

function decodeLogs(bytes memory inputData) internal {
uint256 magicSequenceIndex = findStartIndex(inputData);
if (magicSequenceIndex == inputData.length) {
return; // Magic sequence not found, skip logs
}

// Calculate the length of the data to decode
uint256 dataLength = inputData.length - magicSequenceIndex;

// Initialize memory for the data to decode
bytes memory dataToDecode = new bytes(dataLength);

// Copy the data to decode into the memory array
for (uint256 i = 0; i < dataLength; i++) {
dataToDecode[i] = inputData[magicSequenceIndex + i];
}

(Log[] memory logs) = abi.decode(dataToDecode, (Log[]));
for (uint256 i = 0; i < logs.length; i++) {
emitLog(logs[i]);
}
}

function emitLog(Log memory log) internal {
bytes memory logData = log.data;
uint256 dataLength = logData.length;

if (log.topics.length == 0) {
assembly {
log0(add(logData, 32), dataLength)
}
} else if (log.topics.length == 1) {
bytes32 topic0 = log.topics[0];

assembly {
log1(add(logData, 32), dataLength, topic0)
}
} else if (log.topics.length == 2) {
bytes32 topic0 = log.topics[0];
bytes32 topic1 = log.topics[1];

assembly {
log2(add(logData, 32), dataLength, topic0, topic1)
}
} else if (log.topics.length == 3) {
bytes32 topic0 = log.topics[0];
bytes32 topic1 = log.topics[1];
bytes32 topic2 = log.topics[2];

assembly {
log3(add(logData, 32), dataLength, topic0, topic1, topic2)
}
} else if (log.topics.length == 4) {
bytes32 topic0 = log.topics[0];
bytes32 topic1 = log.topics[1];
bytes32 topic2 = log.topics[2];
bytes32 topic3 = log.topics[3];

assembly {
log4(add(logData, 32), dataLength, topic0, topic1, topic2, topic3)
}
}
}
}
11 changes: 11 additions & 0 deletions src/Suapp.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.8;

import "./Logs.sol";

contract Suapp {
modifier emitOffchainLogs() {
Logs.decodeLogs(msg.data);
_;
}
}
168 changes: 168 additions & 0 deletions test/Logs.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.13;

import "forge-std/Test.sol";
import "forge-std/console.sol";
import "forge-std/Vm.sol";
import "src/Logs.sol";
import "src/Suapp.sol";

contract TestLogs is Test {
// generated with suave-geth TestE2E_EmitLogs test
bytes constant encodedResultTestcase =
hex"0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000420000000000000000000000000030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000001dc013ea64717e828d19b2a2ee201871627a4a65b8e96984868b9391a327be18a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000023ab4eb7c630180e1144d0a956c71b3cb538ea0b9566b6136fe7a87cb222249d20000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000039e4d9541d8ffa37d1346e0a2ee0dd6cb444641c8458993777d3d06e9da3bd66600000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000004b1b8bb6d3c04a82bd66f1d7dbecf2754edafb96d47af76845f87c5245afaec2d00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003";

// getEmittedLog returns the emitted logs in Forge as a Log struct.
function getEmittedLogs() private returns (Logs.Log[] memory) {
VmSafe.Log[] memory logs = vm.getRecordedLogs();

Logs.Log[] memory logs11 = new Logs.Log[](logs.length);
for (uint256 i = 0; i < logs.length; i++) {
Logs.Log memory log;
log.addr = logs[i].emitter;
log.topics = logs[i].topics;
log.data = logs[i].data;

logs11[i] = log;
}

return logs11;
}

// equalLogs checks if two Log structs are equal.
function equalLogs(Logs.Log memory a, Logs.Log memory b) internal {
assertEq(a.addr, b.addr);
assertEq(a.topics.length, b.topics.length);
for (uint256 i = 0; i < a.topics.length; i++) {
assertEq(a.topics[i], b.topics[i]);
}
assertEq(a.data, b.data);
}

event EventAnonymous() anonymous;
event EventTopic1();
event EventTopic2(uint256 indexed num1, uint256 numNoIndex);
event EventTopic3(uint256 indexed num1, uint256 indexed num2, uint256 numNoIndex);
event EventTopic4(uint256 indexed num1, uint256 indexed num2, uint256 indexed num3, uint256 numNoIndex);
event EventWithMultipleArgs(uint256 indexed num1, uint256 num2, uint256 num3);

// testEmitLog tests that the logs emitted with 'emitLog' have the same
// values as the logs emitted with 'emit '.
function testExecResult_EmitLog() public {
vm.recordLogs();

emit EventAnonymous();
emit EventTopic1();
emit EventTopic2(1, 1);
emit EventTopic3(1, 2, 2);
emit EventTopic4(1, 2, 3, 4);
emit EventWithMultipleArgs(1, 2, 3);

// get all the emitted logs and send them again with
// the low-level emitLog function
Logs.Log[] memory logs = getEmittedLogs();

for (uint256 i = 0; i < logs.length; i++) {
Logs.emitLog(logs[i]);
}

// get the emitted logs from 'emitLog' and compare them with the
// original emitted logs
Logs.Log[] memory logs1 = getEmittedLogs();

for (uint256 i = 0; i < logs.length; i++) {
equalLogs(logs[i], logs1[i]);
}
}

function testExecResult_DecodeLogsFull() public {
// emit and record some logs to build the Result struct
vm.recordLogs();

emit EventAnonymous();
emit EventTopic1();
emit EventTopic2(1, 1);

Logs.Log[] memory logs = getEmittedLogs();
bytes memory encodedResult = abi.encode(logs);

// create some dummy prefix + MAGIC + encodedResult
bytes memory inputData = abi.encodePacked(hex"00112233445566", Logs.MAGIC_SEQUENCE, encodedResult);

Logs.decodeLogs(inputData);

// validate that the logs were emitted again
Logs.Log[] memory logs1 = getEmittedLogs();
for (uint256 i = 0; i < logs.length; i++) {
equalLogs(logs[i], logs1[i]);
}
}

function testExecResult_LogsTestcases() public {
// validate that we can parse the result testcase generated by suave-geth
(Logs.Log[] memory logs) = abi.decode(encodedResultTestcase, (Logs.Log[]));

assertEq(logs.length, 5);
assertEq(logs[0].topics.length, 0);
assertEq(logs[1].topics.length, 1);
assertEq(logs[2].topics.length, 2);
assertEq(logs[3].topics.length, 3);
assertEq(logs[4].topics.length, 4);

// 0 and 1 do not have data and 2.. have data
assertEq(logs[0].data.length, 0);
assertEq(logs[1].data.length, 0);
for (uint256 i = 2; i < logs.length; i++) {
if (logs[i].data.length == 0) {
revert("data is empty");
}
}

// all of them have the same address (0x030)
for (uint256 i = 0; i < logs.length; i++) {
assertEq(logs[i].addr, 0x0300000000000000000000000000000000000000);
}

// validate that 'decodeLogs' works too
bytes memory inputData = abi.encodePacked(hex"00112233445566", Logs.MAGIC_SEQUENCE, encodedResultTestcase);

vm.recordLogs();
Logs.decodeLogs(inputData);

Logs.Log[] memory foundLogs = getEmittedLogs();
assertEq(foundLogs.length, logs.length);
}

// testFindStartIndex tests that we can detect when the logs suffix starts
function testExecResult_FindStartIndex() public {
bytes memory noMagic = hex"00112233445566";
uint256 index = Logs.findStartIndex(noMagic);
assertEq(index, noMagic.length);

bytes memory withMagic = abi.encodePacked(noMagic, Logs.MAGIC_SEQUENCE, hex"778899aabbcc");

index = Logs.findStartIndex(withMagic);
assertEq(index, noMagic.length + Logs.MAGIC_SEQUENCE.length);
}

// testExecResult_Modifier tests that the Suapp modifier works
function testExecResult_Modifier() public {
SuappExample suapp = new SuappExample();

// call suapp.testEmit with empty input + the magic sequence + logs
bytes memory callSig = abi.encodeWithSelector(SuappExample.testEmit.selector);
bytes memory inputData = abi.encodePacked(callSig, Logs.MAGIC_SEQUENCE, encodedResultTestcase);

// call the function
vm.recordLogs();
(bool success,) = address(suapp).call(inputData);
assertEq(success, true);

Logs.Log[] memory foundLogs = getEmittedLogs();
assertEq(foundLogs.length, 5);
}
}

contract SuappExample is Suapp {
function testEmit() public emitOffchainLogs {}
}

0 comments on commit aeff1df

Please sign in to comment.