
Wormhole carries messages between blockchain ecosystems. Let’s look at the parts involved, then follow the Solidity message flow from the original May 2023 example.
Historical walkthrough: Goerli and Mumbai are retired, and the SDK and generic-relayer interfaces below belong to that earlier version. The examples have not been rerun in this review. For a new integration, start with the current SDK and supported-network documentation.
How does Wormhole work?
The messaging flow has four useful pieces:
- Core Contracts publish messages on the source chain and verify VAAs on the receiving chain.
- Guardians attest to observed messages.
- VAAs, or Verified Action Approvals, carry those messages and signatures.
- Relayers deliver VAAs to the destination application, which applies its own checks.

What role does Wormchain play?
Wormchain and the Guardian network are related, but they are not interchangeable names. Wormhole Gateway uses a Cosmos SDK/CosmWasm chain to connect the Wormhole stack with the IBC ecosystem.
The EVM example here publishes through a Core Contract, obtains a Guardian-signed VAA and verifies it on another chain. It does not submit the message to Wormchain. You do not need a general claim that Proof of Stake is unsuitable for interoperability to understand that flow.
The Guardian Network
The Guardian network has 19 canonical Guardians. A standard VAA requires 13 signatures. Depending on the chain, observations are made directly by the full set or through a configured delegated subset; the final attestation still uses the 13-of-19 format.
That authenticates the message under Wormhole’s Guardian model. It does not decide whether your application should accept the source contract, recipient or requested action. Those checks belong in the receiving application.

The Relayers
Relayers deliver signed VAAs to the destination chain. A client can submit the destination transaction, or a service can automate delivery. The receiver must still validate the message.
The original article called the automated option “generic relayers”. Wormhole’s current documentation says the Standard Relayer is being deprecated in favor of Executor. The final section preserves the old interface for historical context.
Historical example: Goerli -> Sepolia

