Lesson 4 of 6

Submit a Transaction Directly to a Node with gOuroboros

In 102.2 and 102.3 you submitted transactions through Blockfrost — either via Apollo's bfc.SubmitTx(...) or by pasting CBOR into a wallet. Both routes reach the chain eventually, but they hop through someone else's infrastructure. In this lesson you submit the signed transaction directly to a Cardano node using gOuroboros and the LocalTxSubmission mini-protocol.

This is the same path Dolos, a relay, or any node uses internally. No API keys, no hosted services.


When You'd Choose This Path

Submission route

Depends on

Good for

bfc.SubmitTx (Apollo + Blockfrost)

Blockfrost API + your key

Quick prototypes, testnets

Manual paste into wallet

A user in front of a wallet UI

End-user dApps

LocalTxSubmission via gOuroboros

A local node (Dolos)

Backend services, high volume, no third-party dependency

Once you run your own node, LocalTxSubmission is almost always the right choice for server-side Go code.


Prerequisites

  • Completed 102.3 (you can produce an unsigned CBOR transaction) or 102.2 (signed CBOR)
  • Completed 101.1 (starter-kit set up, CARDANO_NODE_SOCKET_PATH and CARDANO_NODE_MAGIC exported)
  • Dolos running on preprod

The Program You'll Use

gouroboros-starter-kit/cmd/tx-submission/main.go is a tiny wrapper around LocalTxSubmission.SubmitTx. It reads a signed transaction from disk (two formats supported) and hands the bytes to the node.

Two input formats, one required:

go run ./cmd/tx-submission -tx-file transaction.json      # cardano-cli JSON format
# or
go run ./cmd/tx-submission -raw-tx-file transaction.cbor  # raw CBOR bytes

The JSON format is compatible with what cardano-cli transaction sign produces:

{
  "type": "Witnessed Tx ConwayEra",
  "description": "Ledger Cddl Format",
  "cborHex": "84a40081825820..."
}

The raw CBOR format is what you'd get if you Apollo-built, signed, and wrote the bytes directly.


Step 1: Produce a Signed Transaction

Use your 102.2 code path unchanged, but instead of calling bfc.SubmitTx(*tx), write the signed CBOR to a file:

// After apollob = apollob.Sign()
tx := apollob.GetTx()
cborBytes, err := cbor.Marshal(tx)
if err != nil {
    panic(err)
}

if err := os.WriteFile("tx.cbor", cborBytes, 0600); err != nil {
    panic(err)
}
fmt.Println("Wrote signed tx to tx.cbor")

Run your program. You now have a tx.cbor file in the current directory.

Don't skip signing. LocalTxSubmission expects a signed transaction. If you feed it the unsigned CBOR from 102.3, the node will reject it during phase-1 validation (missing witnesses).


Step 2: Copy the CBOR into the Starter-Kit

The tx-submission program needs to read the file, so place it somewhere go run can find it. Easiest:

cp tx.cbor /path/to/gouroboros-starter-kit/tx.cbor
cd /path/to/gouroboros-starter-kit

Step 3: Submit

Make sure your environment points at Dolos (from 101.1):

echo $CARDANO_NODE_SOCKET_PATH   # should be your dolos.socket
echo $CARDANO_NODE_MAGIC         # should be 1

Then:

go run ./cmd/tx-submission -raw-tx-file tx.cbor

Expected output (on success):

The transaction was accepted

That's it. The node has accepted the transaction into its mempool. Next time the chain advances (~20 seconds on preprod), a block producer will include it — exactly like Blockfrost would have routed it, just with one less hop.


Confirm It Landed

Two ways:

# Option 1: watch the mempool locally (from 101.4)
go run ./cmd/tx-monitor

# Option 2: check the explorer
# https://preprod.cardanoscan.io/transaction/<tx-hash>

You need the transaction hash to search on the explorer. If your program from Step 1 printed the hash (Apollo's tx.TransactionBody.Hash()), use that.


What the Program Is Actually Doing

Strip away the file parsing and flag handling, and tx-submission reduces to:

o, _ := ouroboros.NewConnection(
    ouroboros.WithNetworkMagic(cfg.Magic),
    ouroboros.WithErrorChan(errorChan),
    ouroboros.WithNodeToNode(false),
    ouroboros.WithKeepAlive(true),
    ouroboros.WithLocalTxSubmissionConfig(localtxsubmission.NewConfig()),
)
o.Dial("unix", cfg.SocketPath)

txType, _ := ledger.DetermineTransactionType(txBytes)
err := o.LocalTxSubmission().Client.SubmitTx(uint16(txType), txBytes)
if err != nil {
    fmt.Printf("ERROR: %s\n", err)
}

Two things worth knowing:

  • DetermineTransactionType(txBytes) inspects the CBOR prefix to pick the right era (Babbage, Conway, …). You don't pass era explicitly; the bytes tell the library.
  • SubmitTx** is synchronous.** It either returns nil (accepted) or an error (rejected). Unlike Blockfrost, there is no "queued" middle state — the node either took it or didn't.

Common Errors and What They Mean

Error

Meaning

Fix

ApplyTxError ... UtxoFailure ... BadInputs

You spent a UTxO that no longer exists (already spent, or never existed)

Refetch UTxOs and rebuild — your chain view was stale

ApplyTxError ... OutsideValidityInterval

Your invalidBefore / invalidHereafter excludes the current slot

Widen the validity window (102.5) or rebuild with current slot

ApplyTxError ... FeeTooSmall

Fee below the protocol minimum

Let Apollo's Complete() set the fee — don't override unless you know why

handshake failed

Magic mismatch

CARDANO_NODE_MAGIC doesn't match Dolos's upstream

connect: no such file or directory

Dolos not running, or wrong socket path

Start dolos daemon, re-check path

The node's error message is usually specific enough to point at the cause. "Accepted" means phase-1 validation passed — not that the transaction is guaranteed to reach a block, but very close on a quiet testnet.


You'll Know You're Successful When

  • tx-submission prints "The transaction was accepted"
  • Your transaction appears in the local mempool (tx-monitor) within seconds
  • Within ~40 seconds, the transaction shows up on Cardanoscan preprod
  • No Blockfrost API key was involved in the submission path

Practice Tasks

  • Submit the same tx.cbor twice. The second should fail — inputs are now consumed. Read the error carefully.
  • Build a signed tx for mainnet (don't actually do this with real funds), write it, and try submitting via your preprod Dolos. Note how the error tells you the magic is wrong.
  • Compare the time from tx-submission completing to the tx appearing on the explorer. Is it faster, slower, or the same as submitting via Blockfrost?

What's Next

  • 102.5 — add a validity window so your transaction is only valid for a bounded time
  • 102.6 — attach metadata to a transaction
  • Module 201 — watch these submissions stream past your own indexer