Skip to main content

Attestation Types

In Part 1 of this series, you learnt the basics of the Data Connector protocol, and how to use it to prove a payment on an external chain. In this guide you will learn:

  • How to prepare requests for different attestation types.
  • What information you need to provide.
  • What information you get in return.

Out of the full specification, this guide will cover the following four attestation types:

  • AddressValidity. Prove that a string constitutes a valid address on the specified chain. This is useful for ensuring an address is valid before using it in your protocol.

  • BalanceDecreasingTransaction. Prove that a transaction decreased the balance of an address or that the address was the initiator of the transaction.

  • ConfirmedBlockHeightExists. Prove that a block with a specified height is confirmed by a specified number of confirmations and provide additional details about the chain's block production rate.

  • ReferencePaymentNonexistence. Prove that a payment with the specified reference does not exist on the specified chain. This is useful for proving that someone did not honor a payment request.

The specification also includes EVMTransaction, which is more complex and powerful. This will be covered in Part 3 of this series.

Each attestation type is designed to prove a specific aspect, whether it be about transactions, blocks, or offloading expensive computations offchain to have the results available onchain. The team has carefully studied the most important use cases and designed the attestation types to be safe, well-defined, and efficient.

Each type has an associated offchain verifier that deconstructs the encoded request, executes the verification procedure, and returns the proper response. To understand more about how verifiers work, you can read the verifier spec. Each attestation type also comes with a verification contract that verifies the correctness of the response and the Merkle proof, ensuring the response was included in the Merkle tree for the round. These contracts are available in the specification repository, but for this guide, you will use the ones already deployed and made available by the periphery library. Remember, verifying the proof is just the first step. You still need to ensure that the response matches your dapp's requirements (e.g., the payment is large enough, the address is correct, the transaction was successful, the time range is sufficient).

As usual, the full code is available on flare-demo-examples, and you are encouraged to follow along and try it out on your own.

In Part 1, the whole process was described as:

  1. Observe, that something you want has happened

  2. Prepare a request for the attestation using your attestation client API.

  3. Submit the request to the Data Connector.

  4. Wait for the Data Connector to confirm the request.

  5. Once the request is confirmed, prepare the response and use it.

  6. Verify that the response is included in the data connector root using the verification contract.

  7. Validate that the response meets your expectations (e.g., correct payment, correct address).

he process is the same for all attestation types, so we will not repeat it for each type. Instead, we will go through each type, explain how to prepare the request, and what information you receive in return.

Remember, your best friend in this case is the prepareResponse endpoint, which returns the full response without the proof. This is enough to understand how the response looks and see that you receive all the correct information.

Attestation Client API

Before diving into different attestation types, let's explore the generated Swagger documentation for our API. For details on how Swagger works, you can visit swagger.io.

Open the page ${ATTESTATION_HOST}/verifier/btc/api-doc#/ (replace btc with the network you are interested in), where you'll find the full documentation of the API. Here, you can see all available endpoints, the types they accept, and what they return. Remember to authorize yourself with the API key before trying them out (you can do this in the top right corner of the page).

Let's start by preparing a request for the Payment type, which was covered in Part 1, and see what is returned. Pick a Bitcoin transaction from the block explorer and prepare the request for it. If you're doing this on a test network, ensure the sourceId in the example request is correct (it should be testBTC). If you execute a request with an incorrect sourceId, the attestation client will reject it and provide an error message indicating what needs to be fixed.

Apart from the available attestation types, you can also see the diagnostic endpoints, which provide information about the chain the attestation client is observing.

The endpoints are:

  • state: Returns the current state of the attestation client (e.g., indexed blocks, tip height, etc.).

  • block-range: Returns the range of blocks the attestation client is currently observing.

  • transaction: Returns full information about a transaction with the specified ID (do not prefix it with 0x). Try it with a random transaction ID to see all the information you get in return.

  • block: Returns full information about a block with the specified hash. Try it with the current tip height to see the details.

  • confirmedBlockAt: Similar to the block by hash, but you specify the block number.

  • blockHeight: Returns the current tip height of the chain (due to the required number of confirmations, the tip height might differ from the block range). This is the height of the latest block observed but not confirmed by the required number of confirmations.

  • transactionBlock: Returns the block information where the transaction with the specified ID is included.

  • transactions: Returns the list of transactions currently indexed (this is controlled by the block range of the indexer).

These methods are not necessary for using the Data Connector but are very useful for debugging, gathering chain information, and will prove invaluable when building your dapp.

Preparing the Request

The first step is to prepare the request for the attestation. Since attestation requests are prepared in a very similar manner, you will first create a simple function that can prepare the request for any attestation type.

To simplify the process, you will directly check what the response would be, without going through the entire proof process.

attestation.ts
// The same function can also be found in Data Connector utils bundled with the artifact periphery package (`encodeAttestationName`)

// Simple hex encoding
function toHex(data: string): string {
var result = "";
for (var i = 0; i < data.length; i++) {
result += data.charCodeAt(i).toString(16);
}
return "0x" + result.padEnd(64, "0");
}

interface AttestationResponse {
abiEncodedRequest: string;
status: string;
}

async function prepareAttestationRequest(
attestationType: string,
network: string,
sourceId: string,
requestBody: any,
): Promise<AttestationRequest> {
const response = await fetch(
`${ATTESTATION_URL}/verifier/${network}/${attestationType}/prepareRequest`,
{
method: "POST",
headers: {
"X-API-KEY": ATTESTATION_API_KEY as string,
"Content-Type": "application/json",
},
body: JSON.stringify({
attestationType: toHex(attestationType),
sourceId: toHex(sourceId),
requestBody: requestBody,
}),
},
);
const data = await response.json();
return data;
}

async function prepareAttestationResponse(
attestationType: string,
network: string,
sourceId: string,
requestBody: any,
): Promise<any> {
const response = await fetch(
`${ATTESTATION_URL}/verifier/${network}/${attestationType}/prepareResponse`,
{
method: "POST",
headers: {
"X-API-KEY": ATTESTATION_API_KEY as string,
"Content-Type": "application/json",
},
body: JSON.stringify({
attestationType: toHex(attestationType),
sourceId: toHex(sourceId),
requestBody: requestBody,
}),
},
);
const data = await response.json();
return data;
}

Here, we assume that the verifier supports all attestation types and networks. This is not required from all verifiers, but having a single endpoint for all requests and routing them to the correct verifier based on the request is a good practice.

Remember, we will be working on the coston testnet, so we will use testBTC as the sourceID in the attestation type body. Since verifiers are set up for a single deployment, testnet verifiers automatically look at testnets and mainnet verifiers look at mainnets. Therefore, you simply need to specify BTC as the network in the request URL. To change the network type, you would need to update the attestation URL to point to mainnet verifiers.

Similarly, verifier contracts (which check that the response together with the Merkle proof is included in the data connector round) are very similar. The only difference is the type the verification function receives (and thus the type they verify). The type is encoded, hashed, and the rest of the check is the same.

Now, let's take a look at each of the attestation types, see how to prepare the request for them, what needs to provide, and what is returned.

Balance Decreasing Transaction

Full specification on GitHub.

This attestation type is designed to prove that a transaction either decreases the balance of an address or is signed by the source address.

One of the purposes of the Data Connector is to provide connectivity between different blockchains, allowing the use of information from one chain on another. Other chains may not have smart contract capability or support any kind of "fund locking" and unlocking based on conditions. This is where the Data Connector comes into play, allowing the Flare network to monitor (and police) an address on another chain and act upon changes in its balance.

For instance, we can have an address on the Bitcoin network that acts as a vault (think fAssets). If the address owner violates an agreement by sending funds out, the Data Connector can detect it. To enhance security and avoid dependence on a single chain, this attestation type makes minimal assumptions about the violating transaction. A transaction is considered "offending" if the balance of the designated address is lower after the transaction or if the address is among the signers of the transaction (even if its balance is higher than before the transaction).

This allows us to track balance decreases even if the change results from a complex transaction (e.g., multisig, complex scripts, or specific XRPL transactions where a non-participating address can have funds removed).

Type Definition

