Tuesday, December 17, 2024

Unpacking the Transactions


So far, we have written a minimal server and an inadequate client. But for sure, the client is a long way ahead of the server and we'll only be making changes there that we need in order to make progress on the server side. So now, we're going to slow down (a bit) and tackle the basic server functionality. Remember, what I still really want to do is get into the nitty-gritty of testing that this can operate at scale and keep functioning in the face of multiple network problems. Apologies if I'm still going faster than you want - get back to me in the comments and I'll try and address your concerns.

At the end of the last episode, we claimed to have a working client and server, but in reality we have put a JSON transaction onto the wire and alert the server. Before we can go any further we need to unpack it on the server side. I was going to "just do this", but it turns out to be trickier than I thought.

The handler code

We left our handler last time with this code:
// ServeHTTP implements http.Handler.
func (r RecordStorage) ServeHTTP(http.ResponseWriter, *http.Request) {
    log.Println("asked to store record")
}

FIRST_CLIENT_SERVER:internal/clienthandler/recordstorage.go

The http.Request is some abstract thing which, given the context, we assume is a POST of a transaction as a JSON message. We need parse the JSON into a "client" transaction.

(We should (arguably?) also check things like whether it is a POST request, and we should also probably have some kind of session ID - if I don't find I need them before the end of this blog, adding them will be left as an exercise for the reader. This is true of a lot of validation steps at the moment, but I imagine I will end up checking all the signatures I care about.)

On the face of it, it would seem we can read the body of the request using io.ReadAll and convert it from JSON into a Transaction object using json.Unmarshal:
// ServeHTTP implements http.Handler.
func (r RecordStorage) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
    log.Printf("asked to store record with length %d\n", req.ContentLength)

    body, err := io.ReadAll(req.Body)
    if err != nil {
        log.Printf("Error: %v\n", err)
        resp.WriteHeader(http.StatusBadRequest)
        return
    }
    log.Printf("have json input %s\n", string(body))

    var tx = api.Transaction{}
    err = json.Unmarshal(body, &tx)
    if err != nil {
        log.Printf("Error unmarshalling: %v\n", err)
        resp.WriteHeader(http.StatusBadRequest)
        return
    }

    log.Printf("Have transaction %v\n", tx)
}

UNPACK_JSON_1:internal/clienthandler/recordstorage.go

Sadly this does not work. There are two problems: let's tackle the trivial one first. I assumed that when it came to sending a URL over the wire, the marshaller would automatically transform it back into a string, because that's what we all think URLs are. But not so much - this is what we see when we dump the body:
{"ContentLink":{"Scheme":"http","Opaque":"","User":null,
"Host":"tx.info","Path":"/msg1","RawPath":"","OmitHost":false,
"ForceQuery":false,"RawQuery":"","Fragment":"","RawFragment":""},
"ContentHash":{},
"Signatories":[{"Signer":{"Scheme":"https","Opaque":"","User":null,
"Host":"user2.com","Path":"","RawPath":"","OmitHost":false,
"ForceQuery":false,"RawQuery":"","Fragment":"","RawFragment":""},
"Signature":null},
{"Signer":{"Scheme":"https","Opaque":"","User":null,
"Host":"user1.com","Path":"/","RawPath":"","OmitHost":false,
"ForceQuery":false,"RawQuery":"","Fragment":"","RawFragment":""},
"Signature":"oxjfkiItXMa/jME0zSuVwlTjlyAd3ITYxZ/JslmT5o4Aj+cwiSN7tQX7OKBAE+tX4DZ9Qe+yEclwIKJUeCMvjXdQ9zPDBktkC0njdkybjaUbiNXrhLSCWLOz2861lzuNQd92GAoMErwy3bBI9qthhoBG47gMTC/bEzesH33WL+ZenDYQhsyrk0DXlT/BokghGVI9H1XTSpfespOFxOEH7SZMAw1HSBtNFkF5VsE66Je66suirLk3Pb2+ClTjOs4NXwlTGv11a1BZWZA9lKqs4M3rNmzLWJiz8FmGL4XTNpyAS/XNL1o9UOLxK+OnckU+pd4md8fTRTLK0su16I6rwA=="
}]}
Now, the json.Marshal function can adapt its behaviour based on tags associated with a struct field, but as far as I can see, there isn't one that says "use the Stringer interface" (there is a ",string", but that says it only works for primitive fields).

So, it looks like I need to write my own marshalling (and unmarshalling) code for Transaction and Signatory. Fortunately, it's not too hard. It would seem that you can copy the fields into a map and then marshal that:
func (tx Transaction) MarshalJSON() ([]byte, error) {
    var m = make(map[string]any)
    m["ContentLink"] = tx.ContentLink.String()
    m["ContentHash"] = tx.ContentHash
    m["Signatories"] = tx.Signatories
    return json.Marshal(m)
}

CUSTOM_MARSHAL_URL:internal/api/transaction.go

Likewise for Signatory:
func (sig Signatory) MarshalJSON() ([]byte, error) {
    var m = make(map[string]any)
    m["Signer"] = sig.Signer.String()
    m["Signature"] = sig.Signature
    return json.Marshal(m)
}

CUSTOM_MARSHAL_URL:internal/types/signatory.go

