Attestation types

March. 15. 2024

INTERMEDIATE
STATE CONNECTOR
FLARE
ATTESTATION TYPES

In the previous blog post, we have covered the basics of the State Connector protocol and how to use it to prove a payment on an external chain. If you haven't done so already, I recommend reading it first, as we will be building upon the knowledge from it.

This time, we will explore the other attestation types that are available in the State Connector protocol and see what else can be achieved with it.

We will take a look at the following attestation types (all currently active types are listed in the spec):

  • AddressValidity: Prove that the string constitutes a valid address on the specified chain. This is useful if you want to make sure that the address is valid before using it in your protocol.
  • BalanceDecreasingTransaction: Prove that the transaction decreased the balance of the address or that the address was the initiator of the transaction.
  • ConfirmedBLockHeightExists: Prove that block with a specified height is confirmed by a specified number of confirmations and also provide some additional details about the chain block production rate.
  • ReferencePaymentNonexistence: Prove that the payment with the specified reference does not exist on the specified chain. This is useful if you need to prove that someone didn't pay you - did not honor the payment you have requested.

The specification also includes EVMTransaction, but this one is more complicated and powerful, and we will cover it in a separate blog post.

Each of the types is designed to prove a specific thing about - sometimes about transactions, sometimes about blocks, sometimes just to offload an expensive computation off the chain and have the result available on chain. The team has carefully studied the most important use cases and designed the attestation types to be safe, well-defined (to make sure, they are confirmed and understood in a non-ambiguous way), and efficient.

Each of the types has an associated off-chain verifier, that is able to deconstruct the encoded request, execute the verification procedure and return the proper response. To find out more, how verifiers work, you can read the verifier spec. As with the payment type, each attestation type comes with a verification contract, that is able to verify, that the response and the Merkle proof are correct and that the response was indeed included in the Merkle tree for the round. The contracts are available in the specification repository, but as in the previous blog, we will be using the ones already deployed and made available by the periphery library. Don't forget, verifying the proof is just the first part - this tells you, that a request was indeed included in the Merkle tree, but you still need to verify, that the response matches your dapp's requirements (payment is large enough, the address is correct, the transaction was successful, time range is sufficient).

As usual, the whole block with full code is available in the GitHub repository, and you are encouraged to follow if and try it out on your own.

In the previous blog, we have seen, how the whole process works:

  1. Observe, that something you want has happened
  2. Prepare request for the attestation (using your attestation client API)
  3. Submit the request to the State Connector
  4. Wait for the State Connector to confirm the request
  5. Once the request is confirmed, prepare the response and use it
  • Verify, that the response is really included in the state connector root (use the verification contract)
  • Validate, that the response is the one you expect - correct payment, correct address...

The process is the same for all the attestation types, so we will not be repeating the whole process for each type, but we will go through each of the types and see how to prepare the request for them and what we get in return.

Remember, your best friend in that case is the prepareResponse endpoint, which returns the full response - without the proof. But this is enough to get the idea on how response looks and see that you get all the correct information.

Attestation client API

Before jumping into different attestation types, we should take some time to explore the generated swagger for our API (you can get details on how swagger works here).

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

Let's first try to prepare a request for the Payment type (which we already explored in the previous blog post) and see what we get in return. Pick some btc transaction from the block explorer and prepare the request for it. If you are doing this on a test network, the sourceId in the example request is wrong (it should be testBTC), but if you try to execute a request with wrong sourceId, the attestation client will reject it, and you will get a nice error message on what you should fix.

Apart from the available attestation types, you can also see the diagnostic endpoints, that allow you to get information about the chain the attestation client is observing.

The endpoints are:

  • state - returns the current state of the attestation client (what blocks are indexed, tip height, etc.)
  • block-range - returns the range of blocks the attestation client is currently observing
  • transaction - Returns full information about transaction with the specified ID (do not prefix it with 0x). Try it with a random transaction ID and marvel at all the information you get in return.
  • block - Returns full information about block with the specified hash. Try it with the current tip height and see what you get in return.
  • confirmedBlockAt - Same as block by hash, but you specify the block number.
  • blockHeight - Returns the current tip height of the chain (due to the required amount of confirmations, the tip height might be different that the block range). This is the height of latest block that has been observed, but is not confirmed by the required number of confirmations.
  • transactionBlock - Returns the block information in which 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).

The method here are not necessary to use the State connector, but they are very useful for debugging and getting information about the chain and will prove very useful when you are building your dapp.

Attestation types

The first step is to prepare the request for the attestation. They are prepared in very similar manner, so we first prepare a simple function that will be able to prepare the request for any attestation type.

To make matters simpler, we will just check what the response would be directly and not go through the whole proof process.

1// The same function can also be found in State Connector utils bundled with the artifact periphery package (`encodeAttestationName`)
2
3// Simple hex encoding
4function toHex(data: string): string {
5 var result = '';
6 for (var i = 0; i < data.length; i++) {
7 result += data.charCodeAt(i).toString(16);
8 }
9 return '0x' + result.padEnd(64, '0');
10}
11
12interface AttestationResponse {
13 abiEncodedRequest: string;
14 status: string;
15}
16
17async function prepareAttestationRequest(attestationType: string, network: string, sourceId: string, requestBody: any): Promise<AttestationRequest> {
18 const response = await fetch(`${ATTESTATION_URL}/verifier/${network}/${attestationType}/prepareRequest`, {
19 method: 'POST',
20 headers: { 'X-API-KEY': ATTESTATION_API_KEY as string, 'Content-Type': 'application/json' },
21 body: JSON.stringify({
22 attestationType: toHex(attestationType),
23 sourceId: toHex(sourceId),
24 requestBody: requestBody,
25 }),
26 });
27 const data = await response.json();
28 return data;
29}
30
31async function prepareAttestationResponse(attestationType: string, network: string, sourceId: string, requestBody: any): Promise<any> {
32 const response = await fetch(`${ATTESTATION_URL}/verifier/${network}/${attestationType}/prepareResponse`, {
33 method: 'POST',
34 headers: { 'X-API-KEY': ATTESTATION_API_KEY as string, 'Content-Type': 'application/json' },
35 body: JSON.stringify({
36 attestationType: toHex(attestationType),
37 sourceId: toHex(sourceId),
38 requestBody: requestBody,
39 }),
40 });
41 const data = await response.json();
42 return data;
43}

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

Remember, we will be working on coston testnet, so we will be using testBTC as the sourceID (in the body of attestation type). But as the verifiers are set up for a single deployment, testnet verifiers automatically look at testnets and mainnet verifiers at mainnets, so we specify BTC as the network in the request URL. If we wanted to change the network type we look at, we would have to change the attestation url to point to mainnet verifiers.

Similarly, verifier contracts (the ones, that check the response together with the Merkle proof is included in the state connector round) are very similar, the only difference is the type the verification function receives (and thus the type they verify), but then 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 and see how to prepare the request for them, what we need to provide and what we get in return.

Balance Decreasing Transaction

Full specification is available here.

This attestation type is designed to prove that the transaction either decreases the balance for some address or is signed by the source address. Where would one need this? One of the purposes of the State Connector is to provide connectivity between different blockchains and allow for the use of the information from one chain on another chain. Other chains do not necessarily have smart contract capability or support any kind "fund locking" and unlocking based on some conditions. This is where the State Connector comes to play, as it allow the Flare network to monitor (and police) address on another chain and act upon the changes in the balance of the address. This way, we can have an address on Bitcoin network that acts as a vault (think fAssets) and in case the address owner violates the agreement and send the funds out, State Connector can detect it. To make this even more secure and not tied to single chain, the attestation type makes very little assumptions about the violating transaction. It is enough for the transaction and address to be considered "offending" if the balance of the designated address is lower after the transaction has executed or the address is among the signers of the transaction (even if its balance is greater than before the transaction).

This way, we can track the balance decrease of address even if the balance change comes from a complicated transaction (multisig, complex scripts, specific XRPL transactions where a non participating address can have funds removed...).