BalanceDecreasingTransaction.sol
// SPDX-License-Identifier: MIT
pragma solidity >=0.7.6 <0.9;

/**
* @custom:name BalanceDecreasingTransaction
* @custom:id 0x02
* @custom:supported BTC, DOGE, XRP, testBTC, testDOGE, testXRP
* @author Flare
* @notice A detection of a transaction that either decreases the balance for some address or is signed by the source address.
* Such an attestation could prove a violation of an agreement and therefore provides grounds to liquidate some funds locked by a smart contract on Flare.
*
* A transaction is considered “balance decreasing” for the address, if the balance after the transaction is lower than before or the address is among the signers of the transaction (even if its balance is greater than before the transaction).
* @custom:verification The transaction with `transactionId` is fetched from the API of the source blockchain node or relevant indexer.
* If the transaction cannot be fetched or the transaction is in a block that does not have a sufficient [number of confirmations](/specs/attestations/configs.md#finalityconfirmation), the attestation request is rejected.
*
* Once the transaction is received, the response fields are extracted if the transaction is balance decreasing for the indicated address.
* Some of the request and response fields are chain specific as described below.
* The fields can be computed with the help of a [balance decreasing summary](/specs/attestations/external-chains/transactions.md#balance-decreasing-summary).
*
* ### UTXO (Bitcoin and Dogecoin)
*
* - `sourceAddressIndicator` is the index of the transaction input in hex padded to a 0x prefixed 32-byte string.
* If the indicated input does not exist or the indicated input does not have the address, the attestation request is rejected.
* The `sourceAddress` is the address of the indicated transaction input.
* - `spentAmount` is the sum of values of all inputs with sourceAddress minus the sum of all outputs with `sourceAddress`.
* Can be negative.
* - `blockTimestamp` is the mediantime of a block.
*
* ### XRPL
*
* - `sourceAddressIndicator` is the [standard address hash](/specs/attestations/external-chains/standardAddress.md#standard-address-hash) of the address whose balance has been decreased.
* If the address indicated by `sourceAddressIndicator` is not among the signers of the transaction and the balance of the address was not lowered in the transaction, the attestation request is rejected.
*
* - `spentAmount` is the difference between the balance of the indicated address after and before the transaction.
* Can be negative.
* - `blockTimestamp` is the close_time of a ledger converted to unix time.
*
* @custom:lut `blockTimestamp`
*/
interface BalanceDecreasingTransaction {
/**
* @notice Toplevel request
* @param attestationType ID of the attestation type.
* @param sourceId ID of the data source.
* @param messageIntegrityCode `MessageIntegrityCode` that is derived from the expected response.
* @param requestBody Data defining the request. Type (struct) and interpretation is determined by the `attestationType`.
*/
struct Request {
bytes32 attestationType;
bytes32 sourceId;
bytes32 messageIntegrityCode;
RequestBody requestBody;
}

/**
* @notice Toplevel response
* @param attestationType Extracted from the request.
* @param sourceId Extracted from the request.
* @param votingRound The ID of the Data Connector round in which the request was considered. This is a security measure to prevent a collision of attestation hashes.
* @param lowestUsedTimestamp The lowest timestamp used to generate the response.
* @param requestBody Extracted from the request.
* @param responseBody Data defining the response. The verification rules for the construction of the response body and the type are defined per specific `attestationType`.
*/
struct Response {
bytes32 attestationType;
bytes32 sourceId;
uint64 votingRound;
uint64 lowestUsedTimestamp;
RequestBody requestBody;
ResponseBody responseBody;
}

/**
* @notice Toplevel proof
* @param merkleProof Merkle proof corresponding to the attestation response.
* @param data Attestation response.
*/
struct Proof {
bytes32[] merkleProof;
Response data;
}

/**
* @notice Request body for BalanceDecreasingTransaction attestation type
* @param transactionId ID of the payment transaction.
* @param sourceAddressIndicator The indicator of the address whose balance has been decreased.
*/
struct RequestBody {
bytes32 transactionId;
bytes32 sourceAddressIndicator;
}

/**
* @notice Response body for BalanceDecreasingTransaction attestation type.
* @param blockNumber The number of the block in which the transaction is included.
* @param blockTimestamp The timestamp of the block in which the transaction is included.
* @param sourceAddressHash Standard address hash of the address indicated by the `sourceAddressIndicator`.
* @param spentAmount Amount spent by the source address in minimal units.
* @param standardPaymentReference Standard payment reference of the transaction.
*/
struct ResponseBody {
uint64 blockNumber;
uint64 blockTimestamp;
bytes32 sourceAddressHash;
int256 spentAmount;
bytes32 standardPaymentReference;
}
}

The request body consists of only two arguments:

  • transactionId: The ID of the payment transaction we want to prove (same as with payment).

  • sourceAddressIndicator: The indicator of the address whose balance has been decreased.

    • On Bitcoin and Dogecoin, this is the index of the transaction input in hex, padded to a 0x prefixed 32-byte string (very similar to inUtxo in the payment type).
    • On XRPL, this is the standard address hash of the address whose balance we want to prove has decreased.

Once the request is submitted, the verifiers will check the transaction, perform full accounting of the requested source address, and confirm the response if and only if the transaction indeed decreases the balance of the address or the address is among the signers of the transaction. In short, the request won't be confirmed if the balance stays the same and the address is not among the signers of the transaction, ensuring there are no false positives.

If the address has indeed decreased the balance (or participated as a signer), the response will also contain information about when exactly the offending transaction occurred. The balance decrease might be allowed under certain conditions (e.g., after a certain time, or with the correct payment reference).

The response will include the following information:

  • blockNumber: The number of the block in which the transaction is included.

  • blockTimestamp: The timestamp of the block in which the transaction is included. For UTXO chains, this is mediantime; for XRPL, this is close_time of the ledger.

  • sourceAddressHash: The standard address hash of the address indicated by the sourceAddressIndicator. For UTXO chains, this gives us the address that controlled the designated input.

  • spentAmount: The amount spent by the source address in minimal units. If this is negative, the address has received funds in the transaction but might still be among the signers.

  • standardPaymentReference: The standard payment reference of the transaction. This is useful if the transaction is an allowed payment and the payment reference is used to identify it.

Let's see how the verification contract looks.

BalanceDecreasingTransactionVerification.sol
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;

import "../../interface/types/BalanceDecreasingTransaction.sol";
import "../../interface/external/IMerkleRootStorage.sol";
import "./interface/IBalanceDecreasingTransactionVerification.sol";
import {MerkleProof} from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";

contract BalanceDecreasingTransactionVerification is IBalanceDecreasingTransactionVerification {
using MerkleProof for bytes32[];

IMerkleRootStorage public immutable merkleRootStorage;

constructor(IMerkleRootStorage _merkleRootStorage) {
merkleRootStorage = _merkleRootStorage;
}

function verifyBalanceDecreasingTransaction(
BalanceDecreasingTransaction.Proof calldata _proof
) external view returns (bool _proved) {
return _proof.data.attestationType == bytes32("BalanceDecreasingTransaction") &&
_proof.merkleProof.verify(
merkleRootStorage.merkleRoot(_proof.data.votingRound),
keccak256(abi.encode(_proof.data))
);
}
}

If you remember the payment verification contract, this one is very similar. We still use the MerkleProof library to verify the proof, but the type we verify is different. We just ABI encode the response and hash it, and then we verify that the hash is included in the Merkle tree for the round—exactly the same way as with the payment type. All other types are very similar; only the type we verify is different.

Importantly, the verification contract simply checks that this proof indeed proves that the structure we requested was included in a specific round. It does not make any assumptions about the response itself. The response itself should be checked by the dapp to ensure it is the expected one.

In some cases, the verifiers will not confirm the response (as there is no such confirmation), but in this case, they might confirm the response and also indicate that the balance has not decreased (and has indeed increased).

Example

Showing a balance decreasing transaction is simple—we will reuse the script from creating a transaction and just prove that the transaction has indeed decreased the balance of the address. The complete code that produces the following example is present in tryXRPLBalanceDecreasingTransaction.ts.

