EVM signer for single prices implemented (#7)

* evm signer for single prices implemented

* commented code removed

* minor fixes after Kuba's review and tests improvement
This commit is contained in:
Alex Suvorov
2021-07-02 00:56:35 +02:00
committed by GitHub
parent ba4b5338ad
commit daaaef1073
9 changed files with 188 additions and 64 deletions

View File

@@ -2,6 +2,7 @@
"arweaveKeysFile": "./.secrets/arweave-keyfile-33F0QHcb22W7LwWR1iRC8Az1ntZG09XQ03YWuw2ABqA.json",
"useManifestFromSmartContract": "true",
"minimumArBalance": 0.1,
"addEvmSignature": true,
"credentials": {
"infuraProjectId": "XXX",
"ethereumPrivateKey": "0x1111111111111111111111111111111111111111111111111111111111111111"

View File

@@ -4,7 +4,6 @@ import Transaction from "arweave/node/lib/transaction";
import aggregators from "./aggregators";
import broadcaster from "./broadcasters/lambda-broadcaster";
import ArweaveProxy from "./arweave/ArweaveProxy";
import EvmPriceSigner from "./utils/EvmPriceSigner";
import {trackEnd, trackStart, printTrackingState} from "./utils/performance-tracker";
import {Manifest, NodeConfig, PriceDataAfterAggregation, PriceDataSigned, SignedPricePackage} from "./types";
import mode from "../mode";
@@ -12,6 +11,7 @@ import ManifestHelper, {TokensBySource} from "./manifest/ManifestParser";
import ArweaveService from "./arweave/ArweaveService";
import PricesService, {PricesBeforeAggregation, PricesDataFetched} from "./fetchers/PricesService";
import {mergeObjects, readJSON, sleep} from "./utils/objects";
import PriceSignerService from "./signers/PriceSignerService";
const logger = require("./utils/logger")("runner") as Consola;
const pjson = require("../package.json") as any;
@@ -27,8 +27,8 @@ export default class NodeRunner {
private currentManifest?: Manifest;
private pricesService?: PricesService;
private tokensBySource?: TokensBySource;
private evmSigner?: EvmPriceSigner;
private newManifest?: Manifest;
private priceSignerService?: PriceSignerService;
private constructor(
private readonly arweaveService: ArweaveService,
@@ -155,11 +155,21 @@ export default class NodeRunner {
private async doProcessTokens(): Promise<void> {
logger.info("Processing tokens");
// Fetching and aggregating
const aggregatedPrices: PriceDataAfterAggregation[] = await this.fetchPrices();
const arTransaction: Transaction = await this.arweaveService.prepareArweaveTransaction(aggregatedPrices, this.version);
const signedPrices: PriceDataSigned[] = await this.arweaveService.signPrices(
aggregatedPrices, arTransaction.id, this.providerAddress);
const arTransaction: Transaction = await this.arweaveService.prepareArweaveTransaction(
aggregatedPrices,
this.version);
const pricesReadyForSigning = this.pricesService!.preparePricesForSigning(
aggregatedPrices,
arTransaction.id,
this.providerAddress);
// Signing
const signedPrices: PriceDataSigned[] =
await this.priceSignerService!.signPrices(pricesReadyForSigning);
// Broadcasting
await NodeRunner.broadcastPrices(signedPrices);
await this.broadcastEvmPricePackage(signedPrices);
@@ -169,7 +179,6 @@ export default class NodeRunner {
logger.info(
`Transaction posting skipped in non-prod env: ${arTransaction.id}`);
}
}
private async fetchPrices(): Promise<PriceDataAfterAggregation[]> {
@@ -219,9 +228,7 @@ export default class NodeRunner {
logger.info("Broadcasting price package");
const packageBroadcastingTrackingId = trackStart("package-broadcasting");
try {
const signedPackage = this.evmSigner!.getSignedPackage(
signedPrices,
this.nodeConfig.credentials.ethereumPrivateKey);
const signedPackage = this.priceSignerService!.signPricePackage(signedPrices);
await this.broadcastSignedPricePackage(signedPackage);
logger.info("Package broadcasting completed");
} catch (e) {
@@ -315,7 +322,13 @@ export default class NodeRunner {
this.currentManifest = newManifest;
this.pricesService = new PricesService(newManifest, this.nodeConfig.credentials);
this.tokensBySource = ManifestHelper.groupTokensBySource(newManifest);
this.evmSigner = new EvmPriceSigner(this.version, this.currentManifest.evmChainId);
this.priceSignerService = new PriceSignerService({
arweaveService: this.arweaveService,
ethereumPrivateKey: this.nodeConfig.credentials.ethereumPrivateKey,
evmChainId: newManifest.evmChainId,
version: this.version,
addEvmSignature: Boolean(this.nodeConfig.addEvmSignature),
});
this.newManifest = undefined;
}

View File

@@ -4,7 +4,7 @@ import {
Manifest,
PriceDataAfterAggregation,
PriceDataBeforeSigning,
PriceDataSigned
PriceDataSigned,
} from "../types";
import ArweaveProxy from "./ArweaveProxy";
import {trackEnd, trackStart} from "../utils/performance-tracker";
@@ -69,39 +69,13 @@ export default class ArweaveService {
}
}
async signPrices(
prices: PriceDataAfterAggregation[],
idArTransaction: string,
providerAddress: string
): Promise<PriceDataSigned[]> {
const signingTrackingId = trackStart("signing");
const signedPrices: PriceDataSigned[] = [];
for (const price of prices) {
logger.info(`Signing price: ${price.id}`);
//TODO: check if signing in parallel would improve performance - https://app.clickup.com/t/k391rf
const signed: PriceDataSigned = await this.signPrice({
...price,
permawebTx: idArTransaction,
provider: providerAddress,
});
signedPrices.push(signed);
}
trackEnd(signingTrackingId);
return signedPrices;
}
async getCurrentManifest(): Promise<Manifest> {
const jwkAddress = await this.arweave.getAddress();
const result = await providersRegistry.currentManifest(jwkAddress, false, this.arweave.jwk);
return result.manifest.activeManifestContent;
}
private async signPrice(price: PriceDataBeforeSigning): Promise<PriceDataSigned> {
async signPrice(price: PriceDataBeforeSigning): Promise<PriceDataSigned> {
const priceWithSortedProps = deepSortObject(price);
const priceStringified = JSON.stringify(priceWithSortedProps);
const signature = await this.arweave.sign(priceStringified);

View File

@@ -8,6 +8,7 @@ import {
Manifest,
PriceDataAfterAggregation,
PriceDataBeforeAggregation,
PriceDataBeforeSigning,
PriceDataFetched
} from "../types";
import {trackEnd, trackStart} from "../utils/performance-tracker";
@@ -152,6 +153,23 @@ export default class PricesService {
return aggregatedPrices;
}
preparePricesForSigning(
prices: PriceDataAfterAggregation[],
idArTransaction: string,
providerAddress: string): PriceDataBeforeSigning[] {
const pricesBeforeSigning: PriceDataBeforeSigning[] = [];
for (const price of prices) {
pricesBeforeSigning.push({
...price,
permawebTx: idArTransaction,
provider: providerAddress,
});
}
return pricesBeforeSigning;
}
private maxPriceDeviationPercent(priceSymbol: string): number {
const result = ManifestHelper.getMaxDeviationForSymbol(priceSymbol, this.manifest);
if (result === null) {

View File

@@ -6,7 +6,6 @@ import {
PricePackage,
ShortSinglePrice,
SignedPricePackage,
PriceDataSigned,
} from "../types";
import _ from "lodash";
@@ -51,21 +50,6 @@ export default class EvmPriceSigner {
};
}
getSignedPackage(prices: PriceDataSigned[], privateKey: string) {
if (prices.length === 0) {
throw new Error("Price package should contain at least one price");
}
const pricePackage = {
timestamp: prices[0].timestamp,
prices: prices.map(p => _.pick(p, ["symbol", "value"])),
};
return this.signPricePackage(
pricePackage,
privateKey);
}
signPricePackage(pricePackage: PricePackage, privateKey: string): SignedPricePackage {
const data: any = {
types: {

View File

@@ -0,0 +1,79 @@
import {Consola} from "consola";
import _ from "lodash";
import EvmPriceSigner from "./EvmPriceSigner";
import ArweaveService from "../arweave/ArweaveService";
import { PriceDataBeforeSigning, PriceDataSigned, SignedPricePackage } from "../types";
import { trackStart, trackEnd } from "../utils/performance-tracker";
const logger = require("../utils/logger")("ArweaveService") as Consola;
interface PriceSignerConfig {
version: string;
evmChainId: number;
ethereumPrivateKey: string;
arweaveService: ArweaveService;
addEvmSignature: boolean;
};
// Business service that supplies signing operations required by Redstone-Node
export default class PriceSignerService {
private arweaveService: ArweaveService;
private evmSigner: EvmPriceSigner;
private ethereumPrivateKey: string;
private addEvmSignature: boolean;
constructor(config: PriceSignerConfig) {
this.evmSigner = new EvmPriceSigner(config.version, config.evmChainId);
this.arweaveService = config.arweaveService;
this.ethereumPrivateKey = config.ethereumPrivateKey;
this.addEvmSignature = config.addEvmSignature;
}
async signPrices(prices: PriceDataBeforeSigning[], ): Promise<PriceDataSigned[]> {
const signingTrackingId = trackStart("signing");
const signedPrices: PriceDataSigned[] = [];
try {
for (const price of prices) {
logger.info(`Signing price: ${price.id}`);
const signedPrice = await this.signSinglePrice(price);
signedPrices.push(signedPrice);
}
return signedPrices;
} finally {
trackEnd(signingTrackingId);
}
}
async signSinglePrice(price: PriceDataBeforeSigning): Promise<PriceDataSigned> {
logger.info(`Signing price with arweave signer: ${price.id}`);
const signedPrice = await this.arweaveService.signPrice(price);
if (this.addEvmSignature) {
logger.info(`Signing price with evm signer: ${price.id}`);
const packageWithSinglePrice = this.evmSigner.signPricePackage({
prices: [_.pick(price, ["symbol", "value"])],
timestamp: price.timestamp,
}, this.ethereumPrivateKey);
signedPrice.evmSignature = packageWithSinglePrice.signature;
}
return signedPrice;
}
signPricePackage(prices: PriceDataSigned[]): SignedPricePackage {
if (prices.length === 0) {
throw new Error("Price package should contain at least one price");
}
const pricePackage = {
timestamp: prices[0].timestamp,
prices: prices.map(p => _.pick(p, ["symbol", "value"])),
};
return this.evmSigner.signPricePackage(
pricePackage,
this.ethereumPrivateKey);
}
}

View File

@@ -69,6 +69,7 @@ export interface PriceDataBeforeSigning extends PriceDataAfterAggregation {
export interface PriceDataSigned extends PriceDataBeforeSigning {
signature: string;
evmSignature?: string;
};
export interface ShortSinglePrice {
@@ -81,7 +82,7 @@ export interface PricePackage {
timestamp: number;
};
export type SignedPricePackage = {
export interface SignedPricePackage {
pricePackage: PricePackage;
signer: string;
signature: string;
@@ -95,6 +96,7 @@ export interface ArweaveTransactionTags {
export interface NodeConfig {
arweaveKeysFile: string;
useManifestFromSmartContract?: boolean;
addEvmSignature?: boolean;
manifestFile: string;
minimumArBalance: number;
credentials: Credentials;

View File

@@ -1,6 +1,6 @@
import { ethers } from "ethers";
import { SignedPricePackage, PricePackage } from "../src/types";
import EvmPriceSigner from "../src/utils/EvmPriceSigner";
import EvmPriceSigner from "../src/signers/EvmPriceSigner";
const evmSigner = new EvmPriceSigner();
const ethereumPrivateKey = ethers.Wallet.createRandom().privateKey;

View File

@@ -25,6 +25,18 @@ jest.mock("../../src/arweave/ArweaveProxy", () => {
return jest.fn().mockImplementation(() => mockArProxy);
});
jest.mock("../../src/signers/EvmPriceSigner", () => {
return jest.fn().mockImplementation(() => {
return {
signPricePackage: (pricePackage: any) => ({
signature: "mock_evm_signed",
signer: "mock_evm_signer",
pricePackage,
}),
};
})
});
jest.mock("../../src/fetchers/coinbase");
jest.mock("../../src/fetchers/kraken");
@@ -69,6 +81,7 @@ describe("NodeRunner", () => {
infuraProjectId: "ipid",
ethereumPrivateKey: "0x1111111111111111111111111111111111111111111111111111111111111111"
},
addEvmSignature: true,
manifestFile: "",
minimumArBalance: 0.2
}
@@ -239,6 +252,7 @@ describe("NodeRunner", () => {
"http://broadcast.test/prices",
[
{
"evmSignature": "mock_evm_signed",
"id": "00000000-0000-0000-0000-000000000000",
"permawebTx": "mockArTransactionId",
"provider": "mockArAddress",
@@ -255,8 +269,8 @@ describe("NodeRunner", () => {
"http://broadcast.test/packages",
{
timestamp: 111111111,
signature: "0x5b2dd26ee75261b8a9c25b4f3eb8bd44292f4e1aeae9867b6f9a9a61a0b98e397be8b1eb1c972a58ac1baec3d2caefe273a39ee0606a66b6bd3c2d1b8db471471c",
signer: "0x19E7E376E7C213B7E7e7e46cc70A5dD086DAff2A",
signature: "mock_evm_signed",
signer: "mock_evm_signer",
provider: "mockArAddress"
}
);
@@ -295,27 +309,66 @@ describe("NodeRunner", () => {
);
await sut.run();
expect(axios.post).toHaveBeenCalledWith(
mode.broadcasterUrl + "/prices",
[
{
"id": "00000000-0000-0000-0000-000000000000",
"source": {
"coinbase": 444,
"kraken": 445
},
"symbol": "BTC",
"timestamp": 111111111,
"version": "0.4",
"value": 444.5,
"permawebTx": "mockArTransactionId",
"provider": "mockArAddress",
"signature": "mock_signed",
"source": {"coinbase": 444, "kraken": 445},
"symbol": "BTC",
"timestamp": 111111111,
"value": 444.5,
"version": "0.4"
"evmSignature": "mock_evm_signed"
}
]);
]
);
expect(mockArProxy.postTransaction).toHaveBeenCalledWith({
"id": "mockArTransactionId"
});
});
it("should broadcast prices without evm signature when addEvmSignature is not set", async () => {
const sut = await NodeRunner.create(
jwk,
{
...nodeConfig,
addEvmSignature: false,
}
);
await sut.run();
expect(axios.post).toHaveBeenCalledWith(
mode.broadcasterUrl + "/prices",
[
{
"id": "00000000-0000-0000-0000-000000000000",
"source": {
"coinbase": 444,
"kraken": 445
},
"symbol": "BTC",
"timestamp": 111111111,
"version": "0.4",
"value": 444.5,
"permawebTx": "mockArTransactionId",
"provider": "mockArAddress",
"signature": "mock_signed"
}
]
);
});
describe("when useManifestFromSmartContract flag is set", () => {
let nodeConfigManifestFromAr: any;
beforeEach(() => {