Plapps and Predicates Understanding the Generalized Plasma Architecture

We recently published an article that describes our new work on a more generalized plasma architecture . This post goes into more detail about the architecture for those who are interested in how things work under the hood. We’ll go over how everything fits together at a high level, then we’ll jump into the sample implementation in Python. You can scroll forward to the diagrams if you’re already very familiar with plasma.

Introduction

The main takeaways from our previous article are:

Layer 2: Claims about State

The core idea behind “Layer 2” is that we can use data outside of a main blockchain (“off-chain data”) to provide guarantees about assets sitting on the main blockchain (“on-chain data”). For example, that off-chain data might give you the right to withdraw an asset held in an on-chain escrow contract on Ethereum. But the ownership of that asset changed hands without touching Ethereum. Of course, the off-chain data isn’t useful unless it’s possible for something to eventually happen on the main blockchain. This is usually achieved by allowing users to submit “claims” about the off-chain data to an on-chain smart contract where the assets being claimed are held. This claim might be something like “I have this signed message that says I’m allowed to withdraw asset X.” But what happens if after submitting the claim, the person making the aforementioned claim also sings a message sending asset X to someone else ? This would invalidate the above withdrawal. To ensure that any such invalid withdrawals are unsuccessful, we add a dispute period to every claim. Anyone may challenge the claim before the end of the dispute period. Otherwise, the claim is considered valid. Simple enough! Let’s quickly relate this back to plasma. Plasma chains consist of a series of blocks, and each block consists of a series of transactions. Whenever a new plasma chain block is created, a cryptographic commitment to that block is published to the main chain. These commitments can be used to prove that something was inside that block. In our case, this commitment is a merkle root created from all state updates in the block. The plasma smart contract on Ethereum records the order in which plasma blocks are committed by the operator, and prevents commitments from ever being overwritten. Thus, these block commitments give us a sense of time. When users want to make claims about something that happened on plasma chain, they have to also reference when that thing happened through the block number. For example, a user might say, “Here’s transaction X in block Y that gave me asset Z.” This extra time dimension is also important for disputes because you sometimes want to know whether something happened before or after something else!

Abstract Offchaindataism

So as we’ve just seen, Layer 2 is primarily about using some off-chain data to affect things sitting on-chain. So far, off-chain data has mostly been used to represent ownership — who owns what, and whether it changed hands. Alice will deposit an asset into a smart contract on Ethereum and she will become the owner of that asset. She can then sign an off-chain message that transfers ownership of the asset to Bob. Bob can then withdraw the asset back on Ethereum by making a claim using that signed message. We can also represent more complex things than ownership. Let’s say Alice deposits a CryptoKitty and signs a bunch of messages that change the kitty’s fur color. Just like in the example above, she can eventually make a claim on Ethereum about the kitty’s fur color using the last message she signed!

Whether it’s changing the kitty’s fur color or eye color, the smart contract back on Ethereum needs to have a way of understanding these changes. Each new piece of functionality — or new type of “state transition” — requires a change to the logic followed by the plasma contract. In previous plasma specs, adding a feature like this meant that one would have to re-deploy the entire plasma contract and migrate everyone’s assets from the old plasma chain to the new one. This is not secure, scalable, or upgradeable.

Predicates: Making Plasma Useful