The code is practically the same as before; we just make the request to a different endpoint (due to the different attestation type), change the attestationType field in the request body, and specify the transaction and the address we want to prove the balance decrease for.

As mentioned earlier, specifying the address is important since the address's balance might have decreased in the transaction, but its participation might have been minimal (or it was not even part of the initial signers). For UTXO chains, we also need to specify sourceAddressIndicator because many addresses might be involved in the transaction (by signing an array of outputs). We need to specify which one we want to prove the balance decrease for and request the verifiers to do the full accounting.

tryXRPLBalanceDecreasingTransaction.ts
const xrpl = require("xrpl");

const {
XRPL_PRIVATE_KEY,
ATTESTATION_URL,
ATTESTATION_API_KEY,
USE_TESTNET_ATTESTATIONS,
} = process.env;
const receiverAddress = "r9RLXvWuRro3RX33pk4xsN58tefYZ8Tvbj";

function toHex(data: string): string {
var result = "";
for (var i = 0; i < data.length; i++) {
result += data.charCodeAt(i).toString(16);
}
return "0x" + result.padEnd(64, "0");
}

function fromHex(data: string): string {
data = data.replace(/^(0x\.)/, "");
return data
.split(/(\w\w)/g)
.filter((p) => !!p)
.map((c) => String.fromCharCode(parseInt(c, 16)))
.join("");
}

async function prepareAttestationResponse(
attestationType: string,
network: string,
sourceId: string,
requestBody: any,
): Promise<AttestationResponse> {
const response = await fetch(
`${ATTESTATION_URL}/verifier/${network}/${attestationType}/prepareResponse`,
{
method: "POST",
headers: {
"X-API-KEY": ATTESTATION_API_KEY as string,
"Content-Type": "application/json",
},
body: JSON.stringify({
attestationType: toHex(attestationType),
sourceId: toHex(sourceId),
requestBody: requestBody,
}),
},
);
const data = await response.json();
return data;
}

async function getXRPLclient(): Promise<any> {
const client = new xrpl.Client("wss://s.altnet.rippletest.net:51233");
await client.connect();

return client;
}

async function sendXRPLTransaction(
message: string = "",
amount: number = 10,
target: string = "r9RLXvWuRro3RX33pk4xsN58tefYZ8Tvbj",
): Promise<string> {
const client = await getXRPLclient();

const test_wallet = xrpl.Wallet.fromSeed(XRPL_PRIVATE_KEY);

let memos = [];
if (message) {
// Standard payment reference must be 32 bytes - so we right pad with 0
const MemoData = xrpl.convertStringToHex(message).padEnd(64, "0");
const MemoType = xrpl.convertStringToHex("Text");
const MemoFormat = xrpl.convertStringToHex("text/plain");

memos.push({
Memo: {
MemoType: MemoType,
MemoData: MemoData,
MemoFormat: MemoFormat,
},
});
}

const transaction = await client.autofill({
TransactionType: "Payment",
Account: test_wallet.address,
Amount: amount.toString(),
Destination: target,
Memos: memos,
});

const signed = test_wallet.sign(transaction);
console.log(
`See transaction at https://testnet.xrpl.org/transactions/${signed.hash}`,
);
await client.submitAndWait(signed.tx_blob);

await client.disconnect();

// sleep for 10 seconds to allow the transaction to be processed
await new Promise((resolve) => setTimeout(resolve, 10 * 1000));

const result = await prepareAttestationResponse(
"BalanceDecreasingTransaction",
"xrp",
"testXRP",
{
transactionId: "0x" + signed.hash,
sourceAddressIndicator: web3.utils.soliditySha3(test_wallet.address),
},
);

console.log(result);

console.log(fromHex(result.response.responseBody.standardPaymentReference));
}

async function main() {
await sendXRPLTransaction("Hello world!");
}

main().then(() => process.exit(0));

You create a transaction, wait for it to be processed, and then prepare a response to check that it was indeed a balance decreasing transaction.

An example response would look like this:

{
"status": "VALID",
"response": {
"attestationType": "0x42616c616e636544656372656173696e675472616e73616374696f6e00000000",
"sourceId": "0x7465737458525000000000000000000000000000000000000000000000000000",
"votingRound": "0",
"lowestUsedTimestamp": "1708671652",
"requestBody": {
"transactionId": "0xB40C7540D8393D389AAF6006C0429608ADD871C0CA3174B72EA55776D885B77B",
"sourceAddressIndicator": "0xa1ca3089c3e9f4c6e9ccf2bfb65bcf3e9d7544a092c79d642d5d34a54e0267e1"
}, "responseBody": {
"blockNumber": "45629840",
"blockTimestamp": "1708671652",
"sourceAddressHash": "0xa1ca3089c3e9f4c6e9ccf2bfb65bcf3e9d7544a092c79d642d5d34a54e0267e1",
"spentAmount": "22",
"standardPaymentReference": "0x48656C6C6F20776F726C64210000000000000000000000000000000000000000"
}
}
}
Hello world!

All the fields are populated correctly. Most importantly, although the transaction sent 10 XRP drops, the response clearly shows that the balance decreased by 22 drops, as 12 drops were spent on the transaction fee.

Confirmed Block Height Exists

Full specification on GitHub.

You now know how to observe generic transactions and balance decreasing transactions. It would be great if there were a way to obtain information about the block production rate on the external chain. This has multiple use cases—for example, you can check the current top block on the chain and then verify if this corresponds to the timestamp when the transaction on the external chain should occur. It is also a good way to observe if the other chain is progressing and not halted.

Type Definition

ConfirmedBlockHeightExists.sol
// SPDX-License-Identifier: MIT
pragma solidity >=0.7.6 <0.9;

/**
* @custom:name ConfirmedBlockHeightExists
* @custom:id 0x03
* @custom:supported BTC, DOGE, XRP, testBTC, testDOGE, testXRP
* @author Flare
* @notice An assertion that a block with `blockNumber` is confirmed.
* It also provides data to compute the block production rate in the given time range.
* @custom:verification It is checked that the block with `blockNumber` is confirmed by at least `numberOfConfirmations`.
* If it is not, the request is rejected. We note a block on the tip of the chain is confirmed by 1 block.
* Then `lowestQueryWindowBlock` is determined and its number and timestamp are extracted.
*
*
* Current confirmation heights consensus:
*
*
* | `Chain` | `chainId` | `numberOfConfirmations` | `timestamp ` |
* | ------- | --------- | ----------------------- | ------------ |
* | `BTC` | 0 | 6 | mediantime |
* | `DOGE` | 2 | 60 | mediantime |
* | `XRP` | 3 | 3 | close_time |
*
*
*
*
* @custom:lut `lowestQueryWindowBlockTimestamp`
*/
interface ConfirmedBlockHeightExists {
/**
* @notice Toplevel request
* @param attestationType ID of the attestation type.
* @param sourceId ID of the data source.
* @param messageIntegrityCode `MessageIntegrityCode` that is derived from the expected response as defined.
* @param requestBody Data defining the request. Type (struct) and interpretation is determined by the `attestationType`.
*/
struct Request {
bytes32 attestationType;
bytes32 sourceId;
bytes32 messageIntegrityCode;
RequestBody requestBody;
}

/**
* @notice Toplevel response
* @param attestationType Extracted from the request.
* @param sourceId Extracted from the request.
* @param votingRound The ID of the Data Connector round in which the request was considered.
* @param lowestUsedTimestamp The lowest timestamp used to generate the response.
* @param requestBody Extracted from the request.
* @param responseBody Data defining the response. The verification rules for the construction of the response body and the type are defined per specific `attestationType`.
*/
struct Response {
bytes32 attestationType;
bytes32 sourceId;
uint64 votingRound;
uint64 lowestUsedTimestamp;
RequestBody requestBody;
ResponseBody responseBody;
}

/**
* @notice Toplevel proof
* @param merkleProof Merkle proof corresponding to the attestation response.
* @param data Attestation response.
*/
struct Proof {
bytes32[] merkleProof;
Response data;
}

/**
* @notice Request body for ConfirmedBlockHeightExistsType attestation type
* @param blockNumber The number of the block the request wants a confirmation of.
* @param queryWindow The length of the period in which the block production rate is to be computed.
*/
struct RequestBody {
uint64 blockNumber;
uint64 queryWindow;
}

/**
* @notice Response body for ConfirmedBlockHeightExistsType attestation type
* @custom:below `blockNumber`, `lowestQueryWindowBlockNumber`, `blockTimestamp` and `lowestQueryWindowBlockTimestamp` can be used to compute the average block production time in the specified block range.
* @param blockTimestamp The timestamp of the block with `blockNumber`.
* @param numberOfConfirmations The depth at which a block is considered confirmed depending on the chain. All attestation providers must agree on this number.
* @param lowestQueryWindowBlockNumber The block number of the latest block that has a timestamp strictly smaller than `blockTimestamp` - `queryWindow`.
* @param lowestQueryWindowBlockTimestamp The timestamp of the block at height `lowestQueryWindowBlockNumber`.
*/
struct ResponseBody {
uint64 blockTimestamp;
uint64 numberOfConfirmations;
uint64 lowestQueryWindowBlockNumber;
uint64 lowestQueryWindowBlockTimestamp;
}
}

