diff --git a/backend/ethereum/channel/register.go b/backend/ethereum/channel/register.go index 936fac02..cf30a5a9 100644 --- a/backend/ethereum/channel/register.go +++ b/backend/ethereum/channel/register.go @@ -19,7 +19,7 @@ import ( // Register registers a state on-chain. // If the state is a final state, register becomes a no-op. -func (a *Adjudicator) Register(ctx context.Context, req channel.AdjudicatorReq) (*channel.Registered, error) { +func (a *Adjudicator) Register(ctx context.Context, req channel.AdjudicatorReq) (*channel.RegisteredEvent, error) { if req.Tx.State.IsFinal { return a.registerFinal(ctx, req) } @@ -28,21 +28,21 @@ func (a *Adjudicator) Register(ctx context.Context, req channel.AdjudicatorReq) // registerFinal registers a final state. It ensures that the final state is // concluded on the adjudicator conctract. -func (a *Adjudicator) registerFinal(ctx context.Context, req channel.AdjudicatorReq) (*channel.Registered, error) { +func (a *Adjudicator) registerFinal(ctx context.Context, req channel.AdjudicatorReq) (*channel.RegisteredEvent, error) { // In the case of final states, we already call concludeFinal on the // adjudicator. Method ensureConcluded calls concludeFinal for final states. if err := a.ensureConcluded(ctx, req); err != nil { return nil, errors.WithMessage(err, "ensuring Concluded") } - return &channel.Registered{ + return &channel.RegisteredEvent{ ID: req.Params.ID(), Timeout: time.Time{}, // concludeFinal skips registration Version: req.Tx.Version, }, nil } -func (a *Adjudicator) registerNonFinal(ctx context.Context, req channel.AdjudicatorReq) (*channel.Registered, error) { +func (a *Adjudicator) registerNonFinal(ctx context.Context, req channel.AdjudicatorReq) (*channel.RegisteredEvent, error) { _sub, err := a.SubscribeRegistered(ctx, req.Params) if err != nil { return nil, err @@ -165,7 +165,7 @@ func (r *RegisteredSub) hasPast() bool { // is closed. If the subscription is closed, Next immediately returns nil. // If there was a past event when the subscription was set up, the first call to // Next will return it. -func (r *RegisteredSub) Next() *channel.Registered { +func (r *RegisteredSub) Next() *channel.RegisteredEvent { if r.pastStored != nil { s := r.pastStored r.pastStored = nil @@ -205,11 +205,11 @@ func (r *RegisteredSub) updateErr(err error) error { return r.err } -func storedToRegisteredEvent(event *adjudicator.AdjudicatorStored) *channel.Registered { +func storedToRegisteredEvent(event *adjudicator.AdjudicatorStored) *channel.RegisteredEvent { if event == nil { return nil } - return &channel.Registered{ + return &channel.RegisteredEvent{ ID: event.ChannelID, Version: event.Version, Timeout: time.Unix(int64(event.Timeout), 0), diff --git a/backend/ethereum/channel/register_test.go b/backend/ethereum/channel/register_test.go index eafb23f8..5903de63 100644 --- a/backend/ethereum/channel/register_test.go +++ b/backend/ethereum/channel/register_test.go @@ -90,7 +90,7 @@ func registerMultipleConcurrent(t *testing.T, numParts int, parallel bool) { } event, err := adjs[i].Register(ctx, req) assert.NoError(t, err, "Registering should succeed") - assert.NotEqual(t, event, &channel.Registered{}, "registering should return valid event") + assert.NotEqual(t, event, &channel.RegisteredEvent{}, "registering should return valid event") t.Logf("Peer[%d] registered successful", i) } if parallel { @@ -138,7 +138,7 @@ func TestRegister_FinalState(t *testing.T) { } event, err := adjs[0].Register(ctx, req) assert.NoError(t, err, "Registering final state should succeed") - assert.NotEqual(t, event, &channel.Registered{}, "registering should return valid event") + assert.NotEqual(t, event, &channel.RegisteredEvent{}, "registering should return valid event") assert.Equal(t, time.Time{}, event.Timeout, "Registering final state should produce zero timeout") t.Logf("Peer[%d] registered successful", 0) } diff --git a/backend/ethereum/client/dummy_adjudicator_test.go b/backend/ethereum/client/dummy_adjudicator_test.go index 96b2d535..8627c7ac 100644 --- a/backend/ethereum/client/dummy_adjudicator_test.go +++ b/backend/ethereum/client/dummy_adjudicator_test.go @@ -18,8 +18,8 @@ type DummyAdjudicator struct { t *testing.T } -func (d *DummyAdjudicator) Register(ctx context.Context, req perunchannel.AdjudicatorReq) (*perunchannel.Registered, error) { - return &perunchannel.Registered{ +func (d *DummyAdjudicator) Register(ctx context.Context, req perunchannel.AdjudicatorReq) (*perunchannel.RegisteredEvent, error) { + return &perunchannel.RegisteredEvent{ ID: req.Params.ID(), Idx: req.Idx, Version: req.Tx.Version, diff --git a/channel/adjudicator.go b/channel/adjudicator.go index 75d2f4d8..3c99fbfb 100644 --- a/channel/adjudicator.go +++ b/channel/adjudicator.go @@ -18,7 +18,7 @@ type ( // A channel state needs to be registered before the concluded state can be // withdrawn after a possible timeout. // - // Furthermore, it has a method for subscribing to Registered events. Those + // Furthermore, it has a method for subscribing to RegisteredEvents. Those // events might be triggered by a Register call on the adjudicator from any // channel participant. Adjudicator interface { @@ -27,7 +27,7 @@ type ( // even an old state for the same channel. If registration was successful, // it should return the timeout when withdrawal can be initiated with // Withdraw. - Register(context.Context, AdjudicatorReq) (*Registered, error) + Register(context.Context, AdjudicatorReq) (*RegisteredEvent, error) // Withdraw should conclude and withdraw the registered state, so that the // final outcome is set on the asset holders and funds are withdrawn @@ -35,7 +35,7 @@ type ( // account that a peer might already have concluded the same channel. Withdraw(context.Context, AdjudicatorReq) error - // SubscribeRegistered returns a Registered event subscription. The + // SubscribeRegistered returns a RegisteredEvent subscription. The // subscription should be a subscription of the newest past as well as // future events. The subscription should only be valid within the given // context: If the context is canceled, its Next method should return nil @@ -52,18 +52,18 @@ type ( Idx Index } - // Registered is the abstract event that signals a successful state + // RegisteredEvent is the abstract event that signals a successful state // registration on the blockchain. - Registered struct { + RegisteredEvent struct { ID ID // Channel ID Idx Index // Index of the participant who registered the event. Version uint64 // Registered version. Timeout time.Time // Timeout when the event can be concluded or progressed } - // A RegisteredSubscription is a subscription to Registered events for a + // A RegisteredSubscription is a subscription to RegisteredEvents for a // specific channel. The subscription should also return the newest past - // Registered event, if there is any. + // RegisteredEvent, if there is any. // // The usage of the subscription should be similar to that of an iterator. // Next calls should block until a new event is generated (or the first past @@ -72,7 +72,7 @@ type ( RegisteredSubscription interface { // Next returns the newest past or next future event. If the subscription is // closed or any other error occurs, it should return nil. - Next() *Registered + Next() *RegisteredEvent // Err returns the error status of the subscription. After Next returns nil, // Err should be checked for an error. diff --git a/channel/machine.go b/channel/machine.go index 8a284206..eff82ffc 100644 --- a/channel/machine.go +++ b/channel/machine.go @@ -35,11 +35,25 @@ const ( Acting Signing Final - Settled + Registering + Registered + Withdrawing + Withdrawn ) func (p Phase) String() string { - return [...]string{"InitActing", "InitSigning", "Funding", "Acting", "Signing", "Final", "Settled"}[p] + return [...]string{ + "InitActing", + "InitSigning", + "Funding", + "Acting", + "Signing", + "Final", + "Registering", + "Registered", + "Withdrawing", + "Withdrawn", + }[p] } func (t PhaseTransition) String() string { @@ -48,6 +62,8 @@ func (t PhaseTransition) String() string { var signingPhases = []Phase{InitSigning, Signing} +var postInitPhases = []Phase{Funding, Acting, Signing, Final} + // A machine is the channel pushdown automaton that handles phase transitions. // It checks for correct signatures and valid state transitions. // machine only contains implementations for the state transitions common to @@ -274,40 +290,82 @@ func (m *machine) enableStaged(expected PhaseTransition) error { // SetFunded tells the state machine that the channel got funded and progresses // to the Acting phase. func (m *machine) SetFunded() error { - if err := m.expect(PhaseTransition{Funding, Acting}); err != nil { - return err + return m.simplePhaseTransition(Funding, Acting) +} + +// SetRegistering tells the state machine that the current channel state is +// being registered on the adjudicator. This phase can be reached after the +// initial phases are done, i.e., when there's at least one state with +// signatures. +func (m *machine) SetRegistering() error { + if !inPhase(m.phase, postInitPhases) { + return m.phaseErrorf(m.selfTransition(), "can only register after init phases") } - m.setPhase(Acting) + m.setPhase(Registering) + return nil +} + +// SetRegistered tells the state machine that a channel state got registered on +// the adjudicator. This phase can be reached after the initial phases are done, +// i.e., when there's at least one state with signatures. +func (m *machine) SetRegistered() error { + if !inPhase(m.phase, append(postInitPhases, Registering)) { + return m.phaseErrorf(m.selfTransition(), + "state can only be registered after init phases or registering") + } + + m.setPhase(Registered) return nil } -// SetSettled tells the state machine that the final state was settled on the -// blockchain or funding channel and progresses to the Settled state. -func (m *machine) SetSettled() error { - if err := m.expect(PhaseTransition{Final, Settled}); err != nil { +// SetWithdrawing sets the state machine to the Withdrawing phase. The current +// state was registered on-chain and funds withdrawal is in progress. +// This phase can only be reached from the Registered phase. +func (m *machine) SetWithdrawing() error { + return m.simplePhaseTransition(Registered, Withdrawing) +} + +// SetWithdrawn sets the state machine to the final phase Withdrawn. The current +// state was registered on-chain and funds withdrawal was successful. +func (m *machine) SetWithdrawn() error { + return m.simplePhaseTransition(Withdrawing, Withdrawn) +} + +func (m *machine) simplePhaseTransition(from, to Phase) error { + if err := m.expect(PhaseTransition{from, to}); err != nil { return err } - m.setPhase(Settled) + m.setPhase(to) return nil } -var validPhaseTransitions = map[PhaseTransition]bool{ - {InitActing, InitSigning}: true, - {InitSigning, Funding}: true, - {Funding, Acting}: true, - {Acting, Signing}: true, - {Signing, Acting}: true, - {Signing, Final}: true, - {Final, Settled}: true, +var validPhaseTransitions = map[PhaseTransition]struct{}{ + {InitActing, InitSigning}: {}, + {InitSigning, Funding}: {}, + {Funding, Acting}: {}, + {Acting, Signing}: {}, + {Signing, Acting}: {}, + {Signing, Final}: {}, + {Funding, Registering}: {}, + {Acting, Registering}: {}, + {Signing, Registering}: {}, + {Final, Registering}: {}, + {Funding, Registered}: {}, + {Acting, Registered}: {}, + {Signing, Registered}: {}, + {Final, Registered}: {}, + {Registering, Registered}: {}, + {Registered, Withdrawing}: {}, + {Withdrawing, Withdrawn}: {}, } func (m *machine) expect(tr PhaseTransition) error { if m.phase != tr.From { return m.phaseErrorf(tr, "not in correct phase") } - if ok := validPhaseTransitions[PhaseTransition{m.phase, tr.To}]; !ok { + if _, ok := validPhaseTransitions[PhaseTransition{m.phase, tr.To}]; !ok { return m.phaseErrorf(tr, "forbidden phase transition") } return nil diff --git a/client/channel.go b/client/channel.go index 719e31af..0d4d70ed 100644 --- a/client/channel.go +++ b/client/channel.go @@ -172,14 +172,26 @@ func (c *Channel) Settle(ctx context.Context) error { return errors.New("currently, only channels in a final state can be settled") } + if err := c.machine.SetRegistering(); err != nil { + return err // this should never happen + } + req := c.machine.AdjudicatorReq() - if reg, err := c.adjudicator.Register(ctx, req); err != nil { + reg, err := c.adjudicator.Register(ctx, req) + if err != nil { return errors.WithMessage(err, "calling Register") - } else if reg.Version != req.Tx.Version { + } + if err := c.machine.SetRegistered(); err != nil { + return err // this should never happen + } + + if reg.Version != req.Tx.Version { return errors.Errorf( "unexpected version %d registered, expected %d", reg.Version, req.Tx.Version) - } else if reg.Timeout.After(time.Now()) { + } + + if reg.Timeout.After(time.Now()) { c.log.Warnf("Unexpected withdrawal timeout during Settle(). Waiting until %v.", reg.Timeout) timeout := time.After(time.Until(reg.Timeout)) select { @@ -189,9 +201,11 @@ func (c *Channel) Settle(ctx context.Context) error { } } + if err := c.machine.SetWithdrawing(); err != nil { + return err // this should never happen + } if err := c.adjudicator.Withdraw(ctx, req); err != nil { return errors.WithMessage(err, "calling Withdraw") } - - return c.machine.SetSettled() + return c.machine.SetWithdrawn() } diff --git a/client/client_test.go b/client/client_test.go index c4ef720c..3092d9c4 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -76,7 +76,7 @@ type DummyAdjudicator struct { t *testing.T } -func (d *DummyAdjudicator) Register(context.Context, channel.AdjudicatorReq) (*channel.Registered, error) { +func (d *DummyAdjudicator) Register(context.Context, channel.AdjudicatorReq) (*channel.RegisteredEvent, error) { d.t.Error("DummyAdjudicator.Register called") return nil, errors.New("DummyAdjudicator.Register called") } diff --git a/client/happy_test.go b/client/happy_test.go index 35b7d801..be91bcd3 100644 --- a/client/happy_test.go +++ b/client/happy_test.go @@ -102,9 +102,9 @@ func (f *logFunder) Fund(_ context.Context, req channel.FundingReq) error { return nil } -func (a *logAdjudicator) Register(ctx context.Context, req channel.AdjudicatorReq) (*channel.Registered, error) { +func (a *logAdjudicator) Register(ctx context.Context, req channel.AdjudicatorReq) (*channel.RegisteredEvent, error) { a.log.Infof("Register: %v", req) - return &channel.Registered{ + return &channel.RegisteredEvent{ ID: req.Params.ID(), Idx: req.Idx, Version: req.Tx.Version,