Skip to content

Commit

Permalink
State Channels (Payment Channels) (#382)
Browse files Browse the repository at this point in the history
* Added HashExistenceDecider over a range with ForAllSuchThatDecider
* Added SignedByDecider
* Added SignedByQuantifier
* Added State Channel example and associated tests
** Added Or Decider
** Added ThereExistsSuchThatDecider
** Created StateChannelMessage, StateChannelExitClaim, etc.
** Added MessageDB and StateChannelMessageDB


Fixes:
* Fixed AND decider to not throw undecided if either left OR right returns false, adding dispute unit tests for State Channels
  • Loading branch information
willmeister committed Aug 13, 2019
1 parent b5eaa85 commit dd3c275
Show file tree
Hide file tree
Showing 33 changed files with 2,673 additions and 29 deletions.
31 changes: 23 additions & 8 deletions packages/core/src/app/ovm/deciders/and-decider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
ImplicationProofItem,
Property,
} from '../../../types/ovm'
import { CannotDecideError } from './utils'

export interface AndDeciderInput {
left: Property
Expand All @@ -16,26 +17,40 @@ export interface AndDeciderInput {
* Decider that decides true iff both of the provided properties evaluate to true.
*/
export class AndDecider implements Decider {
private static _instance: AndDecider

public static instance(): AndDecider {
if (!AndDecider._instance) {
AndDecider._instance = new AndDecider()
}
return AndDecider._instance
}

public async decide(
input: AndDeciderInput,
witness?: undefined,
noCache?: boolean
): Promise<Decision> {
const [leftDecision, rightDecision] = await Promise.all([
input.left.decider.decide(input.left.input, input.leftWitness, noCache),
input.right.decider.decide(
input.right.input,
input.rightWitness,
noCache
),
input.left.decider
.decide(input.left.input, input.leftWitness, noCache)
.catch(() => undefined),
input.right.decider
.decide(input.right.input, input.rightWitness, noCache)
.catch(() => undefined),
])

if (!leftDecision.outcome) {
if (!!leftDecision && !leftDecision.outcome) {
return this.getDecision(input, leftDecision)
}
if (!rightDecision.outcome) {
if (!!rightDecision && !rightDecision.outcome) {
return this.getDecision(input, rightDecision)
}
if (!leftDecision || !rightDecision) {
throw new CannotDecideError(
'One of the AND deciders could not decide, and neither decided false.'
)
}

const justification: ImplicationProofItem[] = []
if (!!leftDecision.justification.length) {
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/app/ovm/deciders/examples/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './message-nonce-less-than-decider'
export * from './utils'
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Decider, Decision, ImplicationProofItem } from '../../../../types/ovm'
import { ParsedMessage } from '../../../../types/serialization'
import { BigNumber } from '../../../utils'

export interface MessageNonceLessThanInput {
messageWithNonce: ParsedMessage
lessThanThis: BigNumber
}

/**
* Decider that decides true iff the input message has a nonce less than the input nonce.
*/
export class MessageNonceLessThanDecider implements Decider {
private static _instance: MessageNonceLessThanDecider
public static instance(): MessageNonceLessThanDecider {
if (!MessageNonceLessThanDecider._instance) {
MessageNonceLessThanDecider._instance = new MessageNonceLessThanDecider()
}
return MessageNonceLessThanDecider._instance
}

public async decide(
input: MessageNonceLessThanInput,
witness: undefined,
noCache?: boolean
): Promise<Decision> {
const justification: ImplicationProofItem[] = [
{
implication: {
decider: this,
input,
},
implicationWitness: witness,
},
]

return {
outcome: input.messageWithNonce.message.nonce.lt(input.lessThanThis),
justification,
}
}
}
29 changes: 29 additions & 0 deletions packages/core/src/app/ovm/deciders/examples/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { ParsedMessage } from '../../../../types/serialization'
import { objectsEqual } from '../../../utils'

export class Utils {
/**
* Determines whether or not the provided ParsedMessages conflict.
* Conflicting messages have the same channelID and nonce but different data.
*
* @param message The first message
* @param other The second message
* @returns True if they conflict, false otherwise
*/
public static stateChannelMessagesConflict(
message: ParsedMessage,
other: ParsedMessage
): boolean {
return (
!!message &&
!!other &&
message.message.channelId.equals(other.message.channelId) &&
message.message.nonce.equals(other.message.nonce) &&
(message.sender.equals(other.sender) ||
message.sender.equals(other.recipient)) &&
(message.recipient.equals(other.recipient) ||
message.recipient.equals(other.sender)) &&
!objectsEqual(message.message.data, other.message.data)
)
}
}
12 changes: 8 additions & 4 deletions packages/core/src/app/ovm/deciders/for-all-such-that-decider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ export interface ForAllSuchThatInput {
* If any evaluates to false, it will decide false. Otherwise, it is undecidable.
*/
export class ForAllSuchThatDecider implements Decider {
private static _instance: ForAllSuchThatDecider
public static instance(): ForAllSuchThatDecider {
if (!ForAllSuchThatDecider._instance) {
ForAllSuchThatDecider._instance = new ForAllSuchThatDecider()
}
return ForAllSuchThatDecider._instance
}

public async decide(
input: ForAllSuchThatInput,
_witness?: undefined,
Expand Down Expand Up @@ -67,10 +75,6 @@ export class ForAllSuchThatDecider implements Decider {
)
}

private async checkDecision(input: ForAllSuchThatInput): Promise<Decision> {
return this.decide(input, undefined)
}

/**
* Gets the Decision that results from invocation of the ForAllSuchThat Decider.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,12 @@ export class HashPreimageExistenceDecider extends KeyValueStoreDecider {
)
}

const decision: Decision = this.constructDecision(
witness.preimage,
input.hash,
outcome
)

await this.storeDecision(
input,
HashPreimageExistenceDecider.serializeDecision(witness, input, outcome)
)

return decision
return this.constructDecision(witness.preimage, input.hash, outcome)
}

protected getUniqueId(): string {
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/app/ovm/deciders/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './and-decider'
export * from './examples'
export * from './for-all-such-that-decider'
export * from './hash-preimage-existence-decider'
export * from './key-value-store-decider'
Expand Down
122 changes: 122 additions & 0 deletions packages/core/src/app/ovm/deciders/or-decider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import {
Decider,
Decision,
ImplicationProofItem,
Property,
} from '../../../types/ovm'
import { CannotDecideError } from './utils'

export interface OrDeciderInput {
properties: Property[]
witnesses: any[]
}

/**
* Decider that decides true if any of the provided properties evaluate to true.
*/
export class OrDecider implements Decider {
private static _instance: OrDecider

public static instance(): OrDecider {
if (!OrDecider._instance) {
OrDecider._instance = new OrDecider()
}
return OrDecider._instance
}

public async decide(
input: OrDeciderInput,
witness?: undefined,
noCache?: boolean
): Promise<Decision> {
const decisions: Decision[] = await Promise.all(
input.properties.map((property: Property, index: number) => {
return this.decideWithoutThrowingCannotDecide(
property,
input.witnesses[index],
noCache
)
})
)

let trueDecision: Decision
let cannotDecide: boolean = false
const falseJustifications: ImplicationProofItem[] = []
for (const decision of decisions) {
if (!decision) {
cannotDecide = true
continue
}
if (decision.outcome) {
trueDecision = decision
break
} else {
falseJustifications.push(...decision.justification)
}
}

if (trueDecision) {
return this.getDecision(input, trueDecision)
}

if (cannotDecide) {
throw new CannotDecideError(
'At least one of the OR deciders could not decide and none returned true, so this cannot be decided.'
)
}

return this.getDecision(input, {
outcome: false,
justification: falseJustifications,
})
}

/**
* Calls decide on the provided Property's Decider with the appropriate input and catches
* CannotDecideError, returning undefined if it occurs.
*
* @param property the Property with the Decider to decide and the input to pass it
* @param witness the witness for the Decider
* @param noCache whether or not to use the cache if one is available for previous decisions
*/
private async decideWithoutThrowingCannotDecide(
property: Property,
witness: any,
noCache: boolean
): Promise<Decision> {
try {
return await property.decider.decide(property.input, witness, noCache)
} catch (e) {
if (e instanceof CannotDecideError) {
return undefined
}
throw e
}
}

/**
* Gets the Decision that results from invocation of the Or decider, which simply
* returns true if any of the sub-Decisions returned true.
*
* @param input The input that led to the Decision
* @param subDecision The decision of the wrapped Property, provided the witness
* @returns The Decision
*/
private getDecision(input: OrDeciderInput, subDecision: Decision): Decision {
const justification: ImplicationProofItem[] = [
{
implication: {
decider: this,
input,
},
implicationWitness: undefined,
},
...subDecision.justification,
]

return {
outcome: subDecision.outcome,
justification,
}
}
}

0 comments on commit dd3c275

Please sign in to comment.