Generalized Plasma State Spec¶
- Data Structures
stateID: uint- the index of unique, non-fungible states in a plasma chainstateObjectstruct:predicate: address- the state’s predicate rulesetparameters: bytes- input parameters to the predicate ruleset
stateUpdatestructstate: stateObject- the new state for this range ofstateIDsstart: uint- the start of the range ofstateIDsend: uint- the end of the range ofstateIDsplasmaBlockNumber: uint- the plasma block in which a committment was madeplasmaContract: address- the address of the plasma contract the state update is meant for
stateUpdateWitnessstructinclusionProof: bytes[]- array of sibling nodes forming the Merkle inclusion proof
depositstruct:state: stateObject- the initial state of the deposited coinstart: uint- the firststateIDof the deposit. The plasma contract stores a mapping fromdepositEnd->depositprecedingPlasmaBlockNumber: uint- the most recent plasma block leading up to the deposit.
exitableRangestruct:start: uint- The firststateIDof a still-exitable range. The plasma contract stores a mapping fromcexitableRangeEnd->exitableableRangeisSet: bool- whether or not this value in the mapping has been initialized. Needed because EVM can’t differentiate between a mapping set to 0 and an unset mapping.
exitstruct:update: stateUpdate- the state being claimed.exitStart- the start of the update’s subrange claiming to be undeprecated.exitEnd- the end of the update’s subrange claiming to be undeprecated.ethBlockRedeemable: uint- when theexit’s dispute period expires.numChallenges: uint- the number of pending challenges preventing aexit’s redemption.
challengestruct:earlierExitID: uint- the earlier undeprecatedexitbeing used to challenge an invlaidexit.laterExitID: uint- the challengedexit.
- Commitment Contract
- Public Variables:
operator: public(address)- the operator’s address.nextPlasmaBlockNumber: public(uint256)- what the block number of the next block committment will be.lastPublish: public(uint256)- the Ethereum block number of the last plasma block.blockHashes: public(map(uint256, bytes32))- the blocks submitted so far.
- Methods:
setup- Used to initiate the contract with collator pubkeycommitBlock: allows the operator to submit sequential blocksverifyUpdate(update: stateUpdate, subject: address, stateUpdateWitness: bytes): returns bool whether a givencommitmentwas made on behalf of a givensubjectaddress (this is the plasma contract for e.g. a specific ERC20), at a the givenplasmaBlockNumber, based on a validcommitmentWitness
- Block Structure/Proof Validity, as checked by
verifyUpdate: - merkle node format:
[hash: bytes32][subject: address][index: bytes16] - merkle parent function:
parent(leftSibling, rightSibling) = [sha256([leftSibling][rightSibling])][rightSibling.subject][rightSibling.index] - merkle proof format:
siblingMerkleNodes[]- array of the siblings going up the branch - branch validity requires that left
siblingMerkleNodesgoing up the branch are monotonically decreasin - For a given
commitment,subject, andcommitmentWitness, where theleafis the bottommost node in thestateUpdateWitnesswe must have: leaf.subject == subjectstateUpdate.end <= leaf.indexstateUpdate.subject = leaf.subjectstateUpdate.start >= STARTwhereSTARTis either the merkle proof’s deepest left sibling, or0if none exist (this is only the case for the ``0``th element in the tree)
- For a given
- leaf nodes are parsed to
[hash(state)][subject][state.end] - NOTE: for any nodes in the tree whose sibling has the same
subjectaddress, we may remove the address for efficiency, as long as the above conditions are met as if thesubjectis prepended to the index. This is definitely an optimization to consider down the line!
- NOTE: for any nodes in the tree whose sibling has the same
- leaf nodes are parsed to
- merkle node format:
- Block Structure/Proof Validity, as checked by
- Plasma Contract
- Public Variables:
self.commitmentAddress- where the operator is submitting commitmentsself.tokenAddress- the ERC20 contract of for this plasma contract (we’ll have one contract per token)self.deposits[end: uint] -> deposit- mapping of all deposits todepositstructsself.exitableRanges[end: uint] -> exitableRange- mapping of all the unclaimed ranges (“states still in the plasma chain”)self.exits[exitID] -> exit- all of the current exitsself.challenges[challengeID] -> challenge- all of the current challenges on exitsself.DISPUTE_PERIOD: uint- the minimum dispute period before a claim can be redeemed
- Public methods:
deposit(amount, state)- Deposits specify an initial state and the amount of money being deposited into that state
- adds to
self.deposits - extends
self.claimableRangesso that the state is now claimable
exitStateUpdate(exitStart: uint, exitEnd: uint, update: stateUpdate, updateWitness: stateUpdateWitness, initiationWitness: bytes)- allows users to submit a claim on a committed stateassert verifyUpdate(update, self.address, stateUpdateWitness)assert exitStart >= update.startassert exitEnd <= update.endassert update.state.predicate.can_initiate_exit(update, initiationWitness)- if so, adds a new exit to
self.exits - sets the exit’s
ethBlockRedeemableto:eth.block + self.CHALLENGE_PERIOD + state.predicateAddress.getAdditionalLockup(update)
exitDeposit(exitStart: uint, exitEnd: uint, depositEnd: uint, claimabilityWitness:bytes)- allows users to submit an exit on a deposited state- both of the above store an
exitstruct inself.exits[self.exitNonce]and incrementself.exitNonce. - sets the claim’s
ethBlockRedeemableto:eth.block + self.CHALLENGE_PERIOD + state.predicateAddress.getAdditionalLockup(state) - In this case, the
update.plasmaBlockNumbercomes from thedeposit.precedingPlasmaBlockNumber
- both of the above store an
challengeExit(earlierExitID, laterExitID)- allows users to challenge a later exit with an earlier undeprecated exit- this is the way we challenge exits if the operator commits some a state with something undprecated in the history. The function checks that:
earlierExitID’s claimed range intersects that oflaterExitIDearlierExitID.update.plasmaBlockNumber < laterExitID.update.plasmaBlockNumbereth.block < laterExit.ethBlockRedeemable
- if so, it does the following:
- create a
challengeobject inself.challenges[challengeNonce] - increment
challengeNonce - increase the
laterExit.ethBlockRedeemabletoearlierExit.ethBlockRedeemableif the latter is bigger - increment
challengedClaim.numChallenges
- create a
cancelDeprecatedExit(stateID: uint, exitID: uint, deprecationWitness: bytes)- allows users to cancel an exit by demonstrating adeprecationWitnessfor one of the ``state``s in its rangeexit = self.exits[exitID]assert exit.update.predicateAddress.verifyDeprecation(stateID, exit.update, deprecationWitness)- if so, clears the exit, deleting it from the
self.exitsmapping
removeChallenge(challengeID: uint)- allows users to remove a challenge- checks that the
self.challenges[challengeID].earlierExithas been revoked, i.e. that its key is no longer set to a value in self.exits[] - if so, decrements the
self.exits[self.challenges[challengeID].laterExitID].numChallengesand then clears/deletesself.challenges[challengeID]
- checks that the
finalizeExit(exitID, exitableRangeEnds)- asserts
exit’s numChallenges = 0 - tries
isRangeClaimablefor the variousclaimableRangeEnds, reverts if none pass the check - asserts the current
eth.block >= exit.ethBlockRedeemable - approves the ERC20 claim amount (
=start-end) to be transferred by theexit.update.state.predicateAddress - calls
finalizeExit(update)on theupdate.state.predicateAddress
- asserts
- Predicate interface
- Public methods/interface:
verifyDeprecation(stateID: uint, update: stateUpdate, deprecationWitness: bytes) -> bool- returns true/false whether a givendeprecationWitnessis valid (if true the exit may be cancelled)finalizeExit(update: stateUpdate)- called once a claim on a state is redeemed on the plasma contract- in principle, this can do anything, but will almost always call the
ERC20.transferFromfunction to the tune ofexit.start - exit.end, either to itself to initiate an additional dispute period, or to some ultimate beneficiary as devised from theexit.update.state.parameters
- in principle, this can do anything, but will almost always call the
canInitiateExit(update: stateUpdate, initiationWitness: bytes) -> bool- returns true/false whether a claimant is eligible to submit an exit on a given stategetAdditionalDisputePeriod(update: stateUpdate)- returns an additional number of ETH blocks which must elapse, in addition to the standardplasmaContract.DISPUTE_PERIOD, before the exit may be redeemed
- Predicate Examples
- Simple Ownership
struct ownershipDeprecationWitness:newStateUpdate: stateUpdatenewUpdateWitness: stateUpdateWitnesssignature: signature
public function verifyDeprecation(stateID: uint, update: stateUpdate, revocationWitness: bytes):assert verifyUpdate(deprecationWitness.newStateUpdate, revocationWitness.newUpdateWitness)assert verifySignature(revocationWitness.newStateUpdate, signature) = update.state.owner
public function finalizeExit(exit: exit):redeemedAmount: uint = exit.end - exit.start #length of sequential stateIDs claimedERC20.transferFrom(self.address, exit.update.state.owner, )
public function canInitiateExit(update: stateUpdate, initiationWitness: bytes):- assert tx.sender = commitment.state.parameters.owner``
Multisig
Atomic Swap
- Basic Payment Channel
- struct
stateChannelParameters: participants: address[]- array of pubkeys participating in the channelopeningUpdatesHash: bytes32- a hash of all thestateUpdateobjects which must be made for the channel to be considered successfully “opened”failedOpeningRecipient: address- the person to send money to if the opening failed, i.e. the above commitments weren’t madeonChainChannel: address- the on-chain payment channel to send the money to if channel isn’t closed out on-chaincallData: bytes[]- the instantiation data passed to theonChainChannel
- struct
- struct
stateChannelDeprecationWitness closureUpdates: stateUpdate[]- array of the states agreed to close onclosureUpdateWitnesses: stateUpdateWitness[]- array of the proofs that the above updates were madeclosureApprovals: signature[]- array of signatures by each of thestate.parameters.participantsonhash(closureUpdates)agreeing to close
- struct
- public
self.successfulOpenings[upeningUpdatesHash] -> bool- mapping of whether or not a givenopeningUpdatesHashwas successfully made - public
proveOpenings(openingUpdates: commitment[], openingWitnesses: stateUpdateWitness[]) - allows users to prove that a state channel was successfully opened by validating all opening inclusions
- asserts that
verifyUpdatefor eachopeningUpdatestate and its witness - if so, sets ``self.successfulOpenings[hash(openingUpdates) = true]
- public
- struct
openingExitStatus- the struct used if an open channel is being exited because of an unsuccessful closure totalCoins- the total number of coins entered into the payment channelredeemedCoins- the total number of coins whose claims have been redeemed so far
- struct
- public
self.openingExitsInProgress[upeningUpdatesHash:bytes32] -> openingExitStatus- mapping of “in progress” exits on opened channels verifyDeprecation- asserts that
self.openingClaimsInProgress[update.parameters.openingUpdatesHash)].redeemedCoins == 0– if any of the opening state has been redeemed, all state must be redeemed from the openings, no revocation is valid. - asserts that
verifyUpdatefor each commitment in the revocation witness - asserts that each
state.parameters.participantssigned off onhash(closureUpdates)
- asserts that
finalizeExit- checks whether the channel was successfully opened:
assert self.successfulOpenings[openingUpdatesHash] - If it was:
self.openingExitsInProgress[openingUpdatesHash].redeemedCoins += exit.end - exit.start- let
exitInProgress = self.openingExitsInProgress[openingCommitmentsHash] - if
exitInProgress.redeemedCoins == exitInProgress.totalCoins, then forward thetotalCoinsto theexit.update.parameters.onChainChannel(exit.update.parameters.callData)– the opening has been fully claimed and the on-chain channel may take over.
- Otherwise, not all money in the channel has been redeemed from the plasma contract yet, so we must wait.
- checks whether the channel was successfully opened:
- L1<>L2 liquidity predicate (swap PETH for ETH)
- struct
tradeParameters: tradeID: uint- a unique ID for the tradeseller: addresssaleAmount: uint- the amount of ETH the coins are being sold for
- struct
- struct
trade ethSender: addresstargetPlasmaBlock: uint
- struct
- mapping
self.trades[tradeID][ethRecipient][amount] -> trademaps the unique aspects of the trade to the sender and intended block of the new ownership state committment - public method:
submitTrade(tradeID: bytes32, ethRecipient: address, targetPlasmaBlock: uint) - assert that the next plasma block is the
targetPlasmaBlock - assert that
self.trades[tradeID: bytes32][ethRecipient: address][tx.value: uint]is unset - if not:
- set the value with
trade.ethSender = tx.Senderandtrade.targetPlasmaBlock = targetPlasmaBlock - forward the ETH to
ethRecipient
- set the value with
- assert that the next plasma block is the
- public method:
verifyDeprecationdeprecationWitnessconsists of:- a valid
newStateUpdate, satisfying: .startand.endequalling the deprecatedstateUpdate.startand.end- the existance of an entry in
self.trades[stateUpdate.parameters.tradeID][newStateUpdate.parameters.owner][end - start] - the
ethSenderin that entry being thenewStateUpdate.parameters.owner - the
newStateUpdate.plasmaBlockNumber == trade.targetPlasmaBlock
- the
- the existance of an entry in
- a valid
finalizeExit- checks for the existence of an entry in
self.Trades[exit.state.parameters.tradeID][redeemedState.seller][end - start] - if it exists, send to that
trade.ethSender - otherwise, send back to
redeemedState.parameters.seller
- if it exists, send to that
- checks for the existence of an entry in