-
Notifications
You must be signed in to change notification settings - Fork 12
Expand file tree
/
Copy pathtx_handler.go
More file actions
184 lines (164 loc) · 4.78 KB
/
Copy pathtx_handler.go
File metadata and controls
184 lines (164 loc) · 4.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
package arksdk
import (
"sync"
)
const (
settleType = iota
collabExitType
sendType
)
// batchEntry represents the single batch tx (settle or collab-exit) that is
// active or pending at any given time. Its result is published (res/err set
// before done is closed) so a settle deduplicating against it can return it.
type batchEntry struct {
res string
err error
started bool // whether it has been dispatched to run
turn chan struct{} // closed when it is this batch's turn to run
done chan struct{} // closed when it has completed
}
// TxHandler serializes spend operations (send/issue/reissue/burn) and batch
// operations (settle/collab-exit) so they never overlap and double-spend the
// same VTXOs. Only one operation runs at a time. Ordering is a priority queue:
// a pending batch tx always runs before any waiting spend tx (because settles
// race expiring VTXOs that would otherwise fail the queued spends), while
// within each class operations run first-in-first-out.
//
// At most one batch tx is ever active or pending: a second settle deduplicates
// against it (returns its result), and a collab-exit is rejected with
// ErrSettleInProgress.
type TxHandler struct {
mu sync.Mutex
busy bool // an operation's fn() is currently running
lead *batchEntry // the active-or-pending batch tx, nil if none
spends []chan struct{} // FIFO queue of waiting spend txs, each its own turn signal
closed bool // set by stop(); rejects and aborts operations
done chan struct{} // closed by stop() to wake queued waiters
}
func newTxHandler() *TxHandler {
return &TxHandler{done: make(chan struct{})}
}
// stop aborts every queued and future operation. Lock() and Stop() must call it
// before tearing down wallet state (contractManager, stopCtx, the store) so a
// queued waiter doesn't resume and run against a torn-down wallet. Operations
// woken by stop return ErrIsLocked rather than executing their closure.
func (h *TxHandler) stop() {
h.mu.Lock()
defer h.mu.Unlock()
if !h.closed {
h.closed = true
close(h.done)
}
}
// dispatch hands the free slot to the next operation: a pending batch tx takes
// precedence, otherwise the oldest waiting spend tx runs. Caller must hold h.mu.
func (h *TxHandler) dispatch() {
if h.busy {
return
}
if h.lead != nil && !h.lead.started {
h.lead.started = true
h.busy = true
close(h.lead.turn)
return
}
if len(h.spends) > 0 {
turn := h.spends[0]
h.spends = h.spends[1:]
h.busy = true
close(turn)
}
}
// handleBatchTx runs a batch tx (settle or collab-exit) with precedence over
// waiting spend txs and without overlapping any in-flight operation. If a batch
// tx is already active or pending, a settle deduplicates against it (waits for
// it and returns its result) and a collab-exit is rejected.
func (h *TxHandler) handleBatchTx(
txType int, fn func() (string, error),
) (string, error) {
h.mu.Lock()
if h.closed {
h.mu.Unlock()
return "", ErrIsLocked
}
if h.lead != nil {
lead := h.lead
h.mu.Unlock()
if txType == collabExitType {
return "", ErrSettleInProgress
}
// settle: dedup against the active-or-pending batch tx, but abort if
// the wallet is locking down before it completes.
select {
case <-lead.done:
return lead.res, lead.err
case <-h.done:
return "", ErrIsLocked
}
}
entry := &batchEntry{turn: make(chan struct{}), done: make(chan struct{})}
h.lead = entry
h.dispatch()
h.mu.Unlock()
select {
case <-entry.turn:
case <-h.done:
return "", ErrIsLocked
}
// If the wallet locked down while we were queued, release the slot and
// abort instead of running against torn-down wallet state.
h.mu.Lock()
if h.closed {
h.lead = nil
h.busy = false
h.dispatch()
h.mu.Unlock()
return "", ErrIsLocked
}
h.mu.Unlock()
res, err := fn()
h.mu.Lock()
entry.res = res
entry.err = err
h.lead = nil
h.busy = false
h.dispatch()
h.mu.Unlock()
close(entry.done)
return res, err
}
// handleTx runs a spend tx (send/issue/reissue/burn). It waits its turn behind
// any in-flight operation and any pending batch tx, preserving FIFO order among
// spend txs, then runs without overlapping anything else.
func (h *TxHandler) HandleTx(fn func() (any, error)) (any, error) {
h.mu.Lock()
if h.closed {
h.mu.Unlock()
return nil, ErrIsLocked
}
turn := make(chan struct{})
h.spends = append(h.spends, turn)
h.dispatch()
h.mu.Unlock()
select {
case <-turn:
case <-h.done:
return nil, ErrIsLocked
}
// If the wallet locked down while we were queued, release the slot and
// abort instead of running against torn-down wallet state.
h.mu.Lock()
if h.closed {
h.busy = false
h.dispatch()
h.mu.Unlock()
return nil, ErrIsLocked
}
h.mu.Unlock()
res, err := fn()
h.mu.Lock()
h.busy = false
h.dispatch()
h.mu.Unlock()
return res, err
}