The type definition is as follows:

1// SPDX-License-Identifier: MIT
2pragma solidity >=0.7.6 <0.9;
3
4/**
5 * @custom:name BalanceDecreasingTransaction
6 * @custom:id 0x02
7 * @custom:supported BTC, DOGE, XRP, testBTC, testDOGE, testXRP
8 * @author Flare
9 * @notice A detection of a transaction that either decreases the balance for some address or is signed by the source address.
10 * 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.
11 *
12 * 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).
13 * @custom:verification The transaction with `transactionId` is fetched from the API of the source blockchain node or relevant indexer.
14 * 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.
15 *
16 * Once the transaction is received, the response fields are extracted if the transaction is balance decreasing for the indicated address.
17 * Some of the request and response fields are chain specific as described below.
18 * The fields can be computed with the help of a [balance decreasing summary](/specs/attestations/external-chains/transactions.md#balance-decreasing-summary).
19 *
20 * ### UTXO (Bitcoin and Dogecoin)
21 *
22 * - `sourceAddressIndicator` is the the index of the transaction input in hex padded to a 0x prefixed 32-byte string.
23 * If the indicated input does not exist or the indicated input does not have the address, the attestation request is rejected.
24 * The `sourceAddress` is the address of the indicated transaction input.
25 * - `spentAmount` is the sum of values of all inputs with sourceAddress minus the sum of all outputs with `sourceAddress`.
26 * Can be negative.
27 * - `blockTimestamp` is the mediantime of a block.
28 *
29 * ### XRPL
30 *
31 * - `sourceAddressIndicator` is the [standard address hash](/specs/attestations/external-chains/standardAddress.md#standard-address-hash) of the address whose balance has been decreased.
32 * 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.
33 *
34 * - `spentAmount` is the difference between the balance of the indicated address after and before the transaction.
35 * Can be negative.
36 * - `blockTimestamp` is the close_time of a ledger converted to unix time.
37 *
38 * @custom:lut `blockTimestamp`
39 */
40interface BalanceDecreasingTransaction {
41 /**
42 * @notice Toplevel request
43 * @param attestationType ID of the attestation type.
44 * @param sourceId ID of the data source.
45 * @param messageIntegrityCode `MessageIntegrityCode` that is derived from the expected response.
46 * @param requestBody Data defining the request. Type (struct) and interpretation is determined by the `attestationType`.
47 */
48 struct Request {
49 bytes32 attestationType;
50 bytes32 sourceId;
51 bytes32 messageIntegrityCode;
52 RequestBody requestBody;
53 }
54
55 /**
56 * @notice Toplevel response
57 * @param attestationType Extracted from the request.
58 * @param sourceId Extracted from the request.
59 * @param votingRound The ID of the State Connector round in which the request was considered. This is a security measure to prevent a collision of attestation hashes.
60 * @param lowestUsedTimestamp The lowest timestamp used to generate the response.
61 * @param requestBody Extracted from the request.
62 * @param responseBody Data defining the response. The verification rules for the construction of the response body and the type are defined per specific `attestationType`.
63 */
64 struct Response {
65 bytes32 attestationType;
66 bytes32 sourceId;
67 uint64 votingRound;
68 uint64 lowestUsedTimestamp;
69 RequestBody requestBody;
70 ResponseBody responseBody;
71 }
72
73 /**
74 * @notice Toplevel proof
75 * @param merkleProof Merkle proof corresponding to the attestation response.
76 * @param data Attestation response.
77 */
78 struct Proof {
79 bytes32[] merkleProof;
80 Response data;
81 }
82
83 /**
84 * @notice Request body for BalanceDecreasingTransaction attestation type
85 * @param transactionId ID of the payment transaction.
86 * @param sourceAddressIndicator The indicator of the address whose balance has been decreased.
87 */
88 struct RequestBody {
89 bytes32 transactionId;
90 bytes32 sourceAddressIndicator;
91 }
92
93 /**
94 * @notice Response body for BalanceDecreasingTransaction attestation type.
95 * @param blockNumber The number of the block in which the transaction is included.
96 * @param blockTimestamp The timestamp of the block in which the transaction is included.
97 * @param sourceAddressHash Standard address hash of the address indicated by the `sourceAddressIndicator`.
98 * @param spentAmount Amount spent by the source address in minimal units.
99 * @param standardPaymentReference Standard payment reference of the transaction.
100 */
101 struct ResponseBody {
102 uint64 blockNumber;
103 uint64 blockTimestamp;
104 bytes32 sourceAddressHash;
105 int256 spentAmount;
106 bytes32 standardPaymentReference;
107 }
108}

The request body consist 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 as inUtxo in 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, do full accounting of the requested source address and confirm the response if and only if the transaction is indeed decreasing 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, so there is no way to have a false positive.

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

  • 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 - standard address hash of the address indicated by the sourceAddressIndicator. If on utxo chain, this gives us the address that controlled the designated input.
  • spentAmount - 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 - 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

1// SPDX-License-Identifier: MIT
2pragma solidity 0.8.20;
3
4import "../../interface/types/BalanceDecreasingTransaction.sol";
5import "../../interface/external/IMerkleRootStorage.sol";
6import "./interface/IBalanceDecreasingTransactionVerification.sol";
7import {MerkleProof} from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
8
9contract BalanceDecreasingTransactionVerification is IBalanceDecreasingTransactionVerification {
10 using MerkleProof for bytes32[];
11
12 IMerkleRootStorage public immutable merkleRootStorage;
13
14 constructor(IMerkleRootStorage _merkleRootStorage) {
15 merkleRootStorage = _merkleRootStorage;
16 }
17
18 function verifyBalanceDecreasingTransaction(
19 BalanceDecreasingTransaction.Proof calldata _proof
20 ) external view returns (bool _proved) {
21 return _proof.data.attestationType == bytes32("BalanceDecreasingTransaction") &&
22 _proof.merkleProof.verify(
23 merkleRootStorage.merkleRoot(_proof.data.votingRound),
24 keccak256(abi.encode(_proof.data))
25 );
26 }
27}

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 - in exactly the same way as with the payment type. And all the others are very similar, just the type we verify is different. Importantly, the verification contract just checks that this proof indeed proves, that the structure we requested was included in specific round, it does not make any assumptions about the response itself. The response itself should be checked by the dapp to make sure, it is the one you expect. In some cases, the verifiers will not even confirm response (as there is no such confirmation), but in this case, they might confirm the response, but 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 whole 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 said before, specifying address is important, since address' balance might have decreased in the transaction, but its participation was only minimal (or was not even part of the initial signers). For the utxo chains, we also need to specify sourceAddressIndicator, as many addresses might be involved in the transaction (by signing an array of outputs) and we need to specify which one we want to prove the balance decrease for and request the verifiers to do the whole accounting.

