Adapting the Code - Writing Fabric Code

Hyperledger Fabric does not use the term "smart contract", but instead "chaincode".

I prefer this term for two reasons:

  1. Chaincode sounds way cooler.

  2. "Smart" is an adjective and code is not inherently smart or stupid; developers may do something smart or stupid when writing the code. Imbuing code with traits like "smart" is incorrect and leads to a false sense of security.

Getting Started

My previous team and I built an entire SDK to soften up some of the sharp edges in the TypeScript Fabric SDK and make it faster to use.

It's interesting starting from scratch again...

We could just start hacking away at the code but this would result in lots of duplicated code. We now have a handle on our data, and we want to treat our blockchain like a database. We could create some sort of OR/M to do this, or we could created some CRUD methods. In fact, the SDKs for both TypeScript and Go come with GetState() and PutState() functions. Personally, I like to add a bit more structure around my objects and functions so I can lean on the IDE and language to make my life easier.

Defining Keys

This feels a bit like Databases 101, but bear with me.

There are two ways we can key our data:

Each comes with most of the advantages and disadvantages describes in those articles, but there are two important Fabric-specific considerations:

Composite Keys & Querying

Composite keys being created from properties on the object we are storing are basically surrogate keys. The additional benefit here is if we organize the components properly then we can support querying the keys by a range which will improve performance and reduce key conflicts.

Key Collisions

Generating a unique key, like from the transaction ID, for an object is a surrogate key. Creating these unique keys helps us avoid key collisions but may require additional queries to get to our data again later.

Generating Keys

Whichever way we choose to generate the keys, it sure would be nice if we had a consistent way to generate them and fetch them from the "database". 

The language you're writing in will have different best practices for how to accomplish this. With Go, the recommended approach seems to be interfaces:

type ChainObject interface {
	// Returns the chain key + ID components. Chain type should be the first string
	GetIdParts() []string
}