The request body is quite simple. You provide the blockNumber you want to confirm exists on the chain and the queryWindow—the length of the period in which the block production rate is to be computed (relative to the timestamp of the block you are requesting). Importantly, for the block to be considered visible, at least X blocks above it must be confirmed. This ensures that blocks not on the main chain are not confirmed. The number of confirmations required varies by chain and is listed in the specification.

What Do You Get in Return?

As per the specification, you only receive information confirming that the block with blockNumber is confirmed by at least numberOfConfirmations. If the block is not confirmed, the request is rejected (none of the attestation clients will confirm the response, and it will not be included in the Merkle tree). The response body contains the following fields:

  • blockTimestamp: The timestamp of the block with blockNumber.

  • numberOfConfirmations: The depth at which a block is considered confirmed depending on the chain. This is fixed per chain and specified in the documentation.

  • lowestQueryWindowBlockNumber: The block number of the latest block that has a timestamp strictly smaller than blockTimestamp - queryWindow. This allows you to gauge the average block production time in the specified block range.

  • lowestQueryWindowBlockTimestamp: The timestamp of the block at height lowestQueryWindowBlockNumber, indicating when the block was produced.

Example

To check the top block, you would typically query the RPC of the chain, get the top block, subtract the number of confirmations, and then query the attestation client to get the result. Alternatively, you can piggyback on the previous example, create a transaction, see the block it was included in, and proceed from there.

Each attestation provider also exposes several diagnostic endpoints that allow you to get information about the chain it is operating on. The endpoint that is particularly interesting for this purpose is the block-range endpoint, which returns the range of blocks the attestation provider is currently observing.

By querying the block-range endpoint, you can get the range of blocks the attestation provider is observing and then request the confirmation of the top block in that range. This approach allows you to verify the top block efficiently without manually tracking the block production and confirmation process.

Use the following code (also found in tryConfirmedBlockHeightExists.ts) and try to see how prepareResponse fares for blocks that are out of range for the current confirmation limit. This will help you understand how the attestation client handles requests for blocks that have not yet reached the necessary number of confirmations.

tryConfirmedBlockHeightExists.ts
const { ATTESTATION_URL, ATTESTATION_API_KEY } = process.env;

function toHex(data: string): string {
var result = "";
for (var i = 0; i < data.length; i++) {
result += data.charCodeAt(i).toString(16);
}
return "0x" + result.padEnd(64, "0");
}

function fromHex(data: string): string {
data = data.replace(/^(0x\.)/, "");
return data
.split(/(\w\w)/g)
.filter((p) => !!p)
.map((c) => String.fromCharCode(parseInt(c, 16)))
.join("");
}

async function prepareAttestationResponse(
attestationType: string,
network: string,
sourceId: string,
requestBody: any,
): Promise<AttestationResponse> {
const response = await fetch(
`${ATTESTATION_URL}/verifier/${network}/${attestationType}/prepareResponse`,
{
method: "POST",
headers: {
"X-API-KEY": ATTESTATION_API_KEY as string,
"Content-Type": "application/json",
},
body: JSON.stringify({
attestationType: toHex(attestationType),
sourceId: toHex(sourceId),
requestBody: requestBody,
}),
},
);
const data = await response.json();
return data;
}

async function getVerifierBlockRange(network: string): Promise<any> {
return (
await (
await fetch(
`${ATTESTATION_URL}/verifier/${network}/api/indexer/block-range`,
{
method: "GET",
headers: {
"X-API-KEY": ATTESTATION_API_KEY as string,
"Content-Type": "application/json",
},
},
)
).json()
).data;
}

async function main() {
const btcRange = await getVerifierBlockRange("btc");
const dogeRange = await getVerifierBlockRange("doge");
const xrplRange = await getVerifierBlockRange("xrp");

console.log("BTC Range: ", btcRange);
console.log(
await prepareAttestationResponse(
"ConfirmedBlockHeightExists",
"btc",
"testBTC",
{
blockNumber: btcRange.last.toString(),
queryWindow: "123",
},
),
);

console.log("DOGE Range: ", dogeRange);
console.log(
await prepareAttestationResponse(
"ConfirmedBlockHeightExists",
"doge",
"testDOGE",
{
blockNumber: dogeRange.last.toString(),
queryWindow: "123",
},
),
);

console.log("XRPL Range: ", xrplRange);
console.log(
await prepareAttestationResponse(
"ConfirmedBlockHeightExists",
"xrp",
"testXRP",
{
blockNumber: xrplRange.last.toString(),
queryWindow: "123",
},
),
);
}

main().then(() => process.exit(0));

Which will output a response similar to this:

BTC Range:  { first: 2578997, last: 2579392 }
{
status: 'VALID',
response: {
attestationType: '0x436f6e6669726d6564426c6f636b486569676874457869737473000000000000',
sourceId: '0x7465737442544300000000000000000000000000000000000000000000000000',
votingRound: '0',
lowestUsedTimestamp: '1708812188',
requestBody: { blockNumber: '2579392', queryWindow: '123' },
responseBody: {
blockTimestamp: '1708812188',
numberOfConfirmations: '6',
lowestQueryWindowBlockNumber: '2579391',
lowestQueryWindowBlockTimestamp: '1708812020'
}
}
}
DOGE Range: { first: 5706001, last: 5974548 }
{
status: 'VALID',
response: {
attestationType: '0x436f6e6669726d6564426c6f636b486569676874457869737473000000000000',
sourceId: '0x74657374444f4745000000000000000000000000000000000000000000000000',
votingRound: '0',
lowestUsedTimestamp: '1708819752',
requestBody: { blockNumber: '5974548', queryWindow: '123' },
responseBody: {
blockTimestamp: '1708819752',
numberOfConfirmations: '60',
lowestQueryWindowBlockNumber: '5974543',
lowestQueryWindowBlockTimestamp: '1708819511'
}
}
}
XRPL Range: { first: 45585486, last: 45678173 }
{
status: 'VALID',
response: {
attestationType: '0x436f6e6669726d6564426c6f636b486569676874457869737473000000000000',
sourceId: '0x7465737458525000000000000000000000000000000000000000000000000000',
votingRound: '0',
lowestUsedTimestamp: '1708822152',
requestBody: { blockNumber: '45678173', queryWindow: '123' },
responseBody: {
blockTimestamp: '1708822152',
numberOfConfirmations: '1',
lowestQueryWindowBlockNumber: '45678132',
lowestQueryWindowBlockTimestamp: '1708822022'
}
}
}

This attestation type is also useful for observing another important response: INDETERMINATE.

An INDETERMINATE response means that the attestation can't be confirmed (yet) because there are not enough confirmations for the block. This response indicates that the attestation client cannot confirm or reject the block for sure, but it might be valid in the future once more confirmations are received.

To see this in action, take the provided code and check for a block that has not yet been confirmed by the required amount. The easiest way to do this is to add 10 to the block range and observe the response. If done correctly, the response should be:

{
"status": "INDETERMINATE"
}