1const xrpl = require('xrpl');
2
3const { XRPL_PRIVATE_KEY, ATTESTATION_URL, ATTESTATION_API_KEY, USE_TESTNET_ATTESTATIONS } = process.env;
4const receiverAddress = 'r9RLXvWuRro3RX33pk4xsN58tefYZ8Tvbj';
5
6function toHex(data: string): string {
7 var result = '';
8 for (var i = 0; i < data.length; i++) {
9 result += data.charCodeAt(i).toString(16);
10 }
11 return '0x' + result.padEnd(64, '0');
12}
13
14function fromHex(data: string): string {
15 data = data.replace(/^(0x\.)/, '');
16 return data
17 .split(/(\w\w)/g)
18 .filter((p) => !!p)
19 .map((c) => String.fromCharCode(parseInt(c, 16)))
20 .join('');
21}
22
23async function prepareAttestationResponse(attestationType: string, network: string, sourceId: string, requestBody: any): Promise<AttestationResponse> {
24 const response = await fetch(`${ATTESTATION_URL}/verifier/${network}/${attestationType}/prepareResponse`, {
25 method: 'POST',
26 headers: { 'X-API-KEY': ATTESTATION_API_KEY as string, 'Content-Type': 'application/json' },
27 body: JSON.stringify({
28 attestationType: toHex(attestationType),
29 sourceId: toHex(sourceId),
30 requestBody: requestBody,
31 }),
32 });
33 const data = await response.json();
34 return data;
35}
36
37async function getXRPLclient(): Promise<any> {
38 const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233');
39 await client.connect();
40
41 return client;
42}
43
44async function sendXRPLTransaction(message: string = '', amount: number = 10, target: string = 'r9RLXvWuRro3RX33pk4xsN58tefYZ8Tvbj'): Promise<string> {
45 const client = await getXRPLclient();
46
47 const test_wallet = xrpl.Wallet.fromSeed(XRPL_PRIVATE_KEY);
48
49 let memos = [];
50 if (message) {
51 // Standard payment reference must be 32 bytes - so we right pad with 0
52 const MemoData = xrpl.convertStringToHex(message).padEnd(64, '0');
53 const MemoType = xrpl.convertStringToHex('Text');
54 const MemoFormat = xrpl.convertStringToHex('text/plain');
55
56 memos.push({
57 Memo: {
58 MemoType: MemoType,
59 MemoData: MemoData,
60 MemoFormat: MemoFormat,
61 },
62 });
63 }
64
65 const transaction = await client.autofill({
66 TransactionType: 'Payment',
67 Account: test_wallet.address,
68 Amount: amount.toString(),
69 Destination: target,
70 Memos: memos,
71 });
72
73 const signed = test_wallet.sign(transaction);
74 console.log(`See transaction at https://testnet.xrpl.org/transactions/${signed.hash}`);
75 await client.submitAndWait(signed.tx_blob);
76
77 await client.disconnect();
78
79 // sleep for 10 seconds to allow the transaction to be processed
80 await new Promise((resolve) => setTimeout(resolve, 10 * 1000));
81
82 const result = await prepareAttestationResponse('BalanceDecreasingTransaction', 'xrp', 'testXRP', {
83 transactionId: '0x' + signed.hash,
84 sourceAddressIndicator: web3.utils.soliditySha3(test_wallet.address),
85 });
86
87 console.log(result);
88
89 console.log(fromHex(result.response.responseBody.standardPaymentReference));
90}
91
92async function main() {
93 await sendXRPLTransaction('Hello world!');
94}
95
96main().then(() => process.exit(0));

We create a transaction, wait for it to be processed and then prepare response that checks, that it was really a balance decreasing transaction.

An example response would look like this:

1{
2 "status": "VALID",
3 "response": {
4 "attestationType": "0x42616c616e636544656372656173696e675472616e73616374696f6e00000000",
5 "sourceId": "0x7465737458525000000000000000000000000000000000000000000000000000",
6 "votingRound": "0",
7 "lowestUsedTimestamp": "1708671652",
8 "requestBody": {
9 "transactionId": "0xB40C7540D8393D389AAF6006C0429608ADD871C0CA3174B72EA55776D885B77B",
10 "sourceAddressIndicator": "0xa1ca3089c3e9f4c6e9ccf2bfb65bcf3e9d7544a092c79d642d5d34a54e0267e1"
11 }, "responseBody": {
12 "blockNumber": "45629840",
13 "blockTimestamp": "1708671652",
14 "sourceAddressHash": "0xa1ca3089c3e9f4c6e9ccf2bfb65bcf3e9d7544a092c79d642d5d34a54e0267e1",
15 "spentAmount": "22",
16 "standardPaymentReference": "0x48656C6C6F20776F726C64210000000000000000000000000000000000000000"
17 }
18 }
19}
20Hello world!

All the fields are populated correctly and most importantly, although, the transaction sent 10 XRP drops, the response nicely shows, that the balance decreased by 22, as 12 was spent on transaction fee.

Confirmed Block Height Exists

Full specification is available here.

We now know how to observe generic transactions and balance decreasing transactions. It would be great, if there was a way to somehow get information about the block production rate on the external chain. This has multiple use cases - for example, you can check what is the current top block on the chain and then check if this might be near the timestamp that the transaction on external chain should happen. It is also a good way to observe if the other chain is progressing and not halted.

Let's see the type specification:

1// SPDX-License-Identifier: MIT
2pragma solidity >=0.7.6 <0.9;
3
4/**
5 * @custom:name ConfirmedBlockHeightExists
6 * @custom:id 0x03
7 * @custom:supported BTC, DOGE, XRP, testBTC, testDOGE, testXRP
8 * @author Flare
9 * @notice An assertion that a block with `blockNumber` is confirmed.
10 * It also provides data to compute the block production rate in the given time range.
11 * @custom:verification It is checked that the block with `blockNumber` is confirmed by at least `numberOfConfirmations`.
12 * If it is not, the request is rejected. We note a block on the tip of the chain is confirmed by 1 block.
13 * Then `lowestQueryWindowBlock` is determined and its number and timestamp are extracted.
14 *
15 *
16 * Current confirmation heights consensus:
17 *
18 *
19 * | `Chain` | `chainId` | `numberOfConfirmations` | `timestamp ` |
20 * | ------- | --------- | ----------------------- | ------------ |
21 * | `BTC` | 0 | 6 | mediantime |
22 * | `DOGE` | 2 | 60 | mediantime |
23 * | `XRP` | 3 | 3 | close_time |
24 *
25 *
26 *
27 *
28 * @custom:lut `lowestQueryWindowBlockTimestamp`
29 */
30interface ConfirmedBlockHeightExists {
31 /**
32 * @notice Toplevel request
33 * @param attestationType ID of the attestation type.
34 * @param sourceId ID of the data source.
35 * @param messageIntegrityCode `MessageIntegrityCode` that is derived from the expected response as defined.
36 * @param requestBody Data defining the request. Type (struct) and interpretation is determined by the `attestationType`.
37 */
38 struct Request {
39 bytes32 attestationType;
40 bytes32 sourceId;
41 bytes32 messageIntegrityCode;
42 RequestBody requestBody;
43 }
44
45 /**
46 * @notice Toplevel response
47 * @param attestationType Extracted from the request.
48 * @param sourceId Extracted from the request.
49 * @param votingRound The ID of the State Connector round in which the request was considered.
50 * @param lowestUsedTimestamp The lowest timestamp used to generate the response.
51 * @param requestBody Extracted from the request.
52 * @param responseBody Data defining the response. The verification rules for the construction of the response body and the type are defined per specific `attestationType`.
53 */
54 struct Response {
55 bytes32 attestationType;
56 bytes32 sourceId;
57 uint64 votingRound;
58 uint64 lowestUsedTimestamp;
59 RequestBody requestBody;
60 ResponseBody responseBody;
61 }
62
63 /**
64 * @notice Toplevel proof
65 * @param merkleProof Merkle proof corresponding to the attestation response.
66 * @param data Attestation response.
67 */
68 struct Proof {
69 bytes32[] merkleProof;
70 Response data;
71 }
72
73 /**
74 * @notice Request body for ConfirmedBlockHeightExistsType attestation type
75 * @param blockNumber The number of the block the request wants a confirmation of.
76 * @param queryWindow The length of the period in which the block production rate is to be computed.
77 */
78 struct RequestBody {
79 uint64 blockNumber;
80 uint64 queryWindow;
81 }
82
83 /**
84 * @notice Response body for ConfirmedBlockHeightExistsType attestation type
85 * @custom:below `blockNumber`, `lowestQueryWindowBlockNumber`, `blockTimestamp` and `lowestQueryWindowBlockTimestamp` can be used to compute the average block production time in the specified block range.
86 * @param blockTimestamp The timestamp of the block with `blockNumber`.
87 * @param numberOfConfirmations The depth at which a block is considered confirmed depending on the chain. All attestation providers must agree on this number.
88 * @param lowestQueryWindowBlockNumber The block number of the latest block that has a timestamp strictly smaller than `blockTimestamp` - `queryWindow`.
89 * @param lowestQueryWindowBlockTimestamp The timestamp of the block at height `lowestQueryWindowBlockNumber`.
90 */
91 struct ResponseBody {
92 uint64 blockTimestamp;
93 uint64 numberOfConfirmations;
94 uint64 lowestQueryWindowBlockNumber;
95 uint64 lowestQueryWindowBlockTimestamp;
96 }
97}

