import '../setup' ; import { DefaultSignatureProvider , DefaultSignatureVerifier , getLogger , hexStrToBuf , keccak256 , serializeObject , serializeObjectAsHexString , SimpleClient , sleep , } from '@pigi/core-utils' ; import { DB , newInMemoryDB } from '@pigi/core-db' ; import * as assert from 'assert' ; import { ethers , Wallet } from 'ethers' ; import { AGGREGATOR_ADDRESS , AGGREGATOR_MNEMONIC , assertThrowsAsync , BOB_ADDRESS , DummyBlockSubmitter , getGenesisState , } from '../helpers' ; import { FaucetRequest , SignedTransaction , SignedStateReceipt , RollupAggregator , RollupStateMachine , Transfer , abiEncodeStateReceipt , abiEncodeTransaction , TransferTransition , AggregatorServer , abiEncodeTransition , } from '../../src' ; import { AGGREGATOR_API , DefaultRollupStateMachine , PIGI_TOKEN_TYPE , UNI_TOKEN_TYPE , } from '../../src/common' ; const log = getLogger ( 'rollup-aggregator' , true ) ; /*********
 * TESTS *
 *********/ describe ( 'RollupAggregator' , ( ) => { let client ; let aggregatorDB ; let aggregator ; let aggregatorServer ; let rollupStateMachine ; let dummyBlockSubmitter ; let aliceWallet ; beforeEach ( async ( ) => { aliceWallet = ethers . Wallet . createRandom ( ) ; rollupStateMachine = await DefaultRollupStateMachine . create ( getGenesisState ( aliceWallet . address ) , newInMemoryDB ( ) , AGGREGATOR_ADDRESS ) ; aggregatorDB = newInMemoryDB ( ) ; dummyBlockSubmitter = new DummyBlockSubmitter ( ) ; } ) ; describe ( 'server tests' , ( ) => { beforeEach ( async ( ) => { aggregator = await RollupAggregator . create ( aggregatorDB , rollupStateMachine , dummyBlockSubmitter , new DefaultSignatureProvider ( Wallet . fromMnemonic ( AGGREGATOR_MNEMONIC ) ) , DefaultSignatureVerifier . instance ( ) , 2 ) ; aggregatorServer = new AggregatorServer ( aggregator , 'localhost' , 3000 ) ; await aggregatorServer . listen ( ) ; client = new SimpleClient ( 'http://127.0.0.1:3000' ) ; } ) ; afterEach ( async ( ) => { await aggregatorServer . close ( ) ; } ) ; const sendFromAToB = async ( senderWallet , recipient , amount ) : Promise < SignedStateReceipt [ ] > => { const transaction = { sender : senderWallet . address , tokenType : UNI_TOKEN_TYPE , recipient , amount , } ; const signature = await senderWallet . signMessage ( hexStrToBuf ( ethers . utils . keccak256 ( abiEncodeTransaction ( transaction ) ) ) ) ; const tx = { signature , transaction , } ; return client . handle ( AGGREGATOR_API . applyTransaction , tx ) ; } ; const sendFromAliceToBob = async ( amount ) : Promise < SignedStateReceipt [ ] > => { const beforeState = await client . handle ( AGGREGATOR_API . getState , BOB_ADDRESS ) ; log . debug ( `Got before state ${ serializeObject ( beforeState ) }` ) ; const receipts = await sendFromAToB ( aliceWallet , BOB_ADDRESS , amount ) ; log . debug ( `Got tx receipts state ${ serializeObject ( receipts ) }` ) ; const afterState = await client . handle ( AGGREGATOR_API . getState , BOB_ADDRESS ) ; log . debug ( `Got after state ${ serializeObject ( afterState ) }` ) ; if ( ! ! beforeState . stateReceipt . state ) { const uniDiff = afterState . stateReceipt . state . balances [ UNI_TOKEN_TYPE ] - beforeState . stateReceipt . state . balances [ UNI_TOKEN_TYPE ] ; uniDiff . should . equal ( amount ) ; } else { afterState . stateReceipt . state . balances [ UNI_TOKEN_TYPE ] . should . equal ( amount ) ; } return receipts ; } ; const requestFaucetFundsForNewWallet = async ( amount ) : Promise < ethers . Wallet > => { const newWallet = ethers . Wallet . createRandom ( ) ; const transaction = { sender : newWallet . address , amount , } ; const signature = await newWallet . signMessage ( hexStrToBuf ( ethers . utils . keccak256 ( serializeObjectAsHexString ( transaction ) ) ) ) ; const signedRequest = { signature , transaction , } ; await client . handle ( AGGREGATOR_API . requestFaucetFunds , signedRequest ) ; const newWalletState = await client . handle ( AGGREGATOR_API . getState , newWallet . address ) ; newWalletState . stateReceipt . state . balances [ UNI_TOKEN_TYPE ] . should . equal ( amount ) ; newWalletState . stateReceipt . state . balances [ PIGI_TOKEN_TYPE ] . should . equal ( amount ) ; return newWallet ; } ; describe ( 'getState' , ( ) => { it ( 'should allow the balance to be queried' , async ( ) => { await aggregator . onSyncCompleted ( ) ; const response = await client . handle ( AGGREGATOR_API . getState , aliceWallet . address ) ; response . stateReceipt . state . balances . should . deep . equal ( { [ UNI_TOKEN_TYPE ] : 50 , [ PIGI_TOKEN_TYPE ] : 50 , } ) ; } ) ; it ( 'should throw if aggregator is not synced' , async ( ) => { await assertThrowsAsync ( async ( ) => { await client . handle ( AGGREGATOR_API . getState , aliceWallet . address ) ; } ) ; } ) ; } ) ; describe ( 'applyTransaction' , ( ) => { it ( 'should update bobs balance using applyTransaction to send 5 tokens' , async ( ) => { await aggregator . onSyncCompleted ( ) ; const txCountBefore = await aggregator . getTransactionCount ( ) ; await sendFromAliceToBob ( 5 ) ; const transIndex = parseInt ( ( await aggregatorDB . get ( RollupAggregator . LAST_TRANSITION_KEY ) ) . toString ( ) , 10 ) ; transIndex . should . equal ( 1 ) ; txCountBefore . should . equal ( ( await aggregator . getTransactionCount ( ) ) - 1 ) ; aggregator . getPendingBlockNumber ( ) . should . equal ( 1 ) ; } ) ; it ( 'should throw if aggregator is not synced' , async ( ) => { await assertThrowsAsync ( async ( ) => { await sendFromAliceToBob ( 5 ) ; } ) ; } ) ; } ) ; describe ( 'requestFaucetFunds' , ( ) => { it ( 'should send money to the account who requested' , async ( ) => { await aggregator . onSyncCompleted ( ) ; const txCountBefore = await aggregator . getTransactionCount ( ) ; await requestFaucetFundsForNewWallet ( 10 ) ; const transIndex = parseInt ( ( await aggregatorDB . get ( RollupAggregator . LAST_TRANSITION_KEY ) ) . toString ( ) , 10 ) ; txCountBefore . should . equal ( ( await aggregator . getTransactionCount ( ) ) - 1 ) ; transIndex . should . equal ( 0 ) ; dummyBlockSubmitter . submitedBlocks . length . should . equal ( 1 ) ; dummyBlockSubmitter . submitedBlocks [ 0 ] . blockNumber . should . equal ( 1 ) ; dummyBlockSubmitter . submitedBlocks [ 0 ] . transitions . length . should . equal ( 2 ) ; aggregator . getPendingBlockNumber ( ) . should . equal ( 2 ) ; } ) ; it ( 'should throw if aggregator is not synced' , async ( ) => { await assertThrowsAsync ( async ( ) => { await requestFaucetFundsForNewWallet ( 10 ) ; } ) ; } ) ; } ) ; describe ( 'RollupTransaction Receipt Tests' , ( ) => { it ( 'should receive a transaction receipt signed by the aggregator' , async ( ) => { await aggregator . onSyncCompleted ( ) ; const receipts = await sendFromAliceToBob ( 5 ) ; const signer0 = DefaultSignatureVerifier . instance ( ) . verifyMessage ( abiEncodeStateReceipt ( receipts [ 0 ] . stateReceipt ) , receipts [ 0 ] . signature ) ; signer0 . should . equal ( AGGREGATOR_ADDRESS ) ; const signer1 = DefaultSignatureVerifier . instance ( ) . verifyMessage ( abiEncodeStateReceipt ( receipts [ 1 ] . stateReceipt ) , receipts [ 1 ] . signature ) ; signer1 . should . equal ( AGGREGATOR_ADDRESS ) ; } ) ; it ( 'should have subsequent transactions that build on one another' , async ( ) => { await aggregator . onSyncCompleted ( ) ; const receiptsOne = await sendFromAliceToBob ( 5 ) ; const receiptsTwo = await sendFromAliceToBob ( 5 ) ; const blockOne0 = receiptsOne [ 0 ] . stateReceipt . blockNumber ; const blockTwo0 = receiptsTwo [ 0 ] . stateReceipt . blockNumber ; blockOne0 . should . equal ( blockTwo0 ) ; const blockOne1 = receiptsOne [ 1 ] . stateReceipt . blockNumber ; const blockTwo1 = receiptsTwo [ 1 ] . stateReceipt . blockNumber ; blockOne1 . should . equal ( blockTwo1 ) ; const indexOne0 = receiptsOne [ 0 ] . stateReceipt . transitionIndex ; const indexTwo0 = receiptsTwo [ 0 ] . stateReceipt . transitionIndex ; indexOne0 . should . equal ( indexTwo0 - 1 ) ; const indexOne1 = receiptsOne [ 1 ] . stateReceipt . transitionIndex ; const indexTwo1 = receiptsTwo [ 1 ] . stateReceipt . transitionIndex ; indexOne1 . should . equal ( indexTwo1 - 1 ) ; } ) ; } ) ; describe ( 'persisting tx count' , ( ) => { it ( 'should persist tx count after threshold reached' , async ( ) => { await aggregator . onSyncCompleted ( ) ; for ( let i = 0 ; i < RollupAggregator . TX_COUNT_STORAGE_THRESHOLD ; i ++ ) { await sendFromAliceToBob ( 1 ) ; } const txCount = await aggregator . getTransactionCount ( ) ; txCount . should . equal ( RollupAggregator . TX_COUNT_STORAGE_THRESHOLD ) ; const storedTxCountBuffer = await aggregatorDB . get ( RollupAggregator . TRANSACTION_COUNT_KEY ) ; assert ( ! ! storedTxCountBuffer , 'Transaction count should be stored!' ) ; parseInt ( storedTxCountBuffer . toString ( ) , 10 ) . should . equal ( txCount ) ; } ) ; } ) ; } ) ; describe ( 'aggregator init tests' , ( ) => { let trans1 ; beforeEach ( async ( ) => { trans1 = { stateRoot : keccak256 ( Buffer . from ( 'trans 1' ) . toString ( 'hex' ) ) , senderSlotIndex : 1 , recipientSlotIndex : 0 , tokenType : 0 , amount : 10 , signature : await new DefaultSignatureProvider ( ) . sign ( 'trans 1' ) , } ; } ) ; it ( 'should init without any DB state' , async ( ) => { aggregator = await RollupAggregator . create ( aggregatorDB , rollupStateMachine , dummyBlockSubmitter , new DefaultSignatureProvider ( Wallet . fromMnemonic ( AGGREGATOR_MNEMONIC ) ) , DefaultSignatureVerifier . instance ( ) , 2 ) ; aggregator . getPendingBlockNumber ( ) . should . equal ( 1 ) ; aggregator . getNextTransitionIndex ( ) . should . equal ( 0 ) ; const txCount = await aggregator . getTransactionCount ( ) ; txCount . should . equal ( 0 ) ; dummyBlockSubmitter . submitedBlocks . length . should . equal ( 0 ) ; } ) ; it ( 'should init with pending transition' , async ( ) => { await aggregatorDB . put ( RollupAggregator . LAST_TRANSITION_KEY , Buffer . from ( '1' ) ) ; await aggregatorDB . put ( RollupAggregator . getTransitionKey ( 1 ) , hexStrToBuf ( abiEncodeTransition ( trans1 ) ) ) ; await aggregatorDB . put ( RollupAggregator . TRANSACTION_COUNT_KEY , Buffer . from ( '1' ) ) ; aggregator = await RollupAggregator . create ( aggregatorDB , rollupStateMachine , dummyBlockSubmitter , new DefaultSignatureProvider ( Wallet . fromMnemonic ( AGGREGATOR_MNEMONIC ) ) , DefaultSignatureVerifier . instance ( ) , 2 ) ; const txCount = await aggregator . getTransactionCount ( ) ; txCount . should . equal ( 1 ) ; aggregator . getPendingBlockNumber ( ) . should . equal ( 1 ) ; aggregator . getNextTransitionIndex ( ) . should . equal ( 1 ) ; dummyBlockSubmitter . submitedBlocks . length . should . equal ( 0 ) ; } ) ; it ( 'should init with pending transition in pending block > 1' , async ( ) => { await aggregatorDB . put ( RollupAggregator . PENDING_BLOCK_KEY , Buffer . from ( '2' ) ) ; await aggregatorDB . put ( RollupAggregator . LAST_TRANSITION_KEY , Buffer . from ( '1' ) ) ; await aggregatorDB . put ( RollupAggregator . getTransitionKey ( 1 ) , hexStrToBuf ( abiEncodeTransition ( trans1 ) ) ) ; aggregator = await RollupAggregator . create ( aggregatorDB , rollupStateMachine , dummyBlockSubmitter , new DefaultSignatureProvider ( Wallet . fromMnemonic ( AGGREGATOR_MNEMONIC ) ) , DefaultSignatureVerifier . instance ( ) , 2 ) ; aggregator . getPendingBlockNumber ( ) . should . equal ( 2 ) ; aggregator . getNextTransitionIndex ( ) . should . equal ( 1 ) ; dummyBlockSubmitter . submitedBlocks . length . should . equal ( 0 ) ; } ) ; } ) ; describe ( 'block submission delay' , ( ) => { let trans1 ; beforeEach ( async ( ) => { trans1 = { stateRoot : keccak256 ( Buffer . from ( 'trans 1' ) . toString ( 'hex' ) ) , senderSlotIndex : 1 , recipientSlotIndex : 0 , tokenType : 0 , amount : 10 , signature : await new DefaultSignatureProvider ( ) . sign ( 'trans 1' ) , } ; } ) ; it ( 'should submit a block after delay with queued transactions' , async ( ) => { await aggregatorDB . put ( RollupAggregator . LAST_TRANSITION_KEY , Buffer . from ( '1' ) ) ; await aggregatorDB . put ( RollupAggregator . getTransitionKey ( 1 ) , hexStrToBuf ( abiEncodeTransition ( trans1 ) ) ) ; aggregator = await RollupAggregator . create ( aggregatorDB , rollupStateMachine , dummyBlockSubmitter , new DefaultSignatureProvider ( Wallet . fromMnemonic ( AGGREGATOR_MNEMONIC ) ) , DefaultSignatureVerifier . instance ( ) , 2 , 1000 ) ; await sleep ( 1900 ) ; dummyBlockSubmitter . submitedBlocks . length . should . equal ( 1 ) ; dummyBlockSubmitter . submitedBlocks [ 0 ] . blockNumber . should . equal ( 1 ) ; dummyBlockSubmitter . submitedBlocks [ 0 ] . transitions . length . should . equal ( 1 ) ; } ) . timeout ( 10000 ) ; it ( 'should not submit a block after delay without queued transactions' , async ( ) => { aggregator = await RollupAggregator . create ( aggregatorDB , rollupStateMachine , dummyBlockSubmitter , new DefaultSignatureProvider ( Wallet . fromMnemonic ( AGGREGATOR_MNEMONIC ) ) , DefaultSignatureVerifier . instance ( ) , 2 , 1000 ) ; await sleep ( 1900 ) ; dummyBlockSubmitter . submitedBlocks . length . should . equal ( 0 ) ; } ) . timeout ( 10000 ) ; } ) ; } ) ;