One important thing to note is that all numbers are sent as strings (either decimal or hex). The main reason for this is that JavaScript does not have a native 64-bit integer type, and numbers are represented as 64-bit floating-point numbers, which can lead to incorrect representation of large numbers. Even though block numbers might not be that large, encoding JSON numbers as strings ensures they are represented correctly.

Reference Payment Nonexistence

Full specification on GitHub.

You are getting more and more familiar with the attestation types, and you are starting to see that they are very powerful and can be used in many different ways. Let's check a more involved one—the ReferencePaymentNonexistence type.

This type is a bit more difficult to implement and properly use, as it requires the attestation client to do a lot of work—they need to prove that a certain payment has not been made. Instead of looking at the transaction and checking if it is valid, you will be looking at the block range and checking that no valid payment conforming to your requirements has been made in the specified block range.

Type Definition

ReferencedPaymentNonexistence.sol
// SPDX-License-Identifier: MIT
pragma solidity >=0.7.6 <0.9;

/**
* @custom:name ReferencedPaymentNonexistence
* @custom:id 0x04
* @custom:supported BTC, DOGE, XRP, testBTC, testDOGE, testXRP
* @author Flare
* @notice Assertion that an agreed-upon payment has not been made by a certain deadline.
* A confirmed request shows that a transaction meeting certain criteria (address, amount, reference) did not appear in the specified block range.
*
*
* This type of attestation can be used to e.g. provide grounds to liquidate funds locked by a smart contract on Flare when a payment is missed.
*
* @custom:verification If `firstOverflowBlock` cannot be determined or does not have a sufficient [number of confirmations](/specs/attestations/configs.md#finalityconfirmation), the attestation request is rejected.
* If `firstOverflowBlockNumber` is higher or equal to `minimalBlockNumber`, the request is rejected.
* The search range are blocks between heights including `minimalBlockNumber` and excluding `firstOverflowBlockNumber`.
* If the verifier does not have a view of all blocks from `minimalBlockNumber` to `firstOverflowBlockNumber`, the attestation request is rejected.
*
* The request is confirmed if no transaction meeting the specified criteria is found in the search range.
* The criteria and timestamp are chain specific.
* ### UTXO (Bitcoin and Dogecoin)
*
*
* Criteria for the transaction:
*
*
* - It is not coinbase transaction.
* - The transaction has the specified [standardPaymentReference](/specs/attestations/external-chains/standardPaymentReference.md#btc-and-doge-blockchains).
* - The sum of values of all outputs with the specified address minus the sum of values of all inputs with the specified address is greater than `amount` (in practice the sum of all values of the inputs with the specified address is zero).
*
*
* Timestamp is `mediantime`.

* ### XRPL
*
*
*
* Criteria for the transaction:
* - The transaction is of type payment.
* - The transaction has the specified [standardPaymentReference](/specs/attestations/external-chains/standardPaymentReference.md#xrp),
* - One of the following is true:
* - Transaction status is `SUCCESS` and the amount received by the specified destination address is greater than the specified `value`.
* - Transaction status is `RECEIVER_FAILURE` and the specified destination address would receive an amount greater than the specified `value` had the transaction been successful.
*
*
* Timestamp is `close_time` converted to UNIX time.
*
* @custom:lut `minimalBlockTimestamp`
*/
interface ReferencedPaymentNonexistence {
/**
* @notice Toplevel request
* @param attestationType ID of the attestation type.
* @param sourceId ID of the data source.
* @param messageIntegrityCode `MessageIntegrityCode` that is derived from the expected response as defined.
* @param requestBody Data defining the request. Type (struct) and interpretation is determined by the `attestationType`.
*/
struct Request {
bytes32 attestationType;
bytes32 sourceId;
bytes32 messageIntegrityCode;
RequestBody requestBody;
}

/**
* @notice Toplevel response
* @param attestationType Extracted from the request.
* @param sourceId Extracted from the request.
* @param votingRound The ID of the Data Connector round in which the request was considered.
* @param lowestUsedTimestamp The lowest timestamp used to generate the response.
* @param requestBody Extracted from the request.
* @param responseBody Data defining the response. The verification rules for the construction of the response body and the type are defined per specific `attestationType`.
*/
struct Response {
bytes32 attestationType;
bytes32 sourceId;
uint64 votingRound;
uint64 lowestUsedTimestamp;
RequestBody requestBody;
ResponseBody responseBody;
}

/**
* @notice Toplevel proof
* @param merkleProof Merkle proof corresponding to the attestation response.
* @param data Attestation response.
*/
struct Proof {
bytes32[] merkleProof;
Response data;
}

/**
* @notice Request body for ReferencePaymentNonexistence attestation type
* @param minimalBlockNumber The start block of the search range.
* @param deadlineBlockNumber The blockNumber to be included in the search range.
* @param deadlineTimestamp The timestamp to be included in the search range.
* @param destinationAddressHash The standard address hash of the address to which the payment had to be done.
* @param amount The requested amount in minimal units that had to be payed.
* @param standardPaymentReference The requested standard payment reference.
* @custom:below The `standardPaymentReference` should not be zero (as a 32-byte sequence).
*/
struct RequestBody {
uint64 minimalBlockNumber;
uint64 deadlineBlockNumber;
uint64 deadlineTimestamp;
bytes32 destinationAddressHash;
uint256 amount;
bytes32 standardPaymentReference;
}

/**
* @notice Response body for ReferencePaymentNonexistence attestation type.
* @param minimalBlockTimestamp The timestamp of the minimalBlock.
* @param firstOverflowBlockNumber The height of the firstOverflowBlock.
* @param firstOverflowBlockTimestamp The timestamp of the firstOverflowBlock.
* @custom:below `firstOverflowBlock` is the first block that has block number higher than `deadlineBlockNumber` and timestamp later than `deadlineTimestamp`.
* The specified search range are blocks between heights including `minimalBlockNumber` and excluding `firstOverflowBlockNumber`.
*/
struct ResponseBody {
uint64 minimalBlockTimestamp;
uint64 firstOverflowBlockNumber;
uint64 firstOverflowBlockTimestamp;
}
}

Request Body

The request body for the ReferencePaymentNonexistence attestation type is a bit larger, as you need to specify the range of blocks to check and the criteria for the payment to check.

  • minimalBlockNumber: The start block of the search range.

  • deadlineBlockNumber: The block number to be included in the search range.

  • deadlineTimestamp: The timestamp to be included in the search range. By including both block number and timestamp, the requested range will encompass all blocks from minimalBlockNumber to deadlineBlockNumber and all blocks with timestamps from minimalBlockTimestamp to deadlineTimestamp.

  • destinationAddressHash: The standard address hash of the address to which the payment should have been made.

  • amount: The requested amount in minimal units that should have been paid. The amount is chain specific.

  • standardPaymentReference: The requested standard payment reference that the payment should have had.

Response Body

The response body is simpler and essentially contains the searched range:

  • minimalBlockTimestamp: The timestamp of the minimal block that was included in the search range—this is the timestamp of the block with minimalBlockNumber.

  • firstOverflowBlockNumber: The height of the first overflow block. This is the first block with a block number higher than deadlineBlockNumber and a timestamp later than deadlineTimestamp.

  • firstOverflowBlockTimestamp: The timestamp of the first overflow block. This is the timestamp of the first block with a block number higher than deadlineBlockNumber and a timestamp later than deadlineTimestamp.

Confirmation

If the request is confirmed, it means that there was no payment in the specified range (including the minimal block, but excluding the maximal block) with an amount greater than or equal to the requested amount and with the requested reference.

The full rules for verification are quite complex (and chain-dependent) and are available in the specification. The important point is that the request is confirmed if no transaction meeting the specified criteria is found in the search range.

Example

To produce a correct and thorough example that allows you to test everything properly, you need to be careful. Since you are proving a negative, any mistake during request preparation can result in a transaction that was not made (a simple mis-encoding of a memo field would almost certainly produce a non-existing transaction) and give a false sense of security.

