Skip to content

Commit

Permalink
Merge pull request #337 from near/rpc-light-client-proof
Browse files Browse the repository at this point in the history
add rpc for light client proof
  • Loading branch information
vgrichina authored Jun 19, 2020
2 parents fcc3696 + ef58c62 commit 30a720c
Show file tree
Hide file tree
Showing 7 changed files with 190 additions and 3 deletions.
7 changes: 6 additions & 1 deletion lib/providers/json-rpc-provider.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions lib/providers/json-rpc-provider.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 45 additions & 0 deletions lib/providers/provider.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion lib/providers/provider.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 9 additions & 1 deletion src/providers/json-rpc-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import depd from 'depd';
import {
Provider, FinalExecutionOutcome, NodeStatusResult, BlockId, Finality,
BlockResult, ChunkId, ChunkResult, adaptTransactionResult, EpochValidatorInfo,
GenesisConfig
GenesisConfig, LightClientProof, LightClientProofRequest
} from './provider';
import { Network } from '../utils/network';
import { ConnectionInfo, fetchJson } from '../utils/web';
Expand Down Expand Up @@ -123,6 +123,14 @@ export class JsonRpcProvider extends Provider {
return await this.sendJsonRpc('EXPERIMENTAL_genesis_config', []);
}

/**
* Gets EXPERIMENTAL_light_client_proof from RPC (https://github.com/nearprotocol/NEPs/blob/master/specs/ChainSpec/LightClient.md#light-client-proof)
* @returns {Promise<LightClientProof>}
*/
async experimental_lightClientProof(request: LightClientProofRequest): Promise<LightClientProof> {
return await this.sendJsonRpc('EXPERIMENTAL_light_client_proof', request);
}

/**
* Directly call the RPC specifying the method and params
* @param method RPC method
Expand Down
53 changes: 53 additions & 0 deletions src/providers/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,13 @@ export interface ExecutionOutcome {
status: ExecutionStatus | ExecutionStatusBasic;
}

export interface ExecutionOutcomeWithIdView {
proof: MerklePath;
block_hash: string;
id: string;
outcome: ExecutionOutcome;
}

export interface FinalExecutionOutcome {
status: FinalExecutionStatus | FinalExecutionStatusBasic;
transaction: any;
Expand Down Expand Up @@ -183,6 +190,51 @@ export interface EpochValidatorInfo {
epoch_start_height: number;
}

export interface MerkleNode {
hash: string;
direction: string;
}

export type MerklePath = MerkleNode[];

export interface BlockHeaderInnerLiteView {
height: number;
epoch_id: string;
next_epoch_id: string;
prev_state_root: string;
outcome_root: string;
timestamp: number;
next_bp_hash: string;
block_merkle_root: string;
}

export interface LightClientBlockLiteView {
prev_block_hash: string;
inner_rest_hash: string;
inner_lite: BlockHeaderInnerLiteView;
}

export interface LightClientProof {
outcome_proof: ExecutionOutcomeWithIdView;
outcome_root_proof: MerklePath;
block_header_lite: LightClientBlockLiteView;
block_proof: MerklePath;
}

export enum IdType {
Transaction = 'transaction',
Receipt = 'receipt',
}

export interface LightClientProofRequest {
type: IdType;
light_client_head: string;
transaction_hash?: string;
sender_id?: string;
receipt_id?: string;
receiver_id?: string;
}

export abstract class Provider {
abstract async getNetwork(): Promise<Network>;
abstract async status(): Promise<NodeStatusResult>;
Expand All @@ -194,6 +246,7 @@ export abstract class Provider {
abstract async chunk(chunkId: ChunkId): Promise<ChunkResult>;
abstract async validators(blockId: BlockId): Promise<EpochValidatorInfo>;
abstract async experimental_genesisConfig(): Promise<GenesisConfig>;
abstract async experimental_lightClientProof(request: LightClientProofRequest): Promise<LightClientProof>;
}

export function getTransactionLastResult(txResult: FinalExecutionOutcome): any {
Expand Down
64 changes: 64 additions & 0 deletions test/providers.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@

const nearApi = require('../lib/index');
const testUtils = require('./test-utils');
const BN = require('bn.js');

const withProvider = (fn) => {
const config = Object.assign(require('./config')(process.env.NODE_ENV || 'test'));
Expand Down Expand Up @@ -77,3 +79,65 @@ test('final tx result with null', async() => {
};
expect(nearApi.providers.getTransactionLastResult(result)).toEqual(null);
});

test('json rpc light client proof', async() => {
jest.setTimeout(30000);
const nearjs = await testUtils.setUpTestConnection();
const workingAccount = await testUtils.createAccount(await nearjs.account(testUtils.testAccountName), { amount: testUtils.INITIAL_BALANCE.mul(new BN(100)) });
const executionOutcome = await workingAccount.sendMoney(testUtils.testAccountName, new BN(10000));
const provider = nearjs.connection.provider;

async function waitForStatusMatching(isMatching) {
const MAX_ATTEMPTS = 10;
for (let i = 0; i < MAX_ATTEMPTS; i++) {
await testUtils.sleep(500);
const nodeStatus = await provider.status();
if (isMatching(nodeStatus)) {
return nodeStatus;
}
}
throw new Error(`Exceeded ${MAX_ATTEMPTS} attempts waiting for matching node status.`);
}

const comittedStatus = await waitForStatusMatching(status =>
status.sync_info.latest_block_hash !== executionOutcome.transaction_outcome.block_hash);
const BLOCKS_UNTIL_FINAL = 2;
const finalizedStatus = await waitForStatusMatching(status =>
status.sync_info.latest_block_height > comittedStatus.sync_info.latest_block_height + BLOCKS_UNTIL_FINAL);

const block = await provider.block(finalizedStatus.sync_info.latest_block_hash);
const lightClientHead = block.header.last_final_block;
let lightClientRequest = {
type: 'transaction',
light_client_head: lightClientHead,
transaction_hash: executionOutcome.transaction.hash,
sender_id: workingAccount.accountId,
};
const lightClientProof = await provider.experimental_lightClientProof(lightClientRequest);
expect('prev_block_hash' in lightClientProof.block_header_lite).toBe(true);
expect('inner_rest_hash' in lightClientProof.block_header_lite).toBe(true);
expect('inner_lite' in lightClientProof.block_header_lite).toBe(true);
expect(lightClientProof.outcome_proof.id).toEqual(executionOutcome.transaction_outcome.id);
expect('block_hash' in lightClientProof.outcome_proof).toBe(true);
expect(lightClientProof.outcome_root_proof).toEqual([]);
expect(lightClientProof.block_proof.length).toBeGreaterThan(0);

// pass nonexistent hash for light client head will fail
lightClientRequest = {
type: 'transaction',
light_client_head: '11111111111111111111111111111111',
transaction_hash: executionOutcome.transaction.hash,
sender_id: workingAccount.accountId,
};
await expect(provider.experimental_lightClientProof(lightClientRequest)).rejects.toThrow('DB Not Found Error');

// Use old block hash as light client head should fail
lightClientRequest = {
type: 'transaction',
light_client_head: executionOutcome.transaction_outcome.block_hash,
transaction_hash: executionOutcome.transaction.hash,
sender_id: workingAccount.accountId,
};

await expect(provider.experimental_lightClientProof(lightClientRequest)).rejects.toThrow(/.+ block .+ is ahead of head block .+/);
});

0 comments on commit 30a720c

Please sign in to comment.