diff --git a/index.d.ts b/index.d.ts index 65ef5f2..6131c00 100644 --- a/index.d.ts +++ b/index.d.ts @@ -254,15 +254,24 @@ declare module "leap-core" { public static from(height: number, timestamp: number, txList: LeapTransaction[]): Block; } + type PeriodOptions = { + validatorData?: { + slotId: number; + ownerAddr: string | Buffer | number; + casBitmap?: string | Buffer | number; + }; + excludePrevHashFromProof?: Boolean; + }; + class Period { - constructor(prevHash: string, blocks: Array); + constructor(prevHash: string, blocks: Array, opts?: PeriodOptions); addBlock(block: Block): Period; getMerkleTree(): MerkleTree; merkleRoot(): string; proof(tx: Tx): Proof; static periodBlockRange(blockNumber: number): [number, number]; - static periodForBlockRange(plasma: ExtendedWeb3, startBlock: number, endBlock: number): Promise; - static periodForTx(plasma: ExtendedWeb3, tx: LeapTransaction): Promise; + static periodForBlockRange(plasma: ExtendedWeb3, startBlock: number, endBlock: number, periodOpts?: PeriodOptions): Promise; + static periodForTx(plasma: ExtendedWeb3, tx: LeapTransaction, periodOpts?: PeriodOptions): Promise; } export type Proof = string[]; @@ -331,7 +340,7 @@ declare module "leap-core" { }; type PeriodData = { - validatorAddress: string; + ownerAddr: string; slotId: number; casBitmap?: string; periodStart?: number; @@ -360,7 +369,7 @@ declare module "leap-core" { export function periodBlockRange(blockNumber: number): [number, number]; export function getTxWithYoungestBlock(txs: LeapTransaction[]): InputTx; export function getYoungestInputTx(plasma: ExtendedWeb3, tx: Tx): Promise; - export function getProof(plasma: ExtendedWeb3, tx: LeapTransaction, fallbackPeriodData?: PeriodData): Promise; + export function getProof(plasma: ExtendedWeb3, tx: LeapTransaction, periodOpts?: PeriodOptions): Promise; // Depending on plasma instance, resolves to either Web3's Transaction or Ethers' TransactionReceipt export function sendSignedTransaction(plasma: ExtendedWeb3, tx: string): Promise; export function simulateSpendCond(plasma: ExtendedWeb3, tx: Tx): Promise; diff --git a/lib/helpers.js b/lib/helpers.js index 54c5f7f..233428c 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -213,25 +213,26 @@ export function getYoungestInputTx(plasma, tx) { * * @param {ExtendedWeb3|LeapProvider} plasma instance of Leap Web3 * @param {LeapTransaction} tx + * @param {PeriodOpts} periodOpts — options for cosntructed Period as defined in Period constructor * @returns {Promise} promise that resolves to period inclusion proof */ -export function getProof(plasma, tx, fallbackPeriodData) { - return Promise.all([ - Period.periodForTx(plasma, tx), - plasma.getPeriodByBlockHeight(tx.blockNumber) - ]).then(([period, periodData]) => { - const [periodDataObj] = periodData || [fallbackPeriodData]; - if (!periodData || !periodData.length) { +export function getProof(plasma, tx, periodOpts = {}) { + return plasma.getPeriodByBlockHeight(tx.blockNumber) + .then(periodData => { + if (periodData && periodData.length) { + Object.assign(periodOpts, { + validatorData: periodData[0], + }); + } else { const msg = `No period data for the given tx. Height: ${tx.blockNumber}`; - if (!fallbackPeriodData) { + if (!periodOpts.validatorData) { throw new Error(msg); } else { console.warn(msg, 'Using fallback values'); // eslint-disable-line no-console } } - const { slotId, validatorAddress, casBitmap } = periodDataObj; - - period.setValidatorData(slotId, validatorAddress, casBitmap); + return Period.periodForTx(plasma, tx, periodOpts); + }).then((period) => { return period.proof(Tx.fromRaw(tx.raw)); }); } diff --git a/lib/helpers.spec.js b/lib/helpers.spec.js index f423301..9950f00 100644 --- a/lib/helpers.spec.js +++ b/lib/helpers.spec.js @@ -180,7 +180,7 @@ describe('helpers', () => { const transactions = n === 4 ? [{ raw: deposit1.hex() }] : []; return { number: n, timestamp: 123, transactions }; }, - getPeriodByBlockHeight: () => null, + getPeriodByBlockHeight: () => Promise.resolve(null), }; expect( @@ -199,15 +199,18 @@ describe('helpers', () => { const transactions = n === 4 ? [{ raw: deposit1.hex() }] : []; return { number: n, timestamp: 123, transactions }; }, - getPeriodByBlockHeight: () => null, + getPeriodByBlockHeight: () => Promise.resolve(null), }; - const fallbackData = { slotId: 0, validatorAddress: ADDR_1 }; + const periodOpts = { + validatorData: { slotId: 0, ownerAddr: ADDR_1 }, + excludePrevHashFromProof: true, + }; const proof = getProof( plasma, { blockNumber: 4, raw: deposit1.hex() }, - fallbackData + periodOpts ); return expect(proof).to.eventually.eql([ '0x29aa1b0213471dbf84175e8f688e5a63c2e5724ad6bc581a10b9521f4b8a6083', @@ -240,11 +243,15 @@ describe('helpers', () => { }, getPeriodByBlockHeight: n => { expect(n).to.be.equal(4); - return [{ slotId: 0, validatorAddress: ADDR_1, casBitmap }]; + return Promise.resolve([{ slotId: 0, ownerAddr: ADDR_1, casBitmap }]); }, }; - const proof = getProof(plasma, { blockNumber: 4, raw: deposit1.hex() }); + const proof = getProof( + plasma, + { blockNumber: 4, raw: deposit1.hex() }, + { excludePrevHashFromProof: true } + ); return expect(proof).to.eventually.eql([ '0x6eefe22ae29bc837d66e743334a70ecc19635c3c9ef31d4c2987b337b9d015c6', '0x4404003c00000000000000080000000000000000000000000000000000000000', diff --git a/lib/period.js b/lib/period.js index 5c33319..83af4e3 100644 --- a/lib/period.js +++ b/lib/period.js @@ -15,13 +15,40 @@ import Util from './util'; import { BLOCKS_PER_PERIOD } from './constants'; export default class Period { - constructor(prevHash, blocks) { + + /** + * + * Create a Period object linked to the previous Period via `prevHash` and + * containing given list of `blocks`. + * + * Optional parameters can be given via `opts` argument. Format as follows: + * + * type PeriodOptions = { + * validatorData?: { + * slotId: number; + * ownerAddr: string | Buffer | number; + * casBitmap?: string | Buffer | number; + * }; + * excludePrevHashFromProof?: Boolean; + * }; + * + * @param {string} prevHash - previous period root hash + * @param {Array} blocks - array of blocks to include in the period + * @param {PeriodOptions} opts - options defined as above. Default {} + */ + constructor(prevHash, blocks, opts = {}) { this.prevHash = prevHash; this.blockList = []; this.blockHashList = []; + this.usePrev = !opts.excludePrevHashFromProof; if (blocks) { blocks.forEach(block => this.addBlock(block)); } + + const { slotId, ownerAddr, casBitmap } = opts.validatorData || {}; + if ((slotId || slotId === 0) && ownerAddr) { + this.setValidatorData(slotId, ownerAddr, casBitmap); + } } addBlock(block) { @@ -64,6 +91,19 @@ export default class Period { } } + // + /** + * helpful: https://docs.google.com/drawings/d/13oFjua-v_E_yaFYUbralluI-EysgtTaZb_sSgGbxG_A + * + * period root + * / \ + * consensus root CAS root + * / \ / \ + * blocks root meta root CAS bitmap validator root + * / \ / \ + * fees hash prevPeriodHash * slotId 0x0 + * * ownerAddr + */ periodData() { if (typeof this.slotId === 'undefined' || !this.ownerAddr) { throw Error('period is missing validator data to create period root.'); @@ -81,11 +121,20 @@ export default class Period { } validatorRoot.copy(rootBuf, 32); const casRoot = keccak256(rootBuf); - + + // make metaRoot + let metaRoot = Buffer.alloc(32, 0); + if (this.usePrev) { + rootBuf = Buffer.alloc(64, 0); + toBuffer(this.prevHash).copy(rootBuf, 32); + metaRoot = keccak256(rootBuf); + } + // make consensusRoot const blocksRoot = this.getMerkleTree().getRoot(); rootBuf = Buffer.alloc(64, 0); blocksRoot.copy(rootBuf); + metaRoot.copy(rootBuf, 32); const consensusRoot = keccak256(rootBuf); // make period root @@ -96,6 +145,7 @@ export default class Period { return { casRoot, periodRoot, + metaRoot }; } @@ -104,6 +154,44 @@ export default class Period { return periodRoot; } + prevPeriodProof() { + if (!this.usePrev) { + throw Error('not set to use prev period in proofs'); + } + + const { casRoot } = this.periodData(); + + const proof = []; + + // fees hash + proof.push(bufferToHex(Buffer.alloc(32, 0))); + proof.push(this.prevHash); + proof.push(this.merkleRoot()); + proof.push(bufferToHex(casRoot)); + + return proof; + } + + // returns [currentPeriodRoot, prevPeriodRoot] + static evaluatePrevPeriodProof(proof) { + let result; + result = Buffer.alloc(64, 0); + // fees hash + toBuffer(proof[0]).copy(result); + // prevPeriod + toBuffer(proof[1]).copy(result, 32); + const metaRoot = keccak256(result); + result = Buffer.alloc(64, 0); + // blocks root + toBuffer(proof[2]).copy(result); + metaRoot.copy(result, 32); + const consensusRoot = keccak256(result); + result = Buffer.alloc(64, 0); + toBuffer(consensusRoot).copy(result); + toBuffer(proof[3]).copy(result, 32); + return [proof[1], bufferToHex(keccak256(result))]; + } + proof(tx) { let periodPos = -1; for (let i = 0; i < this.blockList.length; i++) { @@ -130,10 +218,10 @@ export default class Period { blockProof.forEach(elem => proof.push(elem)); // add extensible proof structure - const { casRoot, periodRoot } = this.periodData(); + const { casRoot, periodRoot, metaRoot } = this.periodData(); // update the proof - proof.push(bufferToHex(Buffer.alloc(32, 0))); + proof.push(bufferToHex(metaRoot)); proof.push(bufferToHex(casRoot)); proof[0] = periodRoot; return proof; @@ -159,9 +247,10 @@ export default class Period { * @param {ExtendedWeb3} plasma instance of Leap Web3 or Leap Ethers * @param {Number} startBlock first block to include in the period * @param {Number} endBlock last block to include in the period + * @param {PeriodOpts} periodOpts options for Period object as defined in Period constructor * @returns {Period} period */ - static periodForBlockRange(plasma, startBlock, endBlock) { + static periodForBlockRange(plasma, startBlock, endBlock, periodOpts = {}) { return Promise.all( Util.range(startBlock, endBlock).map(n => (plasma.eth || plasma).getBlock(n, true)), ).then((blocks) => { @@ -170,7 +259,7 @@ export default class Period { .map(({ number, timestamp, transactions }) => Block.from(number, timestamp, transactions), ); - return new Period(null, blockList); + return new Period(null, blockList, periodOpts); }); } @@ -179,12 +268,13 @@ export default class Period { * * @param {ExtendedWeb3} plasma instance of Leap Web3 * @param {Transaction} tx transaction to create {Period} for + * @param {PeriodOpts} periodOpts options for Period object as defined in Period constructor * @returns {Period} period */ - static periodForTx(plasma, tx) { + static periodForTx(plasma, tx, periodOpts = {}) { const { blockNumber } = tx; const [startBlock, endBlock] = Period.periodBlockRange(blockNumber); - return Period.periodForBlockRange(plasma, startBlock, endBlock); + return Period.periodForBlockRange(plasma, startBlock, endBlock, periodOpts); } } diff --git a/lib/period.spec.js b/lib/period.spec.js index f4dbebb..532d3ef 100644 --- a/lib/period.spec.js +++ b/lib/period.spec.js @@ -10,6 +10,20 @@ const PRIV = '0x94890218f2b0d04296f30aeafd13655eba4c5bbf1770273276fee52cbe3f2cb4 const ADDR = '0x82e8c6cf42c8d1ff9594b17a3f50e94a12cc860f'; const slotId = 4; +const validatorData = { + slotId, + ownerAddr: ADDR +}; + +/** + * Excluding prevHash from period proof as it was in the original version, + * so that we can verify proofs for older periods + */ +const legacyOpts = { + validatorData, + excludePrevHashFromProof: true +} + describe('periods', () => { it('should allow to get proof from period.', (done) => { const height = 123; @@ -24,8 +38,7 @@ describe('periods', () => { const block2 = new Block(height + 1); block2.addTx(Tx.deposit(2, value * 2, ADDR, color)); - const period = new Period(null, [block1, block2]); - period.setValidatorData(slotId, ADDR); + const period = new Period(null, [block1, block2], legacyOpts); const proof = period.proof(deposit1); expect(proof).to.eql([ period.periodRoot(), @@ -52,8 +65,7 @@ describe('periods', () => { const block2 = new Block(2); block2.addTx(deposit2); - const period = new Period(null, [block1]); - period.setValidatorData(slotId, ADDR); + const period = new Period(null, [block1], legacyOpts); expect( () => period.proof(deposit2) ).to.throw('tx not in this period'); @@ -73,8 +85,14 @@ describe('periods', () => { const block2 = new Block(height + 1); block2.addTx(Tx.deposit(2, value * 2, ADDR, color)); - const period = new Period(null, [block1, block2]); - period.setValidatorData(slotId, ADDR, PRIV); + const period = new Period(null, [block1, block2], { + validatorData: { + slotId, + ownerAddr: ADDR, + casBitmap: '0x4000000000000000000000000000000000000000000000000000000000000000' + }, + excludePrevHashFromProof: true + }); const proof = period.proof(deposit1); expect(proof).to.eql([ period.periodRoot(), @@ -84,7 +102,7 @@ describe('periods', () => { '0x430ce01c495ecaa94a3b4b3154906343e755b7f9e51bf3403b09dd932a0b18ee', '0x77bc0389ba07196637b929d5347b1453f3294175e9015e13b5e3c5fb19f3c0f4', '0x0000000000000000000000000000000000000000000000000000000000000000', - '0x492a381524e41b7bf6f1eef65ff5057dbfacea9e8bfea2c2c895c59b9369e245', + '0xde2318630368656c2cce43bb993c6a3c077bc0b1cc75341375439f5a1206d347', ]); done(); }); @@ -111,8 +129,7 @@ describe('periods', () => { block3.addTx(Tx.deposit(7, value, ADDR, color)); block3.addTx(Tx.deposit(8, value, ADDR, color)); - const period = new Period(null, [block1, block2, block3]); - period.setValidatorData(slotId, ADDR); + const period = new Period(null, [block1, block2, block3], legacyOpts); const proof = period.proof(deposit2); expect(proof).to.eql([ '0x8e08277cfeb7f80da02df9e165d59bd7fccc10221ee24c3d660d7a4739524fc7', @@ -140,8 +157,7 @@ describe('periods', () => { blocks.push(block); } - const period = new Period(null, blocks); - period.setValidatorData(slotId, ADDR); + const period = new Period(null, blocks, legacyOpts); const proof = period.proof(Tx.deposit(12, value, ADDR, color)); expect(proof).to.eql([ '0x275e30e070a5637312ec95d135e8b393824e38c420ab142e8b72bb1528a26088', @@ -175,8 +191,7 @@ describe('periods', () => { } } - const period = new Period(null, blocks); - period.setValidatorData(slotId, ADDR); + const period = new Period(null, blocks, legacyOpts); const proof = period.proof(Tx.deposit(13, value, ADDR, color)); expect(proof).to.eql([ '0x2f66d9caf3a49598e7259c850c5bf3bbf7cdc61be65846f03296b1c0f54386a2', @@ -218,8 +233,7 @@ describe('periods', () => { block = new Block(31).addTx(transfer); blocks.push(block); - const period = new Period(null, blocks); - period.setValidatorData(slotId, ADDR); + const period = new Period(null, blocks, legacyOpts); const proof = period.proof(transfer); expect(proof).to.eql([ @@ -243,6 +257,41 @@ describe('periods', () => { done(); }); + it('should allow to get proof from period with prevPeriod.', (done) => { + const prevTx = '0x7777777777777777777777777777777777777777777777777777777777777777'; + const value = 99000000; + const color = 1337; + const transfer = Tx.transfer( + [new Input(new Outpoint(prevTx, 0))], + [new Output(value / 2, ADDR, color), new Output(value / 2, ADDR, color)], + ); + transfer.sign([PRIV]); + + const blocks = []; + let block; + for (let i = 0; i < 30; i++) { + block = new Block(i).addTx(Tx.deposit(i, value, ADDR, color)); + blocks.push(block); + } + block = new Block(31).addTx(transfer); + blocks.push(block); + + const period = new Period( + "0x8b04de057fe524a3118eb7c8e14a2e55323c67fd7b6080583d1047b700b2d674", + blocks, + { validatorData } + ); + const periodAfter = new Period(period.periodRoot(), blocks, { validatorData }); + periodAfter.setValidatorData(slotId, ADDR); + + const proof = periodAfter.prevPeriodProof(); + const [currentPeriod, prevPeriod] = Period.evaluatePrevPeriodProof(proof); + expect(prevPeriod).to.eql(periodAfter.periodRoot()); + expect(currentPeriod).to.eql(period.periodRoot()); + done(); + }); + + describe('periodForBlockRange', () => { it('should create period for a given block range'); });