Scuttlego

An implementation of the Secure Scuttlebutt protocol.

29 January 2023

boreq

Secure Scuttlebutt ecosystem

Clients:

Implementations:

2

Running go-ssb on iOS

3

Scuttlego

A new Secure Scuttlebutt implementation written in Go.

Reuses some elements of go-ssb:

4

Key concepts

5

Hexagonal architecture

6

Domain Driven Design

"The structure and language of software code should match the business domain."

7

Domain

type Message struct {
    Id       string
    Sequence int
}

func DoSomething(msg Message) error {
    if msg.Id == "" {
        return Message{}, errors.New("empty id")
    }

    if msg.Sequence <= 0 {
        return Message{}, errors.New("sequence must be positive")
    }

    // do things

    return nil
}
8

Domain

type Message struct {
    id       string
    sequence int
}

func NewMessage(id string, sequence int) Message {
    return Message{
        id: id,
        sequence: sequence,
    }
}

func (msg Message) Id() string {
    return msg.id
}

func (msg Message) Sequence() int {
    return msg.sequence
}
9

Domain

type Message struct {
    id       string
    sequence int
}

func NewMessage(id string, sequence int) (Message, error) {
    if id == "" {
        return Message{}, errors.New("empty id")
    }

    if sequence <= 0 {
        return Message{}, errors.New("sequence must be positive")
    }

    return Message{
        id: id,
        sequence: sequence,
    }, nil
}

func (msg Message) Id() string { return msg.id }

func (msg Message) Sequence() int { return msg.sequence }
10

Domain

type Message struct {
    id       Id
    sequence Sequence
}

func NewMessage(id Id, sequence Sequence) Message {
    return Message{
        id: id,
        sequence: sequence,
    }
}

func (msg Message) Id() Id { return msg.id }

func (msg Message) Sequence() Sequence { return msg.sequence }
11

Domain

type Id struct {
    id string
}

func NewId(id string) (Id, error) {
    if id == "" {
        return Id{}, errors.New("empty id")
    }

    return Id{
        id: id,
    }, nil
}
12

Domain

type Sequence struct {
    sequence int
}

func NewSequence(sequence int) (Sequence, error) {
    if sequence <= 0 {
        return Sequence{}, errors.New("sequence must be positive")
    }

    return Sequence{
        sequence: sequence,
    }, nil
}
13

Domain

type Message struct {
    id       Id
    sequence Sequence
}

func NewMessage(id Id, sequence Sequence) (Message, error) {
    if id.IsZero() {
        return Message{}, errors.New("zero value of id")
    }

    if sequence.IsZero() {
        return Message{}, errors.New("zero value of sequence")
    }

    return Message{
        id: id,
        sequence: sequence,
    }, nil
}

func (id Id) IsZero() bool { return id == Id{} }

func (seq Sequence) IsZero() bool { return seq == Sequence{} }
14

Domain

type Message struct {
  // ...
}

type Feed struct {
  Messages []Message
}

func AddToFeed(feed Feed, message Message) error {
    // validate feed
    // validate message
}
15

Domain

type Message struct {
  // ...
}

type Feed struct {
  messages []Message
}

func (f *Feed) AddToFeed(message Message) error {
    if len(f.messages) > 0 {
        // ...

        if !f.lastMsg().ComesDirectlyBefore(message) {
            return errors.New("this is not the next message in this feed")
        }
    } else {
        if !message.IsRootMessage() {
            return errors.New("first message in the feed must be a root message")
        }
    }

    f.messages = append(f.messages, message)
    return nil
}
16

Commands and queries

type AppendMessage struct {
    msg Message
}

type UpdateFeedFn func(f *domain.Feed) error

type FeedRepository interface {
    UpdateFeed(id domain.FeedRef, fn UpdateFeedFn) error
}

type AppendMessageHandler struct {
    repository FeedRepository
}

func NewAppendMessageHandler(repository FeedRepository) AppendMessageHandler {
    return AppendMessageHandler{repository: repository}
}

func (h AppendMessageHandler) Handle(cmd AppendMessage) error {
    return h.repository.UpdateFeed(cmd.msg.Feed(), func(f *domain.Feed) error {
        return f.AppendMessage(cmd.msg)
    })
}
17

Commands and queries

In application layer:

type Commands struct {
  AppendMessage *commands.AppendMessageHandler
}

type Queries struct {
  GetMessage *queries.GetMessageHandler
}

type Application struct {
    Commands Commands
    Queries  Queries
}

Outside of the application layer e.g. in ports:

func (h HTTPHandler) DoSomething(...) error {
    cmd := commands.NewAppendMessage(...)
    return h.app.Commands.AppendMessage.Handle(cmd)
}
18

Replacing the database layer completely

Initially bbolt seemed like a good option.

Problem:

mmap allocate error: cannot allocate memory

// ...
b, err := unix.Mmap(int(db.file.Fd()), 0, sz, syscall.PROT_READ, syscall.MAP_SHARED|db.MmapFlags)
// ...

Solution:

19

Tests

Well-tested domain layer prevents a lot of bugs and allows you to avoid writing component tests for anything other than complex behaviours.

Using github.com/stretchr/testify is a good idea.

Table tests are a good idea.

Test fixtures e.g. SomeProcedureName, SomeBool, SomeDirectory are useful.

20

Performance

Performance tailored for mobile devices:

Noticable performance improvements when using the app mostly due to:

21

Source code

https://github.com/planetary-social/scuttlego

MIT licensed.

22

Thank you

boreq

normal