Replace Outdated Signatures in VAAs#
Introduction#
Cross-chain transactions in Wormhole rely on Verifiable Action Approvals (VAAs), which contain signatures from a trusted set of validators called Guardians. These signatures prove that the network approved an action, such as a token transfer.
However, the set of Guardians changes over time. If a user generates a transaction and waits too long before redeeming it, the Guardian set may have already changed. This means the VAA will contain outdated signatures from Guardians, who are no longer part of the network, causing the transaction to fail.
Instead of discarding these VAAs, we can fetch updated signatures and replace the outdated ones to ensure smooth processing.
In this tutorial, you'll build a script from scratch to:
- Fetch a VAA from Wormholescan
- Validate its signatures against the latest Guardian set
- Replace outdated signatures using the Wormhole SDK
- Output a valid VAA ready for submission
By the end, you'll have a script that ensures VAAs remain valid and processable, avoiding transaction failures.
Prerequisites#
Before you begin, ensure you have the following:
- Node.js and npm installed on your machine
- TypeScript installed globally
Project Setup#
In this section, you will create the directory, initialize a Node.js project, install dependencies, and configure TypeScript.
-
Create the project - set up the directory and navigate into it
-
Initialize a Node.js project - generate a
package.json
file -
Set up TypeScript - create a
tsconfig.json
fileThen, add the following configuration:
-
Install dependencies - add the required packages
@wormhole-foundation/sdk
- handles VAAs and cross-chain interactionsaxios
- makes HTTP requests to the Wormholescan APIweb3
- interacts with Ethereum transactions and contractstsx
- executes TypeScript files without compilation@types/node
- provides Node.js type definitions
-
Create the project structure - set up the required directories and files
mkdir -p src/config && touch src/config/constants.ts src/config/layouts.ts mkdir -p src/helpers && touch src/helpers/vaaHelper.ts mkdir -p src/scripts && touch scripts/replaceSignatures.ts
src/config/*
- stores public configuration variables and layouts for serializing and deserializing data structuressrc/helpers/*
- contains utility functionssrc/scripts/*
- contains scripts for fetching and replacing signatures
-
Set variables - define key constants in
src/config/constants.ts
src/config/constants.tsexport const RPC = 'https://ethereum-rpc.publicnode.com'; export const ETH_CORE = '0x98f3c9e6E3fAce36bAAd05FE09d375Ef1464288B'.toLowerCase(); export const WORMHOLESCAN_API = 'https://api.wormholescan.io/v1'; export const LOG_MESSAGE_PUBLISHED_TOPIC = '0x6eb224fb001ed210e379b335e35efe88672a8ce935d981a6896b27ffdf52a3b2'; export const TXS = [ '0x3ad91ec530187bb2ce3b394d587878cd1e9e037a97e51fbc34af89b2e0719367', '0x3c989a6bb40dcd4719453fbe7bbac420f23962c900ae75793124fc9cc614368c', ];
RPC
- endpoint for interacting with an Ethereum RPC nodeETH_CORE
- Wormhole's Core Contract address on Ethereum responsible for verifying VAAsWORMHOLESCAN_API
- base URL for querying the Wormholescan API to fetch VAA data and Guardian setsLOG_MESSAGE_PUBLISHED_TOPIC
- the event signature hash forLogMessagePublished
, a Wormhole contract event that signals when a VAA has been emitted. This is used to identify relevant logs in transaction receiptsTXS
- list of example transaction hashes that will be used for testing
-
Define data structure for working with VAAs - specify the ABI for the Wormhole Core Contract's
parseAndVerifyVM
function, which parses and verifies VAAs. Defining the data structure, also referred to as a layout, for this function ensures accurate decoding and validation of VAAssrc/config/layouts.tsexport const PARSE_AND_VERIFY_VM_ABI = { inputs: [{ internalType: 'bytes', name: 'encodedVM', type: 'bytes' }], name: 'parseAndVerifyVM', outputs: [ { components: [ { internalType: 'uint8', name: 'version', type: 'uint8' }, { internalType: 'uint32', name: 'timestamp', type: 'uint32' }, { internalType: 'uint32', name: 'nonce', type: 'uint32' }, { internalType: 'uint16', name: 'emitterChainId', type: 'uint16' }, { internalType: 'bytes32', name: 'emitterAddress', type: 'bytes32' }, { internalType: 'uint64', name: 'sequence', type: 'uint64' }, { internalType: 'uint8', name: 'consistencyLevel', type: 'uint8' }, { internalType: 'bytes', name: 'payload', type: 'bytes' }, { internalType: 'uint32', name: 'guardianSetIndex', type: 'uint32' }, { components: [ { internalType: 'bytes32', name: 'r', type: 'bytes32' }, { internalType: 'bytes32', name: 's', type: 'bytes32' }, { internalType: 'uint8', name: 'v', type: 'uint8' }, { internalType: 'uint8', name: 'guardianIndex', type: 'uint8' }, ], internalType: 'struct Structs.Signature[]', name: 'signatures', type: 'tuple[]', }, { internalType: 'bytes32', name: 'hash', type: 'bytes32' }, ], internalType: 'struct Structs.VM', name: 'vm', type: 'tuple', }, { internalType: 'bool', name: 'valid', type: 'bool' }, { internalType: 'string', name: 'reason', type: 'string' }, ], stateMutability: 'view', type: 'function', };
Create VAA Handling Functions#
In this section, we'll create a series of helper functions in the src/helpers/vaaHelper.ts
file that will retrieve and verify VAAs and fetch and replace outdated Guardian signatures to generate a correctly signed VAA.
To get started, import the necessary dependencies:
import axios from 'axios';
import { eth } from 'web3';
import {
deserialize,
serialize,
VAA,
Signature,
} from '@wormhole-foundation/sdk';
import {
RPC,
ETH_CORE,
LOG_MESSAGE_PUBLISHED_TOPIC,
WORMHOLESCAN_API,
} from '../config/constants';
import { PARSE_AND_VERIFY_VM_ABI } from '../config/layouts';
Fetch a VAA ID from a Transaction#
To retrieve a VAA, we first need to get its VAA ID from a transaction hash. This ID allows us to fetch the full VAA later. The VAA ID is structured as follows:
chain
- the Wormhole chain ID (Ethereum is 2)emitter
- the contract address that emitted the VAAsequence
- a unique identifier for the event
We must assemble the ID correctly since this is the format the Wormholescan API expects when querying VAAs.
Follow the below steps to process the transaction logs and construct the VAA ID:
-
Get the transaction receipt - iterate over the array of transaction hashes and fetch the receipt to access its logs
-
Find the Wormhole event - iterate over the transaction logs and check for events emitted by the Wormhole Core contract. Look specifically for
LogMessagePublished
events, which indicate a VAA was created -
Extract the emitter and sequence number - if a matching event is found, extract the emitter address from
log.topics[1]
and remove the0x
prefix. Then, the sequence number fromlog.data
is extracted, converting it from hex to an integer -
Construct the VAA ID - format the extracted data in
chain/emitter/sequence
format
export async function fetchVaaId(txHashes: string[]): Promise<string[]> {
const vaaIds: string[] = [];
for (const tx of txHashes) {
try {
const result = (
await axios.post(RPC, {
jsonrpc: '2.0',
id: 1,
method: 'eth_getTransactionReceipt',
params: [tx],
})
).data.result;
if (!result)
throw new Error(`Unable to fetch transaction receipt for ${tx}`);
for (const log of result.logs) {
if (
log.address === ETH_CORE &&
log.topics?.[0] === LOG_MESSAGE_PUBLISHED_TOPIC
) {
const emitter = log.topics[1].substring(2);
const seq = BigInt(log.data.substring(0, 66)).toString();
vaaIds.push(`2/${emitter}/${seq}`);
}
}
} catch (error) {
console.error(`Error processing ${tx}:`, error);
}
}
return vaaIds;
}
Try it out: VAA ID retrieval
If you want to try out the function before moving forward, create a test file inside the test
directory:
-
Create the directory and file - add a script to call
fetchVaaId
and print the result -
Add the function call
test/fetchVaaId.run.tsimport { fetchVaaId } from '../src/helpers/vaaHelper'; import { TXS } from '../src/config/constants'; const testFetchVaaId = async () => { for (const tx of TXS) { const vaaIds = await fetchVaaId([tx]); if (vaaIds.length > 0) { console.log(`Transaction: ${tx}`); vaaIds.forEach((vaaId) => console.log(`VAA ID: ${vaaId}`)); } else { console.log(`No VAA ID found for transaction: ${tx}`); } } }; testFetchVaaId();
-
Run the script
If successful, the output will be:
npx tsx test/fetchVaaId.run.ts Transaction: 0x3ad91ec530187bb2ce3b394d587878cd1e9e037a97e51fbc34af89b2e0719367 VAA ID: 2/0000000000000000000000003ee18b2214aff97000d974cf647e7c347e8fa585/164170If no VAA ID is found, the script will log an error message.
Fetch the Full VAA#
Now that you have the VAA ID, we can use it to fetch the full VAA payload from the Wormholescan API. This payload contains the VAA bytes, which will later be used for signature validation.
Open src/helpers/vaaHelper.ts
and create the fetchVaa()
function to iterate through VAA IDs and extract the vaaBytes
payload.
export async function fetchVaa(
vaaIds: string[]
): Promise<{ id: string; vaaBytes: string }[]> {
const results: { id: string; vaaBytes: string }[] = [];
for (const id of vaaIds) {
try {
const response = await axios.get(`${WORMHOLESCAN_API}/signed_vaa/${id}`);
const vaaBytes = response.data.vaaBytes;
results.push({ id, vaaBytes });
} catch (error) {
console.error(`Error fetching VAA for ${id}:`, error);
}
}
return results;
}
Try it out: VAA retrieval
If you want to try the function before moving forward, create a script inside the test
directory
-
Create the script file
-
Add the function call
test/fetchVaa.run.tsimport { fetchVaaId, fetchVaa } from '../src/helpers/vaaHelper'; import { TXS } from '../src/config/constants'; const testFetchVaa = async () => { for (const tx of TXS) { const vaaIds = await fetchVaaId([tx]); if (vaaIds.length === 0) { console.log(`No VAA ID found for transaction: ${tx}`); continue; } for (const vaaId of vaaIds) { const vaaBytes = await fetchVaa([vaaId]); console.log( `Transaction: ${tx}\nVAA ID: ${vaaId}\nVAA Bytes: ${ vaaBytes.length > 0 ? vaaBytes[0].vaaBytes : 'Not found' }` ); } } }; testFetchVaa();
-
Run the script
If successful, the output will be:
npx tsx test/fetchVaa.run.ts Transaction: 0x3ad91ec530187bb2ce3b394d587878cd1e9e037a97e51fbc34af89b2e0719367 VAA Bytes: AQAAAAMNANQSwD/HRPcKp7Yxypl1ON8dZeMBzgYJrd2KYz6l9Tq9K9fj72fYJgkMeMaB9h...If no VAA is found, the script will log an error message.
Validate VAA Signatures#
Now, we need to verify its validity. A VAA is only considered valid if it contains signatures from currently active Guardians and is correctly verified by the Wormhole Core contract.
Open src/helpers/vaaHelper.ts
and add the checkVaaValidity()
function. This function verifies whether a VAA is valid by submitting it to an Ethereum RPC node and checking for outdated signatures.
Follow these steps to implement the function:
-
Prepare the VAA for verification - construct the VAA payload in a format that can be sent to the Wormhole Core contract
-
Send an
eth_call
request - submit the VAA to an Ethereum RPC node, calling theparseAndVerifyVM
function on the Wormhole Core contract -
Decode the response - check whether the VAA is valid. If it contains outdated signatures, further action will be required to replace them
export async function checkVaaValidity(vaaBytes: string) {
try {
const vaa = Buffer.from(vaaBytes, 'base64');
vaa[4] = 4; // Set guardian set index to 4
const result = (
await axios.post(RPC, {
jsonrpc: '2.0',
id: 1,
method: 'eth_call',
params: [
{
from: null,
to: ETH_CORE,
data: eth.abi.encodeFunctionCall(PARSE_AND_VERIFY_VM_ABI, [
`0x${vaa.toString('hex')}`,
]),
},
'latest',
],
})
).data.result;
const decoded = eth.abi.decodeParameters(
PARSE_AND_VERIFY_VM_ABI.outputs,
result
);
console.log(
`${decoded.valid ? '✅' : '❌'} VAA Valid: ${decoded.valid}${
decoded.valid ? '' : `, Reason: ${decoded.reason}`
}`
);
return { valid: decoded.valid, reason: decoded.reason };
} catch (error) {
console.error(`Error checking VAA validity:`, error);
return { valid: false, reason: 'RPC error' };
}
}
Try it out: VAA Validity
If you want to try the function before moving forward, create a script inside the test
directory
-
Create the script file
-
Add the function call
test/checkVaaValidity.run.tsimport { fetchVaaId, fetchVaa, checkVaaValidity, } from '../src/helpers/vaaHelper'; import { TXS } from '../src/config/constants'; const testCheckVaaValidity = async () => { for (const tx of TXS) { const vaaIds = await fetchVaaId([tx]); if (vaaIds.length === 0) { console.log(`No VAA ID found for transaction: ${tx}`); continue; } for (const vaaId of vaaIds) { const vaaData = await fetchVaa([vaaId]); if (vaaData.length === 0 || !vaaData[0].vaaBytes) { console.log(`VAA not found for ID: ${vaaId}`); continue; } const result = await checkVaaValidity(vaaData[0].vaaBytes); console.log( `Transaction: ${tx}\nVAA ID: ${vaaId}\nVAA Validity:`, result ); } } }; testCheckVaaValidity();
-
Run the script
If the VAA is valid, the output will be:
npx tsx test/checkVaaValidity.run.ts ✅ VAA Valid: trueIf invalid, the output will include the reason:
npx tsx test/checkVaaValidity.run.ts ❌ VAA Valid: false, Reason: VM signature invalid Transaction: 0x3ad91ec530187bb2ce3b394d587878cd1e9e037a97e51fbc34af89b2e0719367
Fetch Observations (VAA Signatures)#
Before replacing outdated signatures, we need to fetch the original VAA signatures from Wormholescan. This allows us to compare them with the latest Guardian set and determine which ones need updating.
Inside src/helpers/vaaHelper.ts
, create the fetchObservations()
function to query the Wormholescan API for observations related to a given VAA. Format the response by converting Guardian addresses to lowercase for consistency, and return an empty array if an error occurs.
export async function fetchObservations(vaaId: string) {
try {
console.log(`Fetching observations`);
const response = await axios.get(
`https://api.wormholescan.io/api/v1/observations/${vaaId}`
);
return response.data.map((obs: any) => ({
guardianAddr: obs.guardianAddr.toLowerCase(),
signature: obs.signature,
}));
} catch (error) {
console.error(`Error fetching observations:`, error);
return [];
}
}
Try it out: Fetch Observations
If you want to try the function before moving forward, create a script inside the test
directory
-
Create the script file
-
Add the function call
test/fetchObservations.run.tsimport { fetchVaaId, fetchObservations } from '../src/helpers/vaaHelper'; import { TXS } from '../src/config/constants'; const testFetchObservations = async () => { for (const tx of TXS) { const vaaIds = await fetchVaaId([tx]); if (vaaIds.length === 0) { console.log(`No VAA ID found for transaction: ${tx}`); continue; } for (const vaaId of vaaIds) { const observations = await fetchObservations(vaaId); if (observations.length === 0) { console.log(`No observations found for VAA ID: ${vaaId}`); continue; } console.log( `Transaction: ${tx}\nVAA ID: ${vaaId}\nObservations:`, observations ); } } }; testFetchObservations();
-
Run the script
If successful, the output will be:
npx tsx test/fetchObservations.run.ts Fetching observations Transaction: 0x3ad91ec530187bb2ce3b394d587878cd1e9e037a97e51fbc34af89b2e0719367 Observations: [ { guardianAddr: '0xda798f6896a3331f64b48c12d1d57fd9cbe70811', signature: 'ZGFlMDYyOGNjZjFjMmE0ZTk5YzE2OThhZjAzMDM4NzZlYTM1OWMxMzczNDA3YzdlMDMxZTkyNzk0ODkwYjRiYjRiOWFmNzM3NjRiMzIyOTE0ZTQwYzNlMjllMWEzNmM2NTc3ZDc5ZTdhNTM2MzA5YjA4YjExZjE3YzE3MDViNWIwMQ==' }, { guardianAddr: '0x74a3bf913953d695260d88bc1aa25a4eee363ef0', signature: 'MzAyOTU4OGU4MWU0ODc0OTAwNDU3N2EzMGZlM2UxMDJjOWYwMjM0NWVhY2VmZWQ0ZGJlNTFkNmI3YzRhZmQ5ZTNiODFjNTg3MDNmYzUzNmJiYWFiZjNlODc1YTY3OTQwMGE4MmE3ZjZhNGYzOGY3YmRmNDNhM2VhNGQyNWNlNGMwMA==' }, ...]If no observations are found, the script will log an error message.
Fetch the Latest Guardian Set#
Now that we have the original VAA signatures, we must fetch the latest Guardian set from Wormholescan. This will allow us to compare the stored signatures with the current Guardians and determine which signatures need replacing.
Create the fetchGuardianSet()
function inside src/helpers/vaaHelper.ts
to fetch the latest Guardian set.
export async function fetchGuardianSet() {
try {
console.log('Fetching current guardian set');
const response = await axios.get(`${WORMHOLESCAN_API}/guardianset/current`);
const guardians = response.data.guardianSet.addresses.map((addr: string) =>
addr.toLowerCase()
);
const guardianSet = response.data.guardianSet.index;
return [guardians, guardianSet];
} catch (error) {
console.error('Error fetching guardian set:', error);
return [];
}
}
Try it out: Fetch Guardian Set
If you want to try the function before moving forward, create a script inside the test
directory
-
Create the script file
-
Add the function call
test/fetchGuardianSet.run.tsimport { fetchGuardianSet } from '../src/helpers/vaaHelper'; const testFetchGuardianSet = async () => { const [guardians, guardianSetIndex] = await fetchGuardianSet(); console.log('Current Guardian Set Index:', guardianSetIndex); console.log('Guardian Addresses:', guardians); }; testFetchGuardianSet();
-
Run the script
If successful, the output will be:
npx tsx test/fetchGuardianSet.run.ts Fetching current guardian set Current Guardian Set Index: 4 Guardian Addresses: [ '0x5893b5a76c3f739645648885bdccc06cd70a3cd3', '0xff6cb952589bde862c25ef4392132fb9d4a42157', '0x114de8460193bdf3a2fcf81f86a09765f4762fd1', '0x107a0086b32d7a0977926a205131d8731d39cbeb', ...]If an error occurs while fetching the Guardian set, a
500
status error will be logged.
Replace Outdated Signatures#
With the full VAA, Guardian signatures, and the latest Guardian set, we can now update outdated signatures while maintaining the required signature count.
-
Create the
replaceSignatures()
function - opensrc/helpers/vaaHelper.ts
and add the function header. To catch and handle errors properly, all logic will be wrapped inside atry
blocksrc/helpers/vaaHelper.tsexport async function replaceSignatures( vaa: string | Uint8Array<ArrayBufferLike>, observations: { guardianAddr: string; signature: string }[], currentGuardians: string[], guardianSetIndex: number ) { console.log('Replacing Signatures...'); try { // Add logic in the following steps here } catch (error) { console.error('Unexpected error in replaceSignatures:', error); } }
vaa
- original VAA bytesobservations
- observed signatures from the networkcurrentGuardians
- latest Guardian setguardianSetIndex
- current Guardian set index
-
Validate input data - ensure all required parameters are present before proceeding. If any required input is missing, the function throws an error to prevent execution with incomplete data. The Guardian set should never be empty; if it is, this likely indicates an error in fetching the Guardian set in a previous step
-
Filter valid signatures - remove signatures from inactive Guardians, keeping only valid ones. If there aren't enough valid signatures to replace the outdated ones, execution is halted to prevent an incomplete or invalid VAA
-
Convert valid signatures - ensure signatures are correctly formatted for verification. Convert hex-encoded signatures if necessary and extract their components
const formattedSigs = validSigs .map((sig) => { try { const sigBuffer = Buffer.from(sig.signature, 'base64'); // If it's 130 bytes, it's hex-encoded and needs conversion const sigBuffer1 = sigBuffer.length === 130 ? Buffer.from(sigBuffer.toString(), 'hex') : sigBuffer; const r = BigInt('0x' + sigBuffer1.subarray(0, 32).toString('hex')); const s = BigInt('0x' + sigBuffer1.subarray(32, 64).toString('hex')); const vRaw = sigBuffer1[64]; const v = vRaw < 27 ? vRaw : vRaw - 27; return { guardianIndex: currentGuardians.indexOf(sig.guardianAddr), signature: new Signature(r, s, v), }; } catch (error) { console.error( `Failed to process signature for guardian: ${sig.guardianAddr}`, error ); return null; } }) .filter( (sig): sig is { guardianIndex: number; signature: Signature } => sig !== null ); // Remove null values
-
Deserialize the VAA - convert the raw VAA data into a structured format for further processing
-
Identify outdated signatures - compare the current VAA signatures with the newly formatted ones to detect which signatures belong to outdated Guardians. Remove these outdated signatures to ensure only valid ones remain
const outdatedGuardianIndexes = parsedVaa.signatures .filter( (vaaSig) => !formattedSigs.some( (sig) => sig.guardianIndex === vaaSig.guardianIndex ) ) .map((sig) => sig.guardianIndex); console.log('Outdated Guardian Indexes:', outdatedGuardianIndexes); let updatedSignatures = parsedVaa.signatures.filter( (sig) => !outdatedGuardianIndexes.includes(sig.guardianIndex) );
-
Replace outdated signatures - substitute outdated signatures with valid ones while maintaining the correct number of signatures. If there aren’t enough valid replacements, execution stops
const validReplacements = formattedSigs.filter( (sig) => !updatedSignatures.some((s) => s.guardianIndex === sig.guardianIndex) ); // Check if we have enough valid signatures to replace outdated ones** if (outdatedGuardianIndexes.length > validReplacements.length) { console.warn( `Not enough valid replacement signatures! Need ${outdatedGuardianIndexes.length}, but only ${validReplacements.length} available.` ); return; } updatedSignatures = [ ...updatedSignatures, ...validReplacements.slice(0, outdatedGuardianIndexes.length), ]; updatedSignatures.sort((a, b) => a.guardianIndex - b.guardianIndex);
-
Serialize the updated VAA - reconstruct the VAA with the updated signatures and convert it into a format suitable for submission
-
Send the updated VAA for verification and handle errors - submit the updated VAA to an Ethereum RPC node for validation, ensuring it can be proposed for Guardian approval. If an error occurs during submission or signature replacement, log the issue and prevent further execution
try { if (!(patchedVaa instanceof Uint8Array)) throw new Error('Patched VAA is not a Uint8Array!'); const vaaHex = `0x${Buffer.from(patchedVaa).toString('hex')}`; console.log('Sending updated VAA to RPC...'); const result = await axios.post(RPC, { jsonrpc: '2.0', id: 1, method: 'eth_call', params: [ { from: null, to: ETH_CORE, data: eth.abi.encodeFunctionCall(PARSE_AND_VERIFY_VM_ABI, [vaaHex]), }, 'latest', ], }); const verificationResult = result.data.result; console.log('Updated VAA (hex):', vaaHex); return verificationResult; } catch (error) { throw new Error(`Error sending updated VAA to RPC: ${error}`); }
Complete Function
export async function replaceSignatures(
vaa: string | Uint8Array<ArrayBufferLike>,
observations: { guardianAddr: string; signature: string }[],
currentGuardians: string[],
guardianSetIndex: number
) {
console.log('Replacing Signatures...');
try {
if (!vaa) throw new Error('VAA is undefined or empty.');
if (currentGuardians.length === 0)
throw new Error('Guardian set is empty.');
if (observations.length === 0) throw new Error('No observations provided.');
const validSigs = observations.filter((sig) =>
currentGuardians.includes(sig.guardianAddr)
);
if (validSigs.length === 0)
throw new Error('No valid signatures found. Cannot proceed.');
const formattedSigs = validSigs
.map((sig) => {
try {
const sigBuffer = Buffer.from(sig.signature, 'base64');
// If it's 130 bytes, it's hex-encoded and needs conversion
const sigBuffer1 =
sigBuffer.length === 130
? Buffer.from(sigBuffer.toString(), 'hex')
: sigBuffer;
const r = BigInt('0x' + sigBuffer1.subarray(0, 32).toString('hex'));
const s = BigInt('0x' + sigBuffer1.subarray(32, 64).toString('hex'));
const vRaw = sigBuffer1[64];
const v = vRaw < 27 ? vRaw : vRaw - 27;
return {
guardianIndex: currentGuardians.indexOf(sig.guardianAddr),
signature: new Signature(r, s, v),
};
} catch (error) {
console.error(
`Failed to process signature for guardian: ${sig.guardianAddr}`,
error
);
return null;
}
})
.filter(
(sig): sig is { guardianIndex: number; signature: Signature } =>
sig !== null
); // Remove null values
let parsedVaa: VAA<'Uint8Array'>;
try {
parsedVaa = deserialize('Uint8Array', vaa);
} catch (error) {
throw new Error(`Error deserializing VAA: ${error}`);
}
const outdatedGuardianIndexes = parsedVaa.signatures
.filter(
(vaaSig) =>
!formattedSigs.some(
(sig) => sig.guardianIndex === vaaSig.guardianIndex
)
)
.map((sig) => sig.guardianIndex);
console.log('Outdated Guardian Indexes:', outdatedGuardianIndexes);
let updatedSignatures = parsedVaa.signatures.filter(
(sig) => !outdatedGuardianIndexes.includes(sig.guardianIndex)
);
const validReplacements = formattedSigs.filter(
(sig) =>
!updatedSignatures.some((s) => s.guardianIndex === sig.guardianIndex)
);
// Check if we have enough valid signatures to replace outdated ones**
if (outdatedGuardianIndexes.length > validReplacements.length) {
console.warn(
`Not enough valid replacement signatures! Need ${outdatedGuardianIndexes.length}, but only ${validReplacements.length} available.`
);
return;
}
updatedSignatures = [
...updatedSignatures,
...validReplacements.slice(0, outdatedGuardianIndexes.length),
];
updatedSignatures.sort((a, b) => a.guardianIndex - b.guardianIndex);
const updatedVaa: VAA<'Uint8Array'> = {
...parsedVaa,
guardianSet: guardianSetIndex,
signatures: updatedSignatures,
};
let patchedVaa: Uint8Array;
try {
patchedVaa = serialize(updatedVaa);
} catch (error) {
throw new Error(`Error serializing updated VAA: ${error}`);
}
try {
if (!(patchedVaa instanceof Uint8Array))
throw new Error('Patched VAA is not a Uint8Array!');
const vaaHex = `0x${Buffer.from(patchedVaa).toString('hex')}`;
console.log('Sending updated VAA to RPC...');
const result = await axios.post(RPC, {
jsonrpc: '2.0',
id: 1,
method: 'eth_call',
params: [
{
from: null,
to: ETH_CORE,
data: eth.abi.encodeFunctionCall(PARSE_AND_VERIFY_VM_ABI, [vaaHex]),
},
'latest',
],
});
const verificationResult = result.data.result;
console.log('Updated VAA (hex):', vaaHex);
return verificationResult;
} catch (error) {
throw new Error(`Error sending updated VAA to RPC: ${error}`);
}
} catch (error) {
console.error('Unexpected error in replaceSignatures:', error);
}
}
Create Script to Replace Outdated VAA Signatures#
Now that we have all the necessary helper functions, we will create a script to automate replacing outdated VAA signatures. This script will retrieve a transaction’s VAA sequentially, check its validity, fetch the latest Guardian set, and update its signatures. By the end, it will output a correctly signed VAA that can be proposed for Guardian approval.
-
Open the file - inside
src/scripts/replaceSignatures.ts
, import the required helper functions needed to process the VAAs -
Define the main execution function - add the following function inside
src/scripts/replaceSignatures.ts
to process each transaction inTXS
, going step by step through the signature replacement processasync function main() { try { for (const tx of TXS) { console.log(`\nProcessing TX: ${tx}\n`); // 1. Fetch Transaction VAA IDs: const vaaIds = await fetchVaaId([tx]); if (!vaaIds.length) continue; // 2. Fetch VAA Data: const vaaData = await fetchVaa(vaaIds); if (!vaaData.length) continue; const vaaBytes = vaaData[0].vaaBytes; if (!vaaBytes) continue; // 3. Check VAA Validity: const { valid } = await checkVaaValidity(vaaBytes); if (valid) continue; // 4. Fetch Observations (VAA signatures): const observations = await fetchObservations(vaaIds[0]); // 5. Fetch Current Guardian Set: const [currentGuardians, guardianSetIndex] = await fetchGuardianSet(); // 6. Replace Signatures: const response = await replaceSignatures( Buffer.from(vaaBytes, 'base64'), observations, currentGuardians, guardianSetIndex ); if (!response) continue; } } catch (error) { console.error('❌ Error in execution:', error); process.exit(1); } }
-
Make the script executable - ensure it runs when executed
To run the script, use the following command:
npx tsx src/scripts/replaceSignatures.ts Processing TX: 0x3ad91ec530187bb2ce3b394d587878cd1e9e037a97e51fbc34af89b2e0719367 ❌ VAA Valid: false, Reason: VM signature invalid Fetching observations Fetching current guardian set Replacing Signatures... Outdated Guardian Indexes: [ 0 ] Sending updated VAA to RPC... Updated VAA (hex): 0x01000000040d010019447b72d51e33923a3d6b28496ccd3722d5f1e33e2...
The script logs each step, skipping valid VAAs, replacing outdated signatures for invalid VAAs, and logging any errors. It then completes with a valid VAA ready for submission.
Resources#
You can explore the complete project and find all necessary scripts and configurations in Wormhole's demo GitHub repository.
The demo repository includes a bonus script to check the VAA redemption status on Ethereum and Solana, allowing you to verify whether a transaction has already been redeemed on the destination chain.
Conclusion#
You've successfully built a script to fetch, validate, and replace outdated signatures in VAAs using Wormholescan and the Wormhole SDK.
It's important to note that this tutorial does not update VAAs in the Wormhole network. Before redeeming the VAA, you must propose it for Guardian approval to finalize the process.