The request body is pretty simple. We provide the blockNumber we want to confirm exists on 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 we are requesting). Importantly, for the block to be considered visible, at least X blocks above must be confirmed - this ensures, that we do not confirm blocks that are not on the main chain. The numbers of confirmations are different for each chain and are listed in the specification.

What do we get in return? Remember, as per spec, we only get the information 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 the same as in the specification.
  • lowestQueryWindowBlockNumber - the block number of the latest block that has a timestamp strictly smaller than blockTimestamp - queryWindow. This allows us to gauge the average block production time in the specified block range.
  • lowestQueryWindowBlockTimestamp - the timestamp of the block at height lowestQueryWindowBlockNumber. So the time when the block was produced.

Example

What is the easiest way to see, how this works? Well, to check the top block, one would have to 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. We could also piggyback on the previous example, create a transaction and see the block it was included in and proceed from there on.

But we can cheat a bit and get information from the attestation providers. Each attestation provider also exposes a number of diagnostic endpoints, that allow us to get information about the chain it is operating on. The one that is interesting for us is the block-range endpoint, that returns the range of blocks the attestation provider is currently observing. And that is exactly what we will do - we will get the range of blocks the attestation provider is observing and then request the confirmation of the top block in the range.

Go, take the following code (also in tryConfirmedBlockHeightExists.ts) and try to see how prepareResponse fares for blocks, that are out of range for current confirmation limit.

1const { ATTESTATION_URL, ATTESTATION_API_KEY } = process.env;
2
3function toHex(data: string): string {
4 var result = '';
5 for (var i = 0; i < data.length; i++) {
6 result += data.charCodeAt(i).toString(16);
7 }
8 return '0x' + result.padEnd(64, '0');
9}
10
11function fromHex(data: string): string {
12 data = data.replace(/^(0x\.)/, '');
13 return data
14 .split(/(\w\w)/g)
15 .filter((p) => !!p)
16 .map((c) => String.fromCharCode(parseInt(c, 16)))
17 .join('');
18}
19
20async function prepareAttestationResponse(attestationType: string, network: string, sourceId: string, requestBody: any): Promise<AttestationResponse> {
21 const response = await fetch(`${ATTESTATION_URL}/verifier/${network}/${attestationType}/prepareResponse`, {
22 method: 'POST',
23 headers: { 'X-API-KEY': ATTESTATION_API_KEY as string, 'Content-Type': 'application/json' },
24 body: JSON.stringify({
25 attestationType: toHex(attestationType),
26 sourceId: toHex(sourceId),
27 requestBody: requestBody,
28 }),
29 });
30 const data = await response.json();
31 return data;
32}
33
34async function getVerifierBlockRange(network: string): Promise<any> {
35 return (
36 await (
37 await fetch(`${ATTESTATION_URL}/verifier/${network}/api/indexer/block-range`, {
38 method: 'GET',
39 headers: { 'X-API-KEY': ATTESTATION_API_KEY as string, 'Content-Type': 'application/json' },
40 })
41 ).json()
42 ).data;
43}
44
45async function main() {
46 const btcRange = await getVerifierBlockRange('btc');
47 const dogeRange = await getVerifierBlockRange('doge');
48 const xrplRange = await getVerifierBlockRange('xrp');
49
50 console.log('BTC Range: ', btcRange);
51 console.log(
52 await prepareAttestationResponse('ConfirmedBlockHeightExists', 'btc', 'testBTC', {
53 blockNumber: btcRange.last.toString(),
54 queryWindow: '123',
55 })
56 );
57
58 console.log('DOGE Range: ', dogeRange);
59 console.log(
60 await prepareAttestationResponse('ConfirmedBlockHeightExists', 'doge', 'testDOGE', {
61 blockNumber: dogeRange.last.toString(),
62 queryWindow: '123',
63 })
64 );
65
66 console.log('XRPL Range: ', xrplRange);
67 console.log(
68 await prepareAttestationResponse('ConfirmedBlockHeightExists', 'xrp', 'testXRP', {
69 blockNumber: xrplRange.last.toString(),
70 queryWindow: '123',
71 })
72 );
73}
74
75main().then(() => process.exit(0));

And we get the example response

1BTC Range: { first: 2578997, last: 2579392 }
2{
3 status: 'VALID',
4 response: {
5 attestationType: '0x436f6e6669726d6564426c6f636b486569676874457869737473000000000000',
6 sourceId: '0x7465737442544300000000000000000000000000000000000000000000000000',
7 votingRound: '0',
8 lowestUsedTimestamp: '1708812188',
9 requestBody: { blockNumber: '2579392', queryWindow: '123' },
10 responseBody: {
11 blockTimestamp: '1708812188',
12 numberOfConfirmations: '6',
13 lowestQueryWindowBlockNumber: '2579391',
14 lowestQueryWindowBlockTimestamp: '1708812020'
15 }
16 }
17}
18DOGE Range: { first: 5706001, last: 5974548 }
19{
20 status: 'VALID',
21 response: {
22 attestationType: '0x436f6e6669726d6564426c6f636b486569676874457869737473000000000000',
23 sourceId: '0x74657374444f4745000000000000000000000000000000000000000000000000',
24 votingRound: '0',
25 lowestUsedTimestamp: '1708819752',
26 requestBody: { blockNumber: '5974548', queryWindow: '123' },
27 responseBody: {
28 blockTimestamp: '1708819752',
29 numberOfConfirmations: '60',
30 lowestQueryWindowBlockNumber: '5974543',
31 lowestQueryWindowBlockTimestamp: '1708819511'
32 }
33 }
34}
35XRPL Range: { first: 45585486, last: 45678173 }
36{
37 status: 'VALID',
38 response: {
39 attestationType: '0x436f6e6669726d6564426c6f636b486569676874457869737473000000000000',
40 sourceId: '0x7465737458525000000000000000000000000000000000000000000000000000',
41 votingRound: '0',
42 lowestUsedTimestamp: '1708822152',
43 requestBody: { blockNumber: '45678173', queryWindow: '123' },
44 responseBody: {
45 blockTimestamp: '1708822152',
46 numberOfConfirmations: '1',
47 lowestQueryWindowBlockNumber: '45678132',
48 lowestQueryWindowBlockTimestamp: '1708822022'
49 }
50 }
51}

This attestation type is also useful to see another important thing - the INDETERMINATE response. What does that mean? It means that the attestation can't be confirmed (yet), as there is not enough confirmations for the block. In this case, the response is INDETERMINATE - so not confirmed, but it does indicate, that it might be valid in the future (it at least indicates that the attestation client can neither reject nor confirm it for sure).

Go, take the code and try to check for the block that is not yet confirmed by the correct amount and see the response you get. The easiest way is to just add 10 to the block range and see what happens. If you did it correctly, the response should be

1{
2 "status": "INDETERMINATE"
3}

One important thing to notice is that we are sending all numbers as strings (either decimal or hex). The main reason for this is that JavaScript does not have a native 64-bit integer type and the numbers are represented as 64-bit floating point numbers and any big numbers are not represented correctly. Even if block numbers are not that big, we are not taking any chances, and we always encode json numbers as strings, to be absolutely sure that the numbers are represented correctly.

Reference Payment Nonexistence

Full specification is available here.

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 bit more involved one - the ReferencePaymentNonexistence type.

This one is a bit more difficult to implement and properly use, as we are forcing the attestation client to do a lot of work - they need to prove that a certain payment has not been made. In that case, instead of looking at the transaction and checking if it is valid, we are going to be looking at the block range and checking that no valid payment conforming to our requirements has been made in the specified block range.