We ran into this upgradeability problem once we got far enough into our plasma chain design that we wanted to add new functionality. After some brainstorming, we realized there was an easy way to add new functionality without changing the main plasma chain contract. All of the various scenarios we’ve talked about so far have something in common — the off chain data is always for the purpose of “disputing” incorrect on-chain claims. The dispute for a particular claim is basically just some proof that the state referenced in the claim is outdated. A dispute about ownership proves that the user making the claim later transferred ownership to someone else. A dispute about kitty fur color requires proving that the kitty’s fur color has been changed. Our primary breakthrough was realizing that dispute conditions don’t need to be checked in the main smart contract. Instead, we could have other smart contracts implement a function that would tell us if the dispute was valid or not. We could add new functionality by creating a new contract that implemented the necessary logic for that functionality and then have our main contract reference the new one. We call these external contracts ‘predicates’ . Now it was really easy to add new functionality, but we still needed a way to know which predicate to use for a particular state update. A claim about a kitty’s fur color can’t be disputed by a change in the kitty’s eye color. So how do we know which predicate to use? Simple! We just add the requirement that the state update also says which predicate it’s subject to. Now the update message that changes a kitty’s fur color could be, “I’m making this kitty’s fur color blue and disputes can be handled by the predicate sitting at ( 0x123… )” Since users can specify any predicate address they want, anyone can add new functionality to the plasma chain at any time by deploying a new predicate on Ethereum. There’s a little more to predicates than just handling disputes, but we’ll get into that later. What’s important is that predicates just have to implement a standard interface, defined in this next section.

Predicates in Practice

State Objects

Now let’s dive into the details of how this all works in practice. The building block of our plasma chain design is the ‘state object’. A state object is just a piece of data with two attributes:

State objects are effectively assets — a generalization of the concept of the non-fungible “coins” in Plasma Cash. Just like each unique coin had a coinID in Cash, each state object has a stateID . That’s it! The stateID s are assigned sequentially based on deposits into the plasma chain, but there aren’t any rules about what parameters or predicate can be. Each plasma block is a collection of “state updates,” which define new stateObjects s at particular stateID s.

Because we use a range-based Cash variant , stateUpdate s are actually specified over ranges of stateObject IDs:

stateUpdate = {
   start: uint,
   end: uint,
   plasmaBlockNumber: uint,
   stateObject: stateObject
}

The plasma chain contract on Ethereum, where the operator submits plasma blockhashes, implements a verifyUpdate(update: stateUpdate, updateWitness:bytes[]) -> bool which checks a Merkle inclusion proof ( updateWitness ) that the state update was indeed committed.

The Predicate Interface

Predicates need to implement a standard contract interface. Let’s go over those functions. The most important thing thing the plasma contract does is determine the validity of state updates. Particularly, we need to prevent the operator (who has full control over blocks) from being able to “sneak in” a valid state update which has its stateObject.parameters.owner == operator — this would be theft! To accomplish this, we introduce the concept of “state deprecation.” We say that the valid state for a given stateID is that of the earliest update which has not yet been “deprecated.” State deprecation is analogous to an Unspent Transaction Output becoming a Spent Transaction Output for UTXO blockchains. This way, even if the operator sneaks in a later update where stateObject.parameters.owner == operator , an earlier update with stateObject.parameters.owner == alice will prevail, as only she can deprecate the state. Thus, the most important function in the predicate defines the grounds by which its state may be deprecated:

verifyDeprecation(stateID: uint, update: stateUpdate, deprecationWitness: bytes)

verifyDeprecation returns true or false depending on whether the committed stateUpdate has been deprecated for a particular stateID . deprecationWitness is any arbitrary data that the predicate uses to check whether or not the stateObject has been deprecated. For example, by requiring that the deprecationWitness includes a valid signature from the update.stateObject.parameters.owner , we guarantee only the owner can approve a deprecation. Remember, this function doesn’t actually do any deprecation as it regards to plasma’s exits games, disputes, and so on. Rather, the plasma contract calls the function when it needs to know whether a stateObject is deprecated to evaluate a dispute. There are three other functions in the predicate interface — in order of importance, they are:

finalizeExit(exit: bytes)

When an exit is redeemed, the plasma contract sends any assets associated with the claim to the predicate address and then calls this function.

canInitiateExit(stateUpdate: bytes, initiationWitness: bytes) -> bool

This function allows the predicate to restrict who may initiate a claim on the committed state. For example, an ownership predicate probably wants to restrict canInitiateExit to the owner of the asset.

