-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdispute.go
More file actions
439 lines (371 loc) · 14.6 KB
/
dispute.go
File metadata and controls
439 lines (371 loc) · 14.6 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
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
// Package paywall implements dispute resolution framework for escrow payments
package paywall
import (
"errors"
"fmt"
"sync"
"time"
)
var (
// ErrDisputeNotFound indicates the requested dispute does not exist
ErrDisputeNotFound = errors.New("dispute not found")
// ErrDisputeAlreadyResolved indicates the dispute has already been resolved
ErrDisputeAlreadyResolved = errors.New("dispute already resolved")
// ErrInvalidEvidence indicates the evidence provided is invalid or incomplete
ErrInvalidEvidence = errors.New("invalid evidence")
)
// EvidenceType represents the type of evidence submitted in a dispute
type EvidenceType string
const (
// EvidenceText represents textual evidence (description, explanation)
EvidenceText EvidenceType = "text"
// EvidenceImage represents image evidence (screenshot, photo)
EvidenceImage EvidenceType = "image"
// EvidenceDocument represents document evidence (contract, receipt)
EvidenceDocument EvidenceType = "document"
// EvidenceTransaction represents on-chain transaction evidence
EvidenceTransaction EvidenceType = "transaction"
)
// Evidence represents a piece of evidence submitted in a dispute
type Evidence struct {
// ID uniquely identifies the evidence
ID string `json:"id"`
// PaymentID is the payment this evidence relates to
PaymentID string `json:"payment_id"`
// Type indicates the type of evidence
Type EvidenceType `json:"type"`
// SubmittedBy indicates which party submitted the evidence
SubmittedBy MultisigRole `json:"submitted_by"`
// Content contains the evidence data (text, URL, or encoded data)
Content string `json:"content"`
// Timestamp is when the evidence was submitted
Timestamp time.Time `json:"timestamp"`
// Description provides context for the evidence
Description string `json:"description"`
// Signature is the cryptographic signature of the submitter
Signature []byte `json:"signature,omitempty"`
// SubmitterPubKey is the public key of the evidence submitter
SubmitterPubKey []byte `json:"submitter_pub_key,omitempty"`
}
// Resolution represents the arbiter's decision on a dispute
type Resolution struct {
// PaymentID is the payment this resolution applies to
PaymentID string `json:"payment_id"`
// Decision indicates the winning party
Decision MultisigRole `json:"decision"`
// Reason explains the arbiter's decision
Reason string `json:"reason"`
// ArbiterID identifies the arbiter who made the decision
ArbiterID string `json:"arbiter_id"`
// Timestamp is when the resolution was made
Timestamp time.Time `json:"timestamp"`
// Evidence contains references to evidence that influenced the decision
Evidence []string `json:"evidence"`
// Signature is the cryptographic signature of the arbiter
Signature []byte `json:"signature,omitempty"`
// ArbiterPubKey is the public key of the arbiter
ArbiterPubKey []byte `json:"arbiter_pub_key,omitempty"`
}
// Dispute represents a dispute case with all associated evidence and resolution
type Dispute struct {
// PaymentID is the payment under dispute
PaymentID string `json:"payment_id"`
// Requester is the party who initiated the dispute
Requester MultisigRole `json:"requester"`
// Reason is the initial reason for the dispute
Reason string `json:"reason"`
// Evidence contains all evidence submitted by parties
Evidence []*Evidence `json:"evidence"`
// Resolution contains the arbiter's decision (nil if not yet resolved)
Resolution *Resolution `json:"resolution,omitempty"`
// CreatedAt is when the dispute was opened
CreatedAt time.Time `json:"created_at"`
// ResolvedAt is when the dispute was resolved (zero if not yet resolved)
ResolvedAt time.Time `json:"resolved_at,omitempty"`
// Status indicates the current dispute status
Status DisputeStatus `json:"status"`
}
// DisputeStatus represents the current state of a dispute
type DisputeStatus string
const (
// DisputeOpen indicates the dispute is open and accepting evidence
DisputeOpen DisputeStatus = "open"
// DisputeUnderReview indicates the arbiter is reviewing the evidence
DisputeUnderReview DisputeStatus = "under_review"
// DisputeResolved indicates the arbiter has made a decision
DisputeResolved DisputeStatus = "resolved"
// DisputeClosed indicates the dispute was closed without resolution
DisputeClosed DisputeStatus = "closed"
)
// Arbiter defines the interface for dispute resolution services.
//
// # Extensibility
//
// This interface is designed as an extensibility point to support various
// dispute resolution architectures:
//
// - LocalArbiter (default): In-memory storage, suitable for single-instance
// deployments and testing
// - RemoteArbiter (custom): HTTP/gRPC client connecting to external dispute
// resolution services or decentralized arbiter networks
// - BlockchainArbiter (custom): Integration with on-chain dispute resolution
// smart contracts or oracles
//
// Most applications can use LocalArbiter. Implement custom Arbiter when you need:
// - Distributed dispute storage across multiple paywall instances
// - Integration with existing case management systems
// - Compliance with external arbitration service providers
// - Blockchain-based transparent dispute records
//
// # Implementation Requirements
//
// Custom implementations must ensure:
// - Thread-safe concurrent access to dispute data
// - Atomic dispute registration to prevent duplicates
// - Proper validation of requester role (buyer or seller only)
// - Evidence integrity and non-repudiation
type Arbiter interface {
// RegisterDispute registers a new dispute in the arbiter system
// requester specifies which party (buyer or seller) initiated the dispute
// Returns error if registration fails
RegisterDispute(payment *Payment, requester MultisigRole) error
// SubmitEvidence allows parties to submit evidence for a dispute
// Returns error if evidence is invalid or submission fails
SubmitEvidence(paymentID string, evidence *Evidence) error
// GetResolution retrieves the arbiter's decision for a dispute
// Returns error if dispute not found or not yet resolved
GetResolution(paymentID string) (*Resolution, error)
// GetDispute retrieves the full dispute information including evidence
// Returns error if dispute not found
GetDispute(paymentID string) (*Dispute, error)
// ListOpenDisputes returns all disputes awaiting resolution
// Returns error if retrieval fails
ListOpenDisputes() ([]*Dispute, error)
}
// LocalArbiter is the default in-memory implementation of the Arbiter interface.
// It stores disputes in a thread-safe map and is suitable for:
// - Single-instance paywall deployments
// - Testing and development environments
// - Low-volume dispute scenarios
//
// For distributed deployments or external arbitration services, implement
// a custom Arbiter that integrates with your dispute resolution backend.
type LocalArbiter struct {
disputes map[string]*Dispute
mu sync.RWMutex
}
// NewLocalArbiter creates a new in-memory arbiter instance
func NewLocalArbiter() *LocalArbiter {
return &LocalArbiter{
disputes: make(map[string]*Dispute),
}
}
// RegisterDispute registers a new dispute in the arbiter system
// requester specifies which party (buyer or seller) initiated the dispute
func (la *LocalArbiter) RegisterDispute(payment *Payment, requester MultisigRole) error {
la.mu.Lock()
defer la.mu.Unlock()
if payment == nil {
return fmt.Errorf("payment cannot be nil")
}
// Validate requester is buyer or seller
if requester != RoleBuyer && requester != RoleSeller {
return fmt.Errorf("requester must be buyer or seller, got: %s", requester)
}
// Check if dispute already exists
if _, exists := la.disputes[payment.ID]; exists {
return fmt.Errorf("dispute already exists for payment %s", payment.ID)
}
dispute := &Dispute{
PaymentID: payment.ID,
Requester: requester,
Reason: payment.DisputeReason,
Evidence: make([]*Evidence, 0),
CreatedAt: time.Now(),
Status: DisputeOpen,
}
la.disputes[payment.ID] = dispute
return nil
}
// SubmitEvidence allows parties to submit evidence for a dispute
func (la *LocalArbiter) SubmitEvidence(paymentID string, evidence *Evidence) error {
la.mu.Lock()
defer la.mu.Unlock()
dispute, exists := la.disputes[paymentID]
if !exists {
return ErrDisputeNotFound
}
if dispute.Status == DisputeResolved || dispute.Status == DisputeClosed {
return ErrDisputeAlreadyResolved
}
if evidence == nil || evidence.Content == "" {
return ErrInvalidEvidence
}
// Validate evidence signature if provided
if len(evidence.Signature) > 0 && len(evidence.SubmitterPubKey) > 0 {
if err := validateEvidenceSignature(evidence); err != nil {
return fmt.Errorf("invalid evidence signature: %w", err)
}
}
// Set metadata
evidence.ID = fmt.Sprintf("%s-%d", paymentID, len(dispute.Evidence))
evidence.PaymentID = paymentID
evidence.Timestamp = time.Now()
dispute.Evidence = append(dispute.Evidence, evidence)
return nil
}
// GetResolution retrieves the arbiter's decision for a dispute
func (la *LocalArbiter) GetResolution(paymentID string) (*Resolution, error) {
la.mu.RLock()
defer la.mu.RUnlock()
dispute, exists := la.disputes[paymentID]
if !exists {
return nil, ErrDisputeNotFound
}
if dispute.Resolution == nil {
return nil, fmt.Errorf("dispute not yet resolved")
}
return dispute.Resolution, nil
}
// GetDispute retrieves the full dispute information including evidence
func (la *LocalArbiter) GetDispute(paymentID string) (*Dispute, error) {
la.mu.RLock()
defer la.mu.RUnlock()
dispute, exists := la.disputes[paymentID]
if !exists {
return nil, ErrDisputeNotFound
}
return dispute, nil
}
// ListOpenDisputes returns all disputes awaiting resolution
func (la *LocalArbiter) ListOpenDisputes() ([]*Dispute, error) {
la.mu.RLock()
defer la.mu.RUnlock()
var openDisputes []*Dispute
for _, dispute := range la.disputes {
if dispute.Status == DisputeOpen || dispute.Status == DisputeUnderReview {
openDisputes = append(openDisputes, dispute)
}
}
return openDisputes, nil
}
// ResolveDispute allows the arbiter to resolve a dispute with a decision
// This is a helper method specific to LocalArbiter for testing/simple use cases
func (la *LocalArbiter) ResolveDispute(paymentID string, resolution *Resolution) error {
la.mu.Lock()
defer la.mu.Unlock()
dispute, exists := la.disputes[paymentID]
if !exists {
return ErrDisputeNotFound
}
if dispute.Status == DisputeResolved || dispute.Status == DisputeClosed {
return ErrDisputeAlreadyResolved
}
if resolution == nil {
return fmt.Errorf("resolution cannot be nil")
}
// Validate resolution signature if provided
if len(resolution.Signature) > 0 && len(resolution.ArbiterPubKey) > 0 {
if err := validateResolutionSignature(resolution); err != nil {
return fmt.Errorf("invalid resolution signature: %w", err)
}
}
resolution.PaymentID = paymentID
resolution.Timestamp = time.Now()
dispute.Resolution = resolution
dispute.Status = DisputeResolved
dispute.ResolvedAt = time.Now()
return nil
}
// CloseDispute closes a dispute without resolution (e.g., withdrawn by parties)
func (la *LocalArbiter) CloseDispute(paymentID string) error {
la.mu.Lock()
defer la.mu.Unlock()
dispute, exists := la.disputes[paymentID]
if !exists {
return ErrDisputeNotFound
}
if dispute.Status == DisputeResolved || dispute.Status == DisputeClosed {
return ErrDisputeAlreadyResolved
}
dispute.Status = DisputeClosed
dispute.ResolvedAt = time.Now()
return nil
}
// validateEvidenceSignature validates the cryptographic signature on evidence
// This ensures the evidence was submitted by the claimed party and hasn't been tampered with
func validateEvidenceSignature(evidence *Evidence) error {
if evidence == nil {
return fmt.Errorf("evidence cannot be nil")
}
if len(evidence.Signature) == 0 {
return fmt.Errorf("signature is required")
}
if len(evidence.SubmitterPubKey) == 0 {
return fmt.Errorf("submitter public key is required")
}
// Basic signature length validation
if len(evidence.Signature) < 8 {
return fmt.Errorf("signature too short (minimum 8 bytes)")
}
// Parse and validate public key (secp256k1 curve for Bitcoin/Monero compatibility)
// In a production system, you would:
// 1. Hash the evidence content (ID + PaymentID + Type + Content + Description + Timestamp)
// 2. Verify the signature against the hash using the public key
// For now, we do basic format validation
// Note: Full cryptographic verification would use btcec or similar
// This is a placeholder that checks signature format
// In production, implement full ECDSA signature verification
return nil
}
// validateResolutionSignature validates the cryptographic signature on a resolution
// This ensures the resolution was made by an authorized arbiter and hasn't been tampered with
func validateResolutionSignature(resolution *Resolution) error {
if resolution == nil {
return fmt.Errorf("resolution cannot be nil")
}
if len(resolution.Signature) == 0 {
return fmt.Errorf("signature is required")
}
if len(resolution.ArbiterPubKey) == 0 {
return fmt.Errorf("arbiter public key is required")
}
// Basic signature length validation
if len(resolution.Signature) < 8 {
return fmt.Errorf("signature too short (minimum 8 bytes)")
}
// Parse and validate public key
// In a production system, you would:
// 1. Hash the resolution data (PaymentID + Decision + Reason + ArbiterID + Timestamp)
// 2. Verify the signature against the hash using the arbiter's public key
// 3. Check that the arbiter is authorized (part of authorized arbiter list)
// For now, we do basic format validation
// Note: Full cryptographic verification would use btcec or similar
// This is a placeholder that checks signature format
// In production, implement full ECDSA signature verification
return nil
}
// SignEvidence signs evidence with a private key (helper function for clients)
// In production, this would be in a client library, not the server
// This is provided as a reference implementation
func SignEvidence(evidence *Evidence, privateKey []byte) error {
// In production:
// 1. Create hash of evidence data
// 2. Sign hash with private key
// 3. Set evidence.Signature
// For now, this is a placeholder
evidence.Signature = []byte("signature-placeholder-minimum-8-bytes")
return nil
}
// SignResolution signs a resolution with an arbiter's private key
// In production, this would be in an arbiter service, not exposed to clients
// This is provided as a reference implementation
func SignResolution(resolution *Resolution, privateKey []byte) error {
// In production:
// 1. Create hash of resolution data
// 2. Sign hash with arbiter's private key
// 3. Set resolution.Signature
// For now, this is a placeholder
resolution.Signature = []byte("signature-placeholder-minimum-8-bytes")
return nil
}