To ensure accuracy, structure your request as follows:

  1. Create a transaction with a reference payment and some nonzero value.
  2. Confirm Payment attestation request to make sure you get back the correct reference and value, ensuring the transaction is seen. Use the information about when this transaction happened to construct a range for the next step, ensuring it contains your transaction.
  3. Make three requests for non-existing payments:
    • Correct (or lower) value and correct reference: This should return INVALID, as the verifier can't prove the non-existence of such a transaction.
    • Correct value but slightly wrong payment reference: Change just one index of the reference. This should be confirmed, as no such transaction exists (the payment reference does not match).
    • Too large value but correct payment reference: This should be confirmed, as the transaction with the payment reference exists but does not transfer enough value.

XRP Ledger

The example code that showcases this on testnet XRP Ledger is available in tryXRPLPaymentNonExistence.ts.

tryXRPLPaymentNonExistence.ts
const xrpl = require("xrpl");

const { XRPL_PRIVATE_KEY, ATTESTATION_URL, ATTESTATION_API_KEY } = process.env;
const receiverAddress = "r9RLXvWuRro3RX33pk4xsN58tefYZ8Tvbj";

function toHex(data: string): string {
var result = "";
for (var i = 0; i < data.length; i++) {
result += data.charCodeAt(i).toString(16);
}
return "0x" + result.padEnd(64, "0");
}

function fromHex(data: string): string {
data = data.replace(/^(0x\.)/, "");
return data
.split(/(\w\w)/g)
.filter((p) => !!p)
.map((c) => String.fromCharCode(parseInt(c, 16)))
.join("");
}

async function prepareAttestationResponse(
attestationType: string,
network: string,
sourceId: string,
requestBody: any,
): Promise<AttestationResponse> {
const response = await fetch(
`${ATTESTATION_URL}/verifier/${network}/${attestationType}/prepareResponse`,
{
method: "POST",
headers: {
"X-API-KEY": ATTESTATION_API_KEY as string,
"Content-Type": "application/json",
},
body: JSON.stringify({
attestationType: toHex(attestationType),
sourceId: toHex(sourceId),
requestBody: requestBody,
}),
},
);
const data = await response.json();
return data;
}

async function getXRPLclient(): Promise<any> {
const client = new xrpl.Client("wss://s.altnet.rippletest.net:51233");
await client.connect();

return client;
}

async function sendXRPLTransaction(
message: string = "",
amount: number = 10,
target: string = "r9RLXvWuRro3RX33pk4xsN58tefYZ8Tvbj",
): Promise<string> {
const client = await getXRPLclient();

const test_wallet = xrpl.Wallet.fromSeed(XRPL_PRIVATE_KEY);

// Standard payment reference must be 32 bytes - so we right pad with 0
const MemoData = xrpl.convertStringToHex(message).padEnd(64, "0");
const MemoType = xrpl.convertStringToHex("Text");
const MemoFormat = xrpl.convertStringToHex("text/plain");

let memos = [];
if (message) {
memos.push({
Memo: {
MemoType: MemoType,
MemoData: MemoData,
MemoFormat: MemoFormat,
},
});
}

const transaction = await client.autofill({
TransactionType: "Payment",
Account: test_wallet.address,
Amount: amount.toString(),
Destination: target,
Memos: memos,
});

const signed = test_wallet.sign(transaction);
console.log(
`See transaction at https://testnet.xrpl.org/transactions/${signed.hash}`,
);
await client.submitAndWait(signed.tx_blob);

await client.disconnect();

// sleep for 10 seconds to allow the transaction to be processed
await new Promise((resolve) => setTimeout(resolve, 10 * 1000));
console.log("Payment:");
// 1. prove the payment:
const resultPayment = await prepareAttestationResponse(
"Payment",
"xrp",
"testXRP",
{
transactionId: "0x" + signed.hash,
inUtxo: "0",
utxo: "0",
},
);

if (resultPayment.status != "VALID") {
console.log("Something wrong when confirming payment");
}
console.log(resultPayment);
if (
resultPayment.response.responseBody.standardPaymentReference !=
"0x" + MemoData
) {
console.log("Something wrong with message reference");
console.log(resultPayment.response.responseBody.standardPaymentReference);
console.log(MemoData);
}
if (
resultPayment.response.responseBody.receivingAddressHash !=
web3.utils.soliditySha3(target)
) {
console.log("Something wrong with target address hash");
}

// Get information about transaction: block and block timestamp -> we will need this to create the range, where the transaction has happened
console.log("Failing non existence proof:");
const blockNumber = Number(resultPayment.response.responseBody.blockNumber);
const blockTimestamp = Number(
resultPayment.response.responseBody.blockTimestamp,
);

const targetRange = {
minimalBlockNumber: (blockNumber - 5).toString(), // Search few block before
deadlineBlockNumber: (blockNumber + 1).toString(), // Search a few blocks after, but not too much, as they need to already be indexed by attestation clients
deadlineTimestamp: (blockTimestamp + 3).toString(), // Search a bit after
destinationAddressHash: web3.utils.soliditySha3(target), // The target address for transaction
};

// Try to verify non existence for a transaction and correct parameters
// This should not verify it

const resultFailedNonExistence = await prepareAttestationResponse(
"ReferencedPaymentNonexistence",
"xrp",
"testXRP",
{
...targetRange,
amount: amount.toString(),
standardPaymentReference: "0x" + MemoData,
},
);

console.log(resultFailedNonExistence);

if (resultFailedNonExistence.status != "INVALID") {
console.log("Something wrong with failed non existence");
}

console.log("Successful non existence proofs:");

// Change the memo field a bit and successfully prove non existence
let wrongMemoData = xrpl.convertStringToHex(message).padEnd(64, "1"); // We pad 1 instead of 0
const resultWrongMemoNonExistence = await prepareAttestationResponse(
"ReferencedPaymentNonexistence",
"xrp",
"testXRP",
{
...targetRange,
amount: amount.toString(),
standardPaymentReference: "0x" + wrongMemoData,
},
);

console.log(resultWrongMemoNonExistence);

if (resultWrongMemoNonExistence.status != "VALID") {
console.log("Something wrong with wrong memo non existence");
}

// Change the value and successfully prove non existence.

const resultWrongAmountNonExistence = await prepareAttestationResponse(
"ReferencedPaymentNonexistence",
"xrp",
"testXRP",
{
...targetRange,
amount: (amount + 1).toString(), // Increase the amount, so the transaction we made is now invalid
standardPaymentReference: "0x" + MemoData,
},
);

console.log(resultWrongAmountNonExistence);

if (resultWrongAmountNonExistence.status != "VALID") {
console.log("Something wrong with wrong amount non existence");
}
}

async function main() {
await sendXRPLTransaction("Hello world!");
}

main().then(() => process.exit(0));

Keep in mind, that the requested range can be quite large, so the verifiers might not be able to confirm the response (as they might not have the view of all blocks from minimalBlockNumber to firstOverflowBlockNumber), so the request might be rejected.

