Your task for this project to build a working distributed application on Hyperledger Sawtooth, a general-purpose enterprise blockchain. This app will allow users to collect, breed, and trade collectible kaomoji: strings of characters that look like faces, such as (ಠ_ಠ), ( ͡° ͜ʖ ͡°), and ᕕ( ᐛ )ᕗ.
Powering your cryptomoji will be a number of components which run in
individual "containers" using
Docker.
After cloning this repo and installing Node, follow the
instructions for your OS to set up the tools that you need to run
docker
and docker-compose
from your command line.
- Using Docker
- The Curriculum
- The Project
- The Design
- Extra Credit
Docker is a virtualization tool that makes it easy to deploy code in a variety of environments. Dockerfiles create "images" with all the dependencies your code needs installed. From these images, you can create, run, and destroy individual "containers" at will. This is, in effect, like having every component running on its own virtual computer which you can reset to factory settings at any time. It's a profoundly useful tool that takes a little getting used to, but is worth getting familiar with.
This part-two
directory includes a docker-compose file
that contains the instructions for Docker to start up multiple components
and network them together. This includes both custom components built from your
source code and some prepackaged Sawtooth components downloaded from
DockerHub:
Name | Endpoint | Source | Description |
---|---|---|---|
validator | tcp://localhost:4004 | DockerHub | Validates blocks and transactions |
rest-api | http://localhost:8008 | DockerHub | Provides blockchain via HTTP/JSON |
processor | -- | custom | The core smart contract logic for your app |
client | http://localhost:3000 | custom | Your front end, served from client/public/ |
shell | -- | DockerHub | Environment for running Sawtooth commands |
settings-tp | -- | DockerHub | Built-in Sawtooth transaction processor |
First, you will use the docker-compose up
command to start all components.
Note that this might take as long as 30 minutes the first time you run it
(later runs will be much faster; more like 30 seconds). If you are in the same
directory as the docker-compose.yaml
file, the up
command will find the file
by default, so you won't need to provide any other parameters:
cd code/part-two/
docker-compose up
This builds and starts everything defined in your compose file. Once
all the components are running, you can then stop it all with the keyboard
shortcut ctrl-C
. If you want to stop and destroy every container (they will
be rebuilt on your next up
), use this command:
docker-compose down -v
That is really all you need. These commands are the start/stop buttons for your app. You can develop just fine by starting things up, making some changes, tearing everything down, and then starting it all up again. However, if you want fine-grained control, you can leave everything else running but stop, start, or restart individual components using their container name listed in the table above. For example, with your transaction processor, you could open a new terminal window and run one of these commands:
docker stop processor
docker start processor
docker restart processor
Sawtooth has in-depth documentation covering many aspects of the platform. It can be a little overwhelming at first! For the new distributed app developer working with Javascript, start with these documents:
- Sawtooth Introduction
- Architecture
- The Javascript SDK
In addition to reviewing the official Sawtooth documentation, you will watch a lecture which discusses permissioned blockchains, Sawtooth, app development, and the design for Cryptomoji in detail.
When building an application on Sawtooth, much of the nitty-gritty of running and validating a blockchain is handled for you. You won't have to verify signatures and hashes, validate blocks, or confirm consensus. Instead, you must consider how you can break up the functionality of your application into discrete transaction payloads, and how these payloads will alter state data. The typical Sawtooth workflow looks something like this:
- The user initializes some action (i.e., "set 'a' to 1")
- The client takes that action and:
- encodes it in a payload (maybe simply
'{"a": 1}'
) - wraps that payload in a signed transaction and batch
- submits it to the validator
- encodes it in a payload (maybe simply
- The validator confirms the transaction and batch are valid
- The transaction processor receives the payload and:
- decodes it
- verifies it is a valid action (i.e., 'a' can be set to 1)
- modifies state in a way that satisfies the action
(perhaps the address ...000000a becomes
1
)
- Later, the client might read that state, and decode it for display
So, you are responsible only for building two components, the client and the transaction processor, and for keeping those components in agreement on how to encode payloads and state. This application-wide logic is typically referred to in Sawtooth with the term "transaction family".
For Cryptomoji, the client and processor are each in their own directory, with
their own tests and their own READMEs. After completing the basic utilities in
the services/
directories, you should develop the client and processor in
parallel on a feature-by-feature basis. For example, implement collection
creation on both the client and the processor before moving on to sire
selection on either.
For the broad transaction family design, continue reading below.
Directory: client/
README: client/README.md
Tests: client/tests/
A React/Webpack UI which allows users to create collections of cryptomoji, breed them, and (in the extra credit) trade with other users.
Directory: processor/
README: processor/README.md
Tests: processor/tests/
A Node.js process which validates payloads sent from the client, writing data permanently to the blockchain.
There are a few basic questions you need to answer when designing your transaction family:
- What do my transaction payloads look like?
- What does data stored in state look like?
- What addresses in state is that data stored under?
Let's answer these questions for Cryptomoji.
For simplicity and familiarity, we are going to encode both payloads and state data as JSON. To be clear, JSON would not be a great choice in production. It is not space efficient; worse, it is not deterministic. Determinism is very important when writing state to the blockchain. Many many nodes will attempt to the write the same state. If that state is even slightly different (like, say, the keys are in a different order), the validators will think the transactions are invalid.
But JSON is easy and accessible, especially in Javascript. And it is possible to create sorted JSON strings, which should solve the determinism issue (at least, well enough for your purposes). Of course, JSON itself isn't quite enough, because we need raw bytes, not a string. For byte encoding, we are going to use Node's Buffers again.
So your encoding should look something like this:
Buffer.from(JSON.stringify(dataObj, getSortedKeys(dataObj)))
In this case getSortedKeys
is a helper function you would write yourself,
which would return the names of an object's properties sorted. Decoding would
be much simpler:
JSON.parse(dataBytes.toString())
Over the first two days you will implement various utilities relating to signing and encoding, as well as build out the core behavior of your app: collections, moji, and breeding. The design for the state entities and transaction payloads to support this behavior is below.
In order to make the Cryptomoji app work, your transaction processor must write several entities to state.
{
"dna": "<hex string>",
"owner": "<string, public key>",
"breeder": "<string, moji address>",
"sire": "<string, moji address>",
"bred": [ "<strings, moji addresses>" ],
"sired": [ "<strings, moji addresses>" ]
}
Cryptomoji are unique, breedable critters. Each has a DNA string of 36 hex characters, which is converted into an adorable kaomoji for display (using a parsing tool that is included with the client). Aside from storing the identity of the owner, the string will also include breeding information: the identities of the parents (breeder and sire), as well as the identities of any children produced, either in the role of a breeder or a sire (bred/sired).
{
"key": "<string, public key>",
"moji": [ "<strings, moji addresses>" ]
}
A collection of cryptomoji owned by a public key. Each new collection will be created with three new "generation 0" cryptomoji with no breeders or sires.
{
"owner": "<string, public key>",
"sire": "<string, moji address>"
}
Each collection can select one cryptomoji as a sire. This makes the sire publicly available for other collections to use for breeding.
A state address in Sawtooth is 35 bytes long, typically expressed as 70 hexadecimal characters. By convention, the first six characters are reserved for a namespace for the transaction family, allowing many families to coexist on the same blockchain. The remaining 64 characters are up to each family to define.
We will follow this convention with a six-character namespace 5f4d76
, which is
generated from the first six characters of a SHA-512 hash of the family name
“cryptomoji”. Each state entity will have their own scheme for how they use
the remaining 64 characters of their address.
Namespace (6) | Type prefix (2) | Identifier hash (62) |
---|---|---|
5f4d76 |
00 |
1b96dbb5322e410816dd41d93571801e751a4f0cc455d8bd58f5f8ad3d67cb |
A collection will be stored first under a one-byte type prefix: 00
. The
remaining 62 characters are the first 62 characters of a SHA-512 hash of its
public key.
For example, the hash used to create the address above might be generated like this:
createHash('sha512').update('034f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa').digest('hex');
// 1b96dbb5322e410816dd41d93571801e751a4f0cc455d8bd58f5f8ad3d67cbee2e9e5c5df5bb65c282f1aaf516cf7cc5a2f7ff592a80cf920e1abaab8d29279f
Namespace (6) | Type prefix (2) | Collection prefix (8) | Identifier hash (54) |
---|---|---|---|
5f4d76 |
01 |
1b96dbb5 |
0c8514ab2a7cf361062601716bcd762097e41f9011a5e6f8ff6c5f |
Cryptomoji need to be divided up by who owns them. That way, you can query a
partial address to get all of the cryptomoji owned by a public key. So, after a
type prefix of 01
, the next eight characters of a cryptomoji's address are
the collection prefix (the first eight characters from a SHA-512 hash of the
owner's public key). The final 54 characters of a cryptomoji’s address are the
first 54 characters from a SHA-512 hash of its DNA string.
Namespace (6) | Type prefix (2) | Identifier hash (62) |
---|---|---|
5f4d76 |
02 |
1b96dbb5322e410816dd41d93571801e751a4f0cc455d8bd58f5f8ad3d67cb |
Because a collection is only allowed one sire, the sire listings can be stored
under an address that is almost identical to the collection address. The
identifier hash is the same first 62 characters of a SHA-512 hash of the owner's
public key. The only difference between the address of a collection and its sire
listing will be the type prefix
02
.
Cryptomoji payloads will be objects, each with an "action"
key that
designates what event should occur in the transaction processor. Each action is
designated by a specific string written in CONSTANT_CASE. Any additional data
will be included in other keys specific to that payload.
{
"action": "CREATE_COLLECTION"
}
Validation:
- Signer must not already have a collection
Creates a new collection for the signer of the transaction with three new pseudo-random (but deterministic!) cryptomoji. Like other actions, the identity of the signer will come from the public key in the transaction header, so it is not included in the payload itself.
{
"action": "SELECT_SIRE",
"sire": "<string, moji address>"
}
Validation:
- Signer must have a collection
- Cryptomoji must exist
- Signer must own the sire
Lists a cryptomoji as a collection's sire, indicating that it is available for other collections to breed with. A collection can have only one sire at a time.
{
"action": "BREED_MOJI",
"sire": "<string, moji address>",
"breeder": "<string, moji address>"
}
Validation:
- Signer must have a collection
- Cryptomoji must exist
- Signer must own the breeder
- Sire must be listed as a sire
Creates a new cryptomoji for the owner of the breeder. The new cryptomoji is a pseudo-random combination of the DNA from the breeder and the sire.
At this point you should be familiar with the basics of writing a distributed application on Sawtooth. For the next two days, you will gain a deeper understanding by implementing multi-party transactions that add the capability for collections to trade cryptomoji between each other. This is harder than it sounds. One user will need to create an offer, while others add responses until the offer owner accepts them. This is a process that spans multiple transactions, never mind the possibility that one party might change their mind and decide to cancel an offer or response.
The tests for this section are set to pending. To run a test which is
"skipped", you must remove the .skip
from the describe block surrounding the
test in the client/tests/
directory and processor/tests/
directory.
{
"owner": "<string, public key>",
"moji": [ "<strings, moji addresses>" ],
"responses": [
{
"approver": "<string, public key>",
"moji": [ "<strings, moji addresses>" ]
}
]
}
Offers are a way of effectively creating a multi-signer transaction. One user creates the offer, then other users add responses to it. When the offer owner sees a response they like, they can accept it and exchange the cryptomoji.
It is also possible for offer owners to request moji by adding responses to their own offer. These responses would then be approved by the mojis' owner. The "approver" key identifies the collection whose owner is required to approve a response.
Namespace (6) | Type prefix (2) | Collection prefix (8) | Identifier hash (54) |
---|---|---|---|
5f4d76 |
03 |
1b96dbb5 |
f0b9646d76c0e89bb8024d7ff2f7b4cde935f91c703f1a1a888e4a |
Like cryptomoji, offers are owned by a collection. In addition to their type
prefix (03
), an offer has an eight-character prefix, which is the first eight
characters of a SHA-512 hash of the owner's public key. The final 54 characters
are the first 54 characters of a SHA-512 hash. In this case, the hash is
generated from a string created by sorting the addresses of the cryptomoji being
offered, then concatenating those addresses with no spaces.
For example, we might generate the hash for the address above like this:
const moji1 = '5f4d76011b96dbb50c8514ab2a7cf361062601716bcd762097e41f9011a5e6f8ff6c5f';
const moji2 = '5f4d7601bddce3731459230a5a425d9e71ad0110f0e5a76ed88b8cfc1c087b10682492';
createHash('sha512').update(moji1 + moji2).digest('hex');
// f0b9646d76c0e89bb8024d7ff2f7b4cde935f91c703f1a1a888e4a8f6c62ad9a97664eb27bb981021c30d02e3417e03948d7fae7a13ba080e9bf2b421818a80b
{
"action": "CREATE_OFFER",
"moji": [ "<strings, moji addresses>" ]
}
Validation:
- Signer must have a collection
- Cryptomoji must exist
- Signer must own the cryptomoji
- Cryptomoji must not be listed as a sire
Creates an offer to trade some of a collection's moji. Owners of other collections can add responses to the offer.
{
"action": "ADD_RESPONSE",
"offer": "<string, offer address>",
"moji": [ "<strings, moji addresses>" ]
}
Validation:
- Signer must have a collection
- Cryptomoji must exist
- Offer must exist
- Signer must own the moji or be the owner of the offer
- No identical response already exists
Adds a response to an offer. This is the other side of the trade. The owner of the offer can add responses to their own offer, suggesting a trade for moji owned by someone else. Otherwise, the response must come from the owner of the moji.
{
"action": "ACCEPT_RESPONSE",
"offer": "<string, offer address>",
"response": "<number, index of response>"
}
Validation:
- Signer must have a collection
- Signer must be the "approver" on the response
- Exchanged cryptomoji must be owned by the appropriate parties
- Response index must correspond to a valid response on the offer
Accepts the response, exchanges the responder's cryptomoji for the cryptomoji originally listed in the offer, then deletes the offer from state.
{
"action": "CANCEL_OFFER",
"offer": "<string, offer address>"
}
Validation:
- Signer must have a collection
- Offer must exist
- Signer must own the offer
Deletes the offer from state with no changes in moji ownership.
{
"action": "CANCEL_RESPONSE",
"offer": "<string, offer address>",
"response": "<number, index of response>"
}
Validation:
- Signer must have a collection
- Offer must exist
- Signer must be the creator of the response
Deletes the response from the offer's list of responses, leaving null
in its
place.
What features would you like to see added to your version of Cryptomoji? What improvements could you make to the existing features? Brainstorm ideas with your partner and pick 2-3 new features or optimizations you would like to tackle. Design and implement them.
One possible suggestion, in the original Cryptokitties app on Ethereum, much was done using timestamps. After breeding, sires had a short period of downtime, during which they could not be used to sire again. Meanwhile, breeders had an exponentially increasing pregnancy time before they would create a child. Adding this gameplay feature to Cryptomoji will require that you understand and can use Sawtooth's Block Info TP.
Good luck.