The original example sent a message from Goerli to Sepolia. Its general flow remains useful to study:
- The sender publishes an encoded payload through the source Core Contract.
- Guardians attest after the configured observation/finality requirements.
- A client retrieves that message’s VAA and submits it to the receiver.
- The receiver verifies the VAA, source identity, destination and replay state before storing the message.
The code below retains its 2023 imports and network addresses, with explicit application checks added. It is not a tested setup for today’s networks.
1. Sending the Message from Goerli
The original GoerliMessageSender imports a pinned IWormhole interface from the Wormhole repository. Its publishMessage and messageFee functions are the parts we use here.
For a current integration, check the exact network in the official Core Contract address table. The address embedded in this historical Goerli example is not a current deployment recommendation.
function publishMessage(
uint32 nonce,
bytes memory payload,
uint8 consistencyLevel
) external payable returns (uint64 sequence);The arguments are:
nonce: An application-chosen integer. It is not the sequence number, and reusing it does not combine several calls into one message.payload: The bytes the application encodes.consistencyLevel: The observation/finality requirement. The old example’s200is retained as historical code; do not copy it as a universal “instant” setting. Check the current finality guidance for the chosen chain and application.
publishMessage returns the emitted message’s sequence. Keep that value for VAA retrieval.
function messageFee() external view returns (uint256);messageFee() returns the Core Contract’s publishing fee in the source chain’s native currency. Query it instead of assuming testnet fees are zero. The sender below requires exactly that amount; destination delivery has separate transaction costs.
getMessageForAddress encodes our destination chain, receiving contract and message together.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import "https://github.com/wormhole-foundation/wormhole/blob/1c0a1d7b63fc61dc751537c6c2c4d153725d1dc0/ethereum/relayers/contracts/interfaces/IWormhole.sol";
contract GoerliMessageSender {
// Historical 2023 network/imports; this revision has not been compiled or deployed.
address public immutable owner = msg.sender;
struct MyMessage {
uint16 targetChain; // Wormhole chain ID, not EVM chain ID
address recipient;
string message;
}
address private whAddr = 0x706abc4E45D419950511e474C7B9Ed348A4a716c;
IWormhole public immutable wormhole = IWormhole(whAddr);
uint256 public lastMessageSequence;
function getMessageForAddress(uint16 targetChain, address recipient, string calldata message) external pure returns (bytes memory) {
return abi.encode(MyMessage(targetChain, recipient, message));
}
function sendMessage(
bytes memory fullMessage
) public payable {
require(msg.sender == owner, "Only owner");
uint256 fee = wormhole.messageFee();
require(msg.value == fee, "Incorrect message fee");
lastMessageSequence = wormhole.publishMessage{
value: fee
}(1, fullMessage, 200);
}
function emitterAddress() public view returns (bytes32) {
return bytes32(uint256(uint160(address(this))));
}
}And we'll need the emitterAddress view function. This is just for our own convenience and converts our contract address into bytes32. We'll need that to retrieve the VAA as well as double checking the emitter address on the receiver side.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import "https://github.com/wormhole-foundation/wormhole/blob/1c0a1d7b63fc61dc751537c6c2c4d153725d1dc0/ethereum/relayers/contracts/interfaces/IWormhole.sol";
import "https://github.com/wormhole-foundation/wormhole/blob/1c0a1d7b63fc61dc751537c6c2c4d153725d1dc0/ethereum/relayers/contracts/interfaces/IWormholeReceiver.sol";
contract SepoliaMessageReceiver is IWormholeReceiver {
// Historical 2023 interface; this revision has not been compiled or deployed.
address public immutable owner = msg.sender;
address private whAddr = 0x4a8bc80Ed5a4067f1CCf107057b8270E0cC11A78;
IWormhole public immutable wormhole = IWormhole(whAddr);
struct MyMessage {
uint16 targetChain; // Wormhole chain ID, not EVM chain ID
address recipient;
string message;
}
mapping(uint16 => bytes32) public registeredContracts;
mapping(bytes32 => bool) public hasProcessedMessage;
string[] public messageHistory;
function registerEmitter(uint16 chainId, bytes32 emitterAddress) public {
require(msg.sender == owner, "Only owner");
require(chainId != 0 && emitterAddress != bytes32(0), "Invalid emitter configuration");
registeredContracts[chainId] = emitterAddress;
}
function receiveWormholeMessages(
bytes[] memory signedVaas,
bytes[] memory
) public payable override {
require(signedVaas.length == 1, "Expected one VAA");
(IWormhole.VM memory parsed, bool valid, string memory reason)
= wormhole.parseAndVerifyVM(signedVaas[0]);
require(valid, reason);
bytes32 expectedEmitter = registeredContracts[parsed.emitterChainId];
require(expectedEmitter != bytes32(0), "Source chain not registered");
require(
expectedEmitter == parsed.emitterAddress,
"Emitter address not valid"
);
require(!hasProcessedMessage[parsed.hash], "Message already processed");
MyMessage memory message = abi.decode(parsed.payload, (MyMessage));
require(message.targetChain == wormhole.chainId(), "Wrong destination chain");
require(message.recipient == address(this), "Wrong recipient");
hasProcessedMessage[parsed.hash] = true;
messageHistory.push(message.message);
}
function getFullMessageHistory() public view returns (string[] memory) {
return messageHistory;
}
}2. Receiving the Message on Sepolia
The receiver uses the article’s pinned 2023 IWormholeReceiver interface. Here we submit the VAA ourselves, so this function is simply our application’s entry point.
Do not treat its receiveWormholeMessages(bytes[],bytes[]) signature as the current standard for every relaying integration. The Executor migration guide explains the current division of verification and delivery responsibilities.
This receiver expects exactly one VAA and checks the array length before accessing signedVaas[0]. parseAndVerifyVM returns the parsed envelope, validity flag and reason. A valid Guardian attestation is only the first check.
struct VM {
uint8 version;
uint32 timestamp;
uint32 nonce;
uint16 emitterChainId;
bytes32 emitterAddress;
uint64 sequence;
uint8 consistencyLevel;
bytes payload;
uint32 guardianSetIndex;
Signature[] signatures;
bytes32 hash;
}- Check the signed source chain and emitter address against an explicitly configured whitelist. Only the receiver’s administrator may change it.
- Decode the payload using the same schema as the sender. The revised struct includes the destination Wormhole chain ID and recipient; check both.
- Record the VAA hash before processing so the same accepted message cannot be consumed twice by this receiver.
- Review who can publish through the trusted source contract. The revised sender permits only its deploying administrator. Authenticating a source contract would not, by itself, authorize every caller of a public sender.
This is a small message-history demonstration, not a bridge or governance implementation. Its single immutable administrator and payload schema are deliberate teaching choices.
Once all checks are passed, you can store the hash as replay protection and process the message.
3. The historical Remix walkthrough

The screenshots below show the original Remix walkthrough. They help explain the two-chain workflow, but Goerli is retired and the revised payload now has a destination-chain field. A fresh example needs current networks, matching interfaces and tests before deployment.
A. Let's deploy the sender and receiver contract
The original walkthrough deployed the sender on Goerli and the receiver on Sepolia, then attached Remix to the source Core Contract with “At Address”. The screenshot shows those three entries.
Do not deploy against these historical Goerli instructions today. Select current networks and their official Core Contract addresses when preparing a tested replacement.

