From eefdf75bb92a6e5ce86c6fc6a9e6a8fa665f548b Mon Sep 17 00:00:00 2001 From: Bruce Riley Date: Mon, 9 Dec 2024 15:11:25 -0600 Subject: [PATCH] evm: Add AdapterInstructions --- evm/src/libraries/AdapterInstructions.sol | 135 ++++++++++++++++++++++ evm/test/AdapterInstructions.t.sol | 131 +++++++++++++++++++++ evm/test/AdapterRegistry.t.sol | 2 +- 3 files changed, 267 insertions(+), 1 deletion(-) create mode 100644 evm/src/libraries/AdapterInstructions.sol create mode 100644 evm/test/AdapterInstructions.t.sol diff --git a/evm/src/libraries/AdapterInstructions.sol b/evm/src/libraries/AdapterInstructions.sol new file mode 100644 index 0000000..f8a5ae0 --- /dev/null +++ b/evm/src/libraries/AdapterInstructions.sol @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: Apache 2 +pragma solidity >=0.8.8 <0.9.0; + +import "wormhole-solidity-sdk/libraries/BytesParsing.sol"; + +library AdapterInstructions { + using BytesParsing for bytes; + + /// @notice Error thrown when there are too many instructions in the array. + /// @dev Selector 0x3c46992e. + error TooManyInstructions(); + + /// @notice Error thrown when the payload length exceeds the allowed maximum. + /// @dev Selector 0xa3419691. + /// @param size The size of the payload. + error PayloadTooLong(uint256 size); + + /// @notice Error thrown when an Adapter instruction index + /// is greater than the number of registered Adapters + /// @dev We index from 0 so if providedIndex == numAdapters then we're out-of-bounds too + /// @dev Selector 0x689f5016. + /// @param providedIndex The index specified in the instruction + /// @param numAdapters The number of registered Adapters + error InvalidInstructionIndex(uint256 providedIndex, uint256 numAdapters); + + /// @dev Variable-length Adapter-specific instruction that can be passed by the integrator to the endpoint + /// and by the endpoint to the adapter. + /// The index field refers to the index of the adapter that this instruction should be passed to. + /// The serialization format is: + /// - index - 1 byte + /// - payloadLength - 2 bytes + /// - payload - `payloadLength` bytes + struct AdapterInstruction { + uint8 index; + bytes payload; + } + + /// @notice Encodes an adapter instruction. + /// @param instruction The instruction to be encoded. + /// @return encoded The encoded bytes, where the first byte is the index and the next two bytes are the instruction length. + function encodeInstruction(AdapterInstruction memory instruction) public pure returns (bytes memory encoded) { + if (instruction.payload.length > type(uint16).max) { + revert PayloadTooLong(instruction.payload.length); + } + uint16 payloadLength = uint16(instruction.payload.length); + encoded = abi.encodePacked(instruction.index, payloadLength, instruction.payload); + } + + /// @notice Encodes an array of adapter instructions. + /// @param instructions The array of instructions to be encoded. + /// @return address The encoded bytes, where the first byte is the number of entries. + function encodeInstructions(AdapterInstruction[] memory instructions) public pure returns (bytes memory) { + if (instructions.length > type(uint8).max) { + revert TooManyInstructions(); + } + uint256 instructionsLength = instructions.length; + + bytes memory encoded; + for (uint256 i = 0; i < instructionsLength;) { + bytes memory innerEncoded = encodeInstruction(instructions[i]); + encoded = bytes.concat(encoded, innerEncoded); + unchecked { + ++i; + } + } + return abi.encodePacked(uint8(instructionsLength), encoded); + } + + /// @notice Parses a byte array into an adapter instruction. + /// @param encoded The encoded instruction. + /// @return instruction The parsed instruction. + function parseInstruction(bytes memory encoded) public pure returns (AdapterInstruction memory instruction) { + uint256 offset = 0; + (instruction, offset) = parseInstructionUnchecked(encoded, offset); + encoded.checkLength(offset); + } + + /// @notice Parses a byte array into an adapter instruction without checking for leftover bytes. + /// @param encoded The buffer being parsed. + /// @param offset The current offset into the encoded buffer. + /// @return instruction The parsed instruction. + /// @return nextOffset The next index into the array (used for further parsing). + function parseInstructionUnchecked(bytes memory encoded, uint256 offset) + public + pure + returns (AdapterInstruction memory instruction, uint256 nextOffset) + { + (instruction.index, nextOffset) = encoded.asUint8Unchecked(offset); + uint16 instructionLength; + (instructionLength, nextOffset) = encoded.asUint16Unchecked(nextOffset); + (instruction.payload, nextOffset) = encoded.sliceUnchecked(nextOffset, instructionLength); + } + + /// @notice Parses a byte array into an array of adapter instructions. + /// @param encoded The encoded instructions. + /// @param numRegisteredAdapters The total number of registered adapters. + /// @return instructions A sparse array of adapter instructions, where the index into the array is the adapter index. + function parseInstructions(bytes memory encoded, uint256 numRegisteredAdapters) + public + pure + returns (AdapterInstruction[] memory instructions) + { + // We allocate an array with the length of the number of registered Adapters + // This gives us the flexibility to not have to pass instructions for Adapters that + // don't need them. + instructions = new AdapterInstruction[](numRegisteredAdapters); + + if (encoded.length == 0) { + return instructions; + } + + uint256 offset = 0; + uint256 instructionsLength; + (instructionsLength, offset) = encoded.asUint8Unchecked(offset); + + for (uint256 i = 0; i < instructionsLength;) { + AdapterInstruction memory instruction; + (instruction, offset) = parseInstructionUnchecked(encoded, offset); + + uint8 instructionIndex = instruction.index; + + // Instruction index is out of bounds + if (instructionIndex >= numRegisteredAdapters) { + revert InvalidInstructionIndex(instructionIndex, numRegisteredAdapters); + } + + instructions[instructionIndex] = instruction; + unchecked { + ++i; + } + } + + encoded.checkLength(offset); + } +} diff --git a/evm/test/AdapterInstructions.t.sol b/evm/test/AdapterInstructions.t.sol new file mode 100644 index 0000000..902b444 --- /dev/null +++ b/evm/test/AdapterInstructions.t.sol @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.13; + +import {Test, console} from "forge-std/Test.sol"; +import "../src/libraries/AdapterInstructions.sol"; + +contract AdapterInstructionsTest is Test { + function setUp() public {} + + function test_encodeInstruction() public { + // Success case. + bytes memory payload = "The payload"; + bytes memory expected = abi.encodePacked(uint8(0), uint16(payload.length), payload); + AdapterInstructions.AdapterInstruction memory inst = AdapterInstructions.AdapterInstruction(0, payload); + bytes memory encoded = AdapterInstructions.encodeInstruction(inst); + assertEq(keccak256(expected), keccak256(encoded)); + + // Payload too long. + inst = AdapterInstructions.AdapterInstruction(0, new bytes(65537)); + vm.expectRevert(abi.encodeWithSelector(AdapterInstructions.PayloadTooLong.selector, 65537)); + AdapterInstructions.encodeInstruction(inst); + } + + function test_encodeInstructions() public { + // Success case. + bytes memory expected = abi.encodePacked( + uint8(3), + uint8(0), + uint16(29), + "Instructions for adapter zero", + uint8(3), + uint16(30), + "Instructions for adapter three", + uint8(2), + uint16(28), + "Instructions for adapter two" + ); + + AdapterInstructions.AdapterInstruction[] memory insts = new AdapterInstructions.AdapterInstruction[](3); + insts[0] = AdapterInstructions.AdapterInstruction(0, "Instructions for adapter zero"); + insts[1] = AdapterInstructions.AdapterInstruction(3, "Instructions for adapter three"); + insts[2] = AdapterInstructions.AdapterInstruction(2, "Instructions for adapter two"); + bytes memory encoded = AdapterInstructions.encodeInstructions(insts); + assertEq(keccak256(expected), keccak256(encoded)); + + // Too many instructions should revert. + insts = new AdapterInstructions.AdapterInstruction[](257); + for (uint256 idx = 0; idx < 257; ++idx) { + insts[idx] = AdapterInstructions.AdapterInstruction(uint8(idx), "Some instruction"); + } + vm.expectRevert(abi.encodeWithSelector(AdapterInstructions.TooManyInstructions.selector)); + encoded = AdapterInstructions.encodeInstructions(insts); + + // Should be able to encode an empty array. + insts = new AdapterInstructions.AdapterInstruction[](0); + encoded = AdapterInstructions.encodeInstructions(insts); + assertEq(1, encoded.length); + assertEq(0, encoded[0]); + } + + function test_parseInstruction() public pure { + AdapterInstructions.AdapterInstruction memory expected = + AdapterInstructions.AdapterInstruction(0, "The payload"); + bytes memory encoded = AdapterInstructions.encodeInstruction(expected); + AdapterInstructions.AdapterInstruction memory inst = AdapterInstructions.parseInstruction(encoded); + assertEq(expected.index, inst.index); + assertEq(keccak256(expected.payload), keccak256(inst.payload)); + } + + // We need this to make the coverage tool happy, even though this function was called in the previous test. + function test_parseInstructionUnchecked() public pure { + AdapterInstructions.AdapterInstruction memory expected = + AdapterInstructions.AdapterInstruction(0, "The payload"); + bytes memory encoded = AdapterInstructions.encodeInstruction(expected); + (AdapterInstructions.AdapterInstruction memory inst, uint256 nextOffset) = + AdapterInstructions.parseInstructionUnchecked(encoded, 0); + assertEq(expected.index, inst.index); + assertEq(keccak256(expected.payload), keccak256(inst.payload)); + assertEq(encoded.length, nextOffset); + } + + function test_parseInstructions() public { + // Success case. + bytes memory expectedInst0 = "Instructions for adapter zero"; + bytes memory expectedInst2 = "Instructions for adapter two"; + bytes memory expectedInst3 = "Instructions for adapter three"; + AdapterInstructions.AdapterInstruction[] memory expected = new AdapterInstructions.AdapterInstruction[](3); + expected[0] = AdapterInstructions.AdapterInstruction(0, expectedInst0); + expected[1] = AdapterInstructions.AdapterInstruction(3, expectedInst3); + expected[2] = AdapterInstructions.AdapterInstruction(2, expectedInst2); + bytes memory encoded = AdapterInstructions.encodeInstructions(expected); + + AdapterInstructions.AdapterInstruction[] memory insts = AdapterInstructions.parseInstructions(encoded, 4); + assertEq(4, insts.length); + + assertEq(0, insts[0].index); + assertEq(keccak256(expectedInst0), keccak256(insts[0].payload)); + + // Entry one should be empty. + assertEq(0, insts[1].index); + assertEq(0, insts[1].payload.length); + + assertEq(2, insts[2].index); + assertEq(keccak256(expectedInst2), keccak256(insts[2].payload)); + + assertEq(3, insts[3].index); + assertEq(keccak256(expectedInst3), keccak256(insts[3].payload)); + + // Index out of range should revert. + vm.expectRevert(abi.encodeWithSelector(AdapterInstructions.InvalidInstructionIndex.selector, 3, 3)); + AdapterInstructions.parseInstructions(encoded, 3); + + // Should be able to parse an encoded empty array. + insts = new AdapterInstructions.AdapterInstruction[](0); + encoded = AdapterInstructions.encodeInstructions(insts); + insts = AdapterInstructions.parseInstructions(encoded, 4); + assertEq(4, insts.length); + for (uint256 i = 0; i < 4; ++i) { + assertEq(0, insts[i].index); + assertEq(0, insts[i].payload.length); + } + + // Should be able to parse a *really* empty array. + insts = AdapterInstructions.parseInstructions(new bytes(0), 4); + assertEq(4, insts.length); + for (uint256 i = 0; i < 4; ++i) { + assertEq(0, insts[i].index); + assertEq(0, insts[i].payload.length); + } + } +} diff --git a/evm/test/AdapterRegistry.t.sol b/evm/test/AdapterRegistry.t.sol index 54e430b..cd878d2 100644 --- a/evm/test/AdapterRegistry.t.sol +++ b/evm/test/AdapterRegistry.t.sol @@ -166,7 +166,7 @@ contract AdapterRegistryTest is Test { assertEq(adapterRegistry.getEnabledRecvAdaptersBitmapForChain(integrator1, zeroChain), 0); } - // This is a redudant test, as the previous tests already cover this + // This is a redundant test, as the previous tests already cover this function test5() public view { // Send side assertEq(adapterRegistry.getRegisteredAdaptersStorage(integrator1).length, 0);