getAdditionalDisputePeriod(stateUpdate: bytes) -> uint

This function allows the predicate to increase the dispute period of a claim. We only really use this for complex predicates (e.g. atomic swaps) that might require a longer dispute resolution process. This function usually just returns 0 .

Predicates by Example: Ownership Predicate

Everything is easier with examples, so let’s take a look at one. The simplest predicate is the ownership predicate. This state allows its current parameters.owner to exit at any time, or approve any state update. The first step to creating our predicate is to design our state object. Luckily that’s pretty simple, the only data inside the object’s parameters is the address of the current owner. A state object using the ownership predicate might look like this:

OwnedByAlice = {
  parameters: {
    owner: '0xAliceAddress...',
  },
  predicate: '0xOwnershipPredicateAddress...'
}

The most important function to implement is verifyDeprecated , which receives some arbitrary deprecationWitness . In the case of the ownership predicate, a valid deprecationWitness consists of:

verifyDeprecated needs to check that these things are valid, which means checking a signature and a Merkle proof.

Putting it all together, we see how the owner can deprecate its ownership state by approving a new update:

The remaining functions are pretty simple. canInitiateExit needs to check that the claimant is the owner, finalizeExit forwards the assets over to the owner, and getAdditionalDisputePeriod can return 0 . Here’s what that actually looks like in code! Below we’ve included a Python implementation of the simple ownership predicate. We wrote it in Python for simplicity, but it’s pretty easy to do in Solidity or Vyper as well.

class OwnershipDeprecationWitness:
    def __init__(self, next_state_update, signature, inclusion_witness):
        self.next_state_update = next_state_update
        self.signature = signature
        self.inclusion_witness = inclusion_witness

class OwnershipPredicate:

    def __init__(self, parent_plasma_contract):
        self.parent = parent_plasma_contract

    def can_initiate_exit(self, state_update, initiation_witness):
        # Only the owner can submit a claim
        assert state_update.state.owner == initiation_witness
        return True

    def verify_deprecation(self, state_id, state_update, deprecation_witness):
        # Check the state_id is in the deprecation_witness state update
        assert deprecation_witness.next_state_update.start <= state_id \ 
            and deprecation_witness.next_state_update.end > state_id
        # Check inclusion proof for more recent state update
        assert self.parent.commitment_chain.verify_inclusion \
            (deprecation_witness.next_state_update,
                self.parent.address,
                deprecation_witness.inclusion_witness)
        # Check that the previous owner signed off on the change
        assert state_update.state.owner == deprecation_witness.signature
        return True

    def finalize_exit(self, exit):
        # Transfer funds to the owner
        self.parent.erc20_contract.transferFrom \
            (self, exit.state_update.state.owner, \
                exit.state_update.end - exit.state_update.start)

    def get_additional_lockup(self, state):
        return 0

As you can see, we’ve implemented the entire interface described above. Not too difficult :-). And there we have it! A predicate that represents transferable ownership of an asset. Most of the logic here is identical to what was already being done in plasma contracts. We even managed to prototype the change over the course of ETHDenver . It was mostly a matter of moving around code that we’d already written.

Back to the Future

This architecture is a significant step forward in our understanding of plasma. It is analogous to the jump from payment channels to generalized state channels–we are able to fit new features and functionalities in the plasma architecture without upgrading the plasma protocol itself. Further, we believe the predicate design space presents a rich new area of research for plasma. It’s still early days, but some of the predicates we think are possible include:

Resources

Python simulation:

https://github.com/plasma-group/research/tree/master/gen-plasma

Technical specification: https://pigi.readthedocs.io/en/latest/src/specs/generalized-plasma-state.html

Join our Plasma Contributors telegram chat:

https://t.me/plasmacontributors

Our Thanks to:

Alex Attar
Dan Robinson
Dan Tsui
Georgios Konstantopoulos
Jeff Coleman
Mark Tyneway
Matt Slipper
Vitalik Buterin
Xuanji Li