1// SPDX-License-Identifier: MIT
2pragma solidity >=0.7.6 <0.9;
3
4/**
5 * @custom:name ReferencedPaymentNonexistence
6 * @custom:id 0x04
7 * @custom:supported BTC, DOGE, XRP, testBTC, testDOGE, testXRP
8 * @author Flare
9 * @notice Assertion that an agreed-upon payment has not been made by a certain deadline.
10 * A confirmed request shows that a transaction meeting certain criteria (address, amount, reference) did not appear in the specified block range.
11 *
12 *
13 * 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.
14 *
15 * @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.
16 * If `firstOverflowBlockNumber` is higher or equal to `minimalBlockNumber`, the request is rejected.
17 * The search range are blocks between heights including `minimalBlockNumber` and excluding `firstOverflowBlockNumber`.
18 * If the verifier does not have a view of all blocks from `minimalBlockNumber` to `firstOverflowBlockNumber`, the attestation request is rejected.
19 *
20 * The request is confirmed if no transaction meeting the specified criteria is found in the search range.
21 * The criteria and timestamp are chain specific.
22 * ### UTXO (Bitcoin and Dogecoin)
23 *
24 *
25 * Criteria for the transaction:
26 *
27 *
28 * - It is not coinbase transaction.
29 * - The transaction has the specified [standardPaymentReference](/specs/attestations/external-chains/standardPaymentReference.md#btc-and-doge-blockchains).
30 * - 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).
31 *
32 *
33 * Timestamp is `mediantime`.
34
35 * ### XRPL
36 *
37 *
38 *
39 * Criteria for the transaction:
40 * - The transaction is of type payment.
41 * - The transaction has the specified [standardPaymentReference](/specs/attestations/external-chains/standardPaymentReference.md#xrp),
42 * - One of the following is true:
43 * - Transaction status is `SUCCESS` and the amount received by the specified destination address is greater than the specified `value`.
44 * - Transaction status is `RECEIVER_FAILURE` and the specified destination address would receive an amount greater than the specified `value` had the transaction been successful.
45 *
46 *
47 * Timestamp is `close_time` converted to UNIX time.
48 *
49 * @custom:lut `minimalBlockTimestamp`
50 */
51interface ReferencedPaymentNonexistence {
52 /**
53 * @notice Toplevel request
54 * @param attestationType ID of the attestation type.
55 * @param sourceId ID of the data source.
56 * @param messageIntegrityCode `MessageIntegrityCode` that is derived from the expected response as defined.
57 * @param requestBody Data defining the request. Type (struct) and interpretation is determined by the `attestationType`.
58 */
59 struct Request {
60 bytes32 attestationType;
61 bytes32 sourceId;
62 bytes32 messageIntegrityCode;
63 RequestBody requestBody;
64 }
65
66 /**
67 * @notice Toplevel response
68 * @param attestationType Extracted from the request.
69 * @param sourceId Extracted from the request.
70 * @param votingRound The ID of the State Connector round in which the request was considered.
71 * @param lowestUsedTimestamp The lowest timestamp used to generate the response.
72 * @param requestBody Extracted from the request.
73 * @param responseBody Data defining the response. The verification rules for the construction of the response body and the type are defined per specific `attestationType`.
74 */
75 struct Response {
76 bytes32 attestationType;
77 bytes32 sourceId;
78 uint64 votingRound;
79 uint64 lowestUsedTimestamp;
80 RequestBody requestBody;
81 ResponseBody responseBody;
82 }
83
84 /**
85 * @notice Toplevel proof
86 * @param merkleProof Merkle proof corresponding to the attestation response.
87 * @param data Attestation response.
88 */
89 struct Proof {
90 bytes32[] merkleProof;
91 Response data;
92 }
93
94 /**
95 * @notice Request body for ReferencePaymentNonexistence attestation type
96 * @param minimalBlockNumber The start block of the search range.
97 * @param deadlineBlockNumber The blockNumber to be included in the search range.
98 * @param deadlineTimestamp The timestamp to be included in the search range.
99 * @param destinationAddressHash The standard address hash of the address to which the payment had to be done.
100 * @param amount The requested amount in minimal units that had to be payed.
101 * @param standardPaymentReference The requested standard payment reference.
102 * @custom:below The `standardPaymentReference` should not be zero (as a 32-byte sequence).
103 */
104 struct RequestBody {
105 uint64 minimalBlockNumber;
106 uint64 deadlineBlockNumber;
107 uint64 deadlineTimestamp;
108 bytes32 destinationAddressHash;
109 uint256 amount;
110 bytes32 standardPaymentReference;
111 }
112
113 /**
114 * @notice Response body for ReferencePaymentNonexistence attestation type.
115 * @param minimalBlockTimestamp The timestamp of the minimalBlock.
116 * @param firstOverflowBlockNumber The height of the firstOverflowBlock.
117 * @param firstOverflowBlockTimestamp The timestamp of the firstOverflowBlock.
118 * @custom:below `firstOverflowBlock` is the first block that has block number higher than `deadlineBlockNumber` and timestamp later than `deadlineTimestamp`.
119 * The specified search range are blocks between heights including `minimalBlockNumber` and excluding `firstOverflowBlockNumber`.
120 */
121 struct ResponseBody {
122 uint64 minimalBlockTimestamp;
123 uint64 firstOverflowBlockNumber;
124 uint64 firstOverflowBlockTimestamp;
125 }
126}

The request body is a bit bigger this time, as we have to specify the range of blocks we want to check and the criteria for the payment we want to check.

  • minimalBlockNumber - the start block of the search range.
  • deadlineBlockNumber - the blockNumber to be included in the search range.
  • deadlineTimestamp - the timestamp to be included in the search range. As we include both block number and timestamp, the requested range will be the such, that it will include 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 had to be done.
  • amount - the requested amount in minimal units that had to be payed. The amount is chain specific.
  • standardPaymentReference - the requested standard payment reference. This is the reference that the payment had to have.

The response body is a bit simpler and essentially contains the searched range

  • minimalBlockTimestamp - the timestamp of the minimalBlock that was included in the search range - this is the timestamp of the block with minimalBlockNumber.
  • firstOverflowBlockNumber - the height of the firstOverflowBlock. This is the first block that has block number higher than deadlineBlockNumber AND timestamp later than deadlineTimestamp.
  • firstOverflowBlockTimestamp - the timestamp of the firstOverflowBlock. This is the timestamp of the first block that has block number higher than deadlineBlockNumber AND timestamp later than deadlineTimestamp.

If the request is confirmed, it means that there was no payment in such range (including minimal block, but excluding maximal block) with 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, but the important thing is, that the request is confirmed if no transaction meeting the specified criteria is found in the search range.

Example

To produce a nice and correct example that allows us to test everything properly, we will need to be careful. Since we are proving a negative, any mistake we make during request preparation will result in transaction that was not made (a simple misencoding of memo field would almost certainly produce a non-existing transaction) and gave us a false sense of security.

To be a bit more certain, we will structure our request more carefully:

  1. Create a transaction with reference payment and some nonzero value.
  2. First try to confirm Payment attestation request and make sure that we get back the correct reference and value - this means that the transaction is seen. We will then use information when this transaction has happened, to construct a range that will be used in the next step - and we are sure, that it contains our transaction.
  3. We will then make three requests for non-existing payment:
    • One with correct (or lower) value and correct reference - This one will return INVALID, as the verifier can't prove the non existence of such transaction
    • One with the correct value, but slightly wrong payment reference (change just one index) - This one should be confirmed, as no such transaction exists (the payment reference does not match)
    • One with to large value, but correct payment reference. This one should be confirmed, as the transaction with 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.

