Note: This is the first post of a 3-part series. Part 2. Part 3.
Chia is very different from account-based blockchains like Ethereum. The ‘fundamental’ unit on Chia is a coin - which has a parent (i.e., source that created it), an amount of mojos, and a puzzle - which contains the ‘rules’ that control the value locked in the coin. This change from “there are smart accounts and a JavaScript-like language is used to control them” means that a lot of basic stuff is wildly different from Ethereum or other cryptocurrencies.
There have been two major breakthroughs that allow dApps today to exist on Chia. First, singletons allowed a lineage of coins to maintain an ‘identity’ through spends. A coin can only be spent once - but the singleton primitive allows one of the coin’s children to be marked as a ‘successor.’ This makes it possible for NFTs to exist - the unspent (latest) coin holding the NFT itself changes each time you do a transfer, trade, or metadata update. But the underlying NFT id and state (metadata) are preserved through spends.
Second, the launch of TibetSwap provided a ‘sketch’ for simple dApps. Besides showing the community that dApps were possible on Chia (many community members thought they weren’t), TibetSwap had a ‘structure’ that showed one way of:
Holding state variables that change frequently inside a singleton (e.g., the amount of XCH locked in a pair, which changes with each trade)
Emulating Solidity’s function selectors, which allow different ways to interact with accounts based on what you want to do. For example, when you add liquidity to a TibetSwap pair, some of the code that runs is different than the code that runs when you do a XCH → CAT swap.
Even with the two advancements, a small issue still prevented complex dApps from being built on-chain. I call it the problem of uniqueness, and this post is intended to explain it.
Why don’t we have a fully decentralized name service on Chia?
The easiest way to demonstrate the issue is to show a place where it heavily influenced how solutions were designed: name services.
As most crypto users know, a name service is mainly responsible for translating a human-readable string - such as ‘yakuhito’ - to a blockchain address. It’s a decentralized address book. Currently, there are multiple such services on Chia - from the Chia Name Service and NamesDAO, which offer on-chain name NFTs, to sites such as go4me and MyXCH.space. However, unlike their Ethereum counterparts, they are not fully decentralized - someone’s key is used to create your ‘domain’, and that someone can also register the same name twice or refuse your registration request. The question is: why is there a ‘private key’ in the image below?
No one wants the responsibility of being an admin - it adds complexity and a lot of responsibility to a system that should be unstoppable, uncensorable, and available 24/7. Teams like CNS have very deep knowledge of the Chia blockchain - so why settle for something less than ideal?
The core issue is that, while singletons allow you to hold some values that change over time, they come with limitations. Too many values will make your transactions more expensive - less can fit in a block, and users will have to pay more fees. Moreover, there’s no ‘direct’ way of proving something is unique (a list of registered names would get too big after just ‘a few’ registrations). Combined, these issues mean that a registry singleton has no way of checking whether ‘yakuhito’ is an active name when someone makes a request to register it.
A lot of people have been thinking - directly or indirectly - about a solution for more than two years now. I call this the problem of uniqueness - and it shows up almost every time you want to design a complex dApp.
Side-note: Those that know a bit of Solidity will notice that the problem is trivial to solve on Ethereum - just use a ‘mapping’ structure! That’s not available on Chia.
Where does the problem show up?
Aside from name services, there are several apps where the problem shows up. Each time, the solution is highly specific and present several drawbacks.
NFT
NFTs - actually, singletons - were the first puzzles to face this issue. The way you ensure only one unspent coin exists at a time is by having an id - when a coin is spent, it passes that id to one of its children (or, in special cases, it doesn’t - a process that destroys the singleton which we call ‘melting’). The clever solution the creators of the puzzle came up with is to use the first singleton parent’s id as the singleton id. In other words, singletons distinguish themselves by the coin id of the ‘creator’ coin - which is usually called the singleton launcher. This is a great idea because it uses a consensus rule - the one saying that all coins must have a unique id - to give singletons their ids. The major drawback of this design is that it relies on coin ids - which depend on parent ids, making it impossible to use the same system to store custom values. Think of it this way: you can prove uniqueness, but the entries of the list are all random 32-byte values - not ‘yakuhito’ or other names.
TibetSwap
In TibetSwap, a singleton called the ‘router’ launches new pairs. One of its main purposes is to allow people to ‘index’ pools, which are always XCH-CAT and currently only have one fee tier. Ideally, the router would assert a pair for the same assets has not been created before when deploying it - which would allow other dApps to do a lot of cool things. However, as stated above, that wasn’t quite possible when TibetSwap launched - so the second best alternative is to have the router launch pairs for any assets and then make the driver code only mark the first pair valid. This mechanism is off-chain - you can’t prove it on-chain.
warp.green
The bridge had a similar issue - given the id of a message, the singleton would ideally be able to assert that it hasn’t been relayed before. Because, if you bridge 40 USDC to Chia, you should not be allowed to claim it twice under any circumstances. The solution I came up with is to embed the previously bridged message ids in the singleton’s puzzle. When validators sign a message, they also sign the id of the [singleton] coin that can relay that message - which depends on the ids of the messages that were relayed last spend. The id also depends on the parent’s id, which in turn depends on the messages that were relayed two spends ago. Essentially, a ‘message id chain’ is created with each coin id containing the messages that were relayed previously (in a recursive manner). There are two limitations with this approach:
Validators need to sync the relayer singleton’s full history to assert that a message hasn’t been relayed before - and actually perform the check before signing a message
This approach needs a centralized party that does this indexing off-chain. In this case, bridge validators were already there for other reasons - but a lot of dApps will probably not want to insert a centralized component just for uniqueness checks
Up Next
I hope you enjoyed this primer on the problem of uniqueness. As a recap, the problem is that puzzles (or rather, singletons) can’t check that a certain value hasn’t been used before for something. In the next post, I’m going to walk you through the most promising solution that has been put forward - and why it doesn’t work that well. With that said, join me for Part 2.
Special thanks to stl and (or, maybe, a.k.a. 😉 ) Bob for reviewing this article.