Skip to content

Commit

Permalink
[WIP] Port latest changes from go-nitro till commit 7da3527 on Augu…
Browse files Browse the repository at this point in the history
…st 29 (#120)

* Split data in Channel class into OnChain and OffChain

* Store lastBlockSeen value

* Update cerc-io/peer version to use yamux multiplexer

* Handle review comments

---------

Co-authored-by: neeraj <neeraj.rtly@gmail.com>
  • Loading branch information
nikugogoi and neerajvijay1997 authored Sep 15, 2023
1 parent 79b6dbe commit f354d63
Show file tree
Hide file tree
Showing 12 changed files with 240 additions and 1,223 deletions.
2 changes: 1 addition & 1 deletion packages/nitro-node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
"@cerc-io/libp2p": "0.42.2-laconic-0.1.4",
"@cerc-io/nitro-protocol": "^2.0.0-alpha.4-ts-port-0.1.2",
"@cerc-io/nitro-util": "^0.1.10",
"@cerc-io/peer": "^0.2.56",
"@cerc-io/peer": "^0.2.58",
"@cerc-io/ts-channel": "1.0.3-ts-nitro-0.1.1",
"@jpwilliams/waitgroup": "^2.1.0",
"@libp2p/crypto": "^1.0.4",
Expand Down
175 changes: 117 additions & 58 deletions packages/nitro-node/src/channel/channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,47 +15,108 @@ import { Allocation } from './state/outcome/allocation';
import { SignedState } from './state/signedstate';
import { FixedPart, State, ConstructorOptions as FixedPartConstructorOptions } from './state/state';
import {
AllocationUpdatedEvent, ChainEvent, ConcludedEvent, DepositedEvent,
AllocationUpdatedEvent, ChainEvent, ChallengeEvent, ConcludedEvent, DepositedEvent,
} from '../node/engine/chainservice/chainservice';
import { Exit } from './state/outcome/exit';

interface ConstructorOptions extends FixedPartConstructorOptions {
id?: Destination;
myIndex?: Uint;
onChainFunding?: Funds;
fixedPart?: FixedPart;
onChain?: OnChainData;
offChain?: OffChainData;
}

interface OnChainDataConstructorOptions {
holdings?: Funds;
outcome?: Exit;
stateHash?: string
}
interface OffChainDataConstructorOptions {
signedStateForTurnNum?: Map<Uint64, SignedState>;
latestSupportedStateTurnNum?: Uint64;
}

// Channel contains states and metadata and exposes convenience methods.
export class Channel extends FixedPart {
id: Destination = new Destination();
class OnChainData {
holdings: Funds = new Funds();

myIndex: Uint = BigInt(0);
outcome: Exit = new Exit();

onChainFunding: Funds = new Funds();
stateHash: string = '';

latestBlockNumber: Uint64 = BigInt(0); // the latest block number we've seen
constructor(params: OnChainDataConstructorOptions) {
Object.assign(this, params);
}

static jsonEncodingMap: Record<string, FieldDescription> = {
holdings: { type: 'class', value: Funds },
myIndex: { type: 'class', value: Exit },
stateHash: { type: 'string' },
};

static fromJSON(data: string): OnChainData {
const props = fromJSON(this.jsonEncodingMap, data);
return new OnChainData(props);
}

toJSON(): any {
return toJSON(OnChainData.jsonEncodingMap, this);
}
}

class OffChainData {
signedStateForTurnNum: Map<Uint64, SignedState> = new Map();
// Longer term, we should have a more efficient and smart mechanism to store states https://github.com/statechannels/go-nitro/issues/106

// largest uint64 value reserved for "no supported state"
// Can't make it private as access required when constructing VirtualChannel from an existing Channel instance
latestSupportedStateTurnNum: Uint64 = BigInt(0);

constructor(params: OffChainDataConstructorOptions) {
Object.assign(this, params);
}

static jsonEncodingMap: Record<string, FieldDescription> = {
signedStateForTurnNum: { type: 'map', key: { type: 'uint64' }, value: { type: 'class', value: SignedState } },
latestSupportedStateTurnNum: { type: 'uint64' },
};

static fromJSON(data: string): OffChainData {
const props = fromJSON(this.jsonEncodingMap, data);
return new OffChainData(props);
}

toJSON(): any {
return toJSON(OffChainData.jsonEncodingMap, this);
}
}

// Channel contains states and metadata and exposes convenience methods.
export class Channel extends FixedPart {
id: Destination = new Destination();

myIndex: Uint = BigInt(0);

onChain: OnChainData = new OnChainData({});

offChain: OffChainData = new OffChainData({});

static jsonEncodingMap: Record<string, FieldDescription> = {
id: { type: 'class', value: Destination },
myIndex: { type: 'uint' },
onChainFunding: { type: 'class', value: Funds },
latestBlockNumber: { type: 'uint64' },
onChain: { type: 'class', value: OnChainData },
offChain: { type: 'class', value: OffChainData },
...super.jsonEncodingMap,
signedStateForTurnNum: { type: 'map', key: { type: 'uint64' }, value: { type: 'class', value: SignedState } },
latestSupportedStateTurnNum: { type: 'uint64' },
};

static fromJSON(data: string): Channel {
const props = fromJSON(this.jsonEncodingMap, data);
let props;

try {
props = fromJSON(this.jsonEncodingMap, data);
} catch (err) {
throw new Error(`error unmarshaling channel data: ${err}`);
}

return new Channel(props);
}

Expand All @@ -76,22 +137,22 @@ export class Channel extends FixedPart {
c.id = s.channelId();

c.myIndex = myIndex;
c.onChainFunding = new Funds();
c.onChain.holdings = new Funds();
Object.assign(c, s.fixedPart().clone());
c.latestSupportedStateTurnNum = MaxTurnNum; // largest uint64 value reserved for "no supported state"
c.offChain.latestSupportedStateTurnNum = MaxTurnNum; // largest uint64 value reserved for "no supported state"

// Store prefund
c.signedStateForTurnNum = new Map();
c.signedStateForTurnNum.set(PreFundTurnNum, SignedState.newSignedState(s));
c.offChain.signedStateForTurnNum = new Map();
c.offChain.signedStateForTurnNum.set(PreFundTurnNum, SignedState.newSignedState(s));

// Store postfund
const post = s.clone();
post.turnNum = PostFundTurnNum;
c.signedStateForTurnNum.set(PostFundTurnNum, SignedState.newSignedState(post));
c.offChain.signedStateForTurnNum.set(PostFundTurnNum, SignedState.newSignedState(post));

// Set on chain holdings to zero for each asset
for (const [asset] of s.outcome.totalAllocated().value) {
c.onChainFunding.value.set(asset, BigInt(0));
c.onChain.holdings.value.set(asset, BigInt(0));
}

return c;
Expand All @@ -117,43 +178,42 @@ export class Channel extends FixedPart {
// Clone returns a pointer to a new, deep copy of the receiver, or a nil pointer if the receiver is nil.
clone(): Channel {
const d = Channel.new(this.preFundState().clone(), this.myIndex);
d.latestSupportedStateTurnNum = this.latestSupportedStateTurnNum;
d.offChain.latestSupportedStateTurnNum = this.offChain.latestSupportedStateTurnNum;

this.signedStateForTurnNum.forEach((value, key) => {
d.signedStateForTurnNum.set(key, value);
this.offChain.signedStateForTurnNum.forEach((value, key) => {
d.offChain.signedStateForTurnNum.set(key, value);
});
d.onChainFunding = this.onChainFunding.clone();
d.latestBlockNumber = this.latestBlockNumber;
d.onChain.holdings = this.onChain.holdings.clone();
Object.assign(d, super.clone());

return d;
}

// PreFundState() returns the pre fund setup state for the channel.
preFundState(): State {
return this.signedStateForTurnNum.get(PreFundTurnNum)!.state();
return this.offChain.signedStateForTurnNum.get(PreFundTurnNum)!.state();
}

// SignedPreFundState returns the signed pre fund setup state for the channel.
signedPreFundState(): SignedState {
return this.signedStateForTurnNum.get(PreFundTurnNum)!;
return this.offChain.signedStateForTurnNum.get(PreFundTurnNum)!;
}

// PostFundState() returns the post fund setup state for the channel.
postFundState(): State {
assert(this.signedStateForTurnNum);
return this.signedStateForTurnNum.get(PostFundTurnNum)!.state();
assert(this.offChain.signedStateForTurnNum);
return this.offChain.signedStateForTurnNum.get(PostFundTurnNum)!.state();
}

// SignedPostFundState() returns the SIGNED post fund setup state for the channel.
signedPostFundState(): SignedState {
return this.signedStateForTurnNum.get(PostFundTurnNum)!;
return this.offChain.signedStateForTurnNum.get(PostFundTurnNum)!;
}

// PreFundSignedByMe returns true if the calling client has signed the pre fund setup state, false otherwise.
preFundSignedByMe(): boolean {
if (this.signedStateForTurnNum.has(PreFundTurnNum)) {
if (this.signedStateForTurnNum.get(PreFundTurnNum)!.hasSignatureForParticipant(this.myIndex)) {
if (this.offChain.signedStateForTurnNum.has(PreFundTurnNum)) {
if (this.offChain.signedStateForTurnNum.get(PreFundTurnNum)!.hasSignatureForParticipant(this.myIndex)) {
return true;
}
}
Expand All @@ -162,8 +222,8 @@ export class Channel extends FixedPart {

// PostFundSignedByMe returns true if the calling client has signed the post fund setup state, false otherwise.
postFundSignedByMe(): boolean {
if (this.signedStateForTurnNum.has(PostFundTurnNum)) {
if (this.signedStateForTurnNum.get(PostFundTurnNum)!.hasSignatureForParticipant(this.myIndex)) {
if (this.offChain.signedStateForTurnNum.has(PostFundTurnNum)) {
if (this.offChain.signedStateForTurnNum.get(PostFundTurnNum)!.hasSignatureForParticipant(this.myIndex)) {
return true;
}
}
Expand All @@ -172,17 +232,17 @@ export class Channel extends FixedPart {

// PreFundComplete() returns true if I have a complete set of signatures on the pre fund setup state, false otherwise.
preFundComplete(): boolean {
return this.signedStateForTurnNum.get(PreFundTurnNum)!.hasAllSignatures();
return this.offChain.signedStateForTurnNum.get(PreFundTurnNum)!.hasAllSignatures();
}

// PostFundComplete() returns true if I have a complete set of signatures on the pre fund setup state, false otherwise.
postFundComplete(): boolean {
return this.signedStateForTurnNum.get(PostFundTurnNum)!.hasAllSignatures();
return this.offChain.signedStateForTurnNum.get(PostFundTurnNum)!.hasAllSignatures();
}

// FinalSignedByMe returns true if the calling client has signed a final state, false otherwise.
finalSignedByMe(): boolean {
for (const [, ss] of this.signedStateForTurnNum) {
for (const [, ss] of this.offChain.signedStateForTurnNum) {
if (ss.hasSignatureForParticipant(this.myIndex) && ss.state().isFinal) {
return true;
}
Expand All @@ -193,40 +253,40 @@ export class Channel extends FixedPart {

// FinalCompleted returns true if I have a complete set of signatures on a final state, false otherwise.
finalCompleted(): boolean {
if (this.latestSupportedStateTurnNum === MaxTurnNum) {
if (this.offChain.latestSupportedStateTurnNum === MaxTurnNum) {
return false;
}

return this.signedStateForTurnNum.get(this.latestSupportedStateTurnNum)!.state().isFinal;
return this.offChain.signedStateForTurnNum.get(this.offChain.latestSupportedStateTurnNum)!.state().isFinal;
}

// HasSupportedState returns true if the channel has a supported state, false otherwise.
hasSupportedState(): boolean {
return this.latestSupportedStateTurnNum !== MaxTurnNum;
return this.offChain.latestSupportedStateTurnNum !== MaxTurnNum;
}

// LatestSupportedState returns the latest supported state. A state is supported if it is signed
// by all participants.
latestSupportedState(): State {
if (this.latestSupportedStateTurnNum === MaxTurnNum) {
if (this.offChain.latestSupportedStateTurnNum === MaxTurnNum) {
throw new Error('no state is yet supported');
}

return this.signedStateForTurnNum.get(this.latestSupportedStateTurnNum)!.state();
return this.offChain.signedStateForTurnNum.get(this.offChain.latestSupportedStateTurnNum)!.state();
}

// LatestSignedState fetches the state with the largest turn number signed by at least one participant.
latestSignedState(): SignedState {
if (this.signedStateForTurnNum.size === 0) {
if (this.offChain.signedStateForTurnNum.size === 0) {
throw new Error('no states are signed');
}
let latestTurn: Uint64 = BigInt(0);
for (const [k] of this.signedStateForTurnNum) {
for (const [k] of this.offChain.signedStateForTurnNum) {
if (k > latestTurn) {
latestTurn = k;
}
}
return this.signedStateForTurnNum.get(latestTurn)!;
return this.offChain.signedStateForTurnNum.get(latestTurn)!;
}

// Total() returns the total allocated of each asset allocated by the pre fund setup state of the Channel.
Expand Down Expand Up @@ -268,17 +328,17 @@ export class Channel extends FixedPart {
return false;
}

if (this.latestSupportedStateTurnNum !== MaxTurnNum && s.turnNum < this.latestSupportedStateTurnNum) {
if (this.offChain.latestSupportedStateTurnNum !== MaxTurnNum && s.turnNum < this.offChain.latestSupportedStateTurnNum) {
// Stale state
return false;
}

// Store the signatures. If we have no record yet, add one.

const signedState = this.signedStateForTurnNum.get(s.turnNum);
const signedState = this.offChain.signedStateForTurnNum.get(s.turnNum);

if (!signedState) {
this.signedStateForTurnNum.set(s.turnNum, ss);
this.offChain.signedStateForTurnNum.set(s.turnNum, ss);
} else {
try {
signedState.merge(ss);
Expand All @@ -288,8 +348,8 @@ export class Channel extends FixedPart {
}

// Update latest supported state
if (this.signedStateForTurnNum.get(s.turnNum)!.hasAllSignatures()) {
this.latestSupportedStateTurnNum = s.turnNum;
if (this.offChain.signedStateForTurnNum.get(s.turnNum)!.hasAllSignatures()) {
this.offChain.latestSupportedStateTurnNum = s.turnNum;
}

return true;
Expand Down Expand Up @@ -331,23 +391,22 @@ export class Channel extends FixedPart {

// UpdateWithChainEvent mutates the receiver if provided with a "new" chain event (with a greater block number than previously seen)
updateWithChainEvent(event: ChainEvent): Channel {
if (event.blockNum() < this.latestBlockNumber) {
return this; // ignore stale information TODO: is this reorg safe?
}
this.latestBlockNumber = event.blockNum();
switch (event.constructor) {
case AllocationUpdatedEvent: {
const e = event as AllocationUpdatedEvent;
this.onChainFunding.value.set(e.assetAndAmount.assetAddress, e.assetAndAmount.assetAmount!);
break;
this.onChain.holdings.value.set(e.assetAndAmount.assetAddress, e.assetAndAmount.assetAmount!);
break; // TODO: update OnChain.StateHash and OnChain.Outcome
}
case DepositedEvent: {
const e = event as DepositedEvent;
this.onChainFunding.value.set(e.asset, e.nowHeld!);
this.onChain.holdings.value.set(e.asset, e.nowHeld!);
break;
}
case ConcludedEvent: {
break;
break; // TODO: update OnChain.StateHash and OnChain.Outcome
}
case ChallengeEvent: {
break; // TODO: update OnChain.StateHash and OnChain.Outcome
}
default: {
throw new Error(`channel ${this} cannot handle event ${event}`);
Expand Down
6 changes: 4 additions & 2 deletions packages/nitro-node/src/channel/virtual.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,10 @@ export class VirtualChannel extends Channel {
}

getPaidAndRemaining(): [bigint | undefined, bigint | undefined] {
const remaining = this.signedStateForTurnNum.get(this.latestSupportedStateTurnNum)!.state().outcome.value?.[0].allocations.value?.[0].amount;
const paid = this.signedStateForTurnNum.get(this.latestSupportedStateTurnNum)!.state().outcome.value?.[0].allocations.value?.[1].amount;
// eslint-disable-next-line max-len
const remaining = this.offChain.signedStateForTurnNum.get(this.offChain.latestSupportedStateTurnNum)!.state().outcome.value?.[0].allocations.value?.[0].amount;
// eslint-disable-next-line max-len
const paid = this.offChain.signedStateForTurnNum.get(this.offChain.latestSupportedStateTurnNum)!.state().outcome.value?.[0].allocations.value?.[1].amount;
return [remaining, paid];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ export class ConcludedEvent extends CommonEvent {
}
}

export class ChallengeEvent extends CommonEvent {

// TODO fill out other fields
}

// AllocationUpdated is an internal representation of the AllocationUpdated blockchain event
// The event includes the token address and amount at the block that generated the event
export class AllocationUpdatedEvent extends CommonEvent {
Expand Down
Loading

0 comments on commit f354d63

Please sign in to comment.