1const xrpl = require('xrpl');
2
3const { XRPL_PRIVATE_KEY, ATTESTATION_URL, ATTESTATION_API_KEY } = process.env;
4const receiverAddress = 'r9RLXvWuRro3RX33pk4xsN58tefYZ8Tvbj';
5
6function toHex(data: string): string {
7 var result = '';
8 for (var i = 0; i < data.length; i++) {
9 result += data.charCodeAt(i).toString(16);
10 }
11 return '0x' + result.padEnd(64, '0');
12}
13
14function fromHex(data: string): string {
15 data = data.replace(/^(0x\.)/, '');
16 return data
17 .split(/(\w\w)/g)
18 .filter((p) => !!p)
19 .map((c) => String.fromCharCode(parseInt(c, 16)))
20 .join('');
21}
22
23async function prepareAttestationResponse(attestationType: string, network: string, sourceId: string, requestBody: any): Promise<AttestationResponse> {
24 const response = await fetch(`${ATTESTATION_URL}/verifier/${network}/${attestationType}/prepareResponse`, {
25 method: 'POST',
26 headers: { 'X-API-KEY': ATTESTATION_API_KEY as string, 'Content-Type': 'application/json' },
27 body: JSON.stringify({
28 attestationType: toHex(attestationType),
29 sourceId: toHex(sourceId),
30 requestBody: requestBody,
31 }),
32 });
33 const data = await response.json();
34 return data;
35}
36
37async function getXRPLclient(): Promise<any> {
38 const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233');
39 await client.connect();
40
41 return client;
42}
43
44async function sendXRPLTransaction(message: string = '', amount: number = 10, target: string = 'r9RLXvWuRro3RX33pk4xsN58tefYZ8Tvbj'): Promise<string> {
45 const client = await getXRPLclient();
46
47 const test_wallet = xrpl.Wallet.fromSeed(XRPL_PRIVATE_KEY);
48
49 // Standard payment reference must be 32 bytes - so we right pad with 0
50 const MemoData = xrpl.convertStringToHex(message).padEnd(64, '0');
51 const MemoType = xrpl.convertStringToHex('Text');
52 const MemoFormat = xrpl.convertStringToHex('text/plain');
53
54 let memos = [];
55 if (message) {
56 memos.push({
57 Memo: {
58 MemoType: MemoType,
59 MemoData: MemoData,
60 MemoFormat: MemoFormat,
61 },
62 });
63 }
64
65 const transaction = await client.autofill({
66 TransactionType: 'Payment',
67 Account: test_wallet.address,
68 Amount: amount.toString(),
69 Destination: target,
70 Memos: memos,
71 });
72
73 const signed = test_wallet.sign(transaction);
74 console.log(`See transaction at https://testnet.xrpl.org/transactions/${signed.hash}`);
75 await client.submitAndWait(signed.tx_blob);
76
77 await client.disconnect();
78
79 // sleep for 10 seconds to allow the transaction to be processed
80 await new Promise((resolve) => setTimeout(resolve, 10 * 1000));
81 console.log('Payment:');
82 // 1. prove the payment:
83 const resultPayment = await prepareAttestationResponse('Payment', 'xrp', 'testXRP', {
84 transactionId: '0x' + signed.hash,
85 inUtxo: '0',
86 utxo: '0',
87 });
88
89 if (resultPayment.status != 'VALID') {
90 console.log('Something wrong when confirming payment');
91 }
92 console.log(resultPayment);
93 if (resultPayment.response.responseBody.standardPaymentReference != '0x' + MemoData) {
94 console.log('Something wrong with message reference');
95 console.log(resultPayment.response.responseBody.standardPaymentReference);
96 console.log(MemoData);
97 }
98 if (resultPayment.response.responseBody.receivingAddressHash != web3.utils.soliditySha3(target)) {
99 console.log('Something wrong with target address hash');
100 }
101
102 // Get information about transaction: block and block timestamp -> we will need this to create the range, where the transaction has happened
103 console.log('Failing non existence proof:');
104 const blockNumber = Number(resultPayment.response.responseBody.blockNumber);
105 const blockTimestamp = Number(resultPayment.response.responseBody.blockTimestamp);
106
107 const targetRange = {
108 minimalBlockNumber: (blockNumber - 5).toString(), // Search few block before
109 deadlineBlockNumber: (blockNumber + 1).toString(), // Search a few blocks after, but not too much, as they need to already be indexed by attestation clients
110 deadlineTimestamp: (blockTimestamp + 3).toString(), // Search a bit after
111 destinationAddressHash: web3.utils.soliditySha3(target), // The target address for transaction
112 };
113
114 // Try to verify non existence for a transaction and correct parameters
115 // This should not verify it
116
117 const resultFailedNonExistence = await prepareAttestationResponse('ReferencedPaymentNonexistence', 'xrp', 'testXRP', {
118 ...targetRange,
119 amount: amount.toString(),
120 standardPaymentReference: '0x' + MemoData,
121 });
122
123 console.log(resultFailedNonExistence);
124
125 if (resultFailedNonExistence.status != 'INVALID') {
126 console.log('Something wrong with failed non existence');
127 }
128
129 console.log('Successful non existence proofs:');
130
131 // Change the memo field a bit and successfully prove non existence
132 let wrongMemoData = xrpl.convertStringToHex(message).padEnd(64, '1'); // We pad 1 instead of 0
133 const resultWrongMemoNonExistence = await prepareAttestationResponse('ReferencedPaymentNonexistence', 'xrp', 'testXRP', {
134 ...targetRange,
135 amount: amount.toString(),
136 standardPaymentReference: '0x' + wrongMemoData,
137 });
138
139 console.log(resultWrongMemoNonExistence);
140
141 if (resultWrongMemoNonExistence.status != 'VALID') {
142 console.log('Something wrong with wrong memo non existence');
143 }
144
145 // Change the value and successfully prove non existence.
146
147 const resultWrongAmountNonExistence = await prepareAttestationResponse('ReferencedPaymentNonexistence', 'xrp', 'testXRP', {
148 ...targetRange,
149 amount: (amount + 1).toString(), // Increase the amount, so the transaction we made is now invalid
150 standardPaymentReference: '0x' + MemoData,
151 });
152
153 console.log(resultWrongAmountNonExistence);
154
155 if (resultWrongAmountNonExistence.status != 'VALID') {
156 console.log('Something wrong with wrong amount non existence');
157 }
158}
159
160async function main() {
161 await sendXRPLTransaction('Hello world!');
162}
163
164main().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.

1See transaction at https://testnet.xrpl.org/transactions/C2B493B8AE2E3C105D004D8AFBB4AFB5CA758608504CCE895C9331291DA19D75
2Payment:
3{
4 status: 'VALID',
5 response: {
6 attestationType: '0x5061796d656e7400000000000000000000000000000000000000000000000000',
7 sourceId: '0x7465737458525000000000000000000000000000000000000000000000000000',
8 votingRound: '0',
9 lowestUsedTimestamp: '1708830051',
10 requestBody: {
11 transactionId: '0xC2B493B8AE2E3C105D004D8AFBB4AFB5CA758608504CCE895C9331291DA19D75',
12 inUtxo: '0',
13 utxo: '0'
14 },
15 responseBody: {
16 blockNumber: '45680731',
17 blockTimestamp: '1708830051',
18 sourceAddressHash: '0xa1ca3089c3e9f4c6e9ccf2bfb65bcf3e9d7544a092c79d642d5d34a54e0267e1',
19 receivingAddressHash: '0x0555194538763da400394fc7184432e9a006565fa710392ea1a86486eb83920f',
20 intendedReceivingAddressHash: '0x0555194538763da400394fc7184432e9a006565fa710392ea1a86486eb83920f',
21 standardPaymentReference: '0x48656C6C6F20776F726C64210000000000000000000000000000000000000000',
22 spentAmount: '22',
23 intendedSpentAmount: '22',
24 receivedAmount: '10',
25 intendedReceivedAmount: '10',
26 oneToOne: true,
27 status: '0'
28 }
29 }
30}
31Failing non existence proof:
32{ status: 'INVALID' }
33Successful non existence proofs:
34{
35 status: 'VALID',
36 response: {
37 attestationType: '0x5265666572656e6365645061796d656e744e6f6e6578697374656e6365000000',
38 sourceId: '0x7465737458525000000000000000000000000000000000000000000000000000',
39 votingRound: '0',
40 lowestUsedTimestamp: '1708830033',
41 requestBody: {
42 minimalBlockNumber: '45680726',
43 deadlineBlockNumber: '45680732',
44 deadlineTimestamp: '1708830054',
45 destinationAddressHash: '0x0555194538763da400394fc7184432e9a006565fa710392ea1a86486eb83920f',
46 amount: '10',
47 standardPaymentReference: '0x48656C6C6F20776F726C64211111111111111111111111111111111111111111'
48 },
49 responseBody: {
50 minimalBlockTimestamp: '45680726',
51 firstOverflowBlockNumber: '45680733',
52 firstOverflowBlockTimestamp: '1708830060'
53 }
54 }
55}
56{
57 status: 'VALID',
58 response: {
59 attestationType: '0x5265666572656e6365645061796d656e744e6f6e6578697374656e6365000000',
60 sourceId: '0x7465737458525000000000000000000000000000000000000000000000000000',
61 votingRound: '0',
62 lowestUsedTimestamp: '1708830033',
63 requestBody: {
64 minimalBlockNumber: '45680726',
65 deadlineBlockNumber: '45680732',
66 deadlineTimestamp: '1708830054',
67 destinationAddressHash: '0x0555194538763da400394fc7184432e9a006565fa710392ea1a86486eb83920f',
68 amount: '11',
69 standardPaymentReference: '0x48656C6C6F20776F726C64210000000000000000000000000000000000000000'
70 },
71 responseBody: {
72 minimalBlockTimestamp: '45680726',
73 firstOverflowBlockNumber: '45680733',
74 firstOverflowBlockTimestamp: '1708830060'
75 }
76 }
77}

