[wormhole-attester] Remove wasm in js sdk (#537)

* [wormhole-attester] Remove wasm in js sdk

* Cleanup dependencies and unused codes

* Cleanup rust

* Add tests + minor improvements

* Add ignored price status

* Revert cargo lock
This commit is contained in:
Ali Behjati 2023-01-27 17:11:00 +01:00 committed by GitHub
parent 277599b458
commit f9e0145352
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 8060 additions and 4832 deletions

View File

@ -62,18 +62,6 @@ if not ci:
def k8s_yaml_with_ns(objects):
return k8s_yaml(namespace_inject(objects, namespace))
# wasm
local_resource(
name = "wasm-gen",
cmd = "tilt docker build -- -f tilt_devnet/docker_images/Dockerfile.wasm -o type=local,dest=. .",
env = {"DOCKER_BUILDKIT": "1"},
deps = "./wormhole_attester",
labels = ["wasm"],
allow_parallel=True,
trigger_mode = trigger_mode,
)
def build_node_yaml():
node_yaml = read_yaml_stream("tilt_devnet/k8s/node.yaml")
@ -220,7 +208,7 @@ docker_build(
k8s_yaml_with_ns("tilt_devnet/k8s/p2w-terra-relay.yaml")
k8s_resource(
"p2w-terra-relay",
resource_deps = ["pyth", "p2w-attest", "spy", "terra-terrad", "wasm-gen"],
resource_deps = ["pyth", "p2w-attest", "spy", "terra-terrad"],
port_forwards = [
port_forward(4200, name = "Rest API (Status + Query) [:4200]", host = webHost),
port_forward(8081, name = "Prometheus [:8081]", host = webHost)],
@ -230,7 +218,7 @@ k8s_resource(
k8s_yaml_with_ns("tilt_devnet/k8s/p2w-evm-relay.yaml")
k8s_resource(
"p2w-evm-relay",
resource_deps = ["pyth", "p2w-attest", "spy", "eth-devnet", "wasm-gen"],
resource_deps = ["pyth", "p2w-attest", "spy", "eth-devnet"],
port_forwards = [
port_forward(4201, container_port = 4200, name = "Rest API (Status + Query) [:4201]", host = webHost),
port_forward(8082, container_port = 8081, name = "Prometheus [:8082]", host = webHost)],
@ -246,7 +234,7 @@ docker_build(
k8s_yaml_with_ns("tilt_devnet/k8s/pyth-price-server.yaml")
k8s_resource(
"pyth-price-server",
resource_deps = ["pyth", "p2w-attest", "spy", "eth-devnet", "wasm-gen"],
resource_deps = ["pyth", "p2w-attest", "spy", "eth-devnet"],
port_forwards = [
port_forward(4202, container_port = 4200, name = "Rest API (Status + Query) [:4202]", host = webHost),
port_forward(8083, container_port = 8081, name = "Prometheus [:8083]", host = webHost)],

View File

@ -1,39 +0,0 @@
# syntax=docker.io/docker/dockerfile:1.3@sha256:42399d4635eddd7a9b8a24be879d2f9a930d0ed040a61324cfdf59ef1357b3b2
FROM docker.io/library/rust:1.49@sha256:a50165ea96983c21832578afb1c8c028674c965bc1ed43b607871b1f362e06a5 AS build
RUN apt-get update && \
apt-get install -y libssl-dev libudev-dev pkg-config zlib1g-dev llvm clang && \
apt-get clean && rm -rf /var/lib/apt/lists/*
# We default an older nightly since current rust-toolchain makes the
# wasm-pack build unhappy, we introduce it later for our code
RUN rustup component add rustfmt && \
rustup default nightly-2022-01-02
WORKDIR /usr/src
RUN cargo install wasm-pack --vers 0.9.1
ENV RUST_LOG="solana_runtime::system_instruction_processor=trace,solana_runtime::message_processor=trace,solana_bpf_loader=debug,solana_rbpf=debug"
ENV EMITTER_ADDRESS="11111111111111111111111111111115"
ENV BRIDGE_ADDRESS="Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o"
COPY wormhole_attester wormhole_attester
COPY governance/remote_executor governance/remote_executor
# wasm-bindgen 0.2.74 generates JavaScript bindings for SystemInstruction exported from solana-program 1.9.4.
# The generated JavaScript references a non-existent function (wasm.__wbg_systeminstruction_free) that leads
# to an attempted import error when importing the wasm packed for bundler. SystemInstruction isn't used in the sdk,
# so we remove the non-existent function reference as a workaround.
ARG SED_REMOVE_INVALID_REFERENCE="/^\s*wasm.__wbg_systeminstruction_free(ptr);$/d"
# TODO: it appears that wasm-pack ignores our lockfiles even with --locked
# Run unit tests
WORKDIR /usr/src/wormhole_attester/sdk/rust
RUN cargo test --locked && \
/usr/local/cargo/bin/wasm-pack build --target bundler -d bundler -- --features wasm --locked && \
/usr/local/cargo/bin/wasm-pack build --target nodejs -d nodejs -- --features wasm --locked
FROM scratch AS export
COPY --from=build /usr/src/wormhole_attester/sdk/rust/bundler wormhole_attester/sdk/js/src/wasm/bundler
COPY --from=build /usr/src/wormhole_attester/sdk/rust/nodejs wormhole_attester/sdk/js/src/wasm/nodejs

View File

@ -2751,7 +2751,6 @@ dependencies = [
"serde",
"solana-program",
"solitaire",
"wasm-bindgen",
]
[[package]]
@ -5177,8 +5176,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27370197c907c55e3f1a9fbe26f44e937fe6451368324e009cba39e139dc08ad"
dependencies = [
"cfg-if",
"serde",
"serde_json",
"wasm-bindgen-macro",
]

View File

@ -1,23 +0,0 @@
FROM node:16-alpine@sha256:004dbac84fed48e20f9888a23e32fa7cf83c2995e174a78d41d9a9dd1e051a20
# Build ETH
WORKDIR /usr/src/ethereum
COPY ethereum/package.json ethereum/package-lock.json ./
RUN --mount=type=cache,uid=1000,gid=1000,target=/home/node/.npm \
npm ci
COPY ethereum .
# Build Wormhole SDK
WORKDIR /usr/src/sdk/js
COPY sdk/js/package.json sdk/js/package-lock.json ./
RUN --mount=type=cache,uid=1000,gid=1000,target=/home/node/.npm \
npm ci
COPY sdk/js .
# Build wormhole sdk attester in dir preserving directory structure
WORKDIR /usr/src/wormhole_attester/sdk/js/
COPY wormhole_attester/sdk/js/package.json wormhole_attester/sdk/js/package-lock.json ./
RUN --mount=type=cache,uid=1000,gid=1000,target=/home/node/.npm \
npm ci
COPY wormhole_attester/sdk .
RUN npm run build-test

View File

@ -0,0 +1,5 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
};

File diff suppressed because it is too large Load Diff

View File

@ -1,19 +1,17 @@
{
"name": "@pythnetwork/wormhole-attester-sdk",
"version": "1.0.0",
"description": "TypeScript library for interacting with Pyth2Wormhole",
"description": "Pyth Wormhole Attester SDk",
"types": "lib/index.d.ts",
"main": "lib/index.js",
"files": [
"lib/**/*"
],
"scripts": {
"build": "npm run build-lib",
"build-lib": "npm run copy-artifacts && tsc",
"build-watch": "npm run copy-artifacts && tsc --watch",
"build": "tsc",
"format": "prettier --write \"src/**/*.ts\"",
"copy-artifacts": "node scripts/copyWasm.cjs",
"lint": "tslint -p tsconfig.json",
"test": "jest src/",
"postversion": "git push && git push --tags",
"preversion": "npm run lint",
"version": "npm run format && git add -A src"
@ -22,26 +20,22 @@
"type": "git",
"url": "git+https://github.com/pyth-network/pyth-crosschain.git"
},
"author": "https://certus.one",
"author": "Pyth Data Association",
"license": "MIT",
"devDependencies": {
"@openzeppelin/contracts": "^4.2.0",
"@typechain/ethers-v5": "^7.1.2",
"@types/jest": "^29.4.0",
"@types/long": "^4.0.1",
"@types/node": "^16.6.1",
"copy-dir": "^1.3.0",
"find": "^0.3.0",
"jest": "^29.4.1",
"prettier": "^2.3.2",
"ts-jest": "^29.0.5",
"tslint": "^6.1.3",
"tslint-config-prettier": "^1.18.0",
"typescript": "^4.3.5"
},
"peerDependencies": {
"@solana/web3.js": "^1.24.0"
},
"dependencies": {
"@certusone/wormhole-sdk": "0.2.1",
"@improbable-eng/grpc-web-node-http-transport": "^0.14.1",
"@pythnetwork/pyth-sdk-js": "^1.1.0"
},
"bugs": {

View File

@ -1,16 +0,0 @@
const find = require("find");
const fs = require("fs");
const path = require("path");
const SOURCE_ROOT = "src";
const TARGET_ROOT = "lib";
find.eachfile(/\.wasm(\..*)?/, SOURCE_ROOT, (fname) => {
fname_copy = fname.replace(SOURCE_ROOT, TARGET_ROOT);
console.log("copyWasm:", fname, "to", fname_copy);
fs.mkdirSync(path.dirname(fname_copy), { recursive: true });
fs.copyFileSync(fname, fname_copy);
});

View File

@ -0,0 +1,133 @@
import {
parseBatchPriceAttestation,
Price,
PriceFeed,
PriceAttestation,
PriceAttestationStatus,
priceAttestationToPriceFeed,
} from "../index";
describe("Deserializing Batch Price Attestation works", () => {
test("when batch has 3 price feeds", () => {
// Generated from the rust sdk test_batch_serde
const fixture =
"50325748000300010001020003009D01010101010101010101010101010101010101010101010101010" +
"10101010101FEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFE0000002B" +
"AD2FEED70000000000000065FFFFFFFDFFFFFFFFFFFFFFD6000000000000002A010001E14C0004E6D00" +
"000DEADBEEFFADE00000000DADEBEEF00000000DEADBABE0000DEADFACEBEEF000000BADBADBEEFDEAD" +
"BEEFFADEDEAF0202020202020202020202020202020202020202020202020202020202020202FDFDFDF" +
"DFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFD0000002BAD2FEED70000000000" +
"000065FFFFFFFDFFFFFFFFFFFFFFD6000000000000002A010001E14C0004E6D00000DEADBEEFFADE000" +
"00000DADEBEEF00000000DEADBABE0000DEADFACEBEEF000000BADBADBEEFDEADBEEFFADEDEAF030303" +
"0303030303030303030303030303030303030303030303030303030303FCFCFCFCFCFCFCFCFCFCFCFCF" +
"CFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFC0000002BAD2FEED70000000000000065FFFFFFFDFFFF" +
"FFFFFFFFFFD6000000000000002A010001E14C0004E6D00000DEADBEEFFADE00000000DADEBEEF00000" +
"000DEADBABE0000DEADFACEBEEF000000BADBADBEEFDEADBEEFFADEDEAF";
const data = Buffer.from(fixture, "hex");
const batchPriceAttestation = parseBatchPriceAttestation(data);
expect(batchPriceAttestation.priceAttestations.length).toBe(3);
// values are from the rust sdk mock_attestation
batchPriceAttestation.priceAttestations.forEach((pa, idx) => {
expect(pa).toEqual<PriceAttestation>({
productId: Buffer.from(Array(32).fill(idx + 1)).toString("hex"),
priceId: Buffer.from(Array(32).fill(255 - idx - 1)).toString("hex"),
price: (0x2bad2feed7).toString(),
conf: "101",
emaPrice: "-42",
emaConf: "42",
expo: -3,
status: PriceAttestationStatus.Trading,
numPublishers: 123212,
maxNumPublishers: 321232,
attestationTime: 0xdeadbeeffade,
publishTime: 0xdadebeef,
prevPublishTime: 0xdeadbabe,
prevPrice: (0xdeadfacebeef).toString(),
prevConf: (0xbadbadbeef).toString(),
});
});
});
});
describe("Price Attestation to Price Feed works", () => {
test("when status is trading", () => {
const priceAttestation = {
productId: "012345",
priceId: "abcde",
price: "100",
conf: "5",
emaPrice: "103",
emaConf: "3",
expo: -3,
status: PriceAttestationStatus.Trading,
numPublishers: 1,
maxNumPublishers: 2,
attestationTime: 1000,
publishTime: 1000,
prevPublishTime: 998,
prevPrice: "101",
prevConf: "6",
};
const priceFeed = priceAttestationToPriceFeed(priceAttestation);
expect(priceFeed).toEqual(
new PriceFeed({
id: "abcde",
price: new Price({
price: "100",
conf: "5",
expo: -3,
publishTime: 1000,
}),
emaPrice: new Price({
price: "103",
conf: "3",
expo: -3,
publishTime: 1000,
}),
})
);
});
test("when status is not trading", () => {
const priceAttestation = {
productId: "012345",
priceId: "abcde",
price: "100",
conf: "5",
emaPrice: "103",
emaConf: "3",
expo: -3,
status: PriceAttestationStatus.Unknown,
numPublishers: 1,
maxNumPublishers: 2,
attestationTime: 1000,
publishTime: 1000,
prevPublishTime: 998,
prevPrice: "101",
prevConf: "6",
};
const priceFeed = priceAttestationToPriceFeed(priceAttestation);
expect(priceFeed).toEqual(
new PriceFeed({
id: "abcde",
price: new Price({
price: "101",
conf: "6",
expo: -3,
publishTime: 998,
}),
emaPrice: new Price({
price: "103",
conf: "3",
expo: -3,
publishTime: 998,
}),
})
);
});
});

View File

@ -1,19 +1,12 @@
import { getSignedVAA, CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk";
import { zeroPad } from "ethers/lib/utils";
import { PublicKey } from "@solana/web3.js";
import { PriceFeed, Price, UnixTimestamp } from "@pythnetwork/pyth-sdk-js";
export { PriceFeed, Price, UnixTimestamp } from "@pythnetwork/pyth-sdk-js";
let _P2W_WASM: any;
async function importWasm() {
if (!_P2W_WASM) {
if (typeof window === "undefined") {
_P2W_WASM = await import("./wasm/nodejs/pyth_wormhole_attester_sdk");
} else {
_P2W_WASM = await import("./wasm/bundler/pyth_wormhole_attester_sdk");
}
}
return _P2W_WASM;
export enum PriceAttestationStatus {
Unknown = 0,
Trading = 1,
Halted = 2,
Auction = 3,
IGNORED = 4,
}
export type PriceAttestation = {
@ -24,7 +17,7 @@ export type PriceAttestation = {
expo: number;
emaPrice: string;
emaConf: string;
status: number;
status: PriceAttestationStatus;
numPublishers: number;
maxNumPublishers: number;
attestationTime: UnixTimestamp;
@ -38,13 +31,146 @@ export type BatchPriceAttestation = {
priceAttestations: PriceAttestation[];
};
export async function parseBatchPriceAttestation(
arr: Buffer
): Promise<BatchPriceAttestation> {
const wasm = await importWasm();
const rawVal = await wasm.parse_batch_attestation(arr);
/// Precedes every message implementing the wormhole attester serialization format
const P2W_FORMAT_MAGIC: string = "P2WH";
const P2W_FORMAT_VER_MAJOR = 3;
const P2W_FORMAT_VER_MINOR = 0;
const P2W_FORMAT_PAYLOAD_ID = 2;
return rawVal;
export function parsePriceAttestation(bytes: Buffer): PriceAttestation {
let offset = 0;
const productId = bytes.slice(offset, offset + 32).toString("hex");
offset += 32;
const priceId = bytes.slice(offset, offset + 32).toString("hex");
offset += 32;
const price = bytes.readBigInt64BE(offset).toString();
offset += 8;
const conf = bytes.readBigUint64BE(offset).toString();
offset += 8;
const expo = bytes.readInt32BE(offset);
offset += 4;
const emaPrice = bytes.readBigInt64BE(offset).toString();
offset += 8;
const emaConf = bytes.readBigUint64BE(offset).toString();
offset += 8;
const status = bytes.readUint8(offset) as PriceAttestationStatus;
offset += 1;
const numPublishers = bytes.readUint32BE(offset);
offset += 4;
const maxNumPublishers = bytes.readUint32BE(offset);
offset += 4;
const attestationTime = Number(bytes.readBigInt64BE(offset));
offset += 8;
const publishTime = Number(bytes.readBigInt64BE(offset));
offset += 8;
const prevPublishTime = Number(bytes.readBigInt64BE(offset));
offset += 8;
const prevPrice = bytes.readBigInt64BE(offset).toString();
offset += 8;
const prevConf = bytes.readBigUint64BE(offset).toString();
offset += 8;
return {
productId,
priceId,
price,
conf,
expo,
emaPrice,
emaConf,
status,
numPublishers,
maxNumPublishers,
attestationTime,
publishTime,
prevPublishTime,
prevPrice,
prevConf,
};
}
// Read the sdk/rust as the reference implementation and documentation.
export function parseBatchPriceAttestation(
bytes: Buffer
): BatchPriceAttestation {
let offset = 0;
const magic = bytes.slice(offset, offset + 4).toString("utf8");
offset += 4;
if (magic !== P2W_FORMAT_MAGIC) {
throw new Error(`Invalid magic: ${magic}, expected: ${P2W_FORMAT_MAGIC}`);
}
const versionMajor = bytes.readUInt16BE(offset);
offset += 2;
if (versionMajor !== P2W_FORMAT_VER_MAJOR) {
throw new Error(
`Unsupported major version: ${versionMajor}, expected: ${P2W_FORMAT_VER_MAJOR}`
);
}
const versionMinor = bytes.readUInt16BE(offset);
offset += 2;
if (versionMinor < P2W_FORMAT_VER_MINOR) {
throw new Error(
`Unsupported minor version: ${versionMinor}, expected: ${P2W_FORMAT_VER_MINOR}`
);
}
// Header size is added for future-compatibility
const headerSize = bytes.readUint16BE(offset);
offset += 2;
let headerOffset = 0;
const payloadId = bytes.readUint8(offset + headerOffset);
headerOffset += 1;
if (payloadId !== P2W_FORMAT_PAYLOAD_ID) {
throw new Error(
`Invalid payloadId: ${payloadId}, expected: ${P2W_FORMAT_PAYLOAD_ID}`
);
}
offset += headerSize;
const batchLen = bytes.readUInt16BE(offset);
offset += 2;
const attestationSize = bytes.readUint16BE(offset);
offset += 2;
let priceAttestations: PriceAttestation[] = [];
for (let i = 0; i < batchLen; i += 1) {
priceAttestations.push(
parsePriceAttestation(bytes.subarray(offset, offset + attestationSize))
);
offset += attestationSize;
}
if (offset !== bytes.length) {
throw new Error(`Invalid length: ${bytes.length}, expected: ${offset}`);
}
return {
priceAttestations,
};
}
// Returns a hash of all priceIds within the batch, it can be used to identify whether there is a
@ -75,27 +201,6 @@ export function getBatchSummary(batch: BatchPriceAttestation): string {
return JSON.stringify(abstractRepresentation);
}
export async function getSignedAttestation(
host: string,
p2wAddr: string,
sequence: number,
extraGrpcOpts = {}
): Promise<any> {
const [emitter, _] = await PublicKey.findProgramAddress(
[Buffer.from("p2w-emitter")],
new PublicKey(p2wAddr)
);
const emitterHex = sol_addr2buf(emitter).toString("hex");
return await getSignedVAA(
host,
CHAIN_ID_SOLANA,
emitterHex,
"" + sequence,
extraGrpcOpts
);
}
export function priceAttestationToPriceFeed(
priceAttestation: PriceAttestation
): PriceFeed {
@ -108,7 +213,7 @@ export function priceAttestationToPriceFeed(
let price: Price;
if (priceAttestation.status === 1) {
if (priceAttestation.status === PriceAttestationStatus.Trading) {
// 1 means trading
price = new Price({
conf: priceAttestation.conf,
@ -135,7 +240,3 @@ export function priceAttestationToPriceFeed(
price,
});
}
function sol_addr2buf(addr: PublicKey): Buffer {
return Buffer.from(zeroPad(addr.toBytes(), 32));
}

File diff suppressed because it is too large Load Diff

View File

@ -11,14 +11,12 @@ crate-type = ["cdylib", "rlib"]
[features]
default = []
solana = ["solitaire", "solana-program", "pyth-sdk-solana"]
wasm = ["wasm-bindgen", "solana"]
[dependencies]
hex = "0.4.3"
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
pyth-sdk = {version = "0.5.0"}
pyth-sdk-solana = { version = "0.5.0", optional = true }
wasm-bindgen = { version = "0.2.74", features = ["serde-serialize"], optional = true}
solitaire = { git = "https://github.com/wormhole-foundation/wormhole", tag = "v2.8.9", optional = true}
solana-program = { version = "=1.10.31", optional = true }

View File

@ -34,14 +34,6 @@ use {
},
};
#[cfg(feature = "wasm")]
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
pub mod wasm;
#[cfg(feature = "wasm")]
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
use wasm_bindgen::prelude::*;
pub type ErrBox = Box<dyn std::error::Error>;
/// Precedes every message implementing the p2w serialization format
@ -527,7 +519,7 @@ mod tests {
status: PriceStatus::Trading,
num_publishers: 123212u32,
max_num_publishers: 321232u32,
attestation_time: (0xdeadbeeffadedeedu64) as i64,
attestation_time: (0xdeadbeeffadeu64) as i64,
publish_time: 0xdadebeefi64,
prev_publish_time: 0xdeadbabei64,
prev_price: 0xdeadfacebeefi64,
@ -567,7 +559,7 @@ mod tests {
#[test]
fn test_batch_serde() -> Result<(), ErrBox> {
let attestations: Vec<_> = (1..=10)
let attestations: Vec<_> = (1..=3)
.map(|i| {
mock_attestation(
Some([(i % 256) as u8; 32]),

View File

@ -1,33 +0,0 @@
use {
crate::{
BatchPriceAttestation,
P2WEmitter,
PriceAttestation,
},
solana_program::pubkey::Pubkey,
solitaire::Seeded,
std::str::FromStr,
wasm_bindgen::prelude::*,
};
#[wasm_bindgen]
pub fn get_emitter_address(program_id: String) -> Vec<u8> {
let program_id = Pubkey::from_str(program_id.as_str()).unwrap();
let emitter = P2WEmitter::key(None, &program_id);
emitter.to_bytes().to_vec()
}
#[wasm_bindgen]
pub fn parse_attestation(bytes: Vec<u8>) -> JsValue {
let a = PriceAttestation::deserialize(bytes.as_slice()).unwrap();
JsValue::from_serde(&a).unwrap()
}
#[wasm_bindgen]
pub fn parse_batch_attestation(bytes: Vec<u8>) -> JsValue {
let a = BatchPriceAttestation::deserialize(bytes.as_slice()).unwrap();
JsValue::from_serde(&a).unwrap()
}