Skip to content

Commit ad1f449

Browse files
adecaroclaude
andcommitted
fix(fungible): recognize composite-owner outputs in multisig accept view
extractIssueOutputs/extractTransferOutputs now emit a single output owned by the composite multisig/policy identity when a token has multiple recipients, but AcceptCashView still checked outputs.ByRecipient(id) against each responder's individual identity. The strict equality check never matched the composite owner, so the existence assertion panicked on bob's/charlie's node and the per-type balance check was silently skipped, breaking dlog-fabric-t12 (EndToEnd Multisig) across all three P2P transports. Add OutputStream.ByRecipientOrMember, mirroring the membership check already used in AssertTokens, and use it in AcceptCashView so a responder recognizes outputs owned by a composite identity she is a member of. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: Angelo De Caro <adc@zurich.ibm.com>
1 parent 6269df9 commit ad1f449

3 files changed

Lines changed: 65 additions & 2 deletions

File tree

integration/token/fungible/views/accept.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,13 @@ func (a *AcceptCashView) Call(context view.Context) (any, error) {
3535
outputs, err := tx.Outputs()
3636
assert.NoError(err, "failed getting outputs")
3737
assert.True(outputs.Count() > 0)
38-
assert.True(outputs.ByRecipient(id).Count() > 0)
38+
sigService := tx.TokenService().SigService()
39+
assert.True(outputs.ByRecipientOrMember(context.Context(), id, sigService).Count() > 0)
3940

4041
// The recipient here is checking that, for each type of token she is receiving,
4142
// she does not hold already more than 3000 units of that type.
4243
// Just a fancy query to show the capabilities of the services we are using.
43-
for _, output := range outputs.ByRecipient(id).Outputs() {
44+
for _, output := range outputs.ByRecipientOrMember(context.Context(), id, sigService).Outputs() {
4445
if output.Type == "MAX" {
4546
continue
4647
}

plan.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Fix dlog-fabric-t12 (Multisig) panic caused by composite-owner output regression
2+
3+
## Goal
4+
`dlog-fabric-t12` (EndToEnd Multisig, all three P2P transports: websocket, libp2p, replicas)
5+
panics deterministically on bob's/charlie's node inside `AcceptCashView.Call`
6+
(`integration/token/fungible/views/accept.go:38`). Root cause: commit `6269df950`
7+
changed `extractIssueOutputs`/`extractTransferOutputs` (`token/request.go`) to emit a
8+
single `Output` owned by the *composite* multisig/policy identity when a token has
9+
`len(recipients) > 1`, instead of one `Output` per individual co-owner. But
10+
`AcceptCashView.Call` still checks `outputs.ByRecipient(id)` where `id` is the
11+
responder's own *individual* identity (from `RespondRequestRecipientIdentityUsingWallet`),
12+
and `OutputStream.ByRecipient` (`token/stream.go`) does strict `id.Equal(t.Owner)`
13+
equality — which never matches the composite owner, so `assert.True(...)` panics.
14+
That panic propagates back to alice's `MultiSigLockView.Call` panic at `multisig.go:76`
15+
via `collectendorsements.go`'s `distributeTxToParties`/`fanOut`.
16+
17+
Fix: make the ownership check membership-aware (composite-identity-aware), mirroring
18+
the existing pattern already used correctly in `AssertTokens` (`utils.go:32`:
19+
`output.Owner.Equal(id) || tx.TokenService().SigService().IsMe(ctx, output.Owner)`)
20+
and in `MultiSigAcceptSpendView.Call` (`multisig.go:209`).
21+
22+
## Implementation steps
23+
1. [ ] Add a membership-aware output filter to `token/stream.go`, e.g.
24+
`ByRecipientOrMember(ctx context.Context, id Identity, sigService *SignatureService) *OutputStream`,
25+
filtering on `id.Equal(t.Owner) || sigService.IsMe(ctx, t.Owner)`.
26+
2. [ ] Update `AcceptCashView.Call` (`accept.go:38`) to use the new helper instead of
27+
strict `outputs.ByRecipient(id)`.
28+
3. [ ] Update `AcceptCashView.Call` (`accept.go:43`) loop (balance sanity check) to use
29+
the same helper — today it silently iterates zero outputs for composite owners
30+
(no panic, but the balance check is skipped), which is a related correctness gap.
31+
4. [ ] Leave `AssertTokens` (`utils.go:32`) untouched — already correct; it's the
32+
reference pattern.
33+
5. [ ] Confirm no other `ByRecipient` call site (`withdraw.go`, `upgrade.go`, `swap.go`,
34+
`nft/views/accept.go`, `dvp/views/*`) needs the same fix — none of those flows use
35+
multisig/policy composite owners, so they stay on strict `ByRecipient`.
36+
6. [ ] `make checks` and `make lint-auto-fix`.
37+
7. [ ] Run/validate `dlog-fabric-t12` (Multisig, T12 label) locally or via targeted CI run
38+
across the three transport configs.
39+
40+
## Implementation Progress
41+
- [x] Added `ByRecipientOrMember` to `token/stream.go` (membership-aware filter using `sigService.IsMe`)
42+
- [x] Updated `accept.go:38`/`:43` to use it instead of strict `ByRecipient`
43+
- [x] `make checks` and `make lint-auto-fix` clean; `go test ./token/...` passes
44+
- [x] `make integration-tests-dlog-fabric-t12`**3 Passed | 0 Failed** across websocket, libp2p, replicas transports (previously panicked deterministically on all three)
45+
46+
✅ COMPLETE
47+
48+
## Notes & Decisions
49+
- Scope: this fix targets `dlog-fabric-t12` only, per explicit user request. `dlog-fabric-t14`/
50+
`fabricx-dlog-t14` remain separately flagged as still red on PR #1953 despite `6269df950`
51+
claiming to fix them — not investigated/fixed here.
52+
- `withdraw.go`, `upgrade.go`, `swap.go`, nft/dvp accept views confirmed not multisig-related;
53+
left untouched.

token/stream.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,15 @@ func (o *OutputStream) ByRecipient(id Identity) *OutputStream {
8484
})
8585
}
8686

87+
// ByRecipientOrMember filters the OutputStream to only include outputs whose owner is the
88+
// passed recipient, or a composite (e.g., multisig/policy) identity that the recipient is a
89+
// member of, as determined by sigService.IsMe (a signer is registered for each of its members).
90+
func (o *OutputStream) ByRecipientOrMember(ctx context.Context, id Identity, sigService *SignatureService) *OutputStream {
91+
return o.Filter(func(t *Output) bool {
92+
return id.Equal(t.Owner) || sigService.IsMe(ctx, t.Owner)
93+
})
94+
}
95+
8796
// ByType filters the OutputStream to only include outputs that match the passed type.
8897
func (o *OutputStream) ByType(typ token.Type) *OutputStream {
8998
return o.Filter(func(t *Output) bool {

0 commit comments

Comments
 (0)