AddressValidity

The full specification is available here. 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 is able to prove that the string constitutes a valid address on the specified chain. Importantly, different from the Payment type we saw in the previous blog, 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 on-chain. This is useful if you want to make sure that the address is valid before using it in your protocol. The fAssets, for example, need to make sure that the address is valid before they can be used in the protocol, and this is a good way to offload difficult computation (https://bitcoin.design/guide/glossary/address/) on when the bitcoin address is valid off-chain entities.

Let's see the specification:

1// SPDX-License-Identifier: MIT
2pragma solidity >=0.7.6 <0.9;
3
4/**
5 * @custom:name AddressValidity
6 * @custom:id 0x05
7 * @custom:supported BTC, DOGE, XRP, testBTC, testDOGE, testXRP
8 * @author Flare
9 * @notice An assertion whether a string represents a valid address on an external chain.
10 * @custom:verification The address is checked against all validity criteria of the chain with `sourceId`.
11 * Indicator of validity is provided.
12 * If the address is valid, its standard form and standard hash are computed.
13 * Validity criteria for each supported chain:
14 * - [BTC](/specs/attestations/external-chains/address-validity/BTC.md)
15 * - [DOGE](/specs/attestations/external-chains/address-validity/DOGE.md)
16 * - [XRPL](/specs/attestations/external-chains/address-validity/XRPL.md)
17 * @custom:lut `0xffffffffffffffff` ($2^{64}-1$ in hex)
18 */
19interface AddressValidity {
20 /**
21 * @notice Toplevel request
22 * @param attestationType ID of the attestation type.
23 * @param sourceId Id of the data source.
24 * @param messageIntegrityCode `MessageIntegrityCode` that is derived from the expected response.
25 * @param requestBody Data defining the request. Type (struct) and interpretation is determined by the `attestationType`.
26 */
27 struct Request {
28 bytes32 attestationType;
29 bytes32 sourceId;
30 bytes32 messageIntegrityCode;
31 RequestBody requestBody;
32 }
33
34 /**
35 * @notice Toplevel response
36 * @param attestationType Extracted from the request.
37 * @param sourceId Extracted from the request.
38 * @param votingRound The ID of the State Connector round in which the request was considered.
39 * @param lowestUsedTimestamp The lowest timestamp used to generate the response.
40 * @param requestBody Extracted from the request.
41 * @param responseBody Data defining the response. The verification rules for the construction of the response body and the type are defined per specific `attestationType`.
42 */
43 struct Response {
44 bytes32 attestationType;
45 bytes32 sourceId;
46 uint64 votingRound;
47 uint64 lowestUsedTimestamp;
48 RequestBody requestBody;
49 ResponseBody responseBody;
50 }
51
52 /**
53 * @notice Toplevel proof
54 * @param merkleProof Merkle proof corresponding to the attestation response.
55 * @param data Attestation response.
56 */
57 struct Proof {
58 bytes32[] merkleProof;
59 Response data;
60 }
61
62 /**
63 * @notice Request body for AddressValidity attestation type
64 * @param addressStr Address to be verified.
65 */
66 struct RequestBody {
67 string addressStr;
68 }
69
70 /**
71 * @notice Response body for AddressValidity attestation type
72 * @param isValid Boolean indicator of the address validity.
73 * @param standardAddress If `isValid`, standard form of the validated address. Otherwise an empty string.
74 * @param standardAddressHash If `isValid`, standard address hash of the validated address. Otherwise a zero bytes32 string.
75 */
76 struct ResponseBody {
77 bool isValid;
78 string standardAddress;
79 bytes32 standardAddressHash;
80 }
81}

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

The response body has all the meat - the request can always be confirmed (in general), we want to take a look at specific fields:

  • 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 - wether the address is valid or not.
  • standardAddress - if isValid, this is the standard form of the validated address. Otherwise an empty string. This is useful if you want to use the address in your protocol - you can use the standard form of the address and not worry about the different representations of the same address.
  • standardAddressHash - if isValid, this is the standard address hash of the validated address, otherwise a zero bytes32 string. This is useful to verify with the standard address hash returned by Payment and ReferencedPaymentNonexistence.

Think of this more of as an example of what can be offloaded to off-chain computation (and verification) - and try to imagine what other things that are prohibitively expensive (or impossible due to data unavailability) on-chain can be offloaded to off-chain computation.

Example

The script for address validity (tryAddressValidity.ts) is a bit simpler then the scripts so far, as we don't have to create a transaction or anything, we just call prepareResponse endpoint and see the result. Remember, in real usage, we will have to first prepare a request for State Connector, wait for it to get confirmed and only then use the response in our smart contract together with proof. This effectively means, that our smart contract will get just the result of (possibly) huge and expensive calculation (the response body part) together with proof, that this was included in the merkle root - and thus has been calculated and attested to by the validator of the network.

Full code:

1const { ATTESTATION_URL, ATTESTATION_API_KEY } = process.env;
2const exampleXRPLAddress = 'r9RLXvWuRro3RX33pk4xsN58tefYZ8Tvbj';
3const someDogecoinAddress = 'njyMWWyh1L7tSX6QkWRgetMVCVyVtfoDta';
4const someBTCAddress = 'tb1qq3fm2kdklehk545c5rgfxzfhe7ph5tt640cayu';
5
6function toHex(data: string): string {
7 var result = '';
8 for (var i = 0; i < data.length; i++) {
9 result += data.charCodeAt(i).toString(16);
10 }
11 return '0x' + result.padEnd(64, '0');
12}
13
14function fromHex(data: string): string {
15 data = data.replace(/^(0x\.)/, '');
16 return data
17 .split(/(\w\w)/g)
18 .filter((p) => !!p)
19 .map((c) => String.fromCharCode(parseInt(c, 16)))
20 .join('');
21}
22
23async function prepareAttestationResponse(attestationType: string, network: string, sourceId: string, requestBody: any): Promise<AttestationResponse> {
24 const response = await fetch(`${ATTESTATION_URL}/verifier/${network}/${attestationType}/prepareResponse`, {
25 method: 'POST',
26 headers: { 'X-API-KEY': ATTESTATION_API_KEY as string, 'Content-Type': 'application/json' },
27 body: JSON.stringify({
28 attestationType: toHex(attestationType),
29 sourceId: toHex(sourceId),
30 requestBody: requestBody,
31 }),
32 });
33 const data = await response.json();
34 return data;
35}
36
37async function main() {
38 console.log(await prepareAttestationResponse('AddressValidity', 'xrp', 'testXRP', { addressStr: exampleXRPLAddress }));
39 console.log(await prepareAttestationResponse('AddressValidity', 'xrp', 'testXRP', { addressStr: '0xhahahahaha' }));
40 console.log(await prepareAttestationResponse('AddressValidity', 'xrp', 'testXRP', { addressStr: 'Hello world!' }));
41
42 console.log(await prepareAttestationResponse('AddressValidity', 'btc', 'testBTC', { addressStr: someBTCAddress }));
43 console.log(await prepareAttestationResponse('AddressValidity', 'btc', 'testBTC', { addressStr: '0xhahahahaha' }));
44 console.log(await prepareAttestationResponse('AddressValidity', 'btc', 'testBTC', { addressStr: 'Hello world!' }));
45
46 console.log(await prepareAttestationResponse('AddressValidity', 'doge', 'testDOGE', { addressStr: someDogecoinAddress }));
47 console.log(await prepareAttestationResponse('AddressValidity', 'doge', 'testDOGE', { addressStr: '0xhahahahaha' }));
48 console.log(await prepareAttestationResponse('AddressValidity', 'doge', 'testDOGE', { addressStr: 'Hello world!' }));
49}
50
51main().then(() => process.exit(0));

and the response

1{
2 status: 'VALID',
3 response: {
4 attestationType: '0x4164647265737356616c69646974790000000000000000000000000000000000',
5 sourceId: '0x7465737458525000000000000000000000000000000000000000000000000000',
6 votingRound: '0',
7 lowestUsedTimestamp: '0xffffffffffffffff',
8 requestBody: { addressStr: 'r9RLXvWuRro3RX33pk4xsN58tefYZ8Tvbj' },
9 responseBody: {
10 isValid: true,
11 standardAddress: 'r9RLXvWuRro3RX33pk4xsN58tefYZ8Tvbj',
12 standardAddressHash: '0x0555194538763da400394fc7184432e9a006565fa710392ea1a86486eb83920f'
13 }
14 }
15}
16{
17 status: 'VALID',
18 response: {
19 attestationType: '0x4164647265737356616c69646974790000000000000000000000000000000000',
20 sourceId: '0x7465737458525000000000000000000000000000000000000000000000000000',
21 votingRound: '0',
22 lowestUsedTimestamp: '0xffffffffffffffff',
23 requestBody: { addressStr: '0xhahahahaha' },
24 responseBody: {
25 isValid: false,
26 standardAddress: '',
27 standardAddressHash: '0x0000000000000000000000000000000000000000000000000000000000000000'
28 }
29 }
30}
31{
32 status: 'VALID',
33 response: {
34 attestationType: '0x4164647265737356616c69646974790000000000000000000000000000000000',
35 sourceId: '0x7465737458525000000000000000000000000000000000000000000000000000',
36 votingRound: '0',
37 lowestUsedTimestamp: '0xffffffffffffffff',
38 requestBody: { addressStr: 'Hello world!' },
39 responseBody: {
40 isValid: false,
41 standardAddress: '',
42 standardAddressHash: '0x0000000000000000000000000000000000000000000000000000000000000000'
43 }
44 }
45}
46{
47 status: 'VALID',
48 response: {
49 attestationType: '0x4164647265737356616c69646974790000000000000000000000000000000000',
50 sourceId: '0x7465737442544300000000000000000000000000000000000000000000000000',
51 votingRound: '0',
52 lowestUsedTimestamp: '0xffffffffffffffff',
53 requestBody: { addressStr: 'tb1qq3fm2kdklehk545c5rgfxzfhe7ph5tt640cayu' },
54 responseBody: {
55 isValid: true,
56 standardAddress: 'tb1qq3fm2kdklehk545c5rgfxzfhe7ph5tt640cayu',
57 standardAddressHash: '0x085f152e9e9ebd6c009827678785b1b3667733fa3f6b5d78bb462bd1978825ff'
58 }
59 }
60}
61{
62 status: 'VALID',
63 response: {
64 attestationType: '0x4164647265737356616c69646974790000000000000000000000000000000000',
65 sourceId: '0x7465737442544300000000000000000000000000000000000000000000000000',
66 votingRound: '0',
67 lowestUsedTimestamp: '0xffffffffffffffff',
68 requestBody: { addressStr: '0xhahahahaha' },
69 responseBody: {
70 isValid: false,
71 standardAddress: '',
72 standardAddressHash: '0x0000000000000000000000000000000000000000000000000000000000000000'
73 }
74 }
75}
76{
77 status: 'VALID',
78 response: {
79 attestationType: '0x4164647265737356616c69646974790000000000000000000000000000000000',
80 sourceId: '0x7465737442544300000000000000000000000000000000000000000000000000',
81 votingRound: '0',
82 lowestUsedTimestamp: '0xffffffffffffffff',
83 requestBody: { addressStr: 'Hello world!' },
84 responseBody: {
85 isValid: false,
86 standardAddress: '',
87 standardAddressHash: '0x0000000000000000000000000000000000000000000000000000000000000000'
88 }
89 }
90}
91{
92 status: 'VALID',
93 response: {
94 attestationType: '0x4164647265737356616c69646974790000000000000000000000000000000000',
95 sourceId: '0x74657374444f4745000000000000000000000000000000000000000000000000',
96 votingRound: '0',
97 lowestUsedTimestamp: '0xffffffffffffffff',
98 requestBody: { addressStr: 'njyMWWyh1L7tSX6QkWRgetMVCVyVtfoDta' },
99 responseBody: {
100 isValid: true,
101 standardAddress: 'njyMWWyh1L7tSX6QkWRgetMVCVyVtfoDta',
102 standardAddressHash: '0xfc8d6252c5132f771fc711fe13cb3c6e768ed9290ce199efd87d5ec1b6094df6'
103 }
104 }
105}
106{
107 status: 'VALID',
108 response: {
109 attestationType: '0x4164647265737356616c69646974790000000000000000000000000000000000',
110 sourceId: '0x74657374444f4745000000000000000000000000000000000000000000000000',
111 votingRound: '0',
112 lowestUsedTimestamp: '0xffffffffffffffff',
113 requestBody: { addressStr: '0xhahahahaha' },
114 responseBody: {
115 isValid: false,
116 standardAddress: '',
117 standardAddressHash: '0x0000000000000000000000000000000000000000000000000000000000000000'
118 }
119 }
120}
121{
122 status: 'VALID',
123 response: {
124 attestationType: '0x4164647265737356616c69646974790000000000000000000000000000000000',
125 sourceId: '0x74657374444f4745000000000000000000000000000000000000000000000000',
126 votingRound: '0',
127 lowestUsedTimestamp: '0xffffffffffffffff',
128 requestBody: { addressStr: 'Hello world!' },
129 responseBody: {
130 isValid: false,
131 standardAddress: '',
132 standardAddressHash: '0x0000000000000000000000000000000000000000000000000000000000000000'
133 }
134 }
135}

One might ask what use is such an attestation type and why all the checks? Think of it in two ways:

  • The data contains request and response - this makes it possible to observe the input (request) and output (response) of the computation. This 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.
  • The Merkle proof is then used to prove that the response was included in the committed root and thus was confirmed by the verifiers. The "being confirmed" part is important, as it means that the verifiers have indeed seen the request, ran the computation (and arrived at the same result) and included the result we base our actions on in the Merkle root.

Conclusion

Wow, congratulations - you made it this far. Now you see, what the State Connector can do and also know, how to use it and what are some details you need to be careful about. As usual, check the repository for full code and try to play around.

In the next blogpost, we will see, 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 gas standpoint as you are writing a lot of data to memory and decoding nested structures is expensive. But not only this, as 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 pass the limit.