[eth] - optimize ReceiverMessages parseAndVerifyVM (#901)

* feat(eth): optimize ReceiverMessages parseAndVerifyVM

* test(eth): update test setups to use wormholeReceiver

* chore(eth): remove console logging

* feat(eth): optimize & revert return type for parseAndVerifyVM

* fix(eth): add index boundary checks

* perf(eth): optimize verifySignature by passing in primitives instead of structs

* test(eth): add wormhole tests related to guardian set validity

* test(eth): add more parseAndVerify failure test cases

* test(eth): add more failure tests for parseAndVerify

* test(eth): add empty forge test, refactor/deduplicate
This commit is contained in:
swimricky 2023-06-22 14:11:54 -04:00 committed by GitHub
parent d07cc9d1ea
commit 919f71e68f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 657 additions and 59 deletions

View File

@ -20,6 +20,13 @@ library UnsafeCalldataBytesLib {
return _bytes[_start:_start + _length]; return _bytes[_start:_start + _length];
} }
function sliceFrom(
bytes calldata _bytes,
uint256 _start
) internal pure returns (bytes calldata) {
return _bytes[_start:_bytes.length];
}
function toAddress( function toAddress(
bytes calldata _bytes, bytes calldata _bytes,
uint256 _start uint256 _start

View File

@ -75,7 +75,8 @@ abstract contract Pyth is
for (uint i = 0; i < updateData.length; ) { for (uint i = 0; i < updateData.length; ) {
if ( if (
updateData[i].length > 4 && updateData[i].length > 4 &&
UnsafeBytesLib.toUint32(updateData[i], 0) == ACCUMULATOR_MAGIC UnsafeCalldataBytesLib.toUint32(updateData[i], 0) ==
ACCUMULATOR_MAGIC
) { ) {
totalNumUpdates += updatePriceInfosFromAccumulatorUpdate( totalNumUpdates += updatePriceInfosFromAccumulatorUpdate(
updateData[i] updateData[i]
@ -143,7 +144,6 @@ abstract contract Pyth is
// operations have proper require. // operations have proper require.
unchecked { unchecked {
bytes memory encoded = vm.payload; bytes memory encoded = vm.payload;
( (
uint index, uint index,
uint nAttestations, uint nAttestations,

View File

@ -31,7 +31,7 @@ abstract contract PythAccumulator is PythGetters, PythSetters, AbstractPyth {
// This method is also used by batch attestation but moved here // This method is also used by batch attestation but moved here
// as the batch attestation will deprecate soon. // as the batch attestation will deprecate soon.
function parseAndVerifyPythVM( function parseAndVerifyPythVM(
bytes memory encodedVm bytes calldata encodedVm
) internal view returns (IWormhole.VM memory vm) { ) internal view returns (IWormhole.VM memory vm) {
{ {
bool valid; bool valid;
@ -152,7 +152,6 @@ abstract contract PythAccumulator is PythGetters, PythSetters, AbstractPyth {
// TODO: Do we need to emit an update for accumulator update? If so what should we emit? // TODO: Do we need to emit an update for accumulator update? If so what should we emit?
// emit AccumulatorUpdate(vm.chainId, vm.sequence); // emit AccumulatorUpdate(vm.chainId, vm.sequence);
encodedPayload = vm.payload; encodedPayload = vm.payload;
} }
@ -200,16 +199,19 @@ abstract contract PythAccumulator is PythGetters, PythSetters, AbstractPyth {
} }
function parseWormholeMerkleHeaderNumUpdates( function parseWormholeMerkleHeaderNumUpdates(
bytes memory wormholeMerkleUpdate, bytes calldata wormholeMerkleUpdate,
uint offset uint offset
) internal pure returns (uint8 numUpdates) { ) internal pure returns (uint8 numUpdates) {
uint16 whProofSize = UnsafeBytesLib.toUint16( uint16 whProofSize = UnsafeCalldataBytesLib.toUint16(
wormholeMerkleUpdate, wormholeMerkleUpdate,
offset offset
); );
offset += 2; offset += 2;
offset += whProofSize; offset += whProofSize;
numUpdates = UnsafeBytesLib.toUint8(wormholeMerkleUpdate, offset); numUpdates = UnsafeCalldataBytesLib.toUint8(
wormholeMerkleUpdate,
offset
);
} }
function extractPriceInfoFromMerkleProof( function extractPriceInfoFromMerkleProof(

View File

@ -7,11 +7,17 @@ pragma experimental ABIEncoderV2;
import "./ReceiverGetters.sol"; import "./ReceiverGetters.sol";
import "./ReceiverStructs.sol"; import "./ReceiverStructs.sol";
import "../libraries/external/BytesLib.sol"; import "../libraries/external/BytesLib.sol";
import "../libraries/external/UnsafeCalldataBytesLib.sol";
error VmVersionIncompatible();
error SignatureIndexesNotAscending();
contract ReceiverMessages is ReceiverGetters { contract ReceiverMessages is ReceiverGetters {
using BytesLib for bytes; using BytesLib for bytes;
/// @dev parseAndVerifyVM serves to parse an encodedVM and wholy validate it for consumption /// @dev parseAndVerifyVM serves to parse an encodedVM and wholy validate it for consumption
/// WARNING: it intentionally sets vm.signatures to an empty array since it is not needed after it is validated in this function
/// since it not used anywhere. If you need to use vm.signatures, use parseVM and verifyVM separately.
function parseAndVerifyVM( function parseAndVerifyVM(
bytes calldata encodedVM bytes calldata encodedVM
) )
@ -19,8 +25,161 @@ contract ReceiverMessages is ReceiverGetters {
view view
returns (ReceiverStructs.VM memory vm, bool valid, string memory reason) returns (ReceiverStructs.VM memory vm, bool valid, string memory reason)
{ {
vm = parseVM(encodedVM); uint index = 0;
(valid, reason) = verifyVM(vm); unchecked {
{
vm.version = UnsafeCalldataBytesLib.toUint8(encodedVM, index);
index += 1;
if (vm.version != 1) {
revert VmVersionIncompatible();
}
}
ReceiverStructs.GuardianSet memory guardianSet;
{
vm.guardianSetIndex = UnsafeCalldataBytesLib.toUint32(
encodedVM,
index
);
index += 4;
guardianSet = getGuardianSet(vm.guardianSetIndex);
/**
* @dev Checks whether the guardianSet has zero keys
* WARNING: This keys check is critical to ensure the guardianSet has keys present AND to ensure
* that guardianSet key size doesn't fall to zero and negatively impact quorum assessment. If guardianSet
* key length is 0 and vm.signatures length is 0, this could compromise the integrity of both vm and
* signature verification.
*/
if (guardianSet.keys.length == 0) {
return (vm, false, "invalid guardian set");
}
/// @dev Checks if VM guardian set index matches the current index (unless the current set is expired).
if (
vm.guardianSetIndex != getCurrentGuardianSetIndex() &&
guardianSet.expirationTime < block.timestamp
) {
return (vm, false, "guardian set has expired");
}
}
// Parse Signatures
uint256 signersLen = UnsafeCalldataBytesLib.toUint8(
encodedVM,
index
);
index += 1;
{
// 66 is the length of each signature
// 1 (guardianIndex) + 32 (r) + 32 (s) + 1 (v)
uint hashIndex = index + (signersLen * 66);
if (hashIndex > encodedVM.length) {
return (vm, false, "invalid signature length");
}
// Hash the body
vm.hash = keccak256(
abi.encodePacked(
keccak256(
UnsafeCalldataBytesLib.sliceFrom(
encodedVM,
hashIndex
)
)
)
);
}
{
uint8 lastIndex = 0;
for (uint i = 0; i < signersLen; i++) {
ReceiverStructs.Signature memory sig;
sig.guardianIndex = UnsafeCalldataBytesLib.toUint8(
encodedVM,
index
);
index += 1;
sig.r = UnsafeCalldataBytesLib.toBytes32(encodedVM, index);
index += 32;
sig.s = UnsafeCalldataBytesLib.toBytes32(encodedVM, index);
index += 32;
sig.v =
UnsafeCalldataBytesLib.toUint8(encodedVM, index) +
27;
index += 1;
bool signatureValid;
string memory invalidReason;
(signatureValid, invalidReason) = verifySignature(
i,
lastIndex,
vm.hash,
sig.guardianIndex,
sig.r,
sig.s,
sig.v,
guardianSet.keys[sig.guardianIndex]
);
if (!signatureValid) {
return (vm, false, invalidReason);
}
lastIndex = sig.guardianIndex;
}
}
/**
* @dev We're using a fixed point number transformation with 1 decimal to deal with rounding.
* WARNING: This quorum check is critical to assessing whether we have enough Guardian signatures to validate a VM
* if making any changes to this, obtain additional peer review. If guardianSet key length is 0 and
* vm.signatures length is 0, this could compromise the integrity of both vm and signature verification.
*/
if (
(((guardianSet.keys.length * 10) / 3) * 2) / 10 + 1 > signersLen
) {
return (vm, false, "no quorum");
}
// purposely setting vm.signatures to empty array since we don't need it anymore
// and we've already verified it above
vm.signatures = new ReceiverStructs.Signature[](0);
// Parse the body
vm.timestamp = UnsafeCalldataBytesLib.toUint32(encodedVM, index);
index += 4;
vm.nonce = UnsafeCalldataBytesLib.toUint32(encodedVM, index);
index += 4;
vm.emitterChainId = UnsafeCalldataBytesLib.toUint16(
encodedVM,
index
);
index += 2;
vm.emitterAddress = UnsafeCalldataBytesLib.toBytes32(
encodedVM,
index
);
index += 32;
vm.sequence = UnsafeCalldataBytesLib.toUint64(encodedVM, index);
index += 8;
vm.consistencyLevel = UnsafeCalldataBytesLib.toUint8(
encodedVM,
index
);
index += 1;
if (index > encodedVM.length) {
return (vm, false, "invalid payload length");
}
vm.payload = UnsafeCalldataBytesLib.sliceFrom(encodedVM, index);
return (vm, true, "");
}
} }
/** /**
@ -84,6 +243,27 @@ contract ReceiverMessages is ReceiverGetters {
return (true, ""); return (true, "");
} }
function verifySignature(
uint i,
uint8 lastIndex,
bytes32 hash,
uint8 guardianIndex,
bytes32 r,
bytes32 s,
uint8 v,
address guardianSetKey
) private pure returns (bool valid, string memory reason) {
/// Ensure that provided signature indices are ascending only
if (i != 0 && guardianIndex <= lastIndex) {
revert SignatureIndexesNotAscending();
}
/// Check to see if the signer of the signature does not match a specific Guardian key at the provided index
if (ecrecover(hash, v, r, s) != guardianSetKey) {
return (false, "VM signature invalid");
}
return (true, "");
}
/** /**
* @dev verifySignatures serves to validate arbitrary sigatures against an arbitrary guardianSet * @dev verifySignatures serves to validate arbitrary sigatures against an arbitrary guardianSet
* - it intentionally does not solve for expectations within guardianSet (you should use verifyVM if you need these protections) * - it intentionally does not solve for expectations within guardianSet (you should use verifyVM if you need these protections)
@ -98,21 +278,20 @@ contract ReceiverMessages is ReceiverGetters {
uint8 lastIndex = 0; uint8 lastIndex = 0;
for (uint i = 0; i < signatures.length; i++) { for (uint i = 0; i < signatures.length; i++) {
ReceiverStructs.Signature memory sig = signatures[i]; ReceiverStructs.Signature memory sig = signatures[i];
(valid, reason) = verifySignature(
/// Ensure that provided signature indices are ascending only i,
require( lastIndex,
i == 0 || sig.guardianIndex > lastIndex, hash,
"signature indices must be ascending" sig.guardianIndex,
); sig.r,
lastIndex = sig.guardianIndex; sig.s,
sig.v,
/// Check to see if the signer of the signature does not match a specific Guardian key at the provided index
if (
ecrecover(hash, sig.v, sig.r, sig.s) !=
guardianSet.keys[sig.guardianIndex] guardianSet.keys[sig.guardianIndex]
) { );
return (false, "VM signature invalid"); if (!valid) {
return (false, reason);
} }
lastIndex = sig.guardianIndex;
} }
/// If we are here, we've validated that the provided signatures are valid for the provided guardianSet /// If we are here, we've validated that the provided signatures are valid for the provided guardianSet
@ -130,7 +309,9 @@ contract ReceiverMessages is ReceiverGetters {
vm.version = encodedVM.toUint8(index); vm.version = encodedVM.toUint8(index);
index += 1; index += 1;
require(vm.version == 1, "VM version incompatible"); if (vm.version != 1) {
revert VmVersionIncompatible();
}
vm.guardianSetIndex = encodedVM.toUint32(index); vm.guardianSetIndex = encodedVM.toUint32(index);
index += 4; index += 4;

View File

@ -25,6 +25,7 @@ contract GasBenchmark is Test, WormholeTestUtils, PythTestUtils {
// We will have less than 512 price for a foreseeable future. // We will have less than 512 price for a foreseeable future.
uint8 constant MERKLE_TREE_DEPTH = 9; uint8 constant MERKLE_TREE_DEPTH = 9;
IWormhole public wormhole;
IPyth public pyth; IPyth public pyth;
bytes32[] priceIds; bytes32[] priceIds;
@ -51,7 +52,9 @@ contract GasBenchmark is Test, WormholeTestUtils, PythTestUtils {
uint randSeed; uint randSeed;
function setUp() public { function setUp() public {
pyth = IPyth(setUpPyth(setUpWormhole(NUM_GUARDIANS))); address wormholeAddr = setUpWormholeReceiver(NUM_GUARDIANS);
wormhole = IWormhole(wormholeAddr);
pyth = IPyth(setUpPyth(wormholeAddr));
priceIds = new bytes32[](NUM_PRICES); priceIds = new bytes32[](NUM_PRICES);
priceIds[0] = bytes32( priceIds[0] = bytes32(
@ -101,7 +104,6 @@ contract GasBenchmark is Test, WormholeTestUtils, PythTestUtils {
freshPricesWhMerkleUpdateData.push(updateData); freshPricesWhMerkleUpdateData.push(updateData);
freshPricesWhMerkleUpdateFee.push(updateFee); freshPricesWhMerkleUpdateFee.push(updateFee);
} }
// Populate the contract with the initial prices // Populate the contract with the initial prices
( (
cachedPricesWhBatchUpdateData, cachedPricesWhBatchUpdateData,
@ -417,4 +419,8 @@ contract GasBenchmark is Test, WormholeTestUtils, PythTestUtils {
function testBenchmarkGetUpdateFeeWhMerkle5() public view { function testBenchmarkGetUpdateFeeWhMerkle5() public view {
pyth.getUpdateFee(freshPricesWhMerkleUpdateData[4]); pyth.getUpdateFee(freshPricesWhMerkleUpdateData[4]);
} }
function testBenchmarkWormholeParseAndVerifyVMBatchAttestation() public {
wormhole.parseAndVerifyVM(freshPricesWhBatchUpdateData[0]);
}
} }

View File

@ -26,7 +26,7 @@ contract PythWormholeMerkleAccumulatorTest is
uint64 constant MAX_UINT64 = uint64(int64(-1)); uint64 constant MAX_UINT64 = uint64(int64(-1));
function setUp() public { function setUp() public {
pyth = IPyth(setUpPyth(setUpWormhole(1))); pyth = IPyth(setUpPyth(setUpWormholeReceiver(1)));
} }
function assertPriceFeedMessageStored( function assertPriceFeedMessageStored(
@ -476,13 +476,6 @@ contract PythWormholeMerkleAccumulatorTest is
); );
} }
function isNotMatch(
bytes memory a,
bytes memory b
) public pure returns (bool) {
return keccak256(a) != keccak256(b);
}
/// @notice This method creates a forged invalid wormhole update data. /// @notice This method creates a forged invalid wormhole update data.
/// The caller should pass the forgeItem as string and if it matches the /// The caller should pass the forgeItem as string and if it matches the
/// expected value, that item will be forged to be invalid. /// expected value, that item will be forged to be invalid.

View File

@ -19,7 +19,7 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils, RandTestUtils {
uint64 constant MAX_UINT64 = uint64(int64(-1)); uint64 constant MAX_UINT64 = uint64(int64(-1));
function setUp() public { function setUp() public {
pyth = IPyth(setUpPyth(setUpWormhole(1))); pyth = IPyth(setUpPyth(setUpWormholeReceiver(1)));
} }
function generateRandomPriceAttestations( function generateRandomPriceAttestations(

View File

@ -67,7 +67,9 @@ contract VerificationExperiments is
uint64 sequence; uint64 sequence;
function setUp() public { function setUp() public {
address payable wormhole = payable(setUpWormhole(NUM_GUARDIANS)); address payable wormhole = payable(
setUpWormholeReceiver(NUM_GUARDIANS)
);
// Deploy experimental contract // Deploy experimental contract
PythExperimental implementation = new PythExperimental(); PythExperimental implementation = new PythExperimental();
@ -82,7 +84,6 @@ contract VerificationExperiments is
bytes32[] memory emitterAddresses = new bytes32[](1); bytes32[] memory emitterAddresses = new bytes32[](1);
emitterAddresses[0] = PythTestUtils.SOURCE_EMITTER_ADDRESS; emitterAddresses[0] = PythTestUtils.SOURCE_EMITTER_ADDRESS;
pyth.initialize( pyth.initialize(
wormhole, wormhole,
vm.addr(THRESHOLD_KEY), vm.addr(THRESHOLD_KEY),
@ -134,6 +135,7 @@ contract VerificationExperiments is
cachedPricesUpdateData, cachedPricesUpdateData,
cachedPricesUpdateFee cachedPricesUpdateFee
) = generateWormholeUpdateDataAndFee(cachedPrices); ) = generateWormholeUpdateDataAndFee(cachedPrices);
pyth.updatePriceFeeds{value: cachedPricesUpdateFee}( pyth.updatePriceFeeds{value: cachedPricesUpdateFee}(
cachedPricesUpdateData cachedPricesUpdateData
); );

View File

@ -368,7 +368,7 @@ contract PythTestUtilsTest is
function testGenerateWhBatchUpdateWorks() public { function testGenerateWhBatchUpdateWorks() public {
IPyth pyth = IPyth( IPyth pyth = IPyth(
setUpPyth( setUpPyth(
setUpWormhole( setUpWormholeReceiver(
1 // Number of guardians 1 // Number of guardians
) )
) )

View File

@ -7,9 +7,21 @@ import "../../contracts/wormhole/Setup.sol";
import "../../contracts/wormhole/Wormhole.sol"; import "../../contracts/wormhole/Wormhole.sol";
import "../../contracts/wormhole/interfaces/IWormhole.sol"; import "../../contracts/wormhole/interfaces/IWormhole.sol";
import "../../contracts/wormhole-receiver/ReceiverImplementation.sol";
import "../../contracts/wormhole-receiver/ReceiverSetup.sol";
import "../../contracts/wormhole-receiver/WormholeReceiver.sol";
import "../../contracts/wormhole-receiver/ReceiverGovernanceStructs.sol";
import "forge-std/Test.sol"; import "forge-std/Test.sol";
abstract contract WormholeTestUtils is Test { abstract contract WormholeTestUtils is Test {
uint256[] currentSigners;
address wormholeReceiverAddr;
uint16 constant CHAIN_ID = 2; // Ethereum
uint16 constant GOVERNANCE_CHAIN_ID = 1; // solana
bytes32 constant GOVERNANCE_CONTRACT =
0x0000000000000000000000000000000000000000000000000000000000000004;
function setUpWormhole(uint8 numGuardians) public returns (address) { function setUpWormhole(uint8 numGuardians) public returns (address) {
Implementation wormholeImpl = new Implementation(); Implementation wormholeImpl = new Implementation();
Setup wormholeSetup = new Setup(); Setup wormholeSetup = new Setup();
@ -17,9 +29,11 @@ abstract contract WormholeTestUtils is Test {
Wormhole wormhole = new Wormhole(address(wormholeSetup), new bytes(0)); Wormhole wormhole = new Wormhole(address(wormholeSetup), new bytes(0));
address[] memory initSigners = new address[](numGuardians); address[] memory initSigners = new address[](numGuardians);
currentSigners = new uint256[](numGuardians);
for (uint256 i = 0; i < numGuardians; ++i) { for (uint256 i = 0; i < numGuardians; ++i) {
initSigners[i] = vm.addr(i + 1); // i+1 is the private key for the i-th signer. currentSigners[i] = i + 1;
initSigners[i] = vm.addr(currentSigners[i]); // i+1 is the private key for the i-th signer.
} }
// These values are the default values used in our tilt test environment // These values are the default values used in our tilt test environment
@ -27,14 +41,54 @@ abstract contract WormholeTestUtils is Test {
Setup(address(wormhole)).setup( Setup(address(wormhole)).setup(
address(wormholeImpl), address(wormholeImpl),
initSigners, initSigners,
2, // Ethereum chain ID CHAIN_ID, // Ethereum chain ID
1, // Governance source chain ID (1 = solana) GOVERNANCE_CHAIN_ID, // Governance source chain ID (1 = solana)
0x0000000000000000000000000000000000000000000000000000000000000004 // Governance source address GOVERNANCE_CONTRACT // Governance source address
); );
return address(wormhole); return address(wormhole);
} }
function setUpWormholeReceiver(
uint8 numGuardians
) public returns (address) {
ReceiverImplementation wormholeReceiverImpl = new ReceiverImplementation();
ReceiverSetup wormholeReceiverSetup = new ReceiverSetup();
WormholeReceiver wormholeReceiver = new WormholeReceiver(
address(wormholeReceiverSetup),
new bytes(0)
);
address[] memory initSigners = new address[](numGuardians);
currentSigners = new uint256[](numGuardians);
for (uint256 i = 0; i < numGuardians; ++i) {
currentSigners[i] = i + 1;
initSigners[i] = vm.addr(currentSigners[i]); // i+1 is the private key for the i-th signer.
}
// These values are the default values used in our tilt test environment
// and are not important.
ReceiverSetup(address(wormholeReceiver)).setup(
address(wormholeReceiverImpl),
initSigners,
CHAIN_ID, // Ethereum chain ID
GOVERNANCE_CHAIN_ID, // Governance source chain ID (1 = solana)
GOVERNANCE_CONTRACT // Governance source address
);
wormholeReceiverAddr = address(wormholeReceiver);
return wormholeReceiverAddr;
}
function isNotMatch(
bytes memory a,
bytes memory b
) public pure returns (bool) {
return keccak256(a) != keccak256(b);
}
function generateVaa( function generateVaa(
uint32 timestamp, uint32 timestamp,
uint16 emitterChainId, uint16 emitterChainId,
@ -58,7 +112,8 @@ abstract contract WormholeTestUtils is Test {
bytes memory signatures = new bytes(0); bytes memory signatures = new bytes(0);
for (uint256 i = 0; i < numSigners; ++i) { for (uint256 i = 0; i < numSigners; ++i) {
(uint8 v, bytes32 r, bytes32 s) = vm.sign(i + 1, hash); (uint8 v, bytes32 r, bytes32 s) = vm.sign(currentSigners[i], hash);
// encodePacked uses padding for arrays and we don't want it, so we manually concat them. // encodePacked uses padding for arrays and we don't want it, so we manually concat them.
signatures = abi.encodePacked( signatures = abi.encodePacked(
signatures, signatures,
@ -71,37 +126,389 @@ abstract contract WormholeTestUtils is Test {
vaa = abi.encodePacked( vaa = abi.encodePacked(
uint8(1), // Version uint8(1), // Version
uint32(0), // Guardian set index. it is initialized by 0 IWormhole(wormholeReceiverAddr).getCurrentGuardianSetIndex(), // Guardian set index. it is initialized by 0
numSigners, numSigners,
signatures, signatures,
body body
); );
} }
function forgeVaa(
uint32 timestamp,
uint16 emitterChainId,
bytes32 emitterAddress,
uint64 sequence,
bytes memory payload,
uint8 numSigners,
bytes memory forgeItem
) public returns (bytes memory vaa) {
bytes memory body = abi.encodePacked(
timestamp,
uint32(0), // Nonce. It is zero for single VAAs.
emitterChainId,
emitterAddress,
sequence,
uint8(0), // Consistency level (sometimes no. confirmation block). Not important here.
payload
);
bytes32 hash = keccak256(abi.encodePacked(keccak256(body)));
bytes memory signatures = new bytes(0);
for (uint256 i = 0; i < numSigners; ++i) {
(uint8 v, bytes32 r, bytes32 s) = vm.sign(
isNotMatch(forgeItem, "vaaSignature")
? currentSigners[i]
: currentSigners[i] + 1000,
hash
);
// encodePacked uses padding for arrays and we don't want it, so we manually concat them.
signatures = abi.encodePacked(
signatures,
isNotMatch(forgeItem, "vaaSignatureIndex")
? uint8(i)
: uint8(0), // Guardian index of the signature
r,
s,
v - 27 // v is either 27 or 28. 27 is added to v in Eth (following BTC) but Wormhole doesn't use it.
);
}
vaa = abi.encodePacked(
isNotMatch(forgeItem, "vaaVersion") ? uint8(1) : uint8(2), // Version
isNotMatch(forgeItem, "vaaGuardianSetIndex")
? uint32(0)
: uint32(1), // Guardian set index. it is initialized by 0
isNotMatch(forgeItem, "vaaNumSigners+")
? isNotMatch(forgeItem, "vaaNumSigners-")
? numSigners
: numSigners - 1
: numSigners + 1,
signatures,
body
);
}
function upgradeGuardianSet(uint256 numGuardians) public {
IWormhole wormhole = IWormhole(wormholeReceiverAddr);
ReceiverImplementation whReceiverImpl = ReceiverImplementation(
payable(wormholeReceiverAddr)
);
bytes memory newGuardians = new bytes(0);
for (uint256 i = 0; i < numGuardians; ++i) {
// encodePacked uses padding for arrays and we don't want it, so we manually concat them.
newGuardians = abi.encodePacked(newGuardians, vm.addr(i + 1 + 10));
}
uint32 newGuardianSetIndex = uint32(1);
bytes memory upgradeGuardianSetPayload = abi.encodePacked(
bytes32(
0x00000000000000000000000000000000000000000000000000000000436f7265
), // "Core" ReceiverGovernance module
uint8(2), // action
uint16(0), // chain (unused)
wormhole.getCurrentGuardianSetIndex() + 1, // uint32 newGuardianSetIndex;
uint8(numGuardians), // uint8 numGuardians;
newGuardians // ReceiverStructs.GuardianSet newGuardianSet;
);
bytes memory setGuardianSetVaa = generateVaa(
112,
GOVERNANCE_CHAIN_ID, // emitter chainID (solana)
GOVERNANCE_CONTRACT, // gov emitter addr
10,
upgradeGuardianSetPayload,
4
);
whReceiverImpl.submitNewGuardianSet(setGuardianSetVaa);
currentSigners = new uint256[](numGuardians);
for (uint256 i = 0; i < numGuardians; ++i) {
currentSigners[i] = i + 1 + 10;
}
}
} }
contract WormholeTestUtilsTest is Test, WormholeTestUtils { contract WormholeTestUtilsTest is Test, WormholeTestUtils {
uint32 constant TEST_VAA_TIMESTAMP = 112;
uint16 constant TEST_EMITTER_CHAIN_ID = 7;
bytes32 constant TEST_EMITTER_ADDR =
0x0000000000000000000000000000000000000000000000000000000000000bad;
uint64 constant TEST_SEQUENCE = 10;
bytes constant TEST_PAYLOAD = hex"deadbeaf";
uint8 constant TEST_NUM_SIGNERS = 4;
function assertVmMatchesTestValues(
Structs.VM memory vm,
bool valid,
string memory reason,
bytes memory vaa
) private {
assertTrue(valid);
assertEq(reason, "");
assertEq(vm.timestamp, TEST_VAA_TIMESTAMP);
assertEq(vm.emitterChainId, TEST_EMITTER_CHAIN_ID);
assertEq(vm.emitterAddress, TEST_EMITTER_ADDR);
assertEq(vm.sequence, TEST_SEQUENCE);
assertEq(vm.payload, TEST_PAYLOAD);
// parseAndVerifyVM() returns an empty signatures array for gas savings since it's not used
// after its been verified. parseVM() returns the full signatures array.
vm = IWormhole(wormholeReceiverAddr).parseVM(vaa);
assertEq(vm.signatures.length, TEST_NUM_SIGNERS);
}
function testGenerateVaaWorks() public { function testGenerateVaaWorks() public {
IWormhole wormhole = IWormhole(setUpWormhole(5)); IWormhole wormhole = IWormhole(setUpWormholeReceiver(5));
bytes memory vaa = generateVaa( bytes memory vaa = generateVaa(
112, TEST_VAA_TIMESTAMP,
7, TEST_EMITTER_CHAIN_ID,
0x0000000000000000000000000000000000000000000000000000000000000bad, TEST_EMITTER_ADDR,
10, TEST_SEQUENCE,
hex"deadbeaf", TEST_PAYLOAD,
4 TEST_NUM_SIGNERS
); );
(Structs.VM memory vm, bool valid, ) = wormhole.parseAndVerifyVM(vaa); (Structs.VM memory vm, bool valid, string memory reason) = wormhole
assertTrue(valid); .parseAndVerifyVM(vaa);
assertVmMatchesTestValues(vm, valid, reason, vaa);
}
assertEq(vm.timestamp, 112); function testParseAndVerifyWorksWithoutForging() public {
assertEq(vm.emitterChainId, 7); uint8 numGuardians = 5;
assertEq( IWormhole wormhole = IWormhole(setUpWormholeReceiver(numGuardians));
vm.emitterAddress, bytes memory vaa = forgeVaa(
0x0000000000000000000000000000000000000000000000000000000000000bad TEST_VAA_TIMESTAMP,
TEST_EMITTER_CHAIN_ID,
TEST_EMITTER_ADDR,
TEST_SEQUENCE,
TEST_PAYLOAD,
TEST_NUM_SIGNERS,
""
); );
assertEq(vm.payload, hex"deadbeaf"); (Structs.VM memory vm, bool valid, string memory reason) = wormhole
assertEq(vm.signatures.length, 4); .parseAndVerifyVM(vaa);
assertVmMatchesTestValues(vm, valid, reason, vaa);
}
function testParseAndVerifyFailsIfVaaIsNotSignedByEnoughGuardians() public {
IWormhole wormhole = IWormhole(setUpWormholeReceiver(5));
bytes memory vaa = generateVaa(
TEST_VAA_TIMESTAMP,
TEST_EMITTER_CHAIN_ID,
TEST_EMITTER_ADDR,
TEST_SEQUENCE,
TEST_PAYLOAD,
1 //numSigners
);
(, bool valid, string memory reason) = wormhole.parseAndVerifyVM(vaa);
assertEq(valid, false);
assertEq(reason, "no quorum");
}
function testParseAndVerifyFailsIfVaaHasInvalidGuardianSetIndex() public {
uint8 numGuardians = 5;
IWormhole wormhole = IWormhole(setUpWormholeReceiver(numGuardians));
bytes memory vaa = forgeVaa(
TEST_VAA_TIMESTAMP,
TEST_EMITTER_CHAIN_ID,
TEST_EMITTER_ADDR,
TEST_SEQUENCE,
TEST_PAYLOAD,
TEST_NUM_SIGNERS,
"vaaGuardianSetIndex"
);
(, bool valid, string memory reason) = wormhole.parseAndVerifyVM(vaa);
assertEq(valid, false);
assertEq(reason, "invalid guardian set");
}
function testParseAndVerifyFailsIfInvalidGuardianSignatureIndex() public {
uint8 numGuardians = 5;
address whAddr = setUpWormholeReceiver(numGuardians);
IWormhole wormhole = IWormhole(whAddr);
ReceiverImplementation whReceiverImpl = ReceiverImplementation(
payable(whAddr)
);
// generate the vaa and sign with the initial wormhole guardian set
bytes memory vaa = forgeVaa(
TEST_VAA_TIMESTAMP,
TEST_EMITTER_CHAIN_ID,
TEST_EMITTER_ADDR,
TEST_SEQUENCE,
TEST_PAYLOAD,
TEST_NUM_SIGNERS,
"vaaSignatureIndex"
);
vm.expectRevert(
// workaround for this error not being in an external library
abi.encodeWithSignature("SignatureIndexesNotAscending()")
);
wormhole.parseAndVerifyVM(vaa);
}
function testParseAndVerifyFailsIfIncorrectVersion() public {
uint8 numGuardians = 5;
address whAddr = setUpWormholeReceiver(numGuardians);
IWormhole wormhole = IWormhole(whAddr);
ReceiverImplementation whReceiverImpl = ReceiverImplementation(
payable(whAddr)
);
// generate the vaa and sign with the initial wormhole guardian set
bytes memory vaa = forgeVaa(
TEST_VAA_TIMESTAMP,
TEST_EMITTER_CHAIN_ID,
TEST_EMITTER_ADDR,
TEST_SEQUENCE,
TEST_PAYLOAD,
TEST_NUM_SIGNERS,
"vaaVersion"
);
vm.expectRevert(
// workaround for this error not being in an external library
abi.encodeWithSignature("VmVersionIncompatible()")
);
wormhole.parseAndVerifyVM(vaa);
}
function testUpgradeGuardianSetWorks() public {
uint8 numGuardians = 5;
address whAddr = setUpWormholeReceiver(numGuardians);
IWormhole wormhole = IWormhole(whAddr);
ReceiverImplementation whReceiverImpl = ReceiverImplementation(
payable(whAddr)
);
upgradeGuardianSet(5);
// generate the vaa and sign with the new wormhole guardian set
bytes memory vaa = generateVaa(
TEST_VAA_TIMESTAMP,
TEST_EMITTER_CHAIN_ID,
TEST_EMITTER_ADDR,
TEST_SEQUENCE,
TEST_PAYLOAD,
TEST_NUM_SIGNERS
);
uint32 guardianSetIdx = wormhole.getCurrentGuardianSetIndex();
vm.warp(block.timestamp + 5 days);
(Structs.VM memory vm, bool valid, string memory reason) = wormhole
.parseAndVerifyVM(vaa);
assertVmMatchesTestValues(vm, valid, reason, vaa);
}
function testParseAndVerifyWorksIfUsingPreviousVaaGuardianSetBeforeItExpires()
public
{
uint8 numGuardians = 5;
address whAddr = setUpWormholeReceiver(numGuardians);
IWormhole wormhole = IWormhole(whAddr);
ReceiverImplementation whReceiverImpl = ReceiverImplementation(
payable(whAddr)
);
// generate the vaa and sign with the initial wormhole guardian set
bytes memory vaa = generateVaa(
TEST_VAA_TIMESTAMP,
TEST_EMITTER_CHAIN_ID,
TEST_EMITTER_ADDR,
TEST_SEQUENCE,
TEST_PAYLOAD,
TEST_NUM_SIGNERS
);
upgradeGuardianSet(numGuardians);
uint32 guardianSetIdx = wormhole.getCurrentGuardianSetIndex();
uint previousGuardianSetExpiration = wormhole
.getGuardianSet(0)
.expirationTime;
// warp to 5 seconds before the previous guardian set expires
vm.warp(previousGuardianSetExpiration - 5);
(Structs.VM memory vm, bool valid, string memory reason) = wormhole
.parseAndVerifyVM(vaa);
assertVmMatchesTestValues(vm, valid, reason, vaa);
}
function testParseAndVerifyFailsIfVaaGuardianSetHasExpired() public {
uint8 numGuardians = 5;
address whAddr = setUpWormholeReceiver(numGuardians);
IWormhole wormhole = IWormhole(whAddr);
ReceiverImplementation whReceiverImpl = ReceiverImplementation(
payable(whAddr)
);
// generate the vaa and sign with the current wormhole guardian set
bytes memory vaa = generateVaa(
TEST_VAA_TIMESTAMP,
TEST_EMITTER_CHAIN_ID,
TEST_EMITTER_ADDR,
TEST_SEQUENCE,
TEST_PAYLOAD,
TEST_NUM_SIGNERS
);
upgradeGuardianSet(numGuardians);
uint32 guardianSetIdx = wormhole.getCurrentGuardianSetIndex();
vm.warp(block.timestamp + 5 days);
(, bool valid, string memory reason) = wormhole.parseAndVerifyVM(vaa);
assertEq(valid, false);
assertEq(reason, "guardian set has expired");
}
function testParseAndVerifyFailsIfInvalidGuardianSignature() public {
uint8 numGuardians = 5;
address whAddr = setUpWormholeReceiver(numGuardians);
IWormhole wormhole = IWormhole(whAddr);
ReceiverImplementation whReceiverImpl = ReceiverImplementation(
payable(whAddr)
);
// generate the vaa and sign with the current wormhole guardian set
bytes memory vaa = forgeVaa(
TEST_VAA_TIMESTAMP,
TEST_EMITTER_CHAIN_ID,
TEST_EMITTER_ADDR,
TEST_SEQUENCE,
TEST_PAYLOAD,
TEST_NUM_SIGNERS,
"vaaSignature"
);
(, bool valid, string memory reason) = wormhole.parseAndVerifyVM(vaa);
assertEq(valid, false);
assertEq(reason, "VM signature invalid");
}
function testParseAndVerifyFailsIfInvalidNumSignatures() public {
uint8 numGuardians = 5;
address whAddr = setUpWormholeReceiver(numGuardians);
IWormhole wormhole = IWormhole(whAddr);
ReceiverImplementation whReceiverImpl = ReceiverImplementation(
payable(whAddr)
);
// generate the vaa and sign with the current wormhole guardian set
bytes memory vaa = forgeVaa(
TEST_VAA_TIMESTAMP,
TEST_EMITTER_CHAIN_ID,
TEST_EMITTER_ADDR,
TEST_SEQUENCE,
TEST_PAYLOAD,
TEST_NUM_SIGNERS,
"vaaNumSigners+"
);
(, bool valid, string memory reason) = wormhole.parseAndVerifyVM(vaa);
assertEq(valid, false);
assertEq(reason, "invalid signature length");
vaa = forgeVaa(
TEST_VAA_TIMESTAMP,
TEST_EMITTER_CHAIN_ID,
TEST_EMITTER_ADDR,
TEST_SEQUENCE,
TEST_PAYLOAD,
TEST_NUM_SIGNERS,
"vaaNumSigners-"
);
(, valid, reason) = wormhole.parseAndVerifyVM(vaa);
assertEq(valid, false);
assertEq(reason, "VM signature invalid");
} }
} }