import { add0x , getLogger , hexBufToStr , hexStrToBuf , logError , } from '@pigi/core-utils' ; import { Address , AggregatorUnsupportedError , isCreateAndTransferTransition , isStateTransitionError , isSwapTransition , isTransferTransition , LocalFraudProof , LocalMachineError , RollupBlock , RollupStateValidator , RollupTransaction , RollupTransition , RollupTransitionPosition , SignedTransaction , StateSnapshot , TokenType , ValidationOutOfOrderError , } from '../types' ; import { DefaultRollupStateMachine , PIGI_TOKEN_TYPE , UNI_TOKEN_TYPE , UNISWAP_STORAGE_SLOT , } from '../common' ; import { ContractFraudProof , DefaultRollupBlock } from './types' ; const log = getLogger ( 'rollup-state-validator' ) ; export class DefaultRollupStateValidator implements RollupStateValidator { public rollupMachine : DefaultRollupStateMachine ; private currentPosition : RollupTransitionPosition = { blockNumber : 1 , transitionIndex : 0 , } ; private storedBlocks : RollupBlock [ ] = [ ] ; public constructor ( theRollupMachine ) { this . rollupMachine = theRollupMachine ; } public async getCurrentVerifiedPosition ( ) { return { ... this . currentPosition } ; } public async getInputStateSnapshots ( transition ) { const firstSlot = transition . senderSlotIndex ; const secondSlot = isSwapTransition ( transition ) ? UNISWAP_STORAGE_SLOT : transition . recipientSlotIndex ; log . info ( `Returning snapshots for slots ${ firstSlot } and ${ secondSlot }.` ) ; return [ await this . rollupMachine . getSnapshotFromSlot ( firstSlot ) , await this . rollupMachine . getSnapshotFromSlot ( secondSlot ) , ] ; } public async getTransactionFromTransitionAndSnapshots ( transition , snapshots ) { let convertedTx ; if ( isCreateAndTransferTransition ( transition ) ) { const sender = snapshots [ 0 ] . state . pubkey ; const recipient = transition . createdAccountPubkey ; convertedTx = { sender , recipient , tokenType : transition . tokenType as TokenType , amount : transition . amount , } ; } else if ( isTransferTransition ( transition ) ) { const sender = snapshots [ 0 ] . state . pubkey ; const recipient = snapshots [ 1 ] . state . pubkey ; convertedTx = { sender , recipient , tokenType : transition . tokenType as TokenType , amount : transition . amount , } ; return { signature : transition . signature , transaction : convertedTx , } ; } else if ( isSwapTransition ( transition ) ) { const swapper = snapshots [ 0 ] . state . pubkey ; convertedTx = { sender : swapper , tokenType : transition . tokenType as TokenType , inputAmount : transition . inputAmount , minOutputAmount : transition . minOutputAmount , timeout : transition . timeout , } ; } return { signature : transition . signature , transaction : convertedTx , } ; } public async checkNextTransition ( nextTransition ) { let preppedFraudInputs ; let generatedPostRoot ; const transitionPostRoot = hexStrToBuf ( nextTransition . stateRoot ) ; if ( isCreateAndTransferTransition ( nextTransition ) ) { const slotIfSequential = await this . rollupMachine . getNextNewAccountSlot ( ) ; if ( slotIfSequential !== nextTransition . recipientSlotIndex ) { log . error ( `Next slot in Rollup State Machine is ${ slotIfSequential } but next transition recipient slot is ${ nextTransition . recipientSlotIndex }` ) ; throw new AggregatorUnsupportedError ( ) ; } } log . info ( `Getting the pre-state inclusion proofs for a ${ typeof nextTransition }: ${ JSON . stringify ( nextTransition ) }` ) ; preppedFraudInputs = await this . getInputStateSnapshots ( nextTransition ) ; log . info ( `Converting the transition into a transaction...` ) ; const inputAsTransaction = await this . getTransactionFromTransitionAndSnapshots ( nextTransition , preppedFraudInputs ) ; log . info ( `Got back transaction: ${ JSON . stringify ( inputAsTransaction ) }.  Attempting to apply it to local state machine.` ) ; try { await this . rollupMachine . applyTransaction ( inputAsTransaction ) ; generatedPostRoot = await this . rollupMachine . getStateRoot ( ) ; } catch ( error ) { if ( isStateTransitionError ( error ) ) { log . error ( `The transaction did not pass the state machine, must be badly formed!  Returning fraud proof. Transition: ${ JSON . stringify ( nextTransition ) }` ) ; return { fraudPosition : this . currentPosition , fraudInputs : preppedFraudInputs , fraudTransition : nextTransition , } ; } else { logError ( log , `Transaction ingestion threw an error--but for a reason unrelated to the transition itself not passing the state machine. Uh oh! Transition: ${ JSON . stringify ( nextTransition ) }` , error ) ; throw new LocalMachineError ( ) ; } } if ( generatedPostRoot . equals ( transitionPostRoot ) ) { log . debug ( `Ingested valid transition and postRoot matched the aggregator claim.` ) ; this . currentPosition . transitionIndex ++ ; return undefined ; } else { log . error ( `Ingested valid transition and postRoot disagreed with the aggregator claim--returning fraud. Transition: ${ JSON . stringify ( nextTransition ) }. Expected State Root: ${ hexBufToStr ( generatedPostRoot ) }` ) ; return { fraudPosition : this . currentPosition , fraudInputs : preppedFraudInputs , fraudTransition : nextTransition , } ; } } public async storeBlock ( newBlock ) { this . storedBlocks [ newBlock . blockNumber - 1 ] = newBlock ; } public async validateStoredBlock ( blockNumber ) { const blockToValidate = this . storedBlocks [ blockNumber - 1 ] ; if ( ! blockToValidate ) { log . error ( `Tried to check next block, but it has not yet been stored yet. Block Number: ${ blockNumber }` ) ; throw new ValidationOutOfOrderError ( ) ; } const nextBlockNumberToValidate = ( await this . getCurrentVerifiedPosition ( ) ) . blockNumber ; if ( blockToValidate . blockNumber !== nextBlockNumberToValidate ) { log . error ( `Next block to validate is ${ nextBlockNumberToValidate } but trying to validate block ${ blockToValidate . blockNumber }!` ) ; throw new ValidationOutOfOrderError ( ) ; } const transitionCount = ! blockToValidate || ! blockToValidate . transitions ? 0 : blockToValidate . transitions . length ; log . info ( `Starting validation for block ${ blockToValidate . blockNumber }. Number of transitions: ${ transitionCount }` ) ; let transitionNumber = 0 ; for ( const transition of blockToValidate . transitions ) { const fraudCheck = await this . checkNextTransition ( transition ) ; if ( ! ! fraudCheck ) { log . info ( `Found evidence of fraud at transition ${ JSON . stringify ( transition ) }.  The current index is ${ ( await this . getCurrentVerifiedPosition ( ) ) . transitionIndex }.  Submitting fraud proof.` ) ; const generatedProof = await this . generateContractFraudProof ( fraudCheck , blockToValidate ) ; return generatedProof ; } log . info ( `Successfully validated transition ${ ++ transitionNumber } of ${ blockToValidate . transitions . length } of block ${ blockToValidate . blockNumber }` ) ; } log . info ( `Found no fraud in block ${ nextBlockNumberToValidate }, incrementing block number and reseting transition index` ) ; this . currentPosition . blockNumber ++ ; this . currentPosition . transitionIndex = 0 ; return undefined ; } public async generateContractFraudProof ( localProof , block ) { const fraudInputs = localProof . fraudInputs as StateSnapshot [ ] ; log . info ( `Converting the LocalFraudProof's snapshots into contract-friendly includedStorageSlots... ${ JSON . stringify ( fraudInputs ) }` ) ; const includedStorageSlots = [ { storageSlot : { value : { pubkey : fraudInputs [ 0 ] . state . pubkey , balances : [ fraudInputs [ 0 ] . state . balances [ UNI_TOKEN_TYPE ] , fraudInputs [ 0 ] . state . balances [ PIGI_TOKEN_TYPE ] , ] , } , slotIndex : fraudInputs [ 0 ] . slotIndex , } , siblings : fraudInputs [ 1 ] . inclusionProof . map ( ( x ) => add0x ( x ) ) , } , { storageSlot : { value : { pubkey : fraudInputs [ 1 ] . state . pubkey , balances : [ fraudInputs [ 1 ] . state . balances [ UNI_TOKEN_TYPE ] , fraudInputs [ 1 ] . state . balances [ PIGI_TOKEN_TYPE ] , ] , } , slotIndex : fraudInputs [ 1 ] . slotIndex , } , siblings : fraudInputs [ 1 ] . inclusionProof . map ( ( x ) => add0x ( x ) ) , } , ] ; log . info ( `Generating a Merklized block to build inclusions for the fraudulent transition...` ) ; const merklizedBlock = new DefaultRollupBlock ( block . transitions , block . blockNumber ) ; await merklizedBlock . generateTree ( ) ; const curPosition = await this . getCurrentVerifiedPosition ( ) ; const fraudulentTransitionIndex = curPosition . transitionIndex ; log . info ( `Fraudlent transition index is ${ fraudulentTransitionIndex }.  Getting inclusion proof in rollup block...` ) ; const fraudulentIncludedTransition = await merklizedBlock . getIncludedTransition ( fraudulentTransitionIndex ) ; let validIncludedTransition ; if ( fraudulentTransitionIndex > 0 ) { log . info ( `Since the fraud transition index is > 0, we serve the valid pre-transition from the same block at the previous position.` ) ; validIncludedTransition = await merklizedBlock . getIncludedTransition ( fraudulentTransitionIndex - 1 ) ; } else { log . info ( `Since the fraud transition index is  0, we need to grab the valid pre-transition as the last one in the previous block.` ) ; const prevRollupBlockNumber = curPosition . blockNumber - 1 ; const prevRollupBlock = new DefaultRollupBlock ( this . storedBlocks [ prevRollupBlockNumber - 1 ] . transitions , prevRollupBlockNumber ) ; log . info ( `Generating a Merklized block to build transition inclusion from the previous block...` ) ; await prevRollupBlock . generateTree ( ) ; const lastTransitionInLastBlockIndex = prevRollupBlock . transitions . length - 1 ; log . info ( `Grabbing the last transition from the previous block, it has index ${ lastTransitionInLastBlockIndex }` ) ; validIncludedTransition = await prevRollupBlock . getIncludedTransition ( lastTransitionInLastBlockIndex ) ; } return [ validIncludedTransition , fraudulentIncludedTransition , includedStorageSlots , ] ; } }