And then on each struct (because Go thinks it doesn't have classes) we implement this interface and a helper function:

func CreateDungeonIdParts(owner string, name string) []string {
	return []string{KEY_DUNGEON, owner, name}
}

func (dungeon Dungeon) GetIdParts() []string {
	return CreateDungeonIdParts(string(dungeon.Owner), dungeon.Name)
}

Using Keys

From here, we can create some CRUD-y helper methods that handle these ChainObject types:

  • GetChainKeyByParts
    • A helper function to create a chain key by parts. This is in the contract because it depends on the ChaincodeStubInterface ; I don't like creating this dependency in the objects themselves and I don't have a robust enough code base to re-implement it myself (yet).
  • GetChainKeyByObject
    • A helper function to create a chain key from a reference object. Similar to the ...ByParts version, except this requires the caller to construct an object with the key properties set already. Both methods require the caller to know which key objects to pass, but this version lets the reference object be re-used as the fetched object returned from the "database".
  • ReadChainObjectByParts
    • Takes ID parts and a reference object then attempts to fetch it from chain.
  • ReadChainObjectByRef
    • Takes a reference object and attempts to fetch it from chain.
  • DeleteChainObject
    • Deletes the chain object with the provided key
  • WriteChainObject
    • Creates or Updates (because that's how Fabric works natively) the value for the given key.

Writing Functions

Now we have our building blocks:

  • We can define objects we want to write to chain
  • These objects can generate their keys
  • We have CRUD helper functions to make interacting with them easier

This will make it easier to implement some real logic: contract functions. These functions are basically the triggers and constraints in our "database".

Visibility

Just like in regular development, we need to be careful of which functions can be accessed externally. The difference is that the stakes are much higher. If you accidentally make a private function public in an SDK you build, it might allow 3rd party developers to have access to things you did not intend them to, and that may cause unexpected bugs.

In smart contracts, leaving a function like UpdateBalance(Address, NewBalance) publicly visible would have catastrophic impacts. The UpdateBalance function should be private and should only be callable by functions which enforce the business logic; like a TransferBalance(Address, Amount) function which performs all the necessary checks before calling UpdateBalance.

Even in permissioned networks it's essential to prevent bad actors as much as possible.

In addition to the contract-level concerns, which always exist, we also need to think about how we are exposing these functions. For this side quest I'm using Hyperledger Firefly which exposes contract calls through a WebAPI based on the definition provided.

Again, it's not enough to just exclude the function from what we define in FireFly to keep things secure. For example:

  1. External bad actors could potentially side-step our intended execution path (FireFly) and invoke the function directly on the contract
  2. Internal bad actors could call the functions directly
  3. If we leave them insecure now we may forget them later as we decentralize, causing the same problem as #1

Verbosity

Yet another typical development decision. Do we make a single external function like GetObject(Key) and expect the caller to know how to create the on-chain key and convert the returned byte array to the proper type? Or do we want to make sure that logic remains consistent, and allow calls to be clear like GetPlayerProfile(Args...)?

My preference is to have most of the logic built into a generic function like GetObject and build wrapper functions like GetPlayerProfile around it. Some languages support generics very well and it will let the IDE do a lot of work for you (like TypeScript or C#), and other languages don't support it as well (like Go) and you'll lose some of the help the IDE can give you.

Here are some advantages to the more verbose functions:

  • FireFly can easily expose the functions, which limits repetition
  • Internal contract calls and External FireFly calls get better typing
  • When we need to have special logic, like who can fetch objects and who can make certain calls, we will need to either make separate functions for that or embed it all into GetObject. Making the functions up front saves us time later and makes us less likely to make our GetObject function disgustingly complex before we finally refactor it.

Validity

Most of all, we need to know the result of calling a function. This is one of the biggest differences I have noticed between the Go and TypeScript SDKs for Fabric.

The TypeScript SDK doesn't do a good job at letting a function fail and return a meaningful response - it only allows throwing an exception. This is unfortunate because it forces either a lot of re-work, or some bad code patterns.

The Go Fabric SDK does a better job by using what I'll call the "Comma Error" idiom. Go supports multiple return values and the Fabric SDK utilizes this with functions able to return two values like: Value, Error. This allows the caller to check the Error before proceeding with using the value.

Even though Go functions support an arbitrary number of returns from a function, the Go Fabric SDK supports a maximum of two from publicly available functions. I learned this the hard way and had to rewrite some functions calls - it wasn't fun.

Example Function

This is way too many words for me to not at least show some code. You can see all of the functions in the repo, but here's an example:

func (s *SmartContract) NewPlayerProfile(ctx contractapi.TransactionContextInterface,
    name string, externalAddress string, signature string) (*chainTypes.PlayerProfile, error) {

    // TODO: Check that the externalAddress is valid

    _, _, err := getPlayerProfile(ctx, externalAddress)

    if err == nil {
        return nil, errors.New(fmt.Sprintf("Player Profile already exists for %s\n%s", externalAddress, err.Error()))
    }

    _, _, err = getPlayerVault(ctx, externalAddress)

    if err == nil {
        return nil, errors.New(fmt.Sprintf("Player Vault already exists for %s\n%s", externalAddress, err.Error()))
    }

    // Construct the player we want to write
    newPlayerProfile, err := chainTypes.NewPlayerProfile(name, externalAddress, signature)

    if err != nil {
        return nil, err
    }

    newPlayerVault, err := chainTypes.NewPlayerVault(externalAddress)

    if err != nil {
        return nil, err
    }

    writeObjects := []chainTypes.ChainObject{newPlayerProfile, newPlayerVault}

    // We've confirmed both the profile and wallet do not exist, now we write to chain
    for _, o := range writeObjects {
        err = WriteChainObject(ctx, o)
        if err != nil {
            return nil, err
        }
    }

    return newPlayerProfile, nil
}

Code Repository

Coming soon...

Comments