Skip to content

[BUG]: RequestSpendView in multisig blocks forever β€” WithTimeout() exists but is never applied to the channel waitΒ #1671

Description

@Rama542

πŸ‘Ύ Description

RequestSpendView.Call() in token/services/ttx/multisig/spend.go sends a
SpendRequest to every co-signer of a multisig token and then waits for
all replies on an unbuffered channel with no timeout:

for range counter {
// TODO: put a timeout
a := <-answerChannel // blocks forever if any party goes silent
}

The struct already has a timeout time.Duration field and a public
WithTimeout(time.Duration) setter, but the timeout is never applied to
the actual channel receive. If any remote co-signer crashes, drops the
network connection, or simply does not respond, the loop blocks the
calling goroutine permanently no error is returned, no cleanup runs.

Expected behaviour: if a co-signer does not reply within the configured
timeout, RequestSpendView should return a descriptive error and unblock
the caller.

Actual behaviour: the caller hangs indefinitely when any co-signer is
unresponsive, consuming a goroutine and its associated network session
forever.

πŸ› οΈ Steps to reproduce

  1. Create a multisig token with two or more parties.
  2. Start a spend flow via RequestSpendView.
  3. Make one co-signer unavailable (kill the process or drop the network).
  4. Observe that the initiating node never returns from Call() no error,
    no timeout, no progress.

The TODO comment at spend.go:155 explicitly acknowledges the missing
timeout:
// TODO: put a timeout

πŸ“‹ Logs and Screenshots

No error is logged. The goroutine simply parks on the channel receive:
// TODO: put a timeout
a := <-answerChannel

A goroutine dump (SIGQUIT / runtime/pprof) will show the goroutine
blocked at spend.go:156 indefinitely.

🌐 Network Environment

Local (NWO / Integration Test)

πŸ“¦ Fabric Token SDK Version

main (confirmed at token/services/ttx/multisig/spend.go)

🐹 Go Version

go1.24+

πŸ’» Operating System

Linux (Ubuntu/Debian)

βž• Additional context

Proposed fix wire the existing timeout field into the wait loop:

timer := time.NewTimer(c.timeout)
defer timer.Stop()
for range counter {
select {
case a := <-answerChannel:
if a.err != nil {
return nil, errors.Wrapf(a.err, "failed from [%s]", a.party)
}
if a.response.Err != nil {
return nil, errors.Wrapf(a.response.Err, "got failure from [%s]", a.party)
}
case <-timer.C:
return nil, errors.Errorf("timed out waiting for co-signer response after %s", c.timeout)
}
}

A default timeout (e.g. 30s) should also be set in NewRequestSpendView
so callers that forget WithTimeout() are protected automatically.

Affected file:
token/services/ttx/multisig/spend.go:153-156

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

Fields

No fields configured for Bug.

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions