Skip to content

Commit

Permalink
Merge pull request #1 from cloudstruct/feature/initial
Browse files Browse the repository at this point in the history
Initial structure and implementation
  • Loading branch information
agaffney authored Dec 12, 2021
2 parents ec06aa9 + 0ea5424 commit 9c18ec9
Show file tree
Hide file tree
Showing 10 changed files with 419 additions and 2 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*.dll
*.so
*.dylib
/go-ouroboros-network

# Test binary, built with `go test -c`
*.test
Expand Down
22 changes: 22 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
BINARY=go-ouroboros-network

# Determine root directory
ROOT_DIR=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))

# Gather all .go files for use in dependencies below
GO_FILES=$(shell find $(ROOT_DIR) -name '*.go')

# Build our program binary
# Depends on GO_FILES to determine when rebuild is needed
$(BINARY): $(GO_FILES)
# Needed to fetch new dependencies and add them to go.mod
go mod tidy
go build -o $(BINARY) ./cmd/$(BINARY)

.PHONY: build image

# Alias for building program binary
build: $(BINARY)

clean:
rm -f $(BINARY)
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
# go-ouroboros
A Go client implementation of the Cardano Ouroboros protocol
# go-ouroboros-network

A Go client implementation of the Cardano Ouroboros network protocol

This is loosely based on the [official Haskell implementation](https://github.com/input-output-hk/ouroboros-network)
88 changes: 88 additions & 0 deletions cmd/go-ouroboros-network/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package main

import (
"crypto/tls"
"flag"
"fmt"
"github.com/cloudstruct/go-ouroboros-network"
"io"
"net"
"os"
)

const (
TESTNET_MAGIC = 1097911063
MAINNET_MAGIC = 764824073
)

type cmdFlags struct {
socket string
address string
useTls bool
networkMagic int
testnet bool
mainnet bool
}

func main() {
f := cmdFlags{}
flag.StringVar(&f.socket, "socket", "", "UNIX socket path to connect to")
flag.StringVar(&f.address, "address", "", "TCP address to connect to in address:port format")
flag.BoolVar(&f.useTls, "tls", false, "enable TLS")
flag.IntVar(&f.networkMagic, "network-magic", 0, "network magic value")
flag.BoolVar(&f.testnet, "testnet", false, fmt.Sprintf("alias for -network-magic=%d", TESTNET_MAGIC))
flag.BoolVar(&f.mainnet, "mainnet", false, fmt.Sprintf("alias for -network-magic=%d", MAINNET_MAGIC))
flag.Parse()

var conn io.ReadWriteCloser
var err error
var dialProto string
var dialAddress string
if f.socket != "" {
dialProto = "unix"
dialAddress = f.socket
} else if f.address != "" {
dialProto = "tcp"
dialAddress = f.address
} else {
fmt.Printf("You must specify one of -socket or -address\n\n")
flag.PrintDefaults()
os.Exit(1)
}
if f.useTls {
conn, err = tls.Dial(dialProto, dialAddress, nil)
} else {
conn, err = net.Dial(dialProto, dialAddress)
}
if err != nil {
fmt.Printf("Connection failed: %s\n", err)
os.Exit(1)
}
if f.networkMagic == 0 {
if f.testnet {
f.networkMagic = TESTNET_MAGIC
} else if f.mainnet {
f.networkMagic = MAINNET_MAGIC
} else {
fmt.Printf("You must specify one of -testnet, -mainnet, or -network-magic\n\n")
flag.PrintDefaults()
os.Exit(1)
}
}
oOpts := &ouroboros.OuroborosOptions{
Conn: conn,
NetworkMagic: uint32(f.networkMagic),
}
o, err := ouroboros.New(oOpts)
if err != nil {
fmt.Printf("ERROR: %s\n", err)
os.Exit(1)
}
go func() {
for {
err := <-o.ErrorChan
fmt.Printf("ERROR: %s\n", err)
os.Exit(1)
}
}()
}
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/cloudstruct/go-ouroboros-network

go 1.16

require github.com/fxamacker/cbor/v2 v2.3.0
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
github.com/fxamacker/cbor/v2 v2.3.0 h1:aM45YGMctNakddNNAezPxDUpv38j44Abh+hifNuqXik=
github.com/fxamacker/cbor/v2 v2.3.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
100 changes: 100 additions & 0 deletions handshake/handshake.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package handshake

import (
"fmt"
"github.com/cloudstruct/go-ouroboros-network/utils"
)

const (
PROTOCOL_ID_SENDER = 0x0000
PROTOCOL_ID_RECEIVER = 0x8000
MESSAGE_TYPE_REQUEST = 0
MESSAGE_TYPE_RESPONSE_ACCEPT = 1
MESSAGE_TYPE_RESPONSE_REFUSE = 2
REFUSE_REASON_VERSION_MISMATCH = 0
REFUSE_REASON_DECODE_ERROR = 1
REFUSE_REASON_REFUSED = 2
)

type handshakeMessage struct {
_ struct{} `cbor:",toarray"`
MessageType uint8
}

type handshakeRequest struct {
handshakeMessage
VersionMap map[uint16]uint32
}

type handshakeResponseAccept struct {
handshakeMessage
Version uint16
NetworkMagic uint32
}

type handshakeResponseRefuse struct {
handshakeMessage
Reason []interface{}
}

type Handshake struct {
sendChan chan []byte
recvChan chan []byte
}

func New(sendChan chan []byte, recvChan chan []byte) *Handshake {
h := &Handshake{
sendChan: sendChan,
recvChan: recvChan,
}
return h
}

func (h *Handshake) Start(versions []uint16, networkMagic uint32) (uint16, error) {
// Create our request
versionMap := make(map[uint16]uint32)
for _, version := range versions {
versionMap[version] = networkMagic
}
data := handshakeRequest{
handshakeMessage: handshakeMessage{
MessageType: MESSAGE_TYPE_REQUEST,
},
VersionMap: versionMap,
}
dataBytes, err := utils.CborEncode(data)
if err != nil {
return 0, err
}
// Send request
h.sendChan <- dataBytes
// Wait for response
respBytes := <-h.recvChan
// Decode response into generic list until we can determine what type of response it is
var resp []interface{}
if err := utils.CborDecode(respBytes, &resp); err != nil {
return 0, err
}
switch resp[0].(uint64) {
case MESSAGE_TYPE_RESPONSE_ACCEPT:
var respAccept handshakeResponseAccept
if err := utils.CborDecode(respBytes, &respAccept); err != nil {
return 0, err
}
return respAccept.Version, nil
case MESSAGE_TYPE_RESPONSE_REFUSE:
var respRefuse handshakeResponseRefuse
if err := utils.CborDecode(respBytes, &respRefuse); err != nil {
return 0, err
}
switch respRefuse.Reason[0].(uint64) {
case REFUSE_REASON_VERSION_MISMATCH:
return 0, fmt.Errorf("handshake failed: version mismatch")
case REFUSE_REASON_DECODE_ERROR:
return 0, fmt.Errorf("handshake failed: decode error: %s", respRefuse.Reason[2].(string))
case REFUSE_REASON_REFUSED:
return 0, fmt.Errorf("handshake failed: refused: %s", respRefuse.Reason[2].(string))
}
}
return 0, fmt.Errorf("unexpected failure")
}
107 changes: 107 additions & 0 deletions muxer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package ouroboros

import (
"bytes"
"encoding/binary"
"io"
"time"
)

type MessageHeader struct {
Timestamp uint32
ProtocolId uint16
PayloadLength uint16
}

type Message struct {
MessageHeader
Payload []byte
}

func newMessage(protocolId uint16, payload []byte) (*Message, error) {
msgHeader := MessageHeader{
Timestamp: uint32(time.Now().UnixNano() & 0xffffffff),
ProtocolId: protocolId,
}
msgHeader.PayloadLength = uint16(len(payload))
msg := &Message{
MessageHeader: msgHeader,
Payload: payload,
}
return msg, nil
}

type Muxer struct {
conn io.ReadWriteCloser
errorChan chan error
respChan chan *Message
protocolSenders map[uint16]chan []byte
protocolReceivers map[uint16]chan []byte
}

func NewMuxer(conn io.ReadWriteCloser) *Muxer {
m := &Muxer{
conn: conn,
errorChan: make(chan error, 10),
respChan: make(chan *Message, 10),
protocolSenders: make(map[uint16]chan []byte),
protocolReceivers: make(map[uint16]chan []byte),
}
go m.readLoop()
return m
}

func (m *Muxer) registerProtocol(senderProtocolId uint16, receiverProtocolId uint16) (chan []byte, chan []byte) {
// Generate channels
senderChan := make(chan []byte, 10)
receiverChan := make(chan []byte, 10)
// Record channels in protocol sender/receiver maps
m.protocolSenders[senderProtocolId] = senderChan
m.protocolReceivers[receiverProtocolId] = receiverChan
// Start Goroutine to handle outbound messages
go func() {
for {
payload := <-senderChan
m.Send(senderProtocolId, payload)
}
}()
return senderChan, receiverChan
}

func (m *Muxer) Send(protocolId uint16, payload []byte) error {
msg, err := newMessage(protocolId, payload)
if err != nil {
return err
}
buf := &bytes.Buffer{}
err = binary.Write(buf, binary.BigEndian, msg.MessageHeader)
if err != nil {
return err
}
buf.Write(msg.Payload)
_, err = m.conn.Write(buf.Bytes())
if err != nil {
return err
}
return nil
}

func (m *Muxer) readLoop() {
for {
msgHeader := MessageHeader{}
if err := binary.Read(m.conn, binary.BigEndian, &msgHeader); err != nil {
m.errorChan <- err
}
msg := &Message{
MessageHeader: msgHeader,
Payload: make([]byte, msgHeader.PayloadLength),
}
// We use ReadFull because it guarantees to read the expected number of bytes or
// return an error
if _, err := io.ReadFull(m.conn, msg.Payload); err != nil {
m.errorChan <- err
}
// Send message payload to proper receiver
m.protocolReceivers[msgHeader.ProtocolId] <- msg.Payload
}
}
Loading

0 comments on commit 9c18ec9

Please sign in to comment.