Skip to content

Commit d990d56

Browse files
authored
fix(protocol): localtxsubmission idle / done (#1876)
Signed-off-by: Chris Gianelloni <wolf31o2@blinklabs.io>
1 parent b170eaf commit d990d56

4 files changed

Lines changed: 104 additions & 5 deletions

File tree

protocol/localtxsubmission/README.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,14 @@ The LocalTxSubmission protocol submits transactions to the local node's mempool.
1515
```
1616
┌──────┐ SubmitTx ┌──────┐
1717
│ Idle │ ───────────────►│ Busy │
18-
└──────┘ └──┬───┘
19-
▲ │
20-
│ │ AcceptTx
21-
│ │ RejectTx
22-
│ │
18+
└──┬───┘ └──┬───┘
19+
▲ │ │
20+
│ │ Done │ AcceptTx
21+
│ │ │ RejectTx
22+
│ ▼ │
23+
│ ┌──────┐ │
24+
│ │ Done │ │
25+
│ └──────┘ │
2326
└────────────────────────┘
2427
```
2528

@@ -38,13 +41,15 @@ The LocalTxSubmission protocol submits transactions to the local node's mempool.
3841
| `SubmitTx` | 0 | Client → Server | Submit a transaction |
3942
| `AcceptTx` | 1 | Server → Client | Transaction accepted |
4043
| `RejectTx` | 2 | Server → Client | Transaction rejected |
44+
| `Done` | 3 | Client → Server | Terminate protocol |
4145

4246
## State Transitions
4347

4448
### From Idle (Client Agency)
4549
| Message | New State |
4650
|---------|-----------|
4751
| `SubmitTx` | Busy |
52+
| `Done` | Done |
4853

4954
### From Busy (Server Agency)
5055
| Message | New State |

protocol/localtxsubmission/client_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,44 @@ func TestSubmitTxAccept(t *testing.T) {
125125
)
126126
}
127127

128+
// TestSubmitTxAcceptThenDone reproduces the cardano-cli submit flow from dingo
129+
// issue #2788: submit a transaction, receive AcceptTx, then cleanly close the
130+
// protocol by sending MsgDone from the Idle state. Before the Idle->Done
131+
// transition existed, Stop() produced a protocol-state error that tore down the
132+
// node-to-client connection.
133+
func TestSubmitTxAcceptThenDone(t *testing.T) {
134+
testTx := test.DecodeHexString("abcdef0123456789")
135+
conversation := append(
136+
conversationHandshakeSubmitTx,
137+
ouroboros_mock.ConversationEntryOutput{
138+
ProtocolId: localtxsubmission.ProtocolId,
139+
IsResponse: true,
140+
Messages: []protocol.Message{
141+
localtxsubmission.NewMsgAcceptTx(),
142+
},
143+
},
144+
ouroboros_mock.ConversationEntryInput{
145+
ProtocolId: localtxsubmission.ProtocolId,
146+
MessageType: localtxsubmission.MessageTypeDone,
147+
},
148+
)
149+
runTest(
150+
t,
151+
conversation,
152+
func(t *testing.T, oConn *ouroboros.Connection) {
153+
if err := oConn.LocalTxSubmission().Client.SubmitTx(
154+
ledger.TxTypeBabbage,
155+
testTx,
156+
); err != nil {
157+
t.Fatalf("received unexpected error on SubmitTx: %s", err)
158+
}
159+
if err := oConn.LocalTxSubmission().Client.Stop(); err != nil {
160+
t.Fatalf("received unexpected error on Stop: %s", err)
161+
}
162+
},
163+
)
164+
}
165+
128166
func TestSubmitTxRject(t *testing.T) {
129167
testTx := test.DecodeHexString("abcdef0123456789")
130168
expectedErr := localtxsubmission.TransactionRejectedError{

protocol/localtxsubmission/localtxsubmission.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@ var StateMap = protocol.StateMap{
4343
MsgType: MessageTypeSubmitTx,
4444
NewState: stateBusy,
4545
},
46+
{
47+
// The client may terminate the protocol from Idle. cardano-cli
48+
// sends MsgDone to cleanly close after a submit; without this
49+
// transition the connection is torn down with a protocol error
50+
// (dingo issue #2788).
51+
MsgType: MessageTypeDone,
52+
NewState: stateDone,
53+
},
4654
},
4755
},
4856
stateBusy: protocol.StateMapEntry{
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright 2026 Blink Labs Software
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package localtxsubmission
16+
17+
import "testing"
18+
19+
// The LocalTxSubmission client has agency in the Idle state and may terminate
20+
// the protocol by sending MsgDone (per the mini-protocol spec). Without an
21+
// Idle->Done transition, gouroboros rejects cardano-cli's clean shutdown with
22+
// "message *localtxsubmission.MsgDone not allowed in current protocol state
23+
// Idle" and tears down the whole node-to-client connection (dingo issue #2788).
24+
func TestStateMapIdleAllowsDone(t *testing.T) {
25+
entry, ok := StateMap[stateIdle]
26+
if !ok {
27+
t.Fatal("StateMap has no entry for the Idle state")
28+
}
29+
found := false
30+
for _, transition := range entry.Transitions {
31+
if transition.MsgType != MessageTypeDone {
32+
continue
33+
}
34+
found = true
35+
if transition.NewState != stateDone {
36+
t.Fatalf(
37+
"Idle MsgDone transitions to %s, want Done",
38+
transition.NewState,
39+
)
40+
}
41+
}
42+
if !found {
43+
t.Fatal(
44+
"Idle state does not allow MsgDone; cardano-cli cannot cleanly " +
45+
"close the protocol (issue #2788)",
46+
)
47+
}
48+
}

0 commit comments

Comments
 (0)