Generalized Plasma State Spec¶
- Data Structures
stateID: uint
- the index of unique, non-fungible states in a plasma chainstateObject
struct:predicate: address
- the state’s predicate rulesetparameters: bytes
- input parameters to the predicate ruleset
stateUpdate
structstate: stateObject
- the new state for this range ofstateID
sstart: uint
- the start of the range ofstateID
send: uint
- the end of the range ofstateID
splasmaBlockNumber: uint
- the plasma block in which a committment was madeplasmaContract: address
- the address of the plasma contract the state update is meant for
stateUpdateWitness
structinclusionProof: bytes[]
- array of sibling nodes forming the Merkle inclusion proof
deposit
struct:state: stateObject
- the initial state of the deposited coinstart: uint
- the firststateID
of the deposit. The plasma contract stores a mapping fromdepositEnd->deposit
precedingPlasmaBlockNumber: uint
- the most recent plasma block leading up to the deposit.
exitableRange
struct:start: uint
- The firststateID
of a still-exitable range. The plasma contract stores a mapping fromcexitableRangeEnd->exitableableRange
isSet: 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.
exit
struct: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.
challenge
struct:earlierExitID: uint
- the earlier undeprecatedexit
being 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 givencommitment
was made on behalf of a givensubject
address (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
siblingMerkleNodes
going up the branch are monotonically decreasin - For a given
commitment
,subject
, andcommitmentWitness
, where theleaf
is the bottommost node in thestateUpdateWitness
we must have: leaf.subject == subject
stateUpdate.end <= leaf.index
stateUpdate.subject = leaf.subject
stateUpdate.start >= START
whereSTART
is either the merkle proof’s deepest left sibling, or0
if 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
subject
address, we may remove the address for efficiency, as long as the above conditions are met as if thesubject
is 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 todeposit
structsself.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.claimableRanges
so 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.start
assert exitEnd <= update.end
assert update.state.predicate.can_initiate_exit(update, initiationWitness)
- if so, adds a new exit to
self.exits
- sets the exit’s
ethBlockRedeemable
to: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
exit
struct inself.exits[self.exitNonce]
and incrementself.exitNonce
. - sets the claim’s
ethBlockRedeemable
to:eth.block + self.CHALLENGE_PERIOD + state.predicateAddress.getAdditionalLockup(state)
- In this case, the
update.plasmaBlockNumber
comes 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 oflaterExitID
earlierExitID.update.plasmaBlockNumber < laterExitID.update.plasmaBlockNumber
eth.block < laterExit.ethBlockRedeemable
- if so, it does the following:
- create a
challenge
object inself.challenges[challengeNonce]
- increment
challengeNonce
- increase the
laterExit.ethBlockRedeemable
toearlierExit.ethBlockRedeemable
if the latter is bigger - increment
challengedClaim.numChallenges
- create a
cancelDeprecatedExit(stateID: uint, exitID: uint, deprecationWitness: bytes)
- allows users to cancel an exit by demonstrating adeprecationWitness
for 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.exits
mapping
removeChallenge(challengeID: uint)
- allows users to remove a challenge- checks that the
self.challenges[challengeID].earlierExit
has 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].numChallenges
and then clears/deletesself.challenges[challengeID]
- checks that the
finalizeExit(exitID, exitableRangeEnds)
- asserts
exit
’s numChallenges = 0 - tries
isRangeClaimable
for 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 givendeprecationWitness
is 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.transferFrom
function 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: stateUpdate
newUpdateWitness: stateUpdateWitness
signature: 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 claimed
ERC20.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 thestateUpdate
objects 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.participants
onhash(closureUpdates)
agreeing to close
- struct
- public
self.successfulOpenings[upeningUpdatesHash] -> bool
- mapping of whether or not a givenopeningUpdatesHash
was 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
verifyUpdate
for eachopeningUpdate
state 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
verifyUpdate
for each commitment in the revocation witness - asserts that each
state.parameters.participants
signed 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 thetotalCoins
to 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: address
saleAmount: uint
- the amount of ETH the coins are being sold for
- struct
- struct
trade
ethSender: address
targetPlasmaBlock: uint
- struct
- mapping
self.trades[tradeID][ethRecipient][amount] -> trade
maps 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.Sender
andtrade.targetPlasmaBlock = targetPlasmaBlock
- forward the ETH to
ethRecipient
- set the value with
- assert that the next plasma block is the
- public method:
verifyDeprecation
deprecationWitness
consists of:- a valid
newStateUpdate
, satisfying: .start
and.end
equalling the deprecatedstateUpdate
.start
and.end
- the existance of an entry in
self.trades[stateUpdate.parameters.tradeID][newStateUpdate.parameters.owner][end - start]
- the
ethSender
in 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