Reasons for Rell - Compactness

Reasons for Rell - Compactness

When I first looked at Tendermint ~4 years ago (I seriously considered using Tendermint to implement consensus in Postchain), I was not particularly impressed by its programming model -- the example code in Go looked overly verbose.

Tendermint has been treading a similar path to us, they have scaled their private blockchain into a public one called Cosmos, just as we are doing with Postchain and Chromia. So today I had a look at the latest docs for the Cosmos SDK to see what has changed.

The docs look really nice, and they have created a really nice system for building applications. It's nice that the application can be built in a modular fashion using pre-made Cosmos SDK modules, however the verbosity is still there...

Let's look at the code needed to implement a single SetName operation in Cosmos. This operation simply checks that transaction's signer is the owner of the name, and then changes value associated with the name. This should be simple, right? It's probably the simplest thing you can do with blockchain. Let's first look at how one would implement it in Rell:

entity name_entry {
    name; // name of the entry
    owner: pubkey;  // pubkey that can change the value
    mutable value: text;  //  value stored
}

operation set_name_value (name, value: text) {
    val entry = name_entry @ { name }; // find entry by a key (name)
    require(is_signer(entry.owner)); // check if right signer
    entry.value = value; // update the value
}

Even if you don't know anything about Rell, it should be easy to follow -- we first find an entry by a key (name), then check if owner of the entry is the signer of a transaction which includes this operation call, and then we update the value part of the entry. This kind of stuff is also very easy to code in Solidity, the code would be roughly the same.

Now look at the Cosmos tutorial code:

const RouterKey = ModuleName // this was defined in your key.go file 

// MsgSetName defines a SetName message

type MsgSetName struct {
    Name string          json:"name"
    Value string         json:"value"
    Owner sdk.AccAddress json:"owner"
}

// NewMsgSetName is a constructor function for MsgSetName
func NewMsgSetName(name string, value string, owner sdk.AccAddress) MsgSetName {
    return MsgSetName{
        Name:  name,
        Value: value,
        Owner: owner,
    }
}

// Route should return the name of the module
func (msg MsgSetName) Route() string { return RouterKey }

// Type should return the action
func (msg MsgSetName) Type() string { return "set_name" }

// ValidateBasic runs stateless checks on the message
func (msg MsgSetName) ValidateBasic() sdk.Error {
    if msg.Owner.Empty() {
        return sdk.ErrInvalidAddress(msg.Owner.String())
    }
    if len(msg.Name) == 0 || len(msg.Value) == 0 {
        return sdk.ErrUnknownRequest("Name and/or Value cannot be empty")
    }
    return nil
}

// GetSignBytes encodes the message for signing
func (msg MsgSetName) GetSignBytes() []byte {
    return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg))
}

// GetSigners defines whose signature is required
func (msg MsgSetName) GetSigners() []sdk.AccAddress {
    return []sdk.AccAddress{msg.Owner}
}

// x/nameservice/handler.go

// NewHandler returns a handler for "nameservice" type messages.
func NewHandler(keeper Keeper) sdk.Handler {
    return func(ctx sdk.Context, msg sdk.Msg) sdk.Result {
        switch msg := msg.(type) {
        case MsgSetName:
            return handleMsgSetName(ctx, keeper, msg)
        default:
            errMsg := fmt.Sprintf("Unrecognized nameservice Msg type: %v", msg.Type())
            return sdk.ErrUnknownRequest(errMsg).Result()
        }
    }
}

// Handle a message to set name
func handleMsgSetName(ctx sdk.Context, keeper Keeper, msg MsgSetName) sdk.Result {
    if !msg.Owner.Equals(keeper.GetOwner(ctx, msg.Name)) { // Checks if the the msg sender is the same as the current owner
    return sdk.ErrUnauthorized("Incorrect Owner").Result() // If not, throw an error
    }
    keeper.SetName(ctx, msg.Name, msg.Value) // If so, set the name to the value specified in the msg.
    return sdk.Result{}                      // return
}

So what is going on here: Everything which Rell does implicitly - defining data format for an operation, serialization, dispatch, etc. - needs to be written manually in Cosmos-based Go code.


People often ask why we made Rell. This is a simple illustration: to allow a programmer to write stuff in a simple way, 4 lines of code instead of 37. That's almost a factor of ten. And this is a trivial operation which fits well into the KV store provided by Cosmos, for an application which requires a more complex data model the difference would be even bigger.