// See transaction at https://testnet.xrpl.org/transactions/C2B493B8AE2E3C105D004D8AFBB4AFB5CA758608504CCE895C9331291DA19D75
// Payment:
{
status: 'VALID',
response: {
attestationType: '0x5061796d656e7400000000000000000000000000000000000000000000000000',
sourceId: '0x7465737458525000000000000000000000000000000000000000000000000000',
votingRound: '0',
lowestUsedTimestamp: '1708830051',
requestBody: {
transactionId: '0xC2B493B8AE2E3C105D004D8AFBB4AFB5CA758608504CCE895C9331291DA19D75',
inUtxo: '0',
utxo: '0'
},
responseBody: {
blockNumber: '45680731',
blockTimestamp: '1708830051',
sourceAddressHash: '0xa1ca3089c3e9f4c6e9ccf2bfb65bcf3e9d7544a092c79d642d5d34a54e0267e1',
receivingAddressHash: '0x0555194538763da400394fc7184432e9a006565fa710392ea1a86486eb83920f',
intendedReceivingAddressHash: '0x0555194538763da400394fc7184432e9a006565fa710392ea1a86486eb83920f',
standardPaymentReference: '0x48656C6C6F20776F726C64210000000000000000000000000000000000000000',
spentAmount: '22',
intendedSpentAmount: '22',
receivedAmount: '10',
intendedReceivedAmount: '10',
oneToOne: true,
status: '0'
}
}
}
Failing non existence proof:
{ status: 'INVALID' }
Successful non existence proofs:
{
status: 'VALID',
response: {
attestationType: '0x5265666572656e6365645061796d656e744e6f6e6578697374656e6365000000',
sourceId: '0x7465737458525000000000000000000000000000000000000000000000000000',
votingRound: '0',
lowestUsedTimestamp: '1708830033',
requestBody: {
minimalBlockNumber: '45680726',
deadlineBlockNumber: '45680732',
deadlineTimestamp: '1708830054',
destinationAddressHash: '0x0555194538763da400394fc7184432e9a006565fa710392ea1a86486eb83920f',
amount: '10',
standardPaymentReference: '0x48656C6C6F20776F726C64211111111111111111111111111111111111111111'
},
responseBody: {
minimalBlockTimestamp: '45680726',
firstOverflowBlockNumber: '45680733',
firstOverflowBlockTimestamp: '1708830060'
}
}
}
{
status: 'VALID',
response: {
attestationType: '0x5265666572656e6365645061796d656e744e6f6e6578697374656e6365000000',
sourceId: '0x7465737458525000000000000000000000000000000000000000000000000000',
votingRound: '0',
lowestUsedTimestamp: '1708830033',
requestBody: {
minimalBlockNumber: '45680726',
deadlineBlockNumber: '45680732',
deadlineTimestamp: '1708830054',
destinationAddressHash: '0x0555194538763da400394fc7184432e9a006565fa710392ea1a86486eb83920f',
amount: '11',
standardPaymentReference: '0x48656C6C6F20776F726C64210000000000000000000000000000000000000000'
},
responseBody: {
minimalBlockTimestamp: '45680726',
firstOverflowBlockNumber: '45680733',
firstOverflowBlockTimestamp: '1708830060'
}
}
}

Address Validity

Full specification on GitHub.

And there is a sub-specification for each chain that specifies the rules for the address validity for each chain. Be careful: Bitcoin and Dogecoin have different rules for validity on the mainnet and testnet, so make sure to check the correct specification with the correct verifier.

This is a very simple attestation type that can prove that a string constitutes a valid address on the specified chain. Importantly, unlike the Payment type we saw in Part 1, this type does not require a transaction to be proven. It just offloads the computation of the address validity to the verifier so that expensive computation does not have to be done onchain.

This is useful if you want to ensure that the address is valid before using it in your protocol. For example, fAssets need to make sure that an address is valid before they can be used in the protocol, and this is a good way to offload difficult computation regarding when the bitcoin address is valid to offchain entities.

For more information on Bitcoin address validity, see the Bitcoin design guide glossary.

Type Definition

AddressValidity.sol
// SPDX-License-Identifier: MIT
pragma solidity >=0.7.6 <0.9;

/**
* @custom:name AddressValidity
* @custom:id 0x05
* @custom:supported BTC, DOGE, XRP, testBTC, testDOGE, testXRP
* @author Flare
* @notice An assertion whether a string represents a valid address on an external chain.
* @custom:verification The address is checked against all validity criteria of the chain with `sourceId`.
* Indicator of validity is provided.
* If the address is valid, its standard form and standard hash are computed.
* Validity criteria for each supported chain:
* - [BTC](/specs/attestations/external-chains/address-validity/BTC.md)
* - [DOGE](/specs/attestations/external-chains/address-validity/DOGE.md)
* - [XRPL](/specs/attestations/external-chains/address-validity/XRPL.md)
* @custom:lut `0xffffffffffffffff` ($2^{64}-1$ in hex)
*/
interface AddressValidity {
/**
* @notice Toplevel request
* @param attestationType ID of the attestation type.
* @param sourceId Id of the data source.
* @param messageIntegrityCode `MessageIntegrityCode` that is derived from the expected response.
* @param requestBody Data defining the request. Type (struct) and interpretation is determined by the `attestationType`.
*/
struct Request {
bytes32 attestationType;
bytes32 sourceId;
bytes32 messageIntegrityCode;
RequestBody requestBody;
}

/**
* @notice Toplevel response
* @param attestationType Extracted from the request.
* @param sourceId Extracted from the request.
* @param votingRound The ID of the Data Connector round in which the request was considered.
* @param lowestUsedTimestamp The lowest timestamp used to generate the response.
* @param requestBody Extracted from the request.
* @param responseBody Data defining the response. The verification rules for the construction of the response body and the type are defined per specific `attestationType`.
*/
struct Response {
bytes32 attestationType;
bytes32 sourceId;
uint64 votingRound;
uint64 lowestUsedTimestamp;
RequestBody requestBody;
ResponseBody responseBody;
}

/**
* @notice Toplevel proof
* @param merkleProof Merkle proof corresponding to the attestation response.
* @param data Attestation response.
*/
struct Proof {
bytes32[] merkleProof;
Response data;
}

/**
* @notice Request body for AddressValidity attestation type
* @param addressStr Address to be verified.
*/
struct RequestBody {
string addressStr;
}

/**
* @notice Response body for AddressValidity attestation type
* @param isValid Boolean indicator of the address validity.
* @param standardAddress If `isValid`, standard form of the validated address. Otherwise an empty string.
* @param standardAddressHash If `isValid`, standard address hash of the validated address. Otherwise a zero bytes32 string.
*/
struct ResponseBody {
bool isValid;
string standardAddress;
bytes32 standardAddressHash;
}
}

The request body is very simple—it just contains the addressStr, which is the address to be verified according to the chain's rules.

The response body contains all the important details. Generally, the request can always be confirmed. Here are the specific fields to look at:

  • isValid: A boolean indicator of the address validity.
    • If this is true, the address is valid according to the chain's rules.
    • Remember, the Merkle proof is about the validity of this request (if it was confirmed by the verifiers), not about the meaning of its response—whether the address is valid or not.
  • standardAddress: If isValid, this is the standard form of the validated address; otherwise, it is an empty string.
    • This is useful if you want to use the address in your protocol, as you can use the standard form of the address and not worry about different representations of the same address.
  • standardAddressHash: If isValid, this is the standard address hash of the validated address; otherwise, it is a zero bytes32 string.
    • This is useful to verify with the standard address hash returned by Payment and ReferencedPaymentNonexistence.

Think of this as an example of what can be offloaded to offchain computation (and verification). Try to imagine other tasks that are prohibitively expensive (or impossible due to data unavailability) onchain that can be offloaded to offchain computation.

Example

The script for address validity (tryAddressValidity.ts) is simpler than the scripts we've seen so far. There's no need to create a transaction or anything complex; you just call the prepareResponse endpoint and see the result. In actual use, you will first prepare a request for the Data Connector, wait for it to be confirmed, and then use the response in your smart contract together with the proof. This means your smart contract will receive the result of a potentially large and expensive calculation (the response body part) along with proof that it was included in the Merkle root. This attests that it has been calculated and verified by the network validator.

This setup ensures that your smart contract gets a verified result without having to perform the expensive calculation onchain, thereby saving computational resources and improving efficiency.

tryAddressValidity.ts
const { ATTESTATION_URL, ATTESTATION_API_KEY } = process.env;
const exampleXRPLAddress = "r9RLXvWuRro3RX33pk4xsN58tefYZ8Tvbj";
const someDogecoinAddress = "njyMWWyh1L7tSX6QkWRgetMVCVyVtfoDta";
const someBTCAddress = "tb1qq3fm2kdklehk545c5rgfxzfhe7ph5tt640cayu";

function toHex(data: string): string {
var result = "";
for (var i = 0; i < data.length; i++) {
result += data.charCodeAt(i).toString(16);
}
return "0x" + result.padEnd(64, "0");
}

function fromHex(data: string): string {
data = data.replace(/^(0x\.)/, "");
return data
.split(/(\w\w)/g)
.filter((p) => !!p)
.map((c) => String.fromCharCode(parseInt(c, 16)))
.join("");
}

