Skip to content

Commit 0ea5424

Browse files
committed
Initial structure and implementation
1 parent ec06aa9 commit 0ea5424

File tree

10 files changed

+419
-2
lines changed

10 files changed

+419
-2
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*.dll
55
*.so
66
*.dylib
7+
/go-ouroboros-network
78

89
# Test binary, built with `go test -c`
910
*.test

Makefile

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
BINARY=go-ouroboros-network
2+
3+
# Determine root directory
4+
ROOT_DIR=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
5+
6+
# Gather all .go files for use in dependencies below
7+
GO_FILES=$(shell find $(ROOT_DIR) -name '*.go')
8+
9+
# Build our program binary
10+
# Depends on GO_FILES to determine when rebuild is needed
11+
$(BINARY): $(GO_FILES)
12+
# Needed to fetch new dependencies and add them to go.mod
13+
go mod tidy
14+
go build -o $(BINARY) ./cmd/$(BINARY)
15+
16+
.PHONY: build image
17+
18+
# Alias for building program binary
19+
build: $(BINARY)
20+
21+
clean:
22+
rm -f $(BINARY)

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
1-
# go-ouroboros
2-
A Go client implementation of the Cardano Ouroboros protocol
1+
# go-ouroboros-network
2+
3+
A Go client implementation of the Cardano Ouroboros network protocol
4+
5+
This is loosely based on the [official Haskell implementation](https://github.com/input-output-hk/ouroboros-network)

cmd/go-ouroboros-network/main.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package main
2+
3+
import (
4+
"crypto/tls"
5+
"flag"
6+
"fmt"
7+
"github.com/cloudstruct/go-ouroboros-network"
8+
"io"
9+
"net"
10+
"os"
11+
)
12+
13+
const (
14+
TESTNET_MAGIC = 1097911063
15+
MAINNET_MAGIC = 764824073
16+
)
17+
18+
type cmdFlags struct {
19+
socket string
20+
address string
21+
useTls bool
22+
networkMagic int
23+
testnet bool
24+
mainnet bool
25+
}
26+
27+
func main() {
28+
f := cmdFlags{}
29+
flag.StringVar(&f.socket, "socket", "", "UNIX socket path to connect to")
30+
flag.StringVar(&f.address, "address", "", "TCP address to connect to in address:port format")
31+
flag.BoolVar(&f.useTls, "tls", false, "enable TLS")
32+
flag.IntVar(&f.networkMagic, "network-magic", 0, "network magic value")
33+
flag.BoolVar(&f.testnet, "testnet", false, fmt.Sprintf("alias for -network-magic=%d", TESTNET_MAGIC))
34+
flag.BoolVar(&f.mainnet, "mainnet", false, fmt.Sprintf("alias for -network-magic=%d", MAINNET_MAGIC))
35+
flag.Parse()
36+
37+
var conn io.ReadWriteCloser
38+
var err error
39+
var dialProto string
40+
var dialAddress string
41+
if f.socket != "" {
42+
dialProto = "unix"
43+
dialAddress = f.socket
44+
} else if f.address != "" {
45+
dialProto = "tcp"
46+
dialAddress = f.address
47+
} else {
48+
fmt.Printf("You must specify one of -socket or -address\n\n")
49+
flag.PrintDefaults()
50+
os.Exit(1)
51+
}
52+
if f.useTls {
53+
conn, err = tls.Dial(dialProto, dialAddress, nil)
54+
} else {
55+
conn, err = net.Dial(dialProto, dialAddress)
56+
}
57+
if err != nil {
58+
fmt.Printf("Connection failed: %s\n", err)
59+
os.Exit(1)
60+
}
61+
if f.networkMagic == 0 {
62+
if f.testnet {
63+
f.networkMagic = TESTNET_MAGIC
64+
} else if f.mainnet {
65+
f.networkMagic = MAINNET_MAGIC
66+
} else {
67+
fmt.Printf("You must specify one of -testnet, -mainnet, or -network-magic\n\n")
68+
flag.PrintDefaults()
69+
os.Exit(1)
70+
}
71+
}
72+
oOpts := &ouroboros.OuroborosOptions{
73+
Conn: conn,
74+
NetworkMagic: uint32(f.networkMagic),
75+
}
76+
o, err := ouroboros.New(oOpts)
77+
if err != nil {
78+
fmt.Printf("ERROR: %s\n", err)
79+
os.Exit(1)
80+
}
81+
go func() {
82+
for {
83+
err := <-o.ErrorChan
84+
fmt.Printf("ERROR: %s\n", err)
85+
os.Exit(1)
86+
}
87+
}()
88+
}

go.mod

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module github.com/cloudstruct/go-ouroboros-network
2+
3+
go 1.16
4+
5+
require github.com/fxamacker/cbor/v2 v2.3.0

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
github.com/fxamacker/cbor/v2 v2.3.0 h1:aM45YGMctNakddNNAezPxDUpv38j44Abh+hifNuqXik=
2+
github.com/fxamacker/cbor/v2 v2.3.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
3+
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
4+
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=

handshake/handshake.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package handshake
2+
3+
import (
4+
"fmt"
5+
"github.com/cloudstruct/go-ouroboros-network/utils"
6+
)
7+
8+
const (
9+
PROTOCOL_ID_SENDER = 0x0000
10+
PROTOCOL_ID_RECEIVER = 0x8000
11+
MESSAGE_TYPE_REQUEST = 0
12+
MESSAGE_TYPE_RESPONSE_ACCEPT = 1
13+
MESSAGE_TYPE_RESPONSE_REFUSE = 2
14+
REFUSE_REASON_VERSION_MISMATCH = 0
15+
REFUSE_REASON_DECODE_ERROR = 1
16+
REFUSE_REASON_REFUSED = 2
17+
)
18+
19+
type handshakeMessage struct {
20+
_ struct{} `cbor:",toarray"`
21+
MessageType uint8
22+
}
23+
24+
type handshakeRequest struct {
25+
handshakeMessage
26+
VersionMap map[uint16]uint32
27+
}
28+
29+
type handshakeResponseAccept struct {
30+
handshakeMessage
31+
Version uint16
32+
NetworkMagic uint32
33+
}
34+
35+
type handshakeResponseRefuse struct {
36+
handshakeMessage
37+
Reason []interface{}
38+
}
39+
40+
type Handshake struct {
41+
sendChan chan []byte
42+
recvChan chan []byte
43+
}
44+
45+
func New(sendChan chan []byte, recvChan chan []byte) *Handshake {
46+
h := &Handshake{
47+
sendChan: sendChan,
48+
recvChan: recvChan,
49+
}
50+
return h
51+
}
52+
53+
func (h *Handshake) Start(versions []uint16, networkMagic uint32) (uint16, error) {
54+
// Create our request
55+
versionMap := make(map[uint16]uint32)
56+
for _, version := range versions {
57+
versionMap[version] = networkMagic
58+
}
59+
data := handshakeRequest{
60+
handshakeMessage: handshakeMessage{
61+
MessageType: MESSAGE_TYPE_REQUEST,
62+
},
63+
VersionMap: versionMap,
64+
}
65+
dataBytes, err := utils.CborEncode(data)
66+
if err != nil {
67+
return 0, err
68+
}
69+
// Send request
70+
h.sendChan <- dataBytes
71+
// Wait for response
72+
respBytes := <-h.recvChan
73+
// Decode response into generic list until we can determine what type of response it is
74+
var resp []interface{}
75+
if err := utils.CborDecode(respBytes, &resp); err != nil {
76+
return 0, err
77+
}
78+
switch resp[0].(uint64) {
79+
case MESSAGE_TYPE_RESPONSE_ACCEPT:
80+
var respAccept handshakeResponseAccept
81+
if err := utils.CborDecode(respBytes, &respAccept); err != nil {
82+
return 0, err
83+
}
84+
return respAccept.Version, nil
85+
case MESSAGE_TYPE_RESPONSE_REFUSE:
86+
var respRefuse handshakeResponseRefuse
87+
if err := utils.CborDecode(respBytes, &respRefuse); err != nil {
88+
return 0, err
89+
}
90+
switch respRefuse.Reason[0].(uint64) {
91+
case REFUSE_REASON_VERSION_MISMATCH:
92+
return 0, fmt.Errorf("handshake failed: version mismatch")
93+
case REFUSE_REASON_DECODE_ERROR:
94+
return 0, fmt.Errorf("handshake failed: decode error: %s", respRefuse.Reason[2].(string))
95+
case REFUSE_REASON_REFUSED:
96+
return 0, fmt.Errorf("handshake failed: refused: %s", respRefuse.Reason[2].(string))
97+
}
98+
}
99+
return 0, fmt.Errorf("unexpected failure")
100+
}

muxer.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package ouroboros
2+
3+
import (
4+
"bytes"
5+
"encoding/binary"
6+
"io"
7+
"time"
8+
)
9+
10+
type MessageHeader struct {
11+
Timestamp uint32
12+
ProtocolId uint16
13+
PayloadLength uint16
14+
}
15+
16+
type Message struct {
17+
MessageHeader
18+
Payload []byte
19+
}
20+
21+
func newMessage(protocolId uint16, payload []byte) (*Message, error) {
22+
msgHeader := MessageHeader{
23+
Timestamp: uint32(time.Now().UnixNano() & 0xffffffff),
24+
ProtocolId: protocolId,
25+
}
26+
msgHeader.PayloadLength = uint16(len(payload))
27+
msg := &Message{
28+
MessageHeader: msgHeader,
29+
Payload: payload,
30+
}
31+
return msg, nil
32+
}
33+
34+
type Muxer struct {
35+
conn io.ReadWriteCloser
36+
errorChan chan error
37+
respChan chan *Message
38+
protocolSenders map[uint16]chan []byte
39+
protocolReceivers map[uint16]chan []byte
40+
}
41+
42+
func NewMuxer(conn io.ReadWriteCloser) *Muxer {
43+
m := &Muxer{
44+
conn: conn,
45+
errorChan: make(chan error, 10),
46+
respChan: make(chan *Message, 10),
47+
protocolSenders: make(map[uint16]chan []byte),
48+
protocolReceivers: make(map[uint16]chan []byte),
49+
}
50+
go m.readLoop()
51+
return m
52+
}
53+
54+
func (m *Muxer) registerProtocol(senderProtocolId uint16, receiverProtocolId uint16) (chan []byte, chan []byte) {
55+
// Generate channels
56+
senderChan := make(chan []byte, 10)
57+
receiverChan := make(chan []byte, 10)
58+
// Record channels in protocol sender/receiver maps
59+
m.protocolSenders[senderProtocolId] = senderChan
60+
m.protocolReceivers[receiverProtocolId] = receiverChan
61+
// Start Goroutine to handle outbound messages
62+
go func() {
63+
for {
64+
payload := <-senderChan
65+
m.Send(senderProtocolId, payload)
66+
}
67+
}()
68+
return senderChan, receiverChan
69+
}
70+
71+
func (m *Muxer) Send(protocolId uint16, payload []byte) error {
72+
msg, err := newMessage(protocolId, payload)
73+
if err != nil {
74+
return err
75+
}
76+
buf := &bytes.Buffer{}
77+
err = binary.Write(buf, binary.BigEndian, msg.MessageHeader)
78+
if err != nil {
79+
return err
80+
}
81+
buf.Write(msg.Payload)
82+
_, err = m.conn.Write(buf.Bytes())
83+
if err != nil {
84+
return err
85+
}
86+
return nil
87+
}
88+
89+
func (m *Muxer) readLoop() {
90+
for {
91+
msgHeader := MessageHeader{}
92+
if err := binary.Read(m.conn, binary.BigEndian, &msgHeader); err != nil {
93+
m.errorChan <- err
94+
}
95+
msg := &Message{
96+
MessageHeader: msgHeader,
97+
Payload: make([]byte, msgHeader.PayloadLength),
98+
}
99+
// We use ReadFull because it guarantees to read the expected number of bytes or
100+
// return an error
101+
if _, err := io.ReadFull(m.conn, msg.Payload); err != nil {
102+
m.errorChan <- err
103+
}
104+
// Send message payload to proper receiver
105+
m.protocolReceivers[msgHeader.ProtocolId] <- msg.Payload
106+
}
107+
}

0 commit comments

Comments
 (0)