Choose unique VAA for query time if it exists (#853)

* hmm this way works but is complicated

* choose unique vaa

* update tests

* lint

* bump versions
This commit is contained in:
Jayant Krishnamurthy 2023-06-01 07:32:10 -07:00 committed by GitHub
parent af2d7b6e38
commit 64bce66383
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 101 additions and 53 deletions

View File

@ -1,6 +1,6 @@
{
"name": "@pythnetwork/price-service-server",
"version": "3.0.4",
"version": "3.0.5",
"description": "Webservice for retrieving prices from the Pyth oracle.",
"private": "true",
"main": "index.js",

View File

@ -6,53 +6,73 @@ describe("VAA Cache works", () => {
expect(cache.get("a", 3)).toBeUndefined();
cache.set("a", 1, "a-1");
cache.set("a", 1, 0, "a-1");
expect(cache.get("a", 3)).toBeUndefined();
cache.set("a", 4, "a-2");
cache.set("a", 4, 3, "a-2");
expect(cache.get("a", 3)).toEqual<VaaConfig>({
publishTime: 4,
lastAttestedPublishTime: 3,
vaa: "a-2",
});
cache.set("a", 10, "a-3");
cache.set("a", 10, 9, "a-3");
cache.set("a", 10, 10, "a-4");
cache.set("a", 10, 10, "a-5");
cache.set("a", 10, 10, "a-6");
cache.set("a", 11, 11, "a-7");
// Adding some elements with other keys to make sure
// they are not stored separately.
cache.set("b", 3, "b-1");
cache.set("b", 7, "b-2");
cache.set("b", 9, "b-3");
cache.set("b", 3, 2, "b-1");
cache.set("b", 7, 6, "b-2");
cache.set("b", 9, 8, "b-3");
expect(cache.get("a", 3)).toEqual<VaaConfig>({
publishTime: 4,
lastAttestedPublishTime: 3,
vaa: "a-2",
});
expect(cache.get("a", 4)).toEqual<VaaConfig>({
publishTime: 4,
lastAttestedPublishTime: 3,
vaa: "a-2",
});
expect(cache.get("a", 5)).toEqual<VaaConfig>({
publishTime: 10,
lastAttestedPublishTime: 9,
vaa: "a-3",
});
// There are multiple elements at 10, but we prefer to return the one with a lower lastAttestedPublishTime.
expect(cache.get("a", 10)).toEqual<VaaConfig>({
publishTime: 10,
lastAttestedPublishTime: 9,
vaa: "a-3",
});
// If the cache only contains elements where the lastAttestedPublishTime==publishTime, those will be returned.
// Note that this behavior is undesirable (as this means we can return a noncanonical VAA for a query time);
// this test simply documents it.
expect(cache.get("a", 11)).toEqual<VaaConfig>({
publishTime: 11,
lastAttestedPublishTime: 11,
vaa: "a-7",
});
expect(cache.get("b", 3)).toEqual<VaaConfig>({
publishTime: 3,
lastAttestedPublishTime: 2,
vaa: "b-1",
});
expect(cache.get("b", 4)).toEqual<VaaConfig>({
publishTime: 7,
lastAttestedPublishTime: 6,
vaa: "b-2",
});
// When no item item more recent than asked pubTime is asked it should return undefined
expect(cache.get("a", 11)).toBeUndefined();
expect(cache.get("a", 12)).toBeUndefined();
expect(cache.get("b", 10)).toBeUndefined();
// When the asked pubTime is less than the first existing pubTime we are not sure that
@ -68,17 +88,19 @@ describe("VAA Cache works", () => {
// TTL of 500 seconds for the cache
const cache = new VaaCache(500);
cache.set("a", 300, "a-1");
cache.set("a", 700, "a-2");
cache.set("a", 900, "a-3");
cache.set("a", 300, 299, "a-1");
cache.set("a", 700, 699, "a-2");
cache.set("a", 900, 899, "a-3");
expect(cache.get("a", 300)).toEqual<VaaConfig>({
publishTime: 300,
lastAttestedPublishTime: 299,
vaa: "a-1",
});
expect(cache.get("a", 500)).toEqual<VaaConfig>({
publishTime: 700,
lastAttestedPublishTime: 699,
vaa: "a-2",
});
@ -98,12 +120,13 @@ describe("VAA Cache works", () => {
const cache = new VaaCache(500, 100);
cache.runRemoveExpiredValuesLoop();
cache.set("a", 300, "a-1");
cache.set("a", 700, "a-2");
cache.set("a", 900, "a-3");
cache.set("a", 300, 299, "a-1");
cache.set("a", 700, 699, "a-2");
cache.set("a", 900, 899, "a-3");
expect(cache.get("a", 900)).toEqual<VaaConfig>({
publishTime: 900,
lastAttestedPublishTime: 899,
vaa: "a-3",
});

View File

@ -8,7 +8,7 @@ import express, { Express } from "express";
import { StatusCodes } from "http-status-codes";
import request from "supertest";
import { PriceInfo, PriceStore, VaaCache, VaaConfig } from "../listen";
import { RestAPI } from "../rest";
import { RestAPI, VaaResponse } from "../rest";
let priceInfo: PriceStore;
let app: Express;
@ -52,10 +52,18 @@ function dummyPriceInfoPair(
vaa: Buffer.from(vaa, "hex"),
emitterChainId: 0,
priceServiceReceiveTime: 0,
lastAttestedPublishTime: 0,
},
];
}
// Add some dummy data to the provided vaa cache.
function addAbcdDataToCache(id: string, cache: VaaCache) {
cache.set(id, 10, 9, "abcd10");
cache.set(id, 20, 19, "abcd20");
cache.set(id, 30, 29, "abcd30");
}
beforeAll(async () => {
priceInfoMap = new Map<string, PriceInfo>([
dummyPriceInfoPair(expandTo64Len("abcd"), 1, "a1b2c3d4"),
@ -258,16 +266,14 @@ describe("Latest Vaa Bytes Endpoint", () => {
describe("Get VAA endpoint and Get VAA CCIP", () => {
test("When called with valid id and timestamp in the cache returns the correct answer", async () => {
const id = expandTo64Len("abcd");
vaasCache.set(id, 10, "abcd10");
vaasCache.set(id, 20, "abcd20");
vaasCache.set(id, 30, "abcd30");
addAbcdDataToCache(id, vaasCache);
const resp = await request(app).get("/api/get_vaa").query({
id,
publish_time: 16,
});
expect(resp.status).toBe(StatusCodes.OK);
expect(resp.body).toEqual<VaaConfig>({
expect(resp.body).toEqual<VaaResponse>({
vaa: "abcd20",
publishTime: 20,
});
@ -286,9 +292,7 @@ describe("Get VAA endpoint and Get VAA CCIP", () => {
test("When called with valid id with leading 0x and timestamp in the cache returns the correct answer", async () => {
const id = expandTo64Len("abcd");
vaasCache.set(id, 10, "abcd10");
vaasCache.set(id, 20, "abcd20");
vaasCache.set(id, 30, "abcd30");
addAbcdDataToCache(id, vaasCache);
const resp = await request(app)
.get("/api/get_vaa")
@ -297,7 +301,7 @@ describe("Get VAA endpoint and Get VAA CCIP", () => {
publish_time: 16,
});
expect(resp.status).toBe(StatusCodes.OK);
expect(resp.body).toEqual<VaaConfig>({
expect(resp.body).toEqual<VaaResponse>({
vaa: "abcd20",
publishTime: 20,
});
@ -305,9 +309,7 @@ describe("Get VAA endpoint and Get VAA CCIP", () => {
test("When called with target_chain, encodes resulting VAA in the right format", async () => {
const id = expandTo64Len("abcd");
vaasCache.set(id, 10, "abcd10");
vaasCache.set(id, 20, "abcd20");
vaasCache.set(id, 30, "abcd30");
addAbcdDataToCache(id, vaasCache);
const resp = await request(app)
.get("/api/get_vaa")
@ -317,7 +319,7 @@ describe("Get VAA endpoint and Get VAA CCIP", () => {
target_chain: "evm",
});
expect(resp.status).toBe(StatusCodes.OK);
expect(resp.body).toEqual<VaaConfig>({
expect(resp.body).toEqual<VaaResponse>({
vaa: "0x" + Buffer.from("abcd20", "base64").toString("hex"),
publishTime: 20,
});
@ -346,9 +348,7 @@ describe("Get VAA endpoint and Get VAA CCIP", () => {
test("When called with valid id and timestamp not in the cache without db returns vaa not found", async () => {
const id = expandTo64Len("abcd");
vaasCache.set(id, 10, "abcd10");
vaasCache.set(id, 20, "abcd20");
vaasCache.set(id, 30, "abcd30");
addAbcdDataToCache(id, vaasCache);
const resp = await request(app)
.get("/api/get_vaa")
@ -396,9 +396,7 @@ describe("Get VAA endpoint and Get VAA CCIP", () => {
const appWithDb = await apiWithDb.createApp();
const id = expandTo64Len("abcd");
vaasCache.set(id, 10, "abcd10");
vaasCache.set(id, 20, "abcd20");
vaasCache.set(id, 30, "abcd30");
addAbcdDataToCache(id, vaasCache);
const resp = await request(appWithDb)
.get("/api/get_vaa")
@ -407,7 +405,7 @@ describe("Get VAA endpoint and Get VAA CCIP", () => {
publish_time: 5,
});
expect(resp.status).toBe(StatusCodes.OK);
expect(resp.body).toEqual<VaaConfig>({
expect(resp.body).toEqual<VaaResponse>({
vaa: `pythnet${id}5`,
publishTime: 5,
});
@ -451,9 +449,7 @@ describe("Get VAA endpoint and Get VAA CCIP", () => {
const appWithDb = await apiWithDb.createApp();
const id = expandTo64Len("abcd");
vaasCache.set(id, 10, "abcd10");
vaasCache.set(id, 20, "abcd20");
vaasCache.set(id, 30, "abcd30");
addAbcdDataToCache(id, vaasCache);
const resp = await request(appWithDb)
.get("/api/get_vaa")
@ -493,9 +489,7 @@ describe("Get VAA endpoint and Get VAA CCIP", () => {
const appWithDb = await apiWithDb.createApp();
const id = expandTo64Len("abcd");
vaasCache.set(id, 10, "abcd10");
vaasCache.set(id, 20, "abcd20");
vaasCache.set(id, 30, "abcd30");
addAbcdDataToCache(id, vaasCache);
const resp = await request(appWithDb)
.get("/api/get_vaa")

View File

@ -31,6 +31,7 @@ function dummyPriceInfo(id: HexString, vaa: HexString): PriceInfo {
priceFeed: dummyPriceFeed(id),
vaa: Buffer.from(vaa, "hex"),
priceServiceReceiveTime: 4,
lastAttestedPublishTime: -1,
};
}

View File

@ -28,6 +28,7 @@ export type PriceInfo = {
seqNum: number;
publishTime: TimestampInSec;
attestationTime: TimestampInSec;
lastAttestedPublishTime: TimestampInSec;
priceFeed: PriceFeed;
emitterChainId: number;
priceServiceReceiveTime: number;
@ -45,6 +46,7 @@ export function createPriceInfo(
vaa,
publishTime: priceAttestation.publishTime,
attestationTime: priceAttestation.attestationTime,
lastAttestedPublishTime: priceAttestation.lastAttestedPublishTime,
priceFeed,
emitterChainId: emitterChain,
priceServiceReceiveTime: Math.floor(new Date().getTime() / 1000),
@ -78,6 +80,7 @@ type VaaKey = string;
export type VaaConfig = {
publishTime: number;
lastAttestedPublishTime: number;
vaa: string;
};
@ -95,11 +98,16 @@ export class VaaCache {
this.cacheCleanupLoopInterval = cacheCleanupLoopInterval;
}
set(key: VaaKey, publishTime: TimestampInSec, vaa: string): void {
set(
key: VaaKey,
publishTime: TimestampInSec,
lastAttestedPublishTime: TimestampInSec,
vaa: string
): void {
if (this.cache.has(key)) {
this.cache.get(key)!.push({ publishTime, vaa });
this.cache.get(key)!.push({ publishTime, lastAttestedPublishTime, vaa });
} else {
this.cache.set(key, [{ publishTime, vaa }]);
this.cache.set(key, [{ publishTime, lastAttestedPublishTime, vaa }]);
}
}
@ -128,7 +136,10 @@ export class VaaCache {
while (left <= right) {
const middle = Math.floor((left + right) / 2);
if (arr[middle].publishTime === publishTime) {
if (
arr[middle].publishTime === publishTime &&
arr[middle].lastAttestedPublishTime < publishTime
) {
return arr[middle];
} else if (arr[middle].publishTime < publishTime) {
left = middle + 1;
@ -368,6 +379,7 @@ export class Listener implements PriceStore {
this.vaasCache.set(
priceInfo.priceFeed.id,
priceInfo.publishTime,
priceInfo.lastAttestedPublishTime,
priceInfo.vaa.toString("base64")
);
this.priceFeedVaaMap.set(key, priceInfo);

View File

@ -67,6 +67,11 @@ function asyncWrapper(
};
}
export type VaaResponse = {
publishTime: number;
vaa: string;
};
export class RestAPI {
private port: number;
private priceFeedVaaInfo: PriceStore;
@ -92,12 +97,18 @@ export class RestAPI {
async getVaaWithDbLookup(
priceFeedId: string,
publishTime: TimestampInSec
): Promise<VaaConfig | undefined> {
): Promise<VaaResponse | undefined> {
// Try to fetch the vaa from the local cache
let vaa = this.priceFeedVaaInfo.getVaa(priceFeedId, publishTime);
const vaaConfig = this.priceFeedVaaInfo.getVaa(priceFeedId, publishTime);
let vaa: VaaResponse | undefined;
// if publishTime is older than cache ttl or vaa is not found, fetch from db
if (vaa === undefined && this.dbApiEndpoint && this.dbApiCluster) {
if (vaaConfig !== undefined) {
vaa = {
vaa: vaaConfig.vaa,
publishTime: vaaConfig.publishTime,
};
} else if (vaa === undefined && this.dbApiEndpoint && this.dbApiCluster) {
const priceFeedWithoutLeading0x = removeLeading0x(priceFeedId);
try {

View File

@ -26,7 +26,6 @@
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --passWithNoTests",
"eject": "react-scripts eject",
"format": "prettier --write src/"
},

View File

@ -1,6 +1,6 @@
{
"name": "@pythnetwork/wormhole-attester-sdk",
"version": "1.0.0",
"version": "1.1.0",
"description": "Pyth Wormhole Attester SDk",
"private": "true",
"types": "lib/index.d.ts",

View File

@ -14,15 +14,15 @@ describe("Deserializing Batch Price Attestation works", () => {
"50325748000300010001020003009D01010101010101010101010101010101010101010101010101010" +
"10101010101FEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFE0000002B" +
"AD2FEED70000000000000065FFFFFFFDFFFFFFFFFFFFFFD6000000000000002A010001E14C0004E6D00" +
"000DEADBEEFFADE00000000DADEBEEF00000000DEADBABE0000DEADFACEBEEF000000BADBADBEEFDEAD" +
"BEEFFADEDEAF0202020202020202020202020202020202020202020202020202020202020202FDFDFDF" +
"000DEADBEEFFADE00000000DADEBEEF00000000DEADBABE0000DEADFACEBEEF000000BADBADBEEF0000" +
"DEADBEEFFACE0202020202020202020202020202020202020202020202020202020202020202FDFDFDF" +
"DFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFD0000002BAD2FEED70000000000" +
"000065FFFFFFFDFFFFFFFFFFFFFFD6000000000000002A010001E14C0004E6D00000DEADBEEFFADE000" +
"00000DADEBEEF00000000DEADBABE0000DEADFACEBEEF000000BADBADBEEFDEADBEEFFADEDEAF030303" +
"00000DADEBEEF00000000DEADBABE0000DEADFACEBEEF000000BADBADBEEF0000DEADBEEFFACE030303" +
"0303030303030303030303030303030303030303030303030303030303FCFCFCFCFCFCFCFCFCFCFCFCF" +
"CFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFC0000002BAD2FEED70000000000000065FFFFFFFDFFFF" +
"FFFFFFFFFFD6000000000000002A010001E14C0004E6D00000DEADBEEFFADE00000000DADEBEEF00000" +
"000DEADBABE0000DEADFACEBEEF000000BADBADBEEFDEADBEEFFADEDEAF";
"000DEADBABE0000DEADFACEBEEF000000BADBADBEEF0000DEADBEEFFACE";
const data = Buffer.from(fixture, "hex");
const batchPriceAttestation = parseBatchPriceAttestation(data);
@ -47,6 +47,7 @@ describe("Deserializing Batch Price Attestation works", () => {
prevPublishTime: 0xdeadbabe,
prevPrice: (0xdeadfacebeef).toString(),
prevConf: (0xbadbadbeef).toString(),
lastAttestedPublishTime: 0xdeadbeefface,
});
});
});
@ -70,6 +71,7 @@ describe("Price Attestation to Price Feed works", () => {
prevPublishTime: 998,
prevPrice: "101",
prevConf: "6",
lastAttestedPublishTime: 997,
};
const priceFeed = priceAttestationToPriceFeed(priceAttestation);
@ -109,6 +111,7 @@ describe("Price Attestation to Price Feed works", () => {
prevPublishTime: 998,
prevPrice: "101",
prevConf: "6",
lastAttestedPublishTime: 997,
};
const priceFeed = priceAttestationToPriceFeed(priceAttestation);

View File

@ -34,6 +34,7 @@ export type PriceAttestation = {
prevPublishTime: UnixTimestamp;
prevPrice: string;
prevConf: string;
lastAttestedPublishTime: UnixTimestamp;
};
export type BatchPriceAttestation = {
@ -94,6 +95,9 @@ export function parsePriceAttestation(bytes: Buffer): PriceAttestation {
const prevConf = bytes.readBigUint64BE(offset).toString();
offset += 8;
const lastAttestedPublishTime = Number(bytes.readBigInt64BE(offset));
offset += 8;
return {
productId,
priceId,
@ -110,6 +114,7 @@ export function parsePriceAttestation(bytes: Buffer): PriceAttestation {
prevPublishTime,
prevPrice,
prevConf,
lastAttestedPublishTime,
};
}