async function prepareAttestationResponse(
attestationType: string,
network: string,
sourceId: string,
requestBody: any,
): Promise<AttestationResponse> {
const response = await fetch(
`${ATTESTATION_URL}/verifier/${network}/${attestationType}/prepareResponse`,
{
method: "POST",
headers: {
"X-API-KEY": ATTESTATION_API_KEY as string,
"Content-Type": "application/json",
},
body: JSON.stringify({
attestationType: toHex(attestationType),
sourceId: toHex(sourceId),
requestBody: requestBody,
}),
},
);
const data = await response.json();
return data;
}

async function main() {
console.log(
await prepareAttestationResponse("AddressValidity", "xrp", "testXRP", {
addressStr: exampleXRPLAddress,
}),
);
console.log(
await prepareAttestationResponse("AddressValidity", "xrp", "testXRP", {
addressStr: "0xhahahahaha",
}),
);
console.log(
await prepareAttestationResponse("AddressValidity", "xrp", "testXRP", {
addressStr: "Hello world!",
}),
);

console.log(
await prepareAttestationResponse("AddressValidity", "btc", "testBTC", {
addressStr: someBTCAddress,
}),
);
console.log(
await prepareAttestationResponse("AddressValidity", "btc", "testBTC", {
addressStr: "0xhahahahaha",
}),
);
console.log(
await prepareAttestationResponse("AddressValidity", "btc", "testBTC", {
addressStr: "Hello world!",
}),
);

console.log(
await prepareAttestationResponse("AddressValidity", "doge", "testDOGE", {
addressStr: someDogecoinAddress,
}),
);
console.log(
await prepareAttestationResponse("AddressValidity", "doge", "testDOGE", {
addressStr: "0xhahahahaha",
}),
);
console.log(
await prepareAttestationResponse("AddressValidity", "doge", "testDOGE", {
addressStr: "Hello world!",
}),
);
}

main().then(() => process.exit(0));

and the response

{
status: 'VALID',
response: {
attestationType: '0x4164647265737356616c69646974790000000000000000000000000000000000',
sourceId: '0x7465737458525000000000000000000000000000000000000000000000000000',
votingRound: '0',
lowestUsedTimestamp: '0xffffffffffffffff',
requestBody: { addressStr: 'r9RLXvWuRro3RX33pk4xsN58tefYZ8Tvbj' },
responseBody: {
isValid: true,
standardAddress: 'r9RLXvWuRro3RX33pk4xsN58tefYZ8Tvbj',
standardAddressHash: '0x0555194538763da400394fc7184432e9a006565fa710392ea1a86486eb83920f'
}
}
}
{
status: 'VALID',
response: {
attestationType: '0x4164647265737356616c69646974790000000000000000000000000000000000',
sourceId: '0x7465737458525000000000000000000000000000000000000000000000000000',
votingRound: '0',
lowestUsedTimestamp: '0xffffffffffffffff',
requestBody: { addressStr: '0xhahahahaha' },
responseBody: {
isValid: false,
standardAddress: '',
standardAddressHash: '0x0000000000000000000000000000000000000000000000000000000000000000'
}
}
}
{
status: 'VALID',
response: {
attestationType: '0x4164647265737356616c69646974790000000000000000000000000000000000',
sourceId: '0x7465737458525000000000000000000000000000000000000000000000000000',
votingRound: '0',
lowestUsedTimestamp: '0xffffffffffffffff',
requestBody: { addressStr: 'Hello world!' },
responseBody: {
isValid: false,
standardAddress: '',
standardAddressHash: '0x0000000000000000000000000000000000000000000000000000000000000000'
}
}
}
{
status: 'VALID',
response: {
attestationType: '0x4164647265737356616c69646974790000000000000000000000000000000000',
sourceId: '0x7465737442544300000000000000000000000000000000000000000000000000',
votingRound: '0',
lowestUsedTimestamp: '0xffffffffffffffff',
requestBody: { addressStr: 'tb1qq3fm2kdklehk545c5rgfxzfhe7ph5tt640cayu' },
responseBody: {
isValid: true,
standardAddress: 'tb1qq3fm2kdklehk545c5rgfxzfhe7ph5tt640cayu',
standardAddressHash: '0x085f152e9e9ebd6c009827678785b1b3667733fa3f6b5d78bb462bd1978825ff'
}
}
}
{
status: 'VALID',
response: {
attestationType: '0x4164647265737356616c69646974790000000000000000000000000000000000',
sourceId: '0x7465737442544300000000000000000000000000000000000000000000000000',
votingRound: '0',
lowestUsedTimestamp: '0xffffffffffffffff',
requestBody: { addressStr: '0xhahahahaha' },
responseBody: {
isValid: false,
standardAddress: '',
standardAddressHash: '0x0000000000000000000000000000000000000000000000000000000000000000'
}
}
}
{
status: 'VALID',
response: {
attestationType: '0x4164647265737356616c69646974790000000000000000000000000000000000',
sourceId: '0x7465737442544300000000000000000000000000000000000000000000000000',
votingRound: '0',
lowestUsedTimestamp: '0xffffffffffffffff',
requestBody: { addressStr: 'Hello world!' },
responseBody: {
isValid: false,
standardAddress: '',
standardAddressHash: '0x0000000000000000000000000000000000000000000000000000000000000000'
}
}
}
{
status: 'VALID',
response: {
attestationType: '0x4164647265737356616c69646974790000000000000000000000000000000000',
sourceId: '0x74657374444f4745000000000000000000000000000000000000000000000000',
votingRound: '0',
lowestUsedTimestamp: '0xffffffffffffffff',
requestBody: { addressStr: 'njyMWWyh1L7tSX6QkWRgetMVCVyVtfoDta' },
responseBody: {
isValid: true,
standardAddress: 'njyMWWyh1L7tSX6QkWRgetMVCVyVtfoDta',
standardAddressHash: '0xfc8d6252c5132f771fc711fe13cb3c6e768ed9290ce199efd87d5ec1b6094df6'
}
}
}
{
status: 'VALID',
response: {
attestationType: '0x4164647265737356616c69646974790000000000000000000000000000000000',
sourceId: '0x74657374444f4745000000000000000000000000000000000000000000000000',
votingRound: '0',
lowestUsedTimestamp: '0xffffffffffffffff',
requestBody: { addressStr: '0xhahahahaha' },
responseBody: {
isValid: false,
standardAddress: '',
standardAddressHash: '0x0000000000000000000000000000000000000000000000000000000000000000'
}
}
}
{
status: 'VALID',
response: {
attestationType: '0x4164647265737356616c69646974790000000000000000000000000000000000',
sourceId: '0x74657374444f4745000000000000000000000000000000000000000000000000',
votingRound: '0',
lowestUsedTimestamp: '0xffffffffffffffff',
requestBody: { addressStr: 'Hello world!' },
responseBody: {
isValid: false,
standardAddress: '',
standardAddressHash: '0x0000000000000000000000000000000000000000000000000000000000000000'
}
}
}

One might ask what use such an attestation type is and why all the checks are necessary. Consider it in two ways:

  • Data Contains Request and Response: This setup makes it possible to observe both the input (request) and output (response) of the computation. The computation can be very complex and expensive, but for our purposes, we only need to know the result (and, of course, the input we want to be observed), and we can act on it.
  • Merkle Proof: The Merkle proof is used to verify that the response was included in the committed root and was confirmed by the verifiers. The "being confirmed" part is crucial because it means the verifiers have indeed seen the request, performed the computation (arriving at the same result), and included the result in the Merkle root, which we base our actions on.

This approach ensures that complex and resource-intensive computations can be performed offchain while still providing verifiable and trustworthy results that can be used onchain.

Conclusion

Congratulations — you made it this far! Now you see what the Data Connector can do, how to use it, and some details you need to be careful about. As usual, check the repository for the full code and try to play around.

In Part 3, we will explore how information from EVM chains can be relayed and what we can do with it.

A word of warning: while it might be tempting to save the whole proof structure in your smart contract (if you want to do some later operations), this is terribly inefficient from a gas standpoint, as you are writing a lot of data to memory and decoding nested structures is expensive. Additionally, since the structures are nested, even operating on them when in memory (or copying them from calldata to memory) generates large bytecode, which makes contract deployment more expensive or even impossible if you exceed the limit.