import _ from 'lodash' ; import { ChannelConstants , StateVariables , SignedState , Participant , ChannelStoredData , SignedStateWithHash , SignedStateVarsWithHash , StateVariablesWithHash , Funding , SignatureEntry , hashState , calculateChannelId , createSignatureEntry , outcomesEqual } from '@statechannels/wallet-core' ; import { logger } from '../logger' ; import { Errors } from '.' ; export type SignedStateVariables = StateVariables & { signatures : SignatureEntry [ ] ; } ; export class ChannelStoreEntry { private stateVariables : Array < SignedStateVarsWithHash > = [ ] ; public funding : Funding | undefined = undefined ; public readonly myIndex : number ; public readonly channelConstants : ChannelConstants ; public readonly applicationDomain ? : string ; constructor ( channelData ) { const { myIndex , stateVariables , funding , applicationDomain , channelConstants } = channelData ; this . myIndex = myIndex ; this . stateVariables = stateVariables ; this . funding = funding ; this . applicationDomain = applicationDomain ; this . myIndex = channelData . myIndex ; this . channelConstants = channelConstants ; this . stateVariables = channelData . stateVariables ; } public setFunding ( funding ) { this . funding = funding ; } public get sortedStates ( ) { return this . signedStates . map ( s => ( { ... this . channelConstants , ... s } ) ) ; } private mySignature ( stateVars , signatures ) { return signatures . some ( sig => sig . signer === this . myAddress ) ; } public get myAddress ( ) : string { return this . participants [ this . myIndex ] . signingAddress ; } public get myTurn ( ) : boolean { return ( this . supported . turnNum + 1 ) % this . participants . length === this . myIndex ; } private get signedStates ( ) : Array < SignedStateWithHash > { return this . stateVariables . map ( s => ( { ... this . channelConstants , ... s } ) ) ; } get isSupported ( ) { return ! ! this . _supported ; } get hasConclusionProof ( ) { return this . isSupported && this . support . every ( s => s . isFinal ) ; } get isChallenging ( ) { return false ; } private get _supported ( ) { const latestSupport = this . _support ; return latestSupport . length === 0 ? undefined : latestSupport [ 0 ] ; } public get support ( ) : Array < SignedState > { return this . _support . map ( s => ( { ... this . channelConstants , ... s } ) ) ; } private validChain ( firstState , secondState ) { if ( firstState . turnNum + 1 !== secondState . turnNum ) { return false ; } if ( secondState . isFinal ) { return outcomesEqual ( firstState . outcome , secondState . outcome ) ; } if ( secondState . turnNum < 2 * this . nParticipants ( ) ) { return ( outcomesEqual ( firstState . outcome , secondState . outcome ) && firstState . appData === secondState . appData ) ; } return true ; } private get _support ( ) : Array < SignedStateWithHash > { let support = [ ] ; let participantsWhoHaveNotSigned = new Set ( this . participants . map ( p => p . signingAddress ) ) ; let previousState ; for ( const signedState of this . signedStates ) { if ( previousState && ! this . validChain ( signedState , previousState ) ) { support = [ ] ; participantsWhoHaveNotSigned = new Set ( this . participants . map ( p => p . signingAddress ) ) ; } const moverIndex = signedState . turnNum % this . nParticipants ( ) ; const moverForThisTurn = this . participants [ moverIndex ] . signingAddress ; if ( signedState . signatures . some ( s => s . signer === moverForThisTurn ) ) { support . push ( signedState ) ; for ( const signature of signedState . signatures ) { participantsWhoHaveNotSigned . delete ( signature . signer ) ; if ( participantsWhoHaveNotSigned . size === 0 ) { return support ; } } } previousState = signedState ; } return [ ] ; } get supported ( ) { const vars = this . _supported ; if ( ! vars ) throw new Error ( 'No supported state found' ) ; return { ... this . channelConstants , ... vars } ; } get isSupportedByMe ( ) { return ! ! this . _latestSupportedByMe ; } private get _signedByMe ( ) { return this . signedStates . filter ( s => this . mySignature ( s , s . signatures ) ) ; } private get _latestSupportedByMe ( ) { return this . _signedByMe . find ( ( ) => true ) ; } get latestSignedByMe ( ) { const vars = this . _latestSupportedByMe ; if ( ! vars ) throw new Error ( 'No state supported by me' ) ; return { ... this . channelConstants , ... vars } ; } get latest ( ) { return { ... this . channelConstants , ... this . signedStates [ 0 ] } ; } get latestState ( ) { return { ... this . channelConstants , ... this . latest } ; } get channelId ( ) : string { return calculateChannelId ( this . channelConstants ) ; } get participants ( ) : Participant [ ] { return this . channelConstants . participants ; } signAndAdd ( stateVars , privateKey ) { if ( this . isSupportedByMe && this . latestSignedByMe . turnNum >= stateVars . turnNum ) { logger . error ( { entry : this . data ( ) , stateVars } , Errors . staleState ) ; throw Error ( Errors . staleState ) ; } const state = { ... this . channelConstants , ... stateVars } ; const signatureEntry = createSignatureEntry ( state , privateKey ) ; return this . addState ( stateVars , signatureEntry ) ; } addState ( stateVars , signatureEntry ) { const signedStateVars = { ... stateVars , signatures : [ signatureEntry ] } ; const withHash = { ... stateVars , stateHash : hashState ( this . state ( signedStateVars ) ) } ; const { participants } = this . channelConstants ; const signerIndex = participants . findIndex ( p => p . signingAddress === signatureEntry . signer ) ; let entry = this . stateVariables . find ( s => s . stateHash === withHash . stateHash ) ; if ( ! entry ) { entry = { ... withHash , signatures : [ ] } ; this . stateVariables . push ( entry ) ; } if ( signerIndex === - 1 ) { throw new Error ( 'State not signed by a participant of this channel' ) ; } entry . signatures = _ . uniqWith ( _ . concat ( entry . signatures , signatureEntry ) , _ . isEqual ) ; this . clearOldStates ( ) ; this . checkInvariants ( ) ; return this . state ( entry ) ; } private checkInvariants ( ) { const groupedByTurnNum = _ . groupBy ( this . _signedByMe , s => s . turnNum . toString ( ) ) ; const multipleSignedByMe = _ . map ( groupedByTurnNum , s => s . length ) ?. find ( num => num > 1 ) ; if ( multipleSignedByMe ) { logger . error ( { entry : this . data ( ) } , Errors . multipleSignedStates ) ; throw Error ( Errors . multipleSignedStates ) ; } const { signedStates } = this ; const turnNums = _ . map ( signedStates , s => s . turnNum ) ; const duplicateTurnNums = turnNums . some ( ( t , i ) => turnNums . indexOf ( t ) != i ) ; if ( duplicateTurnNums ) { logger . error ( { signedStates } , Errors . duplicateTurnNums ) ; throw Error ( Errors . duplicateTurnNums ) ; } if ( ! isReverseSorted ( turnNums ) ) { logger . error ( { signedStates : _ . map ( signedStates , s => s . turnNum ) } ) ; throw Error ( Errors . notSorted ) ; } } private state ( stateVars ) { return { ... this . channelConstants , ... stateVars } ; } private clearOldStates ( ) { this . stateVariables = _ . reverse ( _ . sortBy ( this . stateVariables , s => s . turnNum ) ) ; if ( this . isSupported ) { const { stateHash : firstSupportStateHash } = this . _support [ this . _support . length - 1 ] ; const supportIndex = this . stateVariables . findIndex ( sv => sv . stateHash === firstSupportStateHash ) ; this . stateVariables = this . stateVariables . slice ( 0 , supportIndex + 1 ) ; } } private nParticipants ( ) { return this . channelConstants . participants . length ; } public data ( ) { const channelConstants = { ... this . channelConstants , challengeDuration : this . channelConstants . challengeDuration , channelNonce : this . channelConstants . channelNonce } ; const stateVariables = _ . cloneDeep ( this . stateVariables ) ; return { stateVariables , channelConstants , funding : this . funding , myIndex : this . myIndex , applicationDomain : this . applicationDomain } ; } static fromJson ( data ) { if ( ! data ) { logger . error ( "Data is undefined or null, Memory Channel Store Entry can't be created." ) ; return data ; } const { channelConstants , funding , myIndex , applicationDomain } = data ; const stateVariables = data . stateVariables ; return new ChannelStoreEntry ( { channelConstants , myIndex , stateVariables , funding , applicationDomain } ) ; } } function isReverseSorted ( arr ) { const len = arr . length - 1 ; for ( let i = 0 ; i < len ; ++ i ) { if ( arr [ i ] < arr [ i + 1 ] ) { return false ; } } return true ; }