B. Let's register the sender with the receiver
emitterAddress() converts the sender’s address to Wormhole’s 32-byte representation. The receiver’s administrator registers that value for the expected source chain.
The old registerEmitter(2, emitterAddress) call used Wormhole’s historical Ethereum/Goerli identifier. Wormhole chain IDs differ from EVM chain IDs, and replacement testnets have distinct entries in the official registry. Use the source network’s actual Wormhole ID, not your wallet’s network number.
C. Let's send the message
For the revised payload, getMessageForAddress(targetWormholeChainId, receiverAddress, "Hello World!") encodes the target chain, recipient and message. Obtain the destination’s Wormhole ID from its Core Contract or the official registry.
The sender’s administrator calls sendMessage with those bytes and exactly the current messageFee(). The old two-argument helper and screenshots predate the target-chain field. This review did not publish a message.
D. Retrieve the VAA
Retrieve the VAA for the actual emitted sequence, together with its source Wormhole chain ID and emitter address. Read the sequence from that transaction’s LogMessagePublished event; the demo also stores its latest sequence in lastMessageSequence. Do not substitute the nonce or assume it is 1.
The old CertusOne URL is historical. Use the current SDK or API referenced in the VAA documentation. If observation is pending, retry retrieval for the same message. Publishing again creates another message.
The JSON below is the original response-format illustration, not a VAA for your contracts or the revised payload.
{"vaaBytes":"AQAAAAABAK/jB/sgQgOhZXnHlytNy/piP9dWizgbPP9rTpXgS/SOHFq+iW5PzP4j6IyB1UWx/Hos6JW3Bje0jrJgqPljuPsBZFZrWAAAAAEAAgAAAAAAAAAAAAAAACyICfoR/iqwChsK5RTo2NO0QUOGAAAAAAAAAAHIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhvbGE="}A VAA decoder can help inspect the envelope and payload. Decoding fields is not the same as verifying Guardian signatures or passing the receiver’s application checks.
E. Let's submit the VAA
Remix expects a Solidity bytes value as 0x-prefixed hexadecimal, not a string of binary digits. Decode the base64 response into bytes, then format those bytes as hex.
For example, with Node.js: const vaaHex = "0x" + Buffer.from(vaaBytes, "base64").toString("hex"); See the Buffer encoding documentation. The old converter screenshot shows the wrong output format for this input.

The historical receiver call has the shape receiveWormholeMessages(["0x..."], []), using the complete VAA bytes for your own message.
Acceptance depends on every signature, source, destination and replay check. The original JSON sample will not encode the revised payload for your contracts. After an accepted message, getFullMessageHistory() returns the stored text; no successful submission was rerun during this review.
How this could be done in a Dapp
For a new Dapp, use the current Wormhole TypeScript SDK, documented as @wormhole-foundation/sdk. The original @certusone/wormhole-sdk reference belongs to the older API.
Manual delivery asks the user to send the destination transaction as well. A backend or an automated delivery service can take that step instead, with its own fees and availability considerations. See the current relaying overview.

Historical generic-relayer example
The snippets below preserve the article’s 2023 MockRelayerIntegration example and IWormholeRelayer.Send API. They are historical reference code.
Wormhole is deprecating the Standard Relayer in favor of Executor. That migration changes the quoting and execution-request interfaces; these snippets are not a current implementation guide. Use the official migration guide when designing a replacement.
IWormholeRelayer.Send memory request = IWormholeRelayer.Send({
targetChain: targetChainId,
targetAddress: relayer.toWormholeFormat(address(destination)),
refundAddress: relayer.toWormholeFormat(address(refundAddress)), // This will be ignored on the target chain if the intent is to perform a forward
maxTransactionFee: msg.value - 3 * wormhole.messageFee() - receiverValue,
receiverValue: receiverValue,
relayParameters: relayer.getDefaultRelayParams()
});
relayer.send{value: msg.value - 2 * wormhole.messageFee()}(request, nonce, relayer.getDefaultRelayProvider());The original mock contracts below show Mumbai and BSC testnet addresses. Mumbai was retired; these old mock deployments and relayer addresses have not been revalidated.
For a replacement integration, select supported networks and the current Core/Executor contracts. Do not mix the old IWormholeRelayer.Send request with a new Executor address.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import "https://github.com/wormhole-foundation/trustless-generic-relayer/blob/9e282508f796a76a8aef03ba1911b68e03c8f627/ethereum/contracts/mock/MockRelayerIntegration.sol";
contract MyWormholeBSC is MockRelayerIntegration {
address private wormholeCore = 0x68605AD7b15c732a30b1BbC62BE8F2A509D74b4D;
address private coreRelayer = 0xda2592C43f2e10cBBA101464326fb132eFD8cB09;
constructor() MockRelayerIntegration(wormholeCore, coreRelayer) {}
}
contract MyWormholeMumbai is MockRelayerIntegration {
address private wormholeCore = 0x0CBE91CF822c73C2315FB05100C2F714765d5c20;
address private coreRelayer = 0xFAd28FcD3B05B73bBf52A3c4d8b638dFf1c5605c;
constructor() MockRelayerIntegration(wormholeCore, coreRelayer) {}
}Conclusion
The useful idea is straightforward: publish a payload on one chain, carry its Guardian-signed VAA to another, and let the destination application decide whether to accept it.
The checks matter as much as the delivery. Know who can send, which source you trust, where the message is intended to go and whether it has already been processed.
The Goerli/Sepolia and Mumbai/BSC walkthroughs above belong to the original 2023 article. For a new Dapp, use current network and SDK documentation and test the complete flow. The protocol has moved on; the old snippets need more than a new RPC URL. ;)




Join the conversation
Comments are hosted by Disqus and load only when you choose to enable them.