Verify Payment Nonexistence
Reference Payment Nonexistence
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
// 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 paid.
* @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 fromminimalBlockNumbertodeadlineBlockNumberand all blocks with timestamps fromminimalBlockTimestamptodeadlineTimestamp. -
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 withminimalBlockNumber. -
firstOverflowBlockNumber: The height of the first overflow block. This is the first block with a block number higher thandeadlineBlockNumberand a timestamp later thandeadlineTimestamp. -
firstOverflowBlockTimestamp: The timestamp of the first overflow block. This is the timestamp of the first block with a block number higher thandeadlineBlockNumberand a timestamp later thandeadlineTimestamp.
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:
- Create a transaction with a reference payment and some nonzero value.
- Confirm
Paymentattestation 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. - 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.
- Correct (or lower) value and correct reference: This should return
XRP Ledger
The example code that showcases this on testnet XRP Ledger is available in 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'
}
}
}