Of course, that now means that you have one more moving part you need to keep synchronized every time you add a field :-(

Now we see the following output from the server when it unpacks the message:
{"ContentHash":{},
"ContentLink":"http://tx.info/msg1",
"Signatories":[
{"Signature":null,"Signer":"https://user2.com"},
{"Signature":"E6x5u3lOi6PwiXuDUCPMd4sv87LVxZVCng50MTe/dtIG9e8HuZFS4Z1K11t/VoKR1PtWPyNMLtIQnsd+dYRhnPFOA1HUErH0Xnd2rwS/tX4DHrISYLj9ioyuD4f0kJyGTEZIORm9nnL01vYBWukwZ+2Ghbfvy65ElzCjmhyexCtwGMOCpL3ao3z0WmRY+RHn4XRTZrMyLX0NkX96Mcpz75WRlu+uwxnbUvJnfpeXJYFT5XA1K/8r/iEhq71EGEg4mKQgJcrwVid9rZDeKu1s8HU1hpJ2zDQQGcxpdFdxbgcGPby0vQ/J1VzmEX0OTpqrgL0WF9JnuVBysVx7mrwN0w==","Signer":"https://user1.com/"}
]}
The bigger problem is that Unmarshalling just fails:
Error unmarshalling: json: cannot unmarshal object into Go struct field Transaction.ContentHash of type hash.Hash
Looking back at the JSON we printed out, that would seem to be most likely connected to the fact that ContentHash is represented as {}. In all likelihood, we need to do something with that as well in order to marshal it correctly.

But, no. It turns out that I have misunderstood the term Hash as used in Go. A hash.Hash is a hasher, not the resultant hash. In order to get that, I need to call Sum, which returns a []byte. That's what I want to store.

But I don't want to declare to all and sundry that it is a []byte, so I'm going to introduce another type in my own space called Hash. This has a ripple effect through the code, but the main thing is that we need to call Sum on the hash in main() and pass that.
  var hasher maphash.Hash
  hasher.WriteString("hello, world")
  h := hasher.Sum(nil)

  tx, err := api.NewTransaction("http://tx.info/msg1", h)

INTRODUCED_HASH_TYPE:cmd/ledgerclient/main.go

It's a somewhat complicated and subtle thing, but apart from the "clarity" that you get from renaming a type in this way, there are practical benefits in Go: "methods" can be associated with types, even if the type is, as here, just a slice of bytes. Because although you could argue that's what it is, the compiler carries around a static type that enables it to understand a "method application" and turn it into a function call with the array of bytes as the method receiver. I don't understand enough yet to know whether this persists at runtime, or whether this is forgotten in the code and it really is just a "byte slice".

Since this is a change on the client side, it automatically propagates to the server and we now see on the that the JSON to be unmarshalled looks like this:
{"ContentHash":"/pWORCcEdbM=",
"ContentLink":"http://tx.info/msg1",
"Signatories":[
{"Signature":null,"Signer":"https://user2.com"},
{"Signature":"Mizxonkpsi/tGykneTeprJ0LovyxMBDWZ3uF6S4LlLX+Ssy+rhPTfq26ILGhDJiIOlPHNebyoGW3+yFQCYu3NFRNuGwZnAGoFajnBDO7oTf2ctk8GBLypb5Ow8IzkiSNU1LPD8c/ZoUkE1qlx6niQXAbH3UpQ1drvh0Td0JP2ja3RktxFKCz9B36X/Hkj3lBOb0hv+ztCD4LVTbd49RSh4ROoLcBFkaqNXx7OuJGIUEMXwBofbvVSnuzZZV4oc6OP/JIl/MUqRHa+uqIGSSOHiZXZZjJ0OVFJKTUYbu5+lTxmzdX/TxcOx8svkbnnst2w1mxBxVVpmCql3VgD3PIDw==","Signer":"https://user1.com/"}
]}
But it still doesn't unmarshal successfully, because those strings cannot be automatically unmarshalled back into url.URL fields in Transaction. To handle this, we need a custom UnmarshalJson method:
func (tx *Transaction) UnmarshalJSON(bs []byte) error {
    var wire struct {
        ContentLink string
        ContentHash []byte
        Signatories []*types.Signatory
    }
    if err := json.Unmarshal(bs, &wire); err != nil {
        return err
    }
    if url, err := url.Parse(wire.ContentLink); err == nil {
        tx.ContentLink = url
    } else {
        return err
    }
    tx.ContentHash = wire.ContentHash
    tx.Signatories = wire.Signatories

    return nil
}

CUSTOM_UNMARSHAL_URL:internal/api/transaction.go

I was actually surprised how hard this was to do; I was thinking there would be some way to specify that you wanted to intervene in the standard unmarshalling process, but you basically need to rewrite the whole unmarshaller, including specifying the wire type. So what this does is to declare an anonymous type which has fields with the same names as the ones transmitted on the wire (this is obviously very important) and the types which were marshalled.

It then parses the URL ContentLink field and assigns it to the Transaction, followed by the other two fields which can just be copied.

There are a couple of things about Go which I've used here that I didn't know yesterday. When I was reading one of the books about go, it talked about the form of the if statement I've used here and it seemed weird to me although it did describe it as "common in idiomatic Go". But seeing this in the example I was referencing from StackOverflow today, it made perfect sense, at least as long as you read it correctly. And, of course, learning to read a new programming language correctly is part of learning a new programming language :-)

It allows you to place the assignment of url and err in a scope which lasts just for the duration of the if and else blocks (and, presumably, any else if blocks). This means that you can reuse the same name err in multiple if statements without conflict, which is something I've been doing up until now that has been annoying me. Look for that to quietly change off camera as we go along.

The other thing is the anonymous type. I haven't thoroughly understood this yet, but it seems to be that the Go compiler builds up a "real" typename which is some kind of view of the structure of the declaration and can decide to use that as opposed to just going off the declared type name, although often it will choose to say that two types are different just because of the name they have been given. Anyway, the important point is that you can create an anonymous type here, in a very lightweight way, and then use it as a stepping stone to your real objective.

We then obviously have to do the same thing with the Signatory struct:
func (sig *Signatory) UnmarshalJSON(bs []byte) error {
    var wire struct {
        Signer    string
        Signature *Signature
    }
    if err := json.Unmarshal(bs, &wire); err != nil {
        return err
    }
    if url, err := url.Parse(wire.Signer); err == nil {
        sig.Signer = url
    } else {
        return err
    }
    sig.Signature = wire.Signature

    return nil
}

CUSTOM_UNMARSHAL_URL:internal/types/signatory.go

Reflections

I am still having some issues with getting used to Go, and in particular the VSCode environment I've chosen to use.

One of the general problems of developing client/server applications is that (by definition) you have two binaries that you need to run simultaneously. This is quite a lot of clicking, and I haven't yet seen a way of reducing it to one click (although believe that such a thing may be possible in VSCode). When running the client, you run it and then it's done. But when it comes to debugging the server, you have to remember to stop it and restart it, otherwise your changes have no effect.

VSCode does have a warning for this, but I turned it off because it warns you all the time when you are writing code; even when you are changing client code that doesn't affect the running server. So there isn't a perfect solution (if there is a perfect solution, please let me know?).

Hopefully most of these issues will go away in a short while when I start writing and running automated tests instead of testing manually.

Saturday, December 14, 2024

A Simple Client and Server

I am going to rattle through a very simple first version of the ledgerclient and chainledger node. The purpose of this is to get to a point where we have a client that can create and sign a message and upload it to a server. It may at first seem we have achieved a lot here; we really haven't.

The Client

I'm going to start with the client, in large part because it's a nice simple thing. I'm going to present the client files one at a time, in their entirety, with commentary among the various parts.

cmd/ledgerclient/main.go

The first section of the file is mainly boilerplate.
package main

import (
    "fmt"
    "hash/maphash"
    "log"
    "net/url"

    "github.com/gmmapowell/ChainLedger/internal/api"
    "github.com/gmmapowell/ChainLedger/internal/client"
)
The package statement identifies the package that this file claims to be in. In order to become a command executable, it must be declared as being in the package main; if you don't do this the function main() will be flagged as not being used anywhere.

The import statement lists all the other packages which are going to be used in this file. If a package is going to be used, it must be listed and, conversely, if it is listed it must be used.

The fmt package is where Go places things like Printf, which will be using to confirm that we have submitted a transaction. We will be using the hash/maphash package to generate an acceptable hash to submit as the (alleged) hash of the contents of the document we are submitting. log contains all the statements to do logging and net/url contains the definition of the URL struct which we will be using everywhere to validate our strings are valid URLs.

The main module delegates most of its work to two internal packages (i.e. packages elsewhere in this same project), api and client. We will cover those when we get there.
func main() {
    repo, e1 := client.MakeMemoryRepo()
    if e1 != nil {
        panic(e1)
    }
    uid := "https://user1.com/"
    uu, e2 := url.Parse(uid)
    if e2 != nil {
        panic(e2)
    }
    pk, e3 := repo.PrivateKey(uu)
    if e3 != nil {
        panic(e3)
    }
As is the case with most C-based languages, go defines a function called main which is the entry point for the entire program. These opening lines show the basic setup of the client. We create an internal repository for things to do with users and nodes (the client and node respositories will be similar, but will be different). For now, we are just using this to hide the details of creating and managing the initial user's private key, but in the log run will store more and more information in there. For now, we are not using any external sources of information (e.g. command line arguments) but we will come back and do that later.

One such thing that should be an argument is the submitting user id, which is hardcoded to be https://user1.com/. I am using URLs as IDs for the simple reason that they have some structure and it makes it easy to have user ids with given relationships. In the long run, I expect that I will want this to be "real" URLs that return a JSON document that describes the user's key information, such as the public key for their signing key.

And then we ask the repository for the private key for the current user. Note that the repository is not going to have ALL the private keys for ALL the users; any given instantiation of the client repository will probably only have ONE private key (for the given user), although it is not going to be a hardwired constraint (mainly because I want to violate it in order to build a stress test). Ultimately, I expect the repository to read from files, databases or something like Amazon SSM parameters, depending on the deployment environment. In that case, you have access to whatever would be available.

Here in this code it is possible to see how much of the code is dominated by error handling in the absence of exception handling. I am optimistic that in the fullness of time I will become better at dealing with this and it will look less ugly.

One of the things that this does surface, however, is just how many things can go wrong, and it does (to a certain extent) force you to think about them. I'm not really sure what to do with any of these errors - for now, at least, nothing should go wrong here, because I am not depending on either external data or external services. I used the panic command because I saw it and thought it probably did what I wanted here: made it clear there was a problem.
    cli, err := client.NewSubmitter("http://localhost:5001", uid, pk)
    if err != nil {
        log.Fatal(err)
        return
    }
In order to connect to the server, we need a "transaction submitter". This is a module which takes the URL of a node to connect to, a the URL id of a user who is going to be submitting the transactions, and their private key. It is then ready to submit all of the transactions on their behalf.

If anything goes wrong, the error is logged and the main function returns. This is subtly different handling from the above, which is because there really are a number of things that can go wrong here, most notably that the server isn't running.
    var h maphash.Hash
    h.WriteString("hello, world")
    tx, err := api.NewTransaction("http://tx.info/msg1", &h)
    if err != nil {
        log.Fatal(err)
        return
    }
    err = tx.SignerId("https://user2.com")
    if err != nil {
        log.Fatal(err)
        return
    }
    err = cli.Submit(tx)
    if err != nil {
        log.Fatal(err)
        return
    }
The rest of the code comes here in a rush, in which we create a new transaction message which is defined by the URL http://tx.info/msg1 and for which we provide the hash of the alleged contents "hello, world". As I said in the design, it is not assumed that either the client or the server can read the URL, and indeed it may not exist, so the URL and the hash must both be provided.

Before the message can be hashed and signed, the transaction must be given a list of all the users who will sign it. Now, obviously, it is not possible for any one user to sign for all the parties, so the transaction will only have one signature - for the submitting party - and, given that the current party is submitting it, the submitting user does not need to explicitly provide their user id.

Finally, by calling Submit, on the cli reference (the submitter), the transaction is signed and uploaded to the server.

All of these methods can reasonably fail, but even so, all I'm doing is logging the errors. In the fullness of time, I probably want to revisit at least the most common errors.
    fmt.Printf("submitted transaction: %v", tx)
}
Finally, if all went well, we report to the user that the transaction was (successfully) submitted.

internal/client/submit.go

The usual boilerplate starts us off:
package client

import (
    "crypto/rsa"
    "net/http"
    "net/url"

    "github.com/gmmapowell/ChainLedger/internal/api"
)
Now, I'm new to Go, so I'm going to cause offence and say we are going to declare a class. It's not a class, it's a struct, and you are (I am) going to hobble yourself if you thing that classes and structs are the same thing (in either direction). But anyway, we are going to declare a class to submit transactions to the server.
type Submitter struct {
    node *url.URL
    iam  *url.URL
    pk   *rsa.PrivateKey
}
This declares a struct with three fields. Because the field names start with lowercase letters, they are all private fields, which means that they can be seen in the methods in this file, but not elsewhere (I think, I'm not entirely sure what the rules are).

The three fields are:
  • node is the URL of the server we are intending to connect to;
  • iam is the URL which identifies the submitting user;
  • pk is the private signing key of the submitting user.
func NewSubmitter(node string, id string, pk *rsa.PrivateKey) (*Submitter, error) {
    nodeAddr, e1 := url.Parse(node)
    if e1 != nil {
        return nil, e1
    }
    iam, err := url.Parse(id)
    if err != nil {
        return nil, err
    }
    return &Submitter{node: nodeAddr, iam: iam, pk: pk}, nil
}
Go does not have constructors as such. Instead a new instance of a struct is created using the syntax shown on the last line of this function: the struct name, followed by field assignments in curly braces. The equivalent to constructors are the kind of functions shown here, which take arguments and then create the appropriate object. So here, we have a function that takes string versions of node and user id, and confirms that they are in fact valid urls before placing them in a Submitter structure.

Note that in order to return a *Submitter, we have to use the & operator to convert an instance into a pointer (or some such technical language).
func (s *Submitter) Submit(tx *api.Transaction) error {
    var e error = tx.Signer(s.iam)
    if e != nil {
        return e
    }
    e = tx.Sign(s.iam, s.pk)
    if e != nil {
        return e
    }
    json, e2 := tx.JsonReader()
    if e2 != nil {
        return e2
    }
    cli := http.Client{}
    _, e3 := cli.Post(s.node.JoinPath("/store").String(), "application/json", json)
    return e3
}
This is how methods are defined and attached to a struct. The parentheses after the func keyword indicate that there is a "target object" (i.e. the instance) which is implicit in the function call, but other than that has an argument name just like any other (there is no built-in this variable, although there would be nothing stopping you calling the variable here this or self everywhere if you wanted to).

This first makes sure that the submitting user is listed as a Signer, and then uses their private key to Sign the key fields in this block. Note that it is the responsibility of the Transaction code to make sure that all the users sign exactly the same block, although this first version of the code doesn't do that: we'll come back to that in a bit.

Finally, the code submits a JSON version of the Transaction struct to the server. Once again, it feels like the error handling stops me writing the fluid code I would want to; it is not clear to me how to put the tx.JsonReader() in the call to cli.Post because I cannot handle the error return.

internal/api/transaction.go

Once again, the boilerplate for completeness so that you can't say I don't show you everything.
package api

import (
    "bytes"
    "crypto"
    "crypto/rand"
    "crypto/rsa"
    "crypto/sha512"
    "encoding/json"
    "fmt"
    "hash"
    "io"
    "net/url"

    "github.com/gmmapowell/ChainLedger/internal/types"
)
The purpose of the Transaction struct is to manage the lifecycle of a transaction on the client side for submission to a server. It will also be stored on the server side until the server has one copy from each of the signatories. We'll get to that later. Quite a bit later.
type Transaction struct {
    ContentLink *url.URL
    ContentHash hash.Hash
    Signatories []*types.Signatory
}
A transaction consists of three things: the link to the content, ContentLink, the hash of said content ContentHash and the signature block Signatories.

For now, I have made all of these public fields, but I think that just reflects the fact that I am still at the experimental phase: do not be surprised if they are made private at some point in the future.
func NewTransaction(linkStr string, h hash.Hash) (*Transaction, error) {
    link, err := url.Parse(linkStr)
    if err != nil {
        return nil, err
    }

    return &Transaction{ContentLink: link, ContentHash: h, Signatories: make([]*types.Signatory, 0)}, nil
}
Again, we have here a "constructor pattern" where NewTransaction is responsible for taking the expected arguments, validating them, and returning a pointer to the Transaction block.

Note the use here of make to create a 0-length array of Signatory items. We will add the signatories individually below, so for now we don't have any.
func (tx *Transaction) SignerId(signerId string) error {
    signer, err := types.OtherSignerId(signerId)
    return tx.addSigner(signer, err)
}

func (tx *Transaction) Signer(signerURL *url.URL) error {
    signer, err := types.OtherSignerURL(signerURL)
    return tx.addSigner(signer, err)
}

func (tx *Transaction) addSigner(signer *types.Signatory, err error) error {
    if err != nil {
        return err
    }
    tx.Signatories = append(tx.Signatories, signer)
    return nil
}

Each of the signers for the transaction needs to be identified. During submission, the Signer method is called with the submitter's URL. All the others need to be explicited added, as happens in main.

The code here has two possible paths, with either with a string or a URL being passed in. The appropriate method in the types module (see below) is called to obtain a Signatory struct, and then the common path in addSigner (its name begins with a lower case letter, because it's an internal method) is called to add the Signatory to the Transaction.

The append function extends the array slice to make room for an additional element at the end, and then puts this new Signatory there.
func (tx *Transaction) Sign(signerURL *url.URL, pk *rsa.PrivateKey) error {
    return tx.doSign(signerURL, pk, nil)
}

func (tx *Transaction) doSign(signer *url.URL, pk *rsa.PrivateKey, e1 error) error {
    if e1 != nil {
        return e1
    }
    h, e2 := tx.makeSignableHash()
    if e2 != nil {
        return e2
    }
    sign, e3 := makeSignature(pk, h)
    if e3 != nil {
        return e3
    }
    done := false
    for _, signatory := range tx.Signatories {
        if signatory.Signer == signer {
            signatory.Signature = sign
            done = true
            break
        }
    }
    if !done {
        return fmt.Errorf("there is no signatory %v", signer)
    }
    return nil
}

func (tx *Transaction) makeSignableHash() (hash.Hash, error) {
    var h = sha512.New()
    h.Write([]byte("hello, world"))
    return h, nil
}

func makeSignature(pk *rsa.PrivateKey, h hash.Hash) (*types.Signature, error) {
    sum := h.Sum(nil)
    sig, err := rsa.SignPSS(rand.Reader, pk, crypto.SHA512, sum, nil)
    if err != nil {
        return nil, err
    }
    var ret types.Signature = sig
    return &ret, nil
}

This is responsible for doing all of the work to create a signature - and even then it doesn't sign the right thing!

The first method follows the same pattern as the block above in order to allow for a Sign method with just a string, but that method does not exist.

The second method (doSign), first calls makeSignableHash to figure out what to sign, then calls makeSignature to sign that hash. Finally, it scans through all the signatory blocks and finds the one whose signatory is the submitting user. It is, of course, an error for there not to be one, and the fmt.Errorf is a special version of Printf which creates an error object.

The makeSignableHash object is supposed to lay out and organise the contents of the Transaction object into a consistent block, regardless of how it was constructed, and then produce a SHA-512 hash of it. At the moment, it does not do this, but rather hashes "hello, world". We'll come back to that later.

Finally, makeSignature takes a private key and the hash, and generates the Sum of the hash and calls SignPSS to sign it. This is all very easy code to write, although I'm not 100% convinced I actually understand what I've done. At some point we will write code on the server side to verify the signatures. If it turns out that we encounter some problems, we will come back and revisit it.

Signature is my own internal type (we'll see it below) and it is for all intents and purposes "the same as" []byte, which is what we get back from SignPSS. However, I cannot directly return the address of sig but have to first pass it to an explicitly typed variable ret before I can take the address. I am sure that there are good reasons for this (that probably involve the word 'contravariance') but I do not know what they are yet.
func (tx *Transaction) JsonReader() (io.Reader, error) {
    json, err := json.Marshal(tx)
    if err != nil {
        return nil, err
    }
    return bytes.NewReader(json), nil
}

func (tx *Transaction) String() string {
    return fmt.Sprintf("Tx[%s]", tx.ContentLink)
}
Finally we have a couple of methods to return a Transaction in a readable format. The JsonReader function simply marshals the transaction to JSON, which is very nicely handled in Go.

Meanwhile the String function is just like a toString method in Java or JavaScript: for any given object, it will convert the contents to a string on demand. This behaviour is defined by the Stringer interface, which Transaction implicitly implements by providing the String method. Clever.

internal/client/repo.go

The client repository is a placeholder for a configurable information store which is intended to hold all of the information needed by the client. For now, we are just using it as a place to hide the public/private key for a user.

We start with the boilerplate:
package client

import (
    "crypto/rand"
    "crypto/rsa"
    "fmt"
    "net/url"
)
Because we will want to have many different ways of collecting the data, we are going to define ClientRepository as an interface:
type ClientRepository interface {
    PrivateKey(user *url.URL) (*rsa.PrivateKey, error)
}
For now, this interface is very simple, just allowing us to ask for the private key of a given user URL.

Internally, we are going to store records about all the users in the system, which are in ClientInfo structs. As I write this, I wonder if the name might want to change to UserInfo. Don't be surprised if it does at some point.
type ClientInfo struct {
    user       *url.URL
    privateKey *rsa.PrivateKey
    publicKey  *rsa.PublicKey
}
Each user has three fields associated with them for now - their unique URL id, their private key and their public key. I am storing all three of these, even though for now, we will only use the private key.
type MemoryClientRepository struct {
    clients map[url.URL]*ClientInfo
}

func MakeMemoryRepo() (ClientRepository, error) {
    mcr := MemoryClientRepository{clients: make(map[url.URL]*ClientInfo)}
    mcr.NewUser("https://user1.com/")
    return mcr, nil
}

MemoryClientRepository is a concrete implementation of ClientRepository and is one that is intended for "toy" use: testing or demonstration purposes. We will come back and build better things as we need them. This basically stores in memory a map of URLs to ClientInfo records. The MakeMemoryRepo function is a constructor of sorts, but it also initializes the repository with our chosen user.

Note that the key of the map here is a url.URL not a *url.URL. That was my original implementation, but it did not work. Why not? Because the keys in a map have to match by equality, and if you use pointers, they don't. I'm not entirely sure how equality works in Go (yet), but it is reasonable that you say that two pointers are equal if they are the same pointer, but that two URLs are the same if they have the same "value", i.e. represent the same address. That's what seems to happen here.
func (cr MemoryClientRepository) PrivateKey(user *url.URL) (pk *rsa.PrivateKey, e error) {
    entry := cr.clients[*user]
    if entry == nil {
        e = fmt.Errorf("there is no user %s", user.String())
    } else {
        pk = entry.privateKey
    }
    return
}

This method recovers the private key for a given user by accessing the map for the URL passed in. Again, note that we are using *user as the key, since we are given a pointer. This still seems weird in my head, but I'm sure I'll wrap my head around it in the end.

If we don't find an entry in the map, we will return an error; otherwise we extract the private key. This assumes that there is a private key associated with the user; we should probably (and probably will at some point) check if the user has a private key and return an error if they do not. Of course, we could say that returning the pair nil, nil means that we did find the user, but they didn't have a private key - test for that case! It depends on how we want to code things.
func (cr *MemoryClientRepository) NewUser(user string) error {
    u, e1 := url.Parse(user)
    if e1 != nil {
        return e1
    }
    if cr.clients[*u] != nil {
        return fmt.Errorf("user %s already exists in the repo", user)
    }
    pk, e2 := rsa.GenerateKey(rand.Reader, 2048)
    if e2 != nil {
        return e2
    }
    cr.clients[*u] = &ClientInfo{user: u, privateKey: pk, publicKey: &pk.PublicKey}
    return nil
}
The final method is the one that adds a new user to the repository. Since it takes a string url, it must first parse that (and returns an error if it does not parse correctly). We check that this is not a duplicate user, because that is kind of the definition of a "new" user. Then we generate a key pair and store the new triple of user, public key and private key in the list of clients.

internal/types/signatory.go

The signatory type is just a struct combining the URL id of a signer and their (optional) signature.

We start with the usual boilerplate.
package types

import (
    "net/url"
)
The actual signatory struct is very simple.
type Signatory struct {
    Signer    *url.URL
    Signature *Signature
}

func OtherSignerURL(u *url.URL) (*Signatory, error) {
    return &Signatory{Signer: u}, nil
}

func OtherSignerId(id string) (*Signatory, error) {
    u, err := url.Parse(id)
    if err != nil {
        return nil, err
    }
    return OtherSignerURL(u)
}
And then there are two constructor methods, one that takes a URL signer id and one that parses it from a string. In both cases, the Signature is initally left blank.

internal/types/signature.go

This final client file is not that complicated. It basically amounts to a type alias.
package types

type Signature []byte
This just says that I can write types.Signature anywhere and it is basically the same as writing []byte, although, as we saw when we used it, that is not true when using the & operator.

The Server

Moving on from the client side, we are going to have a server which initially doesn't do very much. It starts up and makes itself available for clients to submit transactions. It reports that it has done so.

cmd/chainledger/main.go

As with the client, we put the main() function in a file called main.go in the directory cmd/chainledger which causes a binary chainledger to be produced with go build.

It starts with the usual boilerplate:
package main

import (
    "errors"
    "fmt"
    "log"
    "net/http"

    "github.com/gmmapowell/ChainLedger/internal/clienthandler"
)
and then has the main() function which depends heavily on code included from internal:
func main() {
    log.Println("starting chainledger")
    storeRecord := clienthandler.NewRecordStorage()
    cliapi := http.NewServeMux()
    cliapi.Handle("/store", storeRecord)
    err := http.ListenAndServe(":5001", cliapi)
    if err != nil && !errors.Is(err, http.ErrServerClosed) {
        fmt.Printf("error starting server: %s\n", err)
    }
}
Because this is a server, I will be very inclined to log everything that happens. So, we start by acknowledging that we have started.

The NewRecordStorage class is the class which will be responsible for handling the receipt and processing of a transaction coming from the client. I'm not entirely sold on the name at this point, but the whole thing is really just a placeholder at the moment, so I'll probably change it when I feel the need.

The rest of the code just sets up a web server. cliapi is a http.ServeMux, which I think is what you need in order to have different services listen on different ports (but I'm not sure). There is something to be said for the fact that this is a case of "YAGNI", but since I believe I am more likely to need it (and forget that it exists and waste time trying to figure out what's going wrong) that I am to not need it, I have put it there.

This ServeMux then takes the Handle method to associate a web path with a handler (which must implement the ServerHTTP method in order to be an http.Handler); this is what the RecordStorage class does.

The ListenAndServe method opens up a socket to listen on the named port and dispatches any incoming requests to the identified muxer. It's important to note that this method blocks dispatching the incoming messages, so it will be important to call this method in a goroutine if we want to have multiple running concurrently.

The final couple of lines deal with error cases, the most likely of which is that we already have the server running on the identified port.

internal/clienthandler/recordstorage.go

This is the handler for the /store web path.

The usual boilerplate gets us going:
package clienthandler

import (
    "log"
    "net/http"
)
We need to declare a struct and have a constructor for it.
type RecordStorage struct {
}

func NewRecordStorage() RecordStorage {
    return RecordStorage{}
}

This is all very simple, mainly because it is a dummy implementation at the moment. We will come back to it next time when we start doing some serious implementation.
// ServeHTTP implements http.Handler.
func (r RecordStorage) ServeHTTP(http.ResponseWriter, *http.Request) {
    log.Println("asked to store record")
}
The one thing I did manage to get the VSCode plugin to "Quick Fix" for me was declaring RecordStorage and then using it where an http.Handler was needed. It told me it didn't have this method and implemented it for me (including adding the comment). I then added the one line "implementation" which is basically logging the fact that we have received a request to store a record, the said request being a JSON object in the body of the request. Again, we will come back to this next time.

internal/records/storedtransaction.go

Start with the boilerplate:
package records

import (
    "hash"
    "net/url"

    "github.com/gmmapowell/ChainLedger/internal/types"
)
At the moment, this is a bit speculative, because it's not used anywhere, but it feels so key to me in terms of everything we are trying to accomplish that I couldn't imagine not defining it early. Apologies to all the YAGNI folks, but I am going to need it, and I want it there early. Having said that, I believe more fields will ultimately be added to this (in particular the node will sign it), so I'm certainly not going all waterfall on you.
type StoredTransaction struct {
    txid         hash.Hash
    whenReceived types.Timestamp
    contentLink  url.URL
    contentHash  hash.Hash
    signatories  []types.Signatory
}
This is the version of the transaction that we are going to log on the permanent record. In order to make this happen, we need to collect all the copies that are sent across from the clients and match them together. The last three fields obviously match the fields that are in the Transaction object that will be coming over from the client in the body of the request; the other two will be attached when we get around to processing it.

internal/types/timestamp.go

Again, this is a very simple type alias at the moment, although I will probably add more functionality to it later.

The purpose of this is to allow me to store a timestamp using the semantics of JavaScript (milliseconds centred around midnight at the start of Jan 1, 1970 GMT) in a single int64.
package types

type Timestamp int64

Checked In

All of this code is checked in to git at git@github.com:gmmapowell/ChainLedger.git and this version of the code is tagged FIRST_CLIENT_SERVER.

Thursday, December 12, 2024

Getting Started with Go


Normally, I would go slowly through all the setup and getting started with a new programming language, but, while I haven't done anything with Go recently (certainly not recently enough to have a compiler on any of my machines), it isn't totally unfamiliar to me and it also seems to be very well documented.

So, in brief:
(I should perhaps clarify that simply buying or borrowing a book is not the same as reading it. That is an ongoing parallel process.)

I didn't bother with any of the tutorials.

Ready to Go

So, with all that being said, I feel ready to get going on this project. I'm going to whip through the first phase (building a client/server web application) because it isn't what I want to focus on this time.

I created a new project structure (this is not under my usual IgnoranceBlog) and created a new git repo for it at:

https://github.com/gmmapowell/ChainLedger

I populated that with a go.mod file:
module github.com/gmmapowell/ChainLedger

go 1.23
The first line is a unique "module identifier" which seems to be the basic git url of most projects. I can't tell if that's just a convention as it is in Java package names (this is a good one in a world that increasingly uses git and github) or whether it really expects that it can find the source code there if it goes looking for it.

The second line indicates the version of go that is used by the project.

And then I created some of the recommended standard directories:
  • cmd/chainledger/ - a directory to hold the main function for the ChainLedger node; this will build to an executable called chainledger;
  • cmd/ledgerclient/ - a directory to hold the main function for the client; this will build to an executable called ledgerclient;
  • internal/ - a directory to hold all the packages we will create to provide the common code.
In line with what seems to be the recommendation, I plan to have just one file - main.go - in each of the cmd directories, and that will only have one function in it - main() - and everything else will be buried somewhere inside internal. If I have understood things correctly, no code that is not in a subdirectory called pkg will be available to any other Go programs. Don't get me wrong - I'm not selfish - but it seems that at this time I am building something for myself and sharing it to an unsuspecting world would be unkind.

Initial Impressions

My initial impressions of Go (this time around) are generally favourable. While I had some issues getting set up and started, they were relatively minor and just reflected inexperience on my part. I think, as much as anything, I was having issues with VSCode.
  • The error annotations in VSCode seem odd; I think I'm struggling to understand when I see something underlined what the problem is. And in particular, I have had issues with how "opinionated" Go is about things like unused variables and imports. So I was trying to understand why the import I'd just added was an error, rather than getting on and writing the next line of code that, by using the import, would fix the error.
  • In the same vein, sometimes VSCode is able to find a module I've referenced and will automatically include the import when I save the file; other times it makes me do it myself. I'm not sure why.
  • I have found interpreting the error messages to be difficult from time to time, and sometimes even had to Google them to get an explanation. If this happens in the future, I will call it out and explain what I discover.
  • I haven't yet found any errors for which the VSCode plugin can offer me a "quick fix". As somebody who thinks that Eclipse can write all my code by just pushing "C-1" frequently enough, this is annoying.
  • I love the way that code formatting is not up for debate and that the plugin reformats your code correctly every time you save. I don't care (that much) about code layout, but consistency is massively important to me. And not having people argue about trivia is priceless.
  • The use of case to indicate whether something is "public" or "private" is simultaneously very clear and very odd to me. I like the fact that you can look at something and know by its name which it is, but it seems somehow understated.
  • The use of error return codes rather than having exception handling feels, contrariwise, very cluttered. I have a lot of lines of boilerplate error-handling code that doesn't seem "normal" to me. I'm looking to see if there is some convention I have missed. On the other hand, I do know that automated exception unwinding is expensive to implement, so I guess it does mean you have more control and the code can be more efficient. Everything is a trade-off.
  • I have been sufficiently long in the Java/JavaScript world that the explicit use of & and * to turn objects to pointers and back again is confusing to me. As are the consequences (such as that if you store pointers in a map you won't find it by using a different pointer to the same "value"). This is just something I will have to get used to again. It's further complicated by the fact that whenever you pass around an interface, there appears to be an extra layer of automatic "pointer-ness" going on that is messing with my head.
  • It is nice, though, that even though it has the "feel" of a C/C++ language, it does have automatic memory allocation and deallocation, and especially that it automatically handles the placing of objects on the stack or heap depending on what happens to them.
  • The "object definition" model is going to take a bit of getting used to. While I am a big fan of "implicit interface declarations", both the declaration and the use feel a little uncomfortable. We'll see how it goes.
I have a lot to learn. Hopefully I can share some of that.

Let's get started writing some code! If you have checked out the git repository above and are reading this as I publish it, you may well find that the code is ahead of the blog. I have tried very hard to use tags to keep track of the history, but I don't always reference the tag in the blog. I'm working towards having that happen more automatically.

Wednesday, December 11, 2024

ChainLedger


I'm going to write a blockchain thingy.

I've been aware of blockchain technology for a long while, and "involved" in some sense since 2014 when I met with the Ethereum founders shortly before they held their token sale that launched the public chain. As a financial tool, and in terms of the technology, I was not impressed, but what did impress me - and the team I was working with at the time - was the ability to have a globally mediated, public record of arbitrary facts. The key benefit here is "trust".

Who can you trust? This is always a very difficult question, and we usually introduce systems with "checks and balances" generally summarized by the Latin tag "Quis custodiet ipsos custodes?" (who watches the watchers?). With blockchain, everything happens out in the open, and we can all watch. In order to commit fraud on the blockchain, all miners must agree to commit the fraud, and even then anybody who is tracking the chain can see that it happened - even if they can't stop it. Essentially, while committing fraud on a public blockchain may just about be technically possible, it isn't feasible. It's a bit like trying to commit bank fraud when your every move is being reported live on Cable TV.

Around the same time in 2014, I was talking to a director of one of the main Wall St banks who was looking at ways to leverage blockchain technology to record contracts between the banks in a way that would allow the fact of the contract having happened to be a matter of public record without actually exposing the details of the contract. In the event of a subsequent dispute about the terms of the contract, the contract could be opened for judicial examination, and would have to match the data stored on the blockchain. In this way, the "trust" of the blockchain can be extended to private documents.

How would that work? Let's consider tax returns as an example. You want to submit a tax return to the government, but while you don't want to make it public, you don't really trust the government. On the other hand, they don't trust you, either. What you can do is sign your tax return with a digital signature, and then they can't fake it. But what if (for whatever reason) you end up submitting multiple versions? They can delete some of them and claim you only submitted the others. It would be good to have a record of what you submitted.

What could happen is that when you submit your return, the government redirects you to a webpage which contains your return. That webpage has a unique URL. You can see the document and validate its hash, check it thoroughly, and then both you and the government computer could sign the same entry on the blockchain, asserting that at this moment, you both see the same thing. Auditors, judges, whoever could later come back and inspect that blockchain and compare the contents of the webpage with the hash. If you submit multiple versions, each submission, along with its date, is right there on the blockchain. Cast-iron security.

So what is stopping us putting (a record of) all our most important documents on a blockchain? In short, performance: bitcoin can manage something like 10tx/s. To handle (say) 100m US tax returns in the month before the filing deadline, you would need something like 40tx/s - without considering peaks in the load. And that's just for one application.

A lot of things in computer science are trade-offs. But there are a handful of hard-and-fast rules, and possibly the most important (and annoying) of them all is the CAP Theorem, which is one of those "choose any 2" rules, which basically says you can have any two of consistency, availability and tolerating network failures. And bitcoin and ethereum are big on consistency - before a block can be mined, it must be agreed what the block is. Understandably enough.

What I am going to do here is choose the other two, and say "consistency, who needs it?" Or, to be more precise, I will work towards consistency, but at any given moment, latency in the system may mean that different nodes see different things. But there should be a point in the past where they all agree on what the situation was.

What we're doing here

OK, enough general background and waffle. Why am I doing this, and specifically, why am I doing this here, on a blog that is supposed to be about things I know nothing about?
  • Well, to be fair, I don't have a deep understanding of existing blockchains, but I don't think I'm going to understand it here.
  • I think I do understand the fundamentals of a blockchain, and I hope to prove that (to myself, if nobody else).
  • This idea has been bouncing around in my head for a decade now, and I think I have it sorted out, but the only way to be sure is to try it.
  • This place is as good a place as any to "work out loud" and I'd be interested in feedback from anyone who's following along.
I'm going to build this in Go, for "all the right reasons": it is fast; it compiles to a lightweight binary; and it is supported natively by AWS, so I can deploy it there. It is also the case that while I have used Go before, I haven't done that much with it, so this will be an experiment, and hopefully interesting to people out there (especially those that haven't used Go).

I have done a lot of work with truly distributed systems, but I'm aware that makes me one of an ever-smaller minority of software professionals. Computer systems have vastly different characteristics when they are:
  • just a simple program or script running in one place;
  • a multi-threaded program or script running in one process;
  • a set of programs that are working together, whether on one box or in a data center;
  • distributed around the world.
I am hoping to talk about that, along with some of the ways in which we can develop a system in Go on a single developer machine which still has all the characteristics of a system we can deploy around the world.

And then I'm hoping to actually deploy it around the world using AWS, and see if it does in fact work.

What am I planning to build?

So the plan is to build a distributed ledger with multiple nodes which can record "transactions" and then encode them all on a blockchain.

The set of nodes is "pre-defined" and each node knows that all of the others exist and can communicate with them.

The goal will be to have a system with 4 nodes deployed to AWS (using Lambda and DynamoDB) around the world and to have the system able to "keep up" with each node processing 1000tx/s.

Each transaction is a simple record which has three parts:
  • A URL which is supposed to point to the "content" of the transaction; ChainLedger is completely agnostic about this, except that it must be a well-formed URL: the URL does not need to point to an actual resource, and there is no requirement that it be readable;
  • A SHA-512 hash of the contents of the transaction document at the time the transaction is submitted; since ChainLedger has no access to the document, it cannot independently verify its accuracy but depends on the signatories all asserting the document to have the same hash;
  • One or more signatories, each of which consists of the id for a (previously registered) user and a signature for that user; the content that the users sign is a concatenation of the content URL, the hash and the user ids of all the signatories in collated order; ChainLedger must have access to the public keys for each of the signatories' signing keys and must assert that the signatures are valid before accepting a transaction.
The chain consists of:
  • Once accepted, each transaction is rehashed (with the signatures included, and with a timestamp when the transaction was accepted); this is the ID of the transaction. The accepting node will then sign this hash as a guarantee none of it can be changed.
  • Each node will divide the transactions it receives into blocks based on time. Each block will contain the ID of the previous block and the IDs of the individual transactions in ascending collated order.
  • Each node will produce a hash of the block and then sign that with its own signing key. The hash is the ID of the block.
  • Each node distributes all of its work to all the other nodes.
  • Each receiving node double-checks the work of all the originating nodes.
  • On a regular interval, each node will produce a summary record of the world as it believed it to be a short time in the past. All the nodes should produce the same summary records for the same set of nodes at the same point. However, in the presence of latency or network partitioning, the nodes may have access to different sets of blocks, in which case the summaries may diverge. Unlike transactions and blocks, which are only created once and exist exactly as they are, multiple versions of summary records may be produced when more data is available. However, no history will ever be updated; all the summaries for a given time will continue to exist but can be distinguished by the nodes whose data they contain.
  • It is an error for two nodes to disagree about a summary if they are working on the same data.
There is a lot to unpack there, but this is just an overview, not a waterfall specification. We will come back to all of this in a lot more detail later, and hopefully show how it is possible to consider - and discover - more error cases for a distributed system using automated, repeatable testing.

A Note on Performance

It is often the case with distributed systems that if you add more nodes, you add more performance because they share the work. In terms of the "nodes" I am discussing here, that is not the case because of the requirement for them to cross-check each others' work and duplicate it. In fact, the more nodes there are, the slower the system will go (or else, the more resources it will use).

However, internally, each node can be distributed in a more conventional way in which the work is shared and performance increases with the scale of an individual node. We will probably look into this in great detail as we progress on the journey.

The plan

I've divided this project into four phases, and hopefully I make it all the way through:
  • The first phase is a quick whip-through building up a client and a server that can build a non-distributed ledger. Inasmuch as we will slow down to catch breath, I will be talking about my experiences of Go, but basically I'm just going to build something out really quickly.
  • The second phase is to actually build a "distributed" system with cross-checking and a blockchain. But it's still all going to be on one machine and just looking at "the happy path".
  • The third phase is going to remain on one machine (and, I hope, just one process) but I'm going to introduce testing elements that enable us to simulate things going wrong, and then to make things go wrong automatically so that we can test that the blockchain is resistant to that.
  • The fourth phase will hopefully be to put some AWS infrastructure in place to deploy this around the world and see if it can actually deliver the desired performance.
Let's go!

Sunday, October 13, 2024

Scrolling through the Route


So far, none of the route information I've seen has actually required scrolling, but I don't think I can rule it out. On the other hand, I'm very tempted to ignore it because while I am used to the idea of scrolling being handled by the UI toolkit, it seems that this is very much not the case here. So if I am going to do it myself, I am left having to hand-roll it.

Normally, I would consider all my options (sorting the data so that the trams appear in order would be one way to make sure the most relevant information does appear on the screen), but given the nature of this blog and what seems to be involved, I think I am just going to go for it.

Up until now, we have been using the UI toolkit with Layout and built-in components (specifically the TextArea). But it would seem it is also possible to draw directly to the device using the Dc passed into onUpdate. This is, obviously, going to be much harder than just saying "here, TextArea, show this string", but I'm up for the challenge if you are, especially since there are a couple of samples out there on the internet.

So let us go back to square one. I'm going to remove the whole of onLayout and comment out the variable textArea and all it's usages. I'm only going to comment these out - rather than remove them - to remind myself of what the logic should be. When everything is wrapped up, I'll delete anything that's left, but in the meantime I suspect I may just end up slipping in an alternate definition of textArea.

So, starting with onUpdate, we can try showing the "Please Wait..." message without using the textArea. We have a drawing context, dc, and this has (among other things) a drawText method.

This conveniently seems to do centering for us, so if we can find the centre of the screen, we can presumably do what we want fairly easily. Fortuitously, there is an example of doing just that earlier on the page, and the dc object has getWidth and getHeight methods.
    if (showWait) {
      // textArea.setText("\n\nPlease Wait.\nLoading Data...\n");
      showWait = false;
      dc.drawText(
        dc.getWidth() / 2,
        dc.getHeight() / 2,
        Graphics.FONT_SMALL,
        "Please Wait.",
        Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER
      );
    }
Sadly, this doesn't seem to work. It's not immediately clear why. But thinking through what is happening here, I'm aware that onUpdate is called in a context, and the drawing context passed in needs to be established and colors set. Let's try something simpler. Let's set the foreground color to be white to be sure, and then let's try and draw a box in the lower-right (3 o'clock to 6 o'clock) portion of the display.
  function onUpdate(dc as Dc) as Void {
    dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT);
    dc.fillRectangle(dc.getWidth()/2, dc.getHeight()/2, dc.getWidth()/2, dc.getHeight()/2);
    if (showWait) {
No, nothing doing. Reviewing the code and thinking about the logic some more, I realize that there is a not-completely-innocous call to View.onUpdate() at the end of my onUpdate. It even has an associated comment that it redraws the screen. Now, up until now I have wanted that at the end of my function so that any updates I may have made to the TextArea are applied before it is redrawn. But now I am not using the layout but redrawing directly, that is probably clearing off the screen the moment I have drawn to it. Let's reverse that, and call it first.
  function onUpdate(dc as Dc) as Void {
    // Call the parent onUpdate function to redraw the layout
    View.onUpdate(dc);
    
    dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT);
    dc.fillRectangle(dc.getWidth()/2, dc.getHeight()/2, dc.getWidth()/2, dc.getHeight()/2);
Well, at least we get the box. Reviewing the code again, I notice that there is a showWait variable that I've used to make sure that we don't show the "waiting..." message once we have data; but if we come through onUpdate more than once, we will clear off the message. Removing the assignment to false, the code magically works.
    dc.fillRectangle(dc.getWidth()/2, dc.getHeight()/2, dc.getWidth()/2, dc.getHeight()/2);
    if (showWait) {
      // textArea.setText("\n\nPlease Wait.\nLoading Data...\n");
      // showWait = false;
      dc.drawText(
        dc.getWidth() / 2,
        dc.getHeight() / 2,

Let's Throw All That Away

I've checked all that in on a branch (the HEAD is tagged $METROLINK_MC_ALWAYS_SHOW), but only so that I can have it there to show on this blog. I'm going to throw it all away - I went down a blind alley.

Admittedly, I did what I did for good reasons - I could not scroll a text area, so I needed to replace it with something else. It seemed the simplest thing was to draw directly on the screen, but actually that isn't so simple. To carry on from where I was, I would need to figure out how to persist the message to be displayed so that I could keep onUpdate reasonably simple. And that would ultimately lead to another abstraction. Without even doing it, I can see that I would end up reimplementing the whole of the layout mechanism. Why?

The motivator, of course, is that I can't use (as far as I can tell) my own components with the XML layout compiler in Monkey C. But just because I can't use their compiler doesn't mean I can't use my own layout. The compiler in fact generates very simple code. This is to be found in $bin/gen/.../source:
module Rez {
    module Drawables {
      ...
    } // Drawables


    module Layouts {
        ...
        function RouteLayout(dc as Graphics.Dc) as Array<WatchUi.Drawable> {
            var rez_cmp_local_textarea_routeInfo = new WatchUi.TextArea({:identifier=>"routeInfo", :width=>240, :text=>"", :justification=>Gfx.TEXT_JUSTIFY_CENTER, :height=>240, :font=>[Graphics.FONT_MEDIUM] as Array<Graphics.FontType>});


            return [rez_cmp_local_textarea_routeInfo] as Array<WatchUi.Drawable>;
        }
    } // Layouts


    module Strings {
        ...
    } // Strings
} // Rez
And let's face it, that's really quite a simple piece of code. It's basically just creating and initializing a text area and packaging it up in an array. I could do that myself.

So winding back to where we were, I'm going to extract that code and put the line that creates the TextArea in my intialize constructor and put the array line in onLayout and check that everything still works without the XML file being involved.
  function initialize(routes as Array<Route>) {
    self.routes = routes;
    self.textArea = new WatchUi.TextArea({:identifier=>"routeInfo", :width=>240, :text=>"", :justification=>Graphics.TEXT_JUSTIFY_CENTER, :height=>240, :font=>[Graphics.FONT_MEDIUM] as Array<Graphics.FontType>});
    View.initialize();
  }

  function onLayout(dc as Dc) as Void {
    setLayout([self.textArea] as Array<WatchUi.Drawable>);
  }
Unsurprisingly, that now works. So I want to replace the concept of a TextArea with a ScrollArea which is a component which knows how to draw itself and works in every way like a TextArea except it also has methods to scrollUp and scrollDown when we swipe up and down.

ScrollArea

Looking at the manual page for TextArea, I can see that it inherits from WatchUi.Drawable, so I know that I want to do the same. So let's create a skeleton class for ScrollArea and use that instead of TextArea in our view:
import Toybox.WatchUi;
import Toybox.Lang;

class ScrollArea extends Drawable {
  var tx as String?;

  function initialize(options) {
    Drawable.initialize(options);
  }

  function setText(tx as String) {
    self.tx = tx;
  }
}
And that "works" as long as you don't want the text to display.

In order to have the text display, it is necessary to add a draw method to the ScrollArea class, and the simplest implementation of that is just to repeat what we did above directly in the view's onUpdate method. As we learnt before, if you want to be able to read the text, you need to specify a drawing color.

Either unsurprisingly, or amazingly (depending on your general optimism level), that's all we need to do to replace TextArea: the drawText method automatically handles newline processing and centering.
  function draw(dc as Dc) {
    System.println("in draw with " + tx);
    if (tx != null) {
      dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT);
      dc.drawText(
        dc.getWidth() / 2,
        dc.getHeight() / 2,
        Graphics.FONT_SMALL,
        tx,
        Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER
      );
    }
  }
So all we need to do now is enable this to scroll.

We already have an InputDelegate listening for these events, so we just wire up the SWIPE_UP and SWIPE_DOWN events, redirecting them to the view. The view, in turn, handles these events by passing them off to the ScrollArea and then requesting the view to be refreshed. The events are handled in the ScrollArea by either incrementing or decrementing a (scroll) offset, and then the y position in draw is determined by subtracting a quarter of the screen height for every unit of offset.

Everything here is about coordinates and directions: SWIPE_UP and SWIPE_DOWN act in the opposite manner to which I would expect, so I translate SWIPE_UP to scrollDown. This increments offset, because we want to be further down the scroll, but we achieve that by saying we start drawing the (center of) the scroll further up the screen. It might be clearer to not use the word "scroll" at all and connect SWIPE_UP to viewUp which decrements offset, which can then be added to the y value. Or not.

In the navigation handler, we have:
    case SWIPE_UP: {
      view.scrollDown();
      break;
    }
    case SWIPE_DOWN: {
      view.scrollUp();
      break;
    }
In the view, we add:
  function scrollUp() {
    textArea.scrollUp();
    WatchUi.requestUpdate();
  }

  function scrollDown() {
    textArea.scrollDown();
    WatchUi.requestUpdate();
  }
We add these methods in the ScrollArea:
  function scrollDown() {
    offset ++;
  }

  function scrollUp() {
    offset --;
  }
}
And then wrap it all up in draw:
  function draw(dc as Dc) {
      dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT);
      dc.drawText(
        dc.getWidth() / 2,
        dc.getHeight() / 2 - (dc.getHeight() * self.offset)/4,
        Graphics.FONT_SMALL,
I feel that more should be done here - specifically, there should be some kind of "bounds" within which the offset should be kept, but, to be quite honest, I don't care enough. If you do, add it. If I do later, I'll add it. Frankly, there are a bunch more things I'd like to add - but even so, I'm not going to right now. For now, I need to get this blog published :-)

So I checked that in and tagged it as METROLINK_MC_SCROLL_AREA.