This guide covers everything you need to contribute to gouroboros.
# Clone and enter the repository
git clone https://github.com/blinklabs-io/gouroboros.git
cd gouroboros
# Install dependencies
go mod download
# Run tests
make test
# Run linter
make lintRequirements:
- Go 1.24+
- golangci-lint for linting
- golines for line length formatting (optional)
gouroboros/
├── ledger/ # Blockchain ledger types and validation
│ ├── common/ # Shared types and interfaces (start here)
│ ├── byron/ # Byron era
│ ├── shelley/ # Shelley era (base validation)
│ ├── allegra/ # Allegra era
│ ├── mary/ # Mary era (multi-asset)
│ ├── alonzo/ # Alonzo era (Plutus scripts)
│ ├── babbage/ # Babbage era (reference scripts)
│ ├── conway/ # Conway era (governance)
│ └── dijkstra/ # Dijkstra era (includes Leios overlay support)
├── protocol/ # Ouroboros network mini-protocols
│ ├── chainsync/ # Chain synchronization
│ ├── blockfetch/ # Block retrieval
│ ├── txsubmission/ # Transaction submission
│ ├── localstatequery/ # Local node queries
│ └── .../ # Other protocols
├── cbor/ # CBOR encoding utilities
├── kes/ # Key-Evolving Signatures
├── vrf/ # Verifiable Random Functions
├── consensus/ # Consensus primitives
├── connection/ # Network connection management
├── muxer/ # Protocol multiplexing
└── internal/test/ # Test utilities and conformance tests
- Find the right package - Use the structure above to locate where your change belongs
- Read existing code - Check similar implementations in the same package
- Understand era inheritance - Later eras delegate to earlier ones; don't duplicate logic
# Create a branch
git checkout -b feature/your-feature
# Make your changes...
# Format code
make format
# Run tests
make test
# Run linter
make lint
# Commit (we use conventional commits)
git commit -m "feat(ledger): add new validation rule for X"We use Conventional Commits:
type(scope): description
[optional body]
Types: feat, fix, docs, refactor, test, chore
Scopes: ledger, protocol, cbor, kes, vrf, consensus
- Line length: 80 characters (enforced by
golines) - Formatting:
gofmtandgoimports - Run:
make formatbefore committing
Use testify for assertions:
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSomething(t *testing.T) {
result, err := DoSomething()
require.NoError(t, err) // Fatal: stops test on failure
assert.Equal(t, expected, result) // Non-fatal: continues on failure
}- Return specific error types, not generic errors
- Error types live in
{package}/errors.go - Use
errors.Is()anderrors.As()for checking
When creating types that need CBOR encoding:
type MyBlock struct {
cbor.StructAsArray // Encode as array, not map
cbor.DecodeStoreCbor // Preserve original bytes for hashing
Header *BlockHeader
Body *BlockBody
}
// IMPORTANT: If using DecodeStoreCbor, implement UnmarshalCBOR
func (m *MyBlock) UnmarshalCBOR(data []byte) error {
type tMyBlock MyBlock
var tmp tMyBlock
if _, err := cbor.Decode(data, &tmp); err != nil {
return err
}
*m = MyBlock(tmp)
m.SetCbor(data) // Store original bytes!
return nil
}- Create the rule in
ledger/{era}/rules.go:
func UtxoValidateMyRule(
tx common.Transaction,
slot uint64,
ls common.LedgerState,
pp common.ProtocolParameters,
) error {
// Validation logic
if invalid {
return MyRuleError{Details: "..."}
}
return nil
}- Add error type in
ledger/{era}/errors.go:
type MyRuleError struct {
Details string
}
func (e MyRuleError) Error() string {
return fmt.Sprintf("my rule failed: %s", e.Details)
}- Register the rule in the
UtxoValidationRulesslice:
var UtxoValidationRules = []common.UtxoValidationRuleFunc{
// ... existing rules
UtxoValidateMyRule,
}- Write tests using
MockLedgerState:
func TestUtxoValidateMyRule(t *testing.T) {
ls := &test.MockLedgerState{
NetworkIdVal: 0,
UtxoByIdFunc: func(id common.TransactionInput) (common.Utxo, error) {
return testUtxo, nil
},
}
err := UtxoValidateMyRule(tx, slot, ls, pp)
assert.NoError(t, err)
}- Define message type in
protocol/{name}/messages.go:
const MessageTypeMyMsg = 7
type MsgMyMsg struct {
protocol.MessageBase
Field1 string
Field2 uint64
}
func NewMsgMyMsg(field1 string, field2 uint64) *MsgMyMsg {
return &MsgMyMsg{
MessageBase: protocol.MessageBase{
MessageType: MessageTypeMyMsg,
},
Field1: field1,
Field2: field2,
}
}-
Register in message parsing (usually a switch statement in the package)
-
Add client/server methods as needed
Each era extends the previous. When adding era-specific logic:
// In ledger/conway/rules.go - delegate to parent era
func UtxoValidateSomething(tx, slot, ls, pp) error {
// First, run the parent era's validation
if err := babbage.UtxoValidateSomething(tx, slot, ls, pp); err != nil {
return err
}
// Then add Conway-specific checks
// ...
return nil
}# All tests with race detection
make test
# Specific package
go test -v ./ledger/shelley/...
# Conformance tests (314 test vectors)
go test -v ./internal/test/conformance/...
# Single test
go test -v -run TestSpecificName ./ledger/...
# With coverage
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.outLocated in internal/test/conformance/. These test against official Cardano ledger specification vectors.
# Run all conformance tests
go test -v ./internal/test/conformance/...
# Count passing/failing
go test -v ./internal/test/conformance/... 2>&1 | grep -E "--- (PASS|FAIL):" | sort | uniq -cUse internal/test/ledger/ledger.go for mocking:
import test "github.com/blinklabs-io/gouroboros/internal/test/ledger"
ls := &test.MockLedgerState{
NetworkIdVal: 0,
UtxoByIdFunc: func(id common.TransactionInput) (common.Utxo, error) {
// Return test data or error
},
IsStakeRegisteredFunc: func(cred []byte) (bool, error) {
return true, nil
},
}CBOR decode errors:
- Check field order matches CBOR structure
- Verify CBOR tags are correct
- Use
cbor.Diagnose()to inspect raw bytes
Hash mismatches:
- Ensure
SetCbor()is called inUnmarshalCBOR - Use
Cbor()method, not re-encoded data - Check for indefinite vs definite length encoding issues
Validation failures:
- Check which rule failed (error type indicates the rule)
- Verify mock state returns expected values
- Compare with official test vectors if available
# Decode CBOR hex to diagnostic notation
echo "85..." | xxd -r -p | cbor-diag
# Run single test with verbose output
go test -v -run TestName ./package/...
# Check for goroutine leaks
go test -v -race ./...Cardano has two separate era lineages:
Byron (standalone, legacy)
Shelley → Allegra → Mary → Alonzo → Babbage → Conway → Leios
Byron is the original Cardano implementation with its own block/transaction format.
Shelley and later are a fresh start with a new architecture. Each post-Shelley era:
- Extends types from the previous era
- Delegates validation to parent era, then adds new rules
- May add new transaction fields, certificate types, etc.
LedgerState (ledger/common/state.go):
type LedgerState interface {
UtxoState // UTxO lookup
CertState // Stake/pool registration
SlotState // Slot-time conversion
PoolState // Pool queries
RewardState // Reward calculations
GovState // Governance queries
NetworkId() uint
CostModels() map[uint][]int64
}Transaction (ledger/common/tx.go):
type Transaction interface {
TransactionBody
Hash() Blake2b256
Cbor() []byte
IsValid() bool
Consumed() []TransactionInput
Produced() []Utxo
Witnesses() TransactionWitnessSet
}Each mini-protocol package follows this pattern:
{protocol}.go- Protocol definition, state machineclient.go- Client implementationserver.go- Server implementationmessages.go- Message types
- Discord: Blink Labs Discord
- Issues: GitHub Issues
- GoDoc: pkg.go.dev
Apache 2.0 - Include the license header in new files:
// Copyright 2026 Blink Labs Software
//
// Licensed under the Apache License, Version 2.0 (the "License");
// ...