├── .gitignore ├── README.md ├── gen-plasma ├── README.md ├── commitment_chain_contract.py ├── erc20_plasma_contract.py ├── predicates │ ├── __init__.py │ ├── multisig.py │ └── ownership.py ├── test │ ├── __init__.py │ ├── conftest.py │ ├── predicates │ │ ├── test_multisig.py │ │ └── test_ownership.py │ ├── test_erc20_plasma_contract.py │ └── test_utils.py └── utils.py ├── plasma-exit-games └── exit_game_sim.py ├── plasma0 ├── README.md ├── contracts │ └── plasmaprime.vy ├── plasmalib │ ├── __init__.py │ ├── constants.py │ ├── old_tx_tree_utils.py │ ├── operator │ │ ├── __init__.py │ │ ├── block_generator.py │ │ ├── rsa_accumulator.py │ │ ├── transaction_validator.py │ │ └── transactions.py │ ├── state.py │ └── utils.py ├── requirements.txt ├── setup.py └── tests │ ├── conftest.py │ ├── contracts │ ├── test_challenges.py │ ├── test_compilation.py │ ├── test_deposits.py │ ├── test_exits.py │ ├── test_publication.py │ ├── test_responses.py │ └── test_tx_hash.py │ └── operator │ ├── test_block_generator.py │ ├── test_signature_validation_performance.py │ ├── test_state.py │ └── test_transactions.py └── rollup-block-gen ├── block_gen.py ├── sequencer_rollup.py ├── simple_rollup.py ├── test_sequencer_rollup.py └── test_simple_rollup.py /.gitignore: -------------------------------------------------------------------------------- 1 | venv 2 | vyper 3 | *pycache* 4 | *.egg-info 5 | *.swp 6 | *.pyc 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Research 2 | 3 | This repository is mainly used for code related to specific research questions and simulations that are in progress. It is not meant to be used as a final specification for any existing research. 4 | 5 | # License 6 | MIT yay plasma group meow 7 | -------------------------------------------------------------------------------- /gen-plasma/README.md: -------------------------------------------------------------------------------- 1 | # General Purpose Plasma Python Implementation 2 | This repository contains code which simulates a 'generalized plasma' implementation. This means that 3 | it implements a few smart contracts which share a common interface that allow for maximal layer 2 interoperability. 4 | 5 | This repository contains a standard plasma chain, but also includes python implementations of common state channels like 6 | payment channels. It also contains more sophisticated plasma `predicate` logic which handles things like multi-sigs & more. 7 | 8 | Note that this was written in Python so that it can serve as an easy to digest example of these topics. A more special purpose 9 | language would be used in practice, like Vyper. However, the core logic will remain the same even if there are slight data structure 10 | changes & things like signatures and inclusion proofs will not be mocked. 11 | 12 | ## Requirements 13 | - Python3 14 | 15 | ## Installation 16 | ``` 17 | $ cd gen-plasma # navigate to root directory 18 | $ python3 -m venv venv # create virtual enviornment 19 | $ . venv/bin/activate # activate virtual enviornment 20 | $ pip install pytest # install pytest--the only dependency 21 | ``` 22 | 23 | ## Test it out 24 | ``` 25 | $ pytest # run the tests! 26 | 27 | test/test_erc20_plasma_contract.py .. 28 | test/test_utils.py . 29 | test/predicates/test_multisig.py .... 30 | test/predicates/test_transfer.py .... 31 | 32 | ========== x passed in 0.06 seconds ========== 33 | $ # You did it! Now take a look at the code :) 34 | $ vim test/predicates/test_transfer.py # the transfer predicate is a pretty good place to start :) 35 | ``` 36 | -------------------------------------------------------------------------------- /gen-plasma/commitment_chain_contract.py: -------------------------------------------------------------------------------- 1 | ''' Commitment Block Structure 2 | { 3 | subject_0: [commitment_0, commitment_1,...commitment_n], 4 | subject_1: [commitment_0, commitment_1,...commitment_n], 5 | ... 6 | subject_n: [commitment_0, commitment_1,...commitment_n] 7 | } 8 | ''' 9 | 10 | class CommitmentChainContract: 11 | def __init__(self, operator): 12 | self.operator = operator 13 | self.blocks = [] 14 | 15 | def commit_block(self, msg_sender, block): 16 | assert msg_sender == self.operator 17 | self.blocks.append(block) 18 | 19 | def validate_commitment(self, commitment, subject, committment_witness): 20 | # Note that we are not providing merkle proofs and are instead faking it by storing the full blocks. 21 | block = self.blocks[commitment.plasma_block_number] 22 | # Make sure the subject contract address is in fact included in the block. 23 | # NOTE: We are mocking the inclusion & so we don't actually use the commitment witness. 24 | assert subject in block 25 | # Return whether or not this commitment was included 26 | return commitment in block[subject] 27 | -------------------------------------------------------------------------------- /gen-plasma/erc20_plasma_contract.py: -------------------------------------------------------------------------------- 1 | from utils import State, Claim, Commitment, Challenge 2 | 3 | class ClaimableRange: 4 | def __init__(self, start, is_set): 5 | self.start = start 6 | 7 | class Erc20PlasmaContract: 8 | def __init__(self, eth, address, erc20_contract, commitment_chain, DISPUTE_PERIOD): 9 | # Settings 10 | self.eth = eth 11 | self.address = address 12 | self.erc20_contract = erc20_contract 13 | self.commitment_chain = commitment_chain 14 | self.DISPUTE_PERIOD = DISPUTE_PERIOD 15 | # Datastructures 16 | self.total_deposits = 0 17 | self.claimable_ranges = dict() 18 | self.claims = [] 19 | self.challenges = [] 20 | 21 | def deposit(self, depositor, deposit_amount, predicate, parameters): 22 | assert deposit_amount > 0 23 | # Make the transfer 24 | self.erc20_contract.transferFrom(depositor, self.address, deposit_amount) 25 | # Record the deposit first by collecting the preceeding plasma block number 26 | preceding_plasma_block_number = len(self.commitment_chain.blocks) - 1 27 | # Next compute the start and end positions of the deposit 28 | deposit_start = self.total_deposits 29 | deposit_end = self.total_deposits + deposit_amount 30 | # Create the initial state which we will record to in this deposit 31 | initial_state = State(predicate, parameters) 32 | # Create the depoisit object 33 | deposit = Commitment(initial_state, deposit_start, deposit_end, preceding_plasma_block_number) 34 | # And store the deposit in our mapping of ranges which can be claimed 35 | self.claimable_ranges[deposit_end] = deposit 36 | # Increment total deposits 37 | self.total_deposits += deposit_amount 38 | # Return deposit record 39 | return deposit 40 | 41 | def _construct_claim(self, commitment): 42 | additional_lockup_duration = commitment.state.predicate.get_additional_lockup(commitment.state) 43 | eth_block_redeemable = self.eth.block_number + self.DISPUTE_PERIOD + additional_lockup_duration 44 | return Claim(commitment, eth_block_redeemable) 45 | 46 | def claim_deposit(self, deposit_end): 47 | deposit = self.claimable_ranges[deposit_end] 48 | claim = self._construct_claim(deposit) 49 | self.claims.append(claim) 50 | return len(self.claims) - 1 51 | 52 | def claim_commitment(self, commitment, commitment_witness, claimability_witness): 53 | assert self.commitment_chain.validate_commitment(commitment, self.address, commitment_witness) 54 | assert commitment.state.predicate.can_claim(commitment, claimability_witness) 55 | claim = self._construct_claim(commitment) 56 | self.claims.append(claim) 57 | return len(self.claims) - 1 58 | 59 | def revoke_claim(self, state_id, claim_id, revocation_witness): 60 | claim = self.claims[claim_id] 61 | # Call can revoke to check if the predicate allows this revocation attempt 62 | assert claim.commitment.state.predicate.can_revoke(state_id, claim.commitment, revocation_witness) 63 | # Delete the claim 64 | self.claims[claim_id].is_revoked = True 65 | 66 | def remove_challenge(self, challenge_id): 67 | challenge = self.challenges[challenge_id] 68 | earlier_claim = self.claims[challenge.earlier_claim_id] 69 | assert earlier_claim.is_revoked 70 | # All checks have passed, we have an earlier claim that was revoked and the challenge is no longer valid. 71 | # Decrement the challenge count on the later claim 72 | self.claims[challenge.later_claim_id].num_challenges -= 1 73 | # Delete the challenge 74 | del self.challenges[challenge_id] 75 | 76 | def challenge_claim(self, earlier_claim_id, later_claim_id): 77 | earlier_claim = self.claims[earlier_claim_id] 78 | later_claim = self.claims[later_claim_id] 79 | # Make sure they overlap 80 | assert earlier_claim.commitment.start <= later_claim.commitment.end 81 | assert later_claim.commitment.start <= earlier_claim.commitment.end 82 | # Validate that the earlier claim is in fact earlier 83 | assert earlier_claim.commitment.plasma_block_number < later_claim.commitment.plasma_block_number 84 | # Make sure the later claim isn't already redeemable 85 | assert self.eth.block_number < later_claim.eth_block_redeemable 86 | # Create and record our new challenge 87 | new_challenge = Challenge(earlier_claim_id, later_claim_id) 88 | self.challenges.append(new_challenge) 89 | later_claim.num_challenges += 1 90 | # If the `eth_block_redeemable` of the earlier claim is longer than later claim, extend the later claim dispute period 91 | if later_claim.eth_block_redeemable < earlier_claim.eth_block_redeemable: 92 | later_claim.eth_block_redeemable = earlier_claim.eth_block_redeemable 93 | # Return our new challenge object 94 | return len(self.challenges) - 1 95 | 96 | def redeem_claim(self, claim_id, claimable_range_end): 97 | claim = self.claims[claim_id] 98 | # Check the claim's eth_block_redeemable has passed 99 | assert claim.eth_block_redeemable <= self.eth.block_number 100 | # Check that there are no open challenges for the claim 101 | assert claim.num_challenges == 0 102 | # Make sure that the claimable_range_end is actually in claimable_ranges 103 | assert claimable_range_end in self.claimable_ranges 104 | # Make sure the claim is within the claimable range 105 | assert claim.commitment.start >= self.claimable_ranges[claimable_range_end] 106 | assert claim.commitment.end <= claimable_range_end 107 | # Update claimable range 108 | if claim.commitment.start != self.claimable_ranges[claimable_range_end]: 109 | self.claimable_ranges[claim.commitment.start] = self.claimable_ranges[claimable_range_end] 110 | if claim.commitment.end != claimable_range_end: 111 | self.claimable_ranges[claimable_range_end] = claim.commitment.end 112 | # Approve coins for spending in predicate 113 | self.erc20_contract.approve(self.address, claim.commitment.state.predicate, claim.commitment.end - claim.commitment.start) 114 | # Finally redeem the claim 115 | claim.commitment.state.predicate.claim_redeemed(claim) 116 | -------------------------------------------------------------------------------- /gen-plasma/predicates/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karlfloersch/research/3270735bb3e3d317bb21ec4a3256d952cbdc9c7c/gen-plasma/predicates/__init__.py -------------------------------------------------------------------------------- /gen-plasma/predicates/multisig.py: -------------------------------------------------------------------------------- 1 | class MultiSigRevocationWitness: 2 | def __init__(self, next_state_commitment, signatures, inclusion_witness): 3 | self.next_state_commitment = next_state_commitment 4 | self.signatures = signatures 5 | self.inclusion_witness = inclusion_witness 6 | 7 | class MultiSigPredicate: 8 | dispute_duration = 10 9 | 10 | def __init__(self, parent_settlement_contract): 11 | self.parent = parent_settlement_contract 12 | 13 | def can_claim(self, commitment, witness): 14 | # Anyone can submit a claim 15 | return True 16 | 17 | def can_revoke(self, state_id, commitment, revocation_witness): 18 | # Check the state_id is in the commitment 19 | assert commitment.start <= state_id and commitment.end > state_id 20 | # Check the state_id is in the revocation_witness commitment 21 | assert revocation_witness.next_state_commitment.start <= state_id and revocation_witness.next_state_commitment.end > state_id 22 | # Check inclusion proof 23 | assert self.parent.commitment_chain.validate_commitment(revocation_witness.next_state_commitment, 24 | self.parent.address, 25 | revocation_witness.inclusion_witness) 26 | # Check that all owners signed off on the change 27 | assert commitment.state.recipient == revocation_witness.signatures 28 | # Check that the spend is after the claim state 29 | assert commitment.plasma_block_number < revocation_witness.next_state_commitment.plasma_block_number 30 | return True 31 | 32 | def claim_redeemed(self, claim, call_data=None): 33 | # Extract required information from call data 34 | recipients_sigs, destination = call_data 35 | # Check that the resolution is signed off on by all parties in the multisig 36 | assert recipients_sigs == claim.commitment.state.recipient 37 | # Transfer funds to the recipient 38 | self.parent.erc20_contract.transferFrom(self, destination, claim.commitment.end - claim.commitment.start) 39 | 40 | def get_additional_lockup(self, state): 41 | return 0 42 | -------------------------------------------------------------------------------- /gen-plasma/predicates/ownership.py: -------------------------------------------------------------------------------- 1 | class OwnershipRevocationWitness: 2 | def __init__(self, next_state_commitment, signature, inclusion_witness): 3 | self.next_state_commitment = next_state_commitment 4 | self.signature = signature 5 | self.inclusion_witness = inclusion_witness 6 | 7 | class OwnershipPredicate: 8 | dispute_duration = 10 9 | 10 | def __init__(self, parent_settlement_contract): 11 | self.parent = parent_settlement_contract 12 | 13 | def can_claim(self, commitment, witness): 14 | # Anyone can submit a claim 15 | assert commitment.state.owner == witness 16 | return True 17 | 18 | def can_revoke(self, state_id, commitment, revocation_witness): 19 | # Check the state_id is in the commitment 20 | assert commitment.start <= state_id and commitment.end > state_id 21 | # Check the state_id is in the revocation_witness commitment 22 | assert revocation_witness.next_state_commitment.start <= state_id and revocation_witness.next_state_commitment.end > state_id 23 | # Check inclusion proof 24 | assert self.parent.commitment_chain.validate_commitment(revocation_witness.next_state_commitment, 25 | self.parent.address, 26 | revocation_witness.inclusion_witness) 27 | # Check that the previous owner signed off on the change 28 | assert commitment.state.owner == revocation_witness.signature 29 | # Check that the spend is after the claim state 30 | assert commitment.plasma_block_number < revocation_witness.next_state_commitment.plasma_block_number 31 | return True 32 | 33 | def claim_redeemed(self, claim, call_data=None): 34 | # Transfer funds to the owner 35 | self.parent.erc20_contract.transferFrom(self, claim.commitment.state.owner, claim.commitment.end - claim.commitment.start) 36 | 37 | def get_additional_lockup(self, state): 38 | return 0 39 | -------------------------------------------------------------------------------- /gen-plasma/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karlfloersch/research/3270735bb3e3d317bb21ec4a3256d952cbdc9c7c/gen-plasma/test/__init__.py -------------------------------------------------------------------------------- /gen-plasma/test/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from utils import ERC20, User, Eth 3 | from erc20_plasma_contract import Erc20PlasmaContract 4 | from predicates.ownership import OwnershipPredicate 5 | from commitment_chain_contract import CommitmentChainContract 6 | 7 | @pytest.fixture 8 | def alice(): 9 | return User('alice') 10 | 11 | @pytest.fixture 12 | def bob(): 13 | return User('bob') 14 | 15 | @pytest.fixture 16 | def charlie(): 17 | return User('charlie') 18 | 19 | @pytest.fixture 20 | def mallory(): 21 | return User('mallory') 22 | 23 | @pytest.fixture 24 | def erc20_ct(alice, bob, mallory): 25 | return ERC20({alice.address: 1000, bob.address: 1000, mallory.address: 1000}) 26 | 27 | @pytest.fixture 28 | def eth(): 29 | return Eth(0) 30 | 31 | @pytest.fixture 32 | def operator(): 33 | return User('Operator') 34 | 35 | @pytest.fixture 36 | def erc20_plasma_ct(eth, operator, erc20_ct): 37 | eth = Eth(0) 38 | commitment_chain_contract = CommitmentChainContract(operator.address) 39 | return Erc20PlasmaContract(eth, 'erc20_plasma_ct', erc20_ct, commitment_chain_contract, 10) 40 | 41 | @pytest.fixture 42 | def ownership_predicate(erc20_plasma_ct): 43 | return OwnershipPredicate(erc20_plasma_ct) 44 | -------------------------------------------------------------------------------- /gen-plasma/test/predicates/test_multisig.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from utils import State 3 | from predicates.multisig import MultiSigTransitionWitness, MultiSigPredicate 4 | 5 | @pytest.fixture 6 | def multisig_predicate(erc20_plasma_ct): 7 | return MultiSigPredicate(erc20_plasma_ct) 8 | 9 | def skip_test_submit_claim_on_deposit(alice, bob, charlie, erc20_plasma_ct, multisig_predicate): 10 | state0_alice_and_bob_deposit = erc20_plasma_ct.deposit_ERC20(alice.address, 11 | 100, 12 | multisig_predicate, 13 | {'recipient': [alice.address, bob.address]}) 14 | # Try submitting claim 15 | erc20_plasma_ct.submit_claim(state0_alice_and_bob_deposit) 16 | # Check the claim was recorded 17 | assert len(erc20_plasma_ct.claim_queues) == 1 18 | 19 | def skip_test_submit_claim_on_transaction(alice, bob, charlie, erc20_plasma_ct, multisig_predicate): 20 | # Deposit and send a tx 21 | state0_alice_and_bob_deposit = erc20_plasma_ct.deposit_ERC20(alice.address, 22 | 100, 23 | multisig_predicate, 24 | {'recipient': [alice.address, bob.address]}) 25 | state1_alice_and_bob = State(state0_alice_and_bob_deposit.coin_id, 26 | 0, 27 | multisig_predicate, 28 | {'recipient': [charlie.address]}) 29 | erc20_plasma_ct.add_commitment([state1_alice_and_bob]) # Add the tx to the first commitment 30 | # Try submitting claim 31 | erc20_plasma_ct.submit_claim(state1_alice_and_bob, 0) 32 | # Check the claim was recorded 33 | assert len(erc20_plasma_ct.claim_queues) == 1 34 | 35 | def skip_test_submit_dispute_on_deposit(alice, bob, charlie, erc20_plasma_ct, multisig_predicate): 36 | # Deposit and send a tx 37 | state0_alice_and_bob_deposit = erc20_plasma_ct.deposit_ERC20(alice.address, 38 | 100, 39 | multisig_predicate, 40 | {'recipient': [alice.address, bob.address]}) 41 | state1_alice_and_bob = State(state0_alice_and_bob_deposit.coin_id, 42 | 0, 43 | multisig_predicate, 44 | {'recipient': [charlie.address]}) 45 | erc20_plasma_ct.add_commitment([state1_alice_and_bob]) # Add the tx to the first commitment 46 | # Create witness based on this commitment 47 | transition_witness0_alice_and_bob = MultiSigTransitionWitness([alice.address, bob.address], 0) 48 | # Try submitting claim on deposit 49 | deposit_claim = erc20_plasma_ct.submit_claim(state0_alice_and_bob_deposit) 50 | # Check the claim was recorded 51 | assert len(erc20_plasma_ct.claim_queues[state1_alice_and_bob.coin_id]) == 1 52 | # Now bob disputes claim with the spend 53 | erc20_plasma_ct.dispute_claim(bob.address, deposit_claim, transition_witness0_alice_and_bob, state1_alice_and_bob) 54 | # Check the claim was deleted 55 | assert len(erc20_plasma_ct.claim_queues[state1_alice_and_bob.coin_id]) == 0 56 | 57 | def skip_test_invalid_tx_exit_queue_resolution(alice, bob, mallory, erc20_plasma_ct, multisig_predicate, erc20_ct): 58 | # Deposit and commit to an invalid state 59 | state0_alice_and_bob_deposit = erc20_plasma_ct.deposit_ERC20(alice.address, 60 | 100, 61 | multisig_predicate, 62 | {'recipient': [alice.address, bob.address]}) 63 | state1_mallory_to_mallory = State(state0_alice_and_bob_deposit.coin_id, 64 | 0, 65 | multisig_predicate, 66 | {'recipient': [mallory.address]}) 67 | erc20_plasma_ct.add_commitment([state1_mallory_to_mallory]) # Add the invalid tx to the first commitment 68 | # Submit a claim for the invalid state 69 | invalid_claim = erc20_plasma_ct.submit_claim(state1_mallory_to_mallory, 0) 70 | # Alice notices the invalid claim, and submits her own claim. Note that it is based on her deposit which is before the tx 71 | valid_claim = erc20_plasma_ct.submit_claim(state0_alice_and_bob_deposit) 72 | # Wait for the dispute period to end. 73 | erc20_plasma_ct.eth.block_number += multisig_predicate.dispute_duration 74 | # Mallory attempts and fails to withdraw because there's another claim with priority 75 | try: 76 | erc20_plasma_ct.resolve_claim(mallory.address, invalid_claim) 77 | throws = False 78 | except Exception: 79 | throws = True 80 | assert throws 81 | # Now alice and bob agree to send the money to a new on-chain multisig 82 | erc20_plasma_ct.resolve_claim(alice.address, valid_claim, ([alice.address, bob.address], 'on chain multisig address')) 83 | # Check that the balances have updated 84 | assert erc20_ct.balanceOf('on chain multisig address') == 100 85 | assert erc20_ct.balanceOf(erc20_plasma_ct.address) == 0 86 | -------------------------------------------------------------------------------- /gen-plasma/test/predicates/test_ownership.py: -------------------------------------------------------------------------------- 1 | from utils import State, Commitment 2 | from predicates.ownership import OwnershipRevocationWitness 3 | 4 | def test_submit_claim_on_deposit(alice, erc20_plasma_ct, ownership_predicate): 5 | commit0_alice_deposit = erc20_plasma_ct.deposit(alice.address, 100, ownership_predicate, {'owner': alice.address}) 6 | # Try submitting claim 7 | erc20_plasma_ct.claim_deposit(commit0_alice_deposit.end) 8 | # Check the claim was recorded 9 | assert len(erc20_plasma_ct.claims) == 1 10 | 11 | def test_submit_claim_on_commitment(alice, bob, operator, erc20_plasma_ct, ownership_predicate): 12 | # Deposit and send a tx 13 | commit0_alice_deposit = erc20_plasma_ct.deposit(alice.address, 100, ownership_predicate, {'owner': alice.address}) # Add deposit 14 | state_bob_ownership = State(ownership_predicate, {'owner': bob.address}) 15 | commit1_alice_to_bob = Commitment(state_bob_ownership, commit0_alice_deposit.start, commit0_alice_deposit.end, 0) # Create commitment 16 | # Add the commit 17 | erc20_plasma_ct.commitment_chain.commit_block(operator.address, {erc20_plasma_ct.address: [commit1_alice_to_bob]}) 18 | # Try submitting claim 19 | claim_id = erc20_plasma_ct.claim_commitment(commit1_alice_to_bob, 'merkle proof', bob.address) 20 | # Check the claim was recorded 21 | assert len(erc20_plasma_ct.claims) == 1 22 | # Now increment the eth block to the redeemable block 23 | erc20_plasma_ct.eth.block_number = erc20_plasma_ct.claims[claim_id].eth_block_redeemable 24 | # Finally try withdrawing the money! 25 | erc20_plasma_ct.redeem_claim(claim_id, commit1_alice_to_bob.end) 26 | # Check bob's balance! 27 | assert erc20_plasma_ct.erc20_contract.balanceOf(bob.address) == 1100 # 1100 comes from bob having been sent 100 & already having 1000 28 | 29 | def test_revoke_claim_on_deposit(alice, bob, operator, erc20_plasma_ct, ownership_predicate): 30 | # Deposit and send a tx 31 | commit0_alice_deposit = erc20_plasma_ct.deposit(alice.address, 100, ownership_predicate, {'owner': alice.address}) # Add deposit 32 | state_bob_ownership = State(ownership_predicate, {'owner': bob.address}) 33 | commit1_alice_to_bob = Commitment(state_bob_ownership, commit0_alice_deposit.start, commit0_alice_deposit.end, 0) # Create commitment 34 | # Add the commitment 35 | erc20_plasma_ct.commitment_chain.commit_block(operator.address, {erc20_plasma_ct.address: [commit1_alice_to_bob]}) 36 | revocation_witness0_alice_to_bob = OwnershipRevocationWitness(commit1_alice_to_bob, alice.address, 'merkle proof') 37 | # Try submitting claim on deposit 38 | deposit_claim_id = erc20_plasma_ct.claim_deposit(100) 39 | # Check the claim was recorded 40 | assert len(erc20_plasma_ct.claims) == 1 41 | # Now bob revokes the claim with the spend inside the revocation witness 42 | erc20_plasma_ct.revoke_claim(10, deposit_claim_id, revocation_witness0_alice_to_bob) 43 | # Check the claim was revoked 44 | assert erc20_plasma_ct.claims[deposit_claim_id].is_revoked 45 | 46 | def test_challenge_claim_with_invalid_state(alice, mallory, operator, erc20_plasma_ct, ownership_predicate): 47 | # Deposit and commit to invalid state 48 | commit0_alice_deposit = erc20_plasma_ct.deposit(alice.address, 100, ownership_predicate, {'owner': alice.address}) # Add deposit 49 | # Check that alice's balance was reduced 50 | assert erc20_plasma_ct.erc20_contract.balanceOf(alice.address) == 900 51 | # Uh oh! Malory creates an invalid state & commits it!!! 52 | state_mallory_ownership = State(ownership_predicate, {'owner': mallory.address}) 53 | invalid_commit1_alice_to_mallory = Commitment(state_mallory_ownership, 54 | commit0_alice_deposit.start, 55 | commit0_alice_deposit.end, 56 | 0) # Create commitment 57 | # Add the commitment 58 | erc20_plasma_ct.commitment_chain.commit_block(operator.address, {erc20_plasma_ct.address: [invalid_commit1_alice_to_mallory]}) 59 | # Submit a claim for the invalid state 60 | invalid_commitment_claim_id = erc20_plasma_ct.claim_commitment(invalid_commit1_alice_to_mallory, 'merkle proof', mallory.address) 61 | # Oh no! Alice notices bad behavior and attempts withdrawal of deposit state 62 | deposit_claim_id = erc20_plasma_ct.claim_deposit(commit0_alice_deposit.end) 63 | # Alice isn't letting that other claim go through. She challenges it with her deposit! 64 | challenge = erc20_plasma_ct.challenge_claim(deposit_claim_id, invalid_commitment_claim_id) 65 | # Verify that the challenge was recorded 66 | assert challenge is not None and len(erc20_plasma_ct.challenges) == 1 67 | # Fast forward in time until the eth block allows the claim to be redeemable 68 | erc20_plasma_ct.eth.block_number = erc20_plasma_ct.claims[invalid_commitment_claim_id].eth_block_redeemable 69 | # Mallory attempts and fails to withdraw because there's another claim with priority 70 | try: 71 | erc20_plasma_ct.redeem_claim(mallory.address, invalid_commit1_alice_to_mallory.end) 72 | throws = False 73 | except Exception: 74 | throws = True 75 | assert throws 76 | # Now instead alice withdraws 77 | erc20_plasma_ct.redeem_claim(deposit_claim_id, erc20_plasma_ct.claims[deposit_claim_id].commitment.end) 78 | # Check that alice was sent her money! 79 | assert erc20_plasma_ct.erc20_contract.balanceOf(alice.address) == 1000 80 | 81 | def test_redeem_challenged_claim(alice, mallory, operator, erc20_plasma_ct, ownership_predicate): 82 | # Deposit and then submit an invalid challenge 83 | commit0_mallory_deposit = erc20_plasma_ct.deposit(mallory.address, 100, ownership_predicate, {'owner': mallory.address}) # Add deposit 84 | # Create a new state & commitment for alice ownership 85 | state_alice_ownership = State(ownership_predicate, {'owner': alice.address}) 86 | commit1_mallory_to_alice = Commitment(state_alice_ownership, commit0_mallory_deposit.start, commit0_mallory_deposit.end, 0) # Create commitment 87 | # Add the commit 88 | erc20_plasma_ct.commitment_chain.commit_block(operator.address, {erc20_plasma_ct.address: [commit1_mallory_to_alice]}) 89 | # Now alice wants to withdraw, so submit a new claim on the funds 90 | claim_id = erc20_plasma_ct.claim_commitment(commit1_mallory_to_alice, 'merkle proof', alice.address) 91 | # Uh oh! Mallory decides to withdraw and challenge the claim 92 | revoked_claim_id = erc20_plasma_ct.claim_deposit(commit0_mallory_deposit.end) 93 | challenge_id = erc20_plasma_ct.challenge_claim(revoked_claim_id, claim_id) 94 | # This revoked claim is then swiftly canceled by alice 95 | revocation_witness0_mallory_to_alice = OwnershipRevocationWitness(commit1_mallory_to_alice, mallory.address, 'merkle proof') 96 | erc20_plasma_ct.revoke_claim(10, revoked_claim_id, revocation_witness0_mallory_to_alice) 97 | # Remove the challenge for the revoked claim 98 | erc20_plasma_ct.remove_challenge(challenge_id) 99 | # Increment the eth block number 100 | erc20_plasma_ct.eth.block_number = erc20_plasma_ct.claims[claim_id].eth_block_redeemable 101 | # Now alice can withdraw! 102 | erc20_plasma_ct.redeem_claim(claim_id, erc20_plasma_ct.claims[claim_id].commitment.end) 103 | # Check that alice was sent her money! 104 | assert erc20_plasma_ct.erc20_contract.balanceOf(alice.address) == 1100 105 | -------------------------------------------------------------------------------- /gen-plasma/test/test_erc20_plasma_contract.py: -------------------------------------------------------------------------------- 1 | from utils import State, Commitment 2 | 3 | def test_deposit(alice, erc20_ct, erc20_plasma_ct, ownership_predicate): 4 | # Deposit some funds 5 | erc20_plasma_ct.deposit(alice.address, 100, ownership_predicate, {'recipient': alice.address}) 6 | # Assert the balances have changed 7 | assert erc20_ct.balanceOf(alice.address) == 900 8 | assert erc20_ct.balanceOf(erc20_plasma_ct.address) == 100 9 | # Assert that we recorded the deposit and incremented total_deposits 10 | assert len(erc20_plasma_ct.claimable_ranges) == 1 and isinstance(next(iter(erc20_plasma_ct.claimable_ranges.values())), Commitment) 11 | assert erc20_plasma_ct.total_deposits == 100 12 | 13 | def test_commitments(alice, bob, operator, erc20_plasma_ct, ownership_predicate): 14 | # Deposit some funds 15 | commit0_alice_deposit = erc20_plasma_ct.deposit(alice.address, 100, ownership_predicate, {'recipient': alice.address}) 16 | commit1_bob_deposit = erc20_plasma_ct.deposit(alice.address, 100, ownership_predicate, {'recipient': bob.address}) 17 | # Create the new state updates which we plan to commit 18 | state_bob_ownership = State(ownership_predicate, {'recipient': bob.address}) 19 | state_alice_ownership = State(ownership_predicate, {'recipient': alice.address}) 20 | # Create the commitment objects based on the states which will be included in plasma blocks 21 | commit2_alice_to_bob = Commitment(state_bob_ownership, commit0_alice_deposit.start, commit0_alice_deposit.end, 0) 22 | commit3_bob_to_alice = Commitment(state_alice_ownership, commit1_bob_deposit.start, commit1_bob_deposit.end, 0) 23 | # Add the commitments 24 | erc20_plasma_ct.commitment_chain.commit_block(operator.address, {erc20_plasma_ct.address: [commit2_alice_to_bob, commit3_bob_to_alice]}) 25 | # Assert inclusion of our commitments 26 | assert erc20_plasma_ct.commitment_chain.validate_commitment(commit2_alice_to_bob, erc20_plasma_ct.address, None) 27 | assert erc20_plasma_ct.commitment_chain.validate_commitment(commit3_bob_to_alice, erc20_plasma_ct.address, None) 28 | -------------------------------------------------------------------------------- /gen-plasma/test/test_utils.py: -------------------------------------------------------------------------------- 1 | def test_erc20_transfers(alice, bob, erc20_ct): 2 | assert erc20_ct.balanceOf(alice.address) == 1000 and erc20_ct.balanceOf(bob.address) == 1000 3 | erc20_ct.transferFrom(alice.address, bob.address, 500) 4 | assert erc20_ct.balanceOf(alice.address) == 500 and erc20_ct.balanceOf(bob.address) == 1500 5 | -------------------------------------------------------------------------------- /gen-plasma/utils.py: -------------------------------------------------------------------------------- 1 | class Eth: 2 | ''' Eth object contains information about the mocked Ethereum blockchain ''' 3 | def __init__(self, block_number): 4 | self.block_number = block_number 5 | 6 | class User: 7 | ''' 8 | User class which is given an address and can be used to generate fake signatures. 9 | Note addresses here are human-readable & signatures are not secure--all to facilitate testing. 10 | ''' 11 | 12 | def __init__(self, address): 13 | self.address = address 14 | 15 | def sign(self, message): 16 | return {'message': message, 'signature': self.address} 17 | 18 | def __str__(self): 19 | return self.address 20 | 21 | class ERC20: 22 | ''' ERC20-like python contract ''' 23 | 24 | def __init__(self, initial_balances): 25 | self.balances = initial_balances 26 | 27 | def balanceOf(self, token_holder): 28 | return self.balances[token_holder] 29 | 30 | def approve(self, sender, recipient, amount): 31 | # Because everyone is honest, approve just transfers the funds for now... 32 | self.transferFrom(sender, recipient, amount) 33 | return True 34 | 35 | def transferFrom(self, sender, recipient, tokens): 36 | assert self.balances[sender] >= tokens 37 | self.balances[sender] -= tokens 38 | if recipient not in self.balances: 39 | self.balances[recipient] = 0 40 | self.balances[recipient] += tokens 41 | return True 42 | 43 | class State: 44 | def __init__(self, predicate, parameters): 45 | self.predicate = predicate 46 | for key in parameters: 47 | setattr(self, key, parameters[key]) 48 | 49 | class Commitment: 50 | def __init__(self, state, start, end, plasma_block_number): 51 | assert isinstance(state, State) 52 | self.state = state 53 | self.start = start 54 | self.end = end 55 | self.plasma_block_number = plasma_block_number 56 | 57 | class Claim: 58 | def __init__(self, commitment, eth_block_redeemable): 59 | self.commitment = commitment 60 | self.eth_block_redeemable = eth_block_redeemable 61 | self.num_challenges = 0 62 | 63 | class Challenge: 64 | def __init__(self, earlier_claim_id, later_claim_id): 65 | self.earlier_claim_id = earlier_claim_id 66 | self.later_claim_id = later_claim_id 67 | self.is_revoked = False 68 | 69 | class ClaimQueue: 70 | def __init__(self, initial_claim): 71 | self.claims = {} 72 | # TODO: Dispute duration should change for everything to the right of the plasma_block_number 73 | self.dispute_duration = 0 74 | self.is_open = True 75 | self.add(initial_claim) 76 | 77 | def add(self, claim): 78 | assert self.is_open 79 | if self.dispute_duration < claim.state.new_predicate.dispute_duration: 80 | self.dispute_duration = claim.state.new_predicate.dispute_duration 81 | self.claims[claim.state.plasma_block_number] = claim 82 | 83 | def __len__(self): 84 | return len(self.claims) 85 | 86 | def remove(self, claim): 87 | assert self.is_open 88 | del self.claims[claim.state.plasma_block_number] 89 | 90 | def first(self): 91 | return self.claims[sorted(self.claims.keys())[0]] 92 | 93 | def close(self): 94 | self.is_open = False 95 | -------------------------------------------------------------------------------- /plasma-exit-games/exit_game_sim.py: -------------------------------------------------------------------------------- 1 | # import sys 2 | 3 | # Color lib for pretty output 4 | class bcolors: 5 | GREEN = '\033[92m' 6 | PINK = '\033[95m' 7 | PURPLE = '\033[94m' 8 | YELLOW = '\033[93m' 9 | RED = '\033[91m' 10 | ENDC = '\033[0m' 11 | BOLD = '\033[1m' 12 | UNDERLINE = '\033[4m' 13 | def red(string): 14 | return bcolors.RED + string + bcolors.ENDC 15 | def green(string): 16 | return bcolors.GREEN + string + bcolors.ENDC 17 | def yellow(string): 18 | return bcolors.YELLOW + string + bcolors.ENDC 19 | def purp(string): 20 | return bcolors.PURPLE + string + bcolors.ENDC 21 | def pink(string): 22 | return bcolors.PINK + string + bcolors.ENDC 23 | 24 | # Data Structures 25 | class Coin: 26 | def __init__(self, id, prev_block, sender, recipient): 27 | self.id = id 28 | self.prev_block = prev_block 29 | self.sender = sender 30 | self.recipient = recipient 31 | 32 | def __str__(self): 33 | coin = 'id: ' + purp(str(self.id)) 34 | prev_block = ', prev_block: ' + purp(str(self.prev_block)) 35 | sender = ', sender: ' + self.sender 36 | recipient = ', recipient: ' + self.recipient 37 | return str('[' + coin + prev_block + sender + recipient + ']') 38 | 39 | class Block: 40 | def __init__(self, number, coins): 41 | self.coins = coins 42 | self.number = number 43 | 44 | def __str__(self): 45 | output = '' 46 | for coin in self.coins: 47 | output += str(coin) + '\n' 48 | return output 49 | 50 | def includes(self, coin): 51 | return coin in self.coins 52 | 53 | class Exit: 54 | def __init__(self, eth_block_number, coin_id, owner, plasma_block): 55 | self.eth_block_number = eth_block_number 56 | self.coin_id = coin_id 57 | self.owner = owner 58 | self.plasma_block = plasma_block 59 | 60 | def cancel(self, spend_coin): 61 | if (self.plasma_block == spend_coin.prev_plasma_block and self.owner == spend_coin.sender): 62 | return True 63 | return False 64 | 65 | class PlasmaContract: 66 | def __init__(self): 67 | self.eth_block_number = 0 # the current eth block number -- this can be used to simulate time passing in the exit game 68 | self.exit_queues = dict() # coin_id -> plasma_block_number -> exit_object 69 | self.exited_coins = dict() # coin_id -> True / False 70 | 71 | def exit_coin(self, coin_id, owner, plasma_block): 72 | new_exit = Exit(self.eth_block_number, coin_id, owner, plasma_block) # create a new exit object 73 | if coin_id not in self.exit_queues: 74 | self.exit_queues[coin_id] = dict() 75 | self.exit_queues[coin_id][new_exit.plasma_block.number] = new_exit # add this exit to the queue 76 | return new_exit # return our new exit object 77 | 78 | def cancel_exit(self, exit, coin_spend): 79 | # Check that the coin which spends the exit is from the owner 80 | # assert exit.owner == coin_spend.sender 81 | # ...and that the coin which spends the exit references that coin with prev_block 82 | # assert exit.plasma_block == coin_spend.prev_block 83 | # ...and that the exit is really in the block 84 | # ... 85 | # Then cancel the exit 86 | # del self.exit_queues[exit.coin_id][exit.plasma_block.number] 87 | pass 88 | 89 | def finalize_exit(self, exit): 90 | # Check if the coin has already been exited 91 | # assert self.exit in self.exited_coins 92 | # ...and that the exit period is over 93 | # assert exit.eth_block_number + EXIT_PERIOD < self.eth_block_number 94 | # Determine the exit winner 95 | # winner = the lowest `plasma_block_number` inside of self.exit_queues[exit.coin_id] 96 | # Record this exit 97 | # self.exited_coins[exit.exit_id] = True 98 | pass 99 | 100 | def validate_coin(state, coin): 101 | ''' Check if a particular state & coin combination is valid ''' 102 | if state[coin.id]['block'] != coin.prev_block: 103 | raise Exception('Invalid prev_block: ' + str(coin.prev_block) + '. Expected: ' + str(state[coin.id]['block'])) 104 | if coin.sender != state[coin.id]['owner']: 105 | raise Exception('Invalid sender:' + str(coin.sender) + '. Expected: ' + state[coin.id].owner) 106 | return True 107 | 108 | def apply_block(state, block): 109 | ''' Applies the block & thows if the block is invalid ''' 110 | for coin in block.coins: 111 | print(coin) 112 | if coin.sender == DEPOSIT or validate_coin(state, coin): 113 | state[coin.id] = { 114 | 'block': state['block'], 115 | 'owner': coin.recipient 116 | } 117 | else: 118 | raise Exception('Invalid tx:' + str(coin)) 119 | state['block'] += 1 120 | 121 | def apply_blocks(state, blocks): 122 | for block in blocks: 123 | apply_block(state, block) 124 | return state 125 | 126 | DEPOSIT = pink('DEPOSIT') 127 | alice_sigs = [yellow('alice1'), yellow('alice2')] 128 | bob_sigs = [green('bob')] 129 | 130 | # TODO: Test spent coin 131 | state = {'block': 0} # Genesis state 132 | coins = [ # Coins 133 | Coin(0, 0, DEPOSIT, alice_sigs[0]), 134 | Coin(0, 0, alice_sigs[0], bob_sigs[0]), 135 | Coin(0, 1, bob_sigs[0], alice_sigs[1]) 136 | ] 137 | blocks = [ # Blocks 138 | Block(0, [coins[0]]), 139 | Block(1, [coins[1]]), 140 | Block(2, [coins[2]]) 141 | ] 142 | # State after having applied blocks -- this just checks validity. Note validity is not checked in the smart contract 143 | state = apply_blocks(state, blocks) 144 | print(state) # this state stuff is mostly for checking if we have any invalid blocks 145 | 146 | # Now that we know all the blocks are valid and can calculate the correct owner, let's try exiting a spent coin 147 | plasma_contract = PlasmaContract() # create new Plasma contract 148 | plasma_contract.exit_coin(0, alice_sigs[0], blocks[0]) # try to exit spent coin 149 | # TODO: Challenge spent coin 150 | # TODO: Test exiting an invalid coin and then reacting 151 | # TODO: Test normal exits... test everything! 152 | -------------------------------------------------------------------------------- /plasma0/README.md: -------------------------------------------------------------------------------- 1 | **WARNING:** this is an experimental project. not for mainnet deployment. 2 | 3 | I'm currently in the process of rewriting the client-side construction of inclusion proofs for the exit game. Also, the vyper contract is still missing some challenge mechanisms. **Don't deposit any testnet eth if you plan on keeping it.** 4 | 5 | # installation 6 | ``` 7 | git clone https://github.com/endorphin/plasmaprime.git 8 | cd plasmaprime 9 | pip install -r requirements.txt 10 | python setup.py install 11 | ``` 12 | # client usage 13 | create a new account (send your testnet eth to the created address): 14 | ``` 15 | cd client 16 | plasma new 17 | ``` 18 | deposit some test eth (all values are in wei): 19 | ``` 20 | plasma deposit --contract
--amount 21 | ``` 22 | send a transaction: 23 | ``` 24 | plasma send --to
--amount 25 | ``` 26 | check your balance (and what ranges you own): 27 | ``` 28 | plasma query 29 | ``` 30 | watch for malicious exits (**not yet implemented**): 31 | ``` 32 | plasma guard 33 | ``` 34 | withdraw: 35 | ``` 36 | plasma exit --amount 37 | ``` 38 | # protocol 39 | transaction format: 40 | ``` 41 | { 42 | sender: address, 43 | recipient: address, 44 | start: uint, 45 | offset: uint, 46 | signature: { 47 | sig_v: uint, 48 | sig_r: uint, 49 | sig_s: uint, 50 | }, 51 | } 52 | ``` 53 | # todo 54 | - challenges for exits that include coins previously exited 55 | - challenges for transaction history 56 | - challenges for subsequent spends 57 | - implement plasma guard 58 | - concise exclusion proofs with RSA accumulators 59 | - proper merkle sum tree 60 | 61 | 62 | 63 | # Plan 64 | - Dynamodb for DB 65 | - Get lock with lambda for address->owned ranges. Atomic read / update. 66 | - Once updated, append transaction to the list of transactions 67 | - A block is generated based on the `START_TX_POS` and `END_TX_POS` which are the indexes 68 | of the transactions in our `TX_LIST`. That way there is no blocking needed. 69 | -------------------------------------------------------------------------------- /plasma0/contracts/plasmaprime.vy: -------------------------------------------------------------------------------- 1 | ####### DEPRECATED ####### 2 | ####### DEPRECATED ####### 3 | ####### DEPRECATED ####### 4 | ####### DEPRECATED ####### 5 | ####### DEPRECATED ####### 6 | ####### DEPRECATED ####### 7 | 8 | 9 | 10 | 11 | 12 | operator: public(address) 13 | deposits: public(wei_value[address]) 14 | total_deposits: public(wei_value) 15 | plasma_block_number: public(uint256) 16 | last_publish: public(uint256) # ethereum block number of most recent plasma block 17 | hash_chain: public(bytes32[uint256]) 18 | exits: public( 19 | { 20 | owner: address, 21 | plasma_block: uint256, 22 | eth_block: uint256, 23 | start: uint256, 24 | offset: uint256, 25 | challenge_count: uint256, 26 | }[uint256]) 27 | challenges: public( 28 | { 29 | exit_id: uint256, 30 | ongoing: bool, 31 | token_index: uint256, 32 | }[uint256]) 33 | exit_nonce: public(uint256) 34 | challenge_nonce: public(uint256) 35 | 36 | # period (of ethereum blocks) during which an exit can be challenged 37 | CHALLENGE_PERIOD: constant(uint256) = 20 38 | # minimum number of ethereum blocks between new plasma blocks 39 | PLASMA_BLOCK_INTERVAL: constant(uint256) = 10 40 | # 41 | MAX_TREE_DEPTH: constant(uint256) = 8 42 | 43 | # @public 44 | # def ecrecover_util(message_hash: bytes32, signature: bytes[65]) -> address: 45 | # v: uint256 = extract32(slice(signature, start=0, len=32), 0, type=uint256) 46 | # r: uint256 = extract32(slice(signature, start=32, len=64), 0, type=uint256) 47 | # s: bytes[1] = slice(signature, start=64, len=1) 48 | # s_pad: uint256 = extract32(s, 0, type=uint256) 49 | # 50 | # addr: address = ecrecover(message_hash, v, r, s_pad) 51 | # return addr 52 | 53 | @public 54 | def addr_to_bytes(addr: address) -> bytes[20]: 55 | addr_bytes32: bytes[32] = concat(convert(addr, bytes32), "") 56 | return slice(addr_bytes32, start=12, len=20) 57 | 58 | @public 59 | def plasma_message_hash( 60 | sender: address, 61 | recipient: address, 62 | start: uint256, 63 | offset: uint256, 64 | ) -> bytes32: 65 | return sha3(concat( 66 | self.addr_to_bytes(sender), 67 | self.addr_to_bytes(recipient), 68 | convert(start, bytes32), 69 | convert(offset, bytes32), 70 | )) 71 | 72 | @public 73 | def tx_hash( 74 | sender: address, 75 | recipient: address, 76 | start: uint256, 77 | offset: uint256, 78 | sig_v: uint256, 79 | sig_r: uint256, 80 | sig_s: uint256, 81 | ) -> bytes32: 82 | return sha3(concat( 83 | self.addr_to_bytes(sender), 84 | self.addr_to_bytes(recipient), 85 | convert(start, bytes32), 86 | convert(offset, bytes32), 87 | convert(sig_v, bytes32), 88 | convert(sig_r, bytes32), 89 | convert(sig_s, bytes32), 90 | )) 91 | 92 | @public 93 | def __init__(): 94 | self.operator = msg.sender 95 | self.total_deposits = 0 96 | self.plasma_block_number = 0 97 | self.exit_nonce = 0 98 | self.last_publish = 0 99 | self.challenge_nonce = 0 100 | 101 | @public 102 | @payable 103 | def deposit() -> uint256: 104 | r: uint256 = as_unitless_number(self.total_deposits) 105 | self.deposits[msg.sender] += msg.value 106 | self.total_deposits += msg.value 107 | return r 108 | 109 | @public 110 | def publish_hash(block_hash: bytes32): 111 | assert msg.sender == self.operator 112 | assert block.number >= self.last_publish + PLASMA_BLOCK_INTERVAL 113 | 114 | self.hash_chain[self.plasma_block_number] = block_hash 115 | self.plasma_block_number += 1 116 | self.last_publish = block.number 117 | 118 | @public 119 | def submit_exit(bn: uint256, start: uint256, offset: uint256) -> uint256: 120 | assert bn < self.plasma_block_number 121 | assert offset > 0 122 | assert offset <= as_unitless_number(self.total_deposits) 123 | 124 | en: uint256 = self.exit_nonce 125 | self.exits[en].owner = msg.sender 126 | self.exits[en].plasma_block = bn 127 | self.exits[en].eth_block = block.number 128 | self.exits[en].start = start 129 | self.exits[en].offset = offset 130 | self.exits[en].challenge_count = 0 131 | self.exit_nonce += 1 132 | return en 133 | 134 | @public 135 | def finalize_exit(exit_id: uint256): 136 | assert block.number >= self.exits[exit_id].eth_block + CHALLENGE_PERIOD 137 | assert self.exits[exit_id].challenge_count == 0 138 | 139 | send(self.exits[exit_id].owner, as_wei_value(self.exits[exit_id].offset, 'wei')) 140 | self.total_deposits -= as_wei_value(self.exits[exit_id].offset, 'wei') 141 | 142 | @public 143 | def challenge_completeness( 144 | exit_id: uint256, 145 | token_index: uint256, 146 | ) -> uint256: 147 | # check the exit being challenged exists 148 | assert exit_id < self.exit_nonce 149 | 150 | # check the token index being challenged is in the range being exited 151 | assert token_index >= self.exits[exit_id].start 152 | assert token_index < self.exits[exit_id].start + self.exits[exit_id].offset 153 | 154 | # store challenge 155 | cn: uint256 = self.challenge_nonce 156 | self.challenges[cn].exit_id = exit_id 157 | self.challenges[cn].ongoing = True 158 | self.challenges[cn].token_index = token_index 159 | self.exits[exit_id].challenge_count += 1 160 | 161 | self.challenge_nonce += 1 162 | return cn 163 | 164 | @public 165 | def respond_completeness( 166 | challenge_id: uint256, 167 | sender: address, 168 | recipient: address, 169 | start: uint256, 170 | offset: uint256, 171 | sig_v: uint256, 172 | sig_r: uint256, 173 | sig_s: uint256, 174 | proof: bytes32[8], 175 | ): 176 | assert self.challenges[challenge_id].ongoing == True 177 | 178 | exit_id: uint256 = self.challenges[challenge_id].exit_id 179 | exit_owner: address = self.exits[exit_id].owner 180 | exit_plasma_block: uint256 = self.exits[exit_id].plasma_block 181 | challenged_index: uint256 = self.challenges[challenge_id].token_index 182 | 183 | # compute message hash 184 | message_hash: bytes32 = self.plasma_message_hash(sender, recipient, start, offset) 185 | 186 | # check transaction is signed correctly 187 | addr: address = ecrecover(message_hash, sig_v, sig_r, sig_s) 188 | assert addr == sender 189 | 190 | # check exit owner is indeed recipient 191 | assert recipient == exit_owner 192 | 193 | # check transaction covers challenged index 194 | assert challenged_index >= start 195 | assert challenged_index < (start + offset) 196 | 197 | # check transaction was included in plasma block hash 198 | root: bytes32 = self.tx_hash( 199 | sender, 200 | recipient, 201 | start, 202 | offset, 203 | sig_v, 204 | sig_r, 205 | sig_s, 206 | ) 207 | for i in range(8): 208 | if convert(proof[i], uint256) == 0: 209 | break 210 | root = sha3(concat(root, proof[i])) 211 | assert root == self.hash_chain[exit_plasma_block] 212 | 213 | # response was successful 214 | self.challenges[challenge_id].ongoing = False 215 | self.exits[exit_id].challenge_count -= 1 216 | -------------------------------------------------------------------------------- /plasma0/plasmalib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karlfloersch/research/3270735bb3e3d317bb21ec4a3256d952cbdc9c7c/plasma0/plasmalib/__init__.py -------------------------------------------------------------------------------- /plasma0/plasmalib/constants.py: -------------------------------------------------------------------------------- 1 | 2 | CHALLENGE_PERIOD = 20 3 | PLASMA_BLOCK_INTERVAL = 10 4 | MAX_TREE_DEPTH = 8 5 | -------------------------------------------------------------------------------- /plasma0/plasmalib/old_tx_tree_utils.py: -------------------------------------------------------------------------------- 1 | # THIS FILE CAN SERVE AS REFERENCE FOR FUTURE IMPLEMENTATIONS, BUT SHOULD NOT BE USED DIRECTLY 2 | 3 | from ethereum.utils import sha3, bytes_to_int, int_to_bytes, encode_hex 4 | import json 5 | import random 6 | 7 | class EphemDB(): 8 | def __init__(self, kv=None): 9 | self.kv = kv or {} 10 | 11 | def get(self, k): 12 | return self.kv.get(k, None) 13 | 14 | def put(self, k, v): 15 | self.kv[k] = v 16 | 17 | def delete(self, k): 18 | del self.kv[k] 19 | 20 | def fill_tx_list_with_notxs(txs): 21 | next_start_pos = 0 22 | full_tx_list = [] 23 | for t in txs: 24 | # Add notx range if needed 25 | if t['contents']['start'] > next_start_pos: 26 | notx_offset = t['contents']['start'] - next_start_pos 27 | full_tx_list.append({'type': 'notx', 'contents': {'start': next_start_pos, 'offset': notx_offset}}) 28 | next_start_pos += notx_offset 29 | assert t['contents']['start'] == next_start_pos 30 | next_start_pos += t['contents']['offset'] 31 | full_tx_list.append(t) 32 | return full_tx_list 33 | 34 | def construct_tree(db, nodes): 35 | if len(nodes) < 2: 36 | return nodes[0] 37 | remaining_nodes = [] 38 | for i in range(0, len(nodes), 2): 39 | if i+1 == len(nodes): 40 | remaining_nodes.append(nodes[i]) 41 | break 42 | new_value = b''.join([nodes[i], nodes[i+1]]) 43 | new_sum = int_to_bytes(bytes_to_int(nodes[i+1][32:]) + bytes_to_int(nodes[i][32:])).rjust(8, b"\x00") 44 | new_hash = b''.join([sha3(new_value), new_sum]) 45 | print('Left:', encode_hex(nodes[i]), 'parent:', encode_hex(new_hash)) 46 | print('Right:', encode_hex(nodes[i+1]), 'parent:', encode_hex(new_hash)) 47 | db.put(new_hash, new_value) 48 | remaining_nodes.append(new_hash) 49 | return construct_tree(db, remaining_nodes) 50 | 51 | def is_json(myjson): 52 | try: 53 | json.loads(myjson) 54 | except ValueError: 55 | return False 56 | return True 57 | 58 | def get_proof_of_index(db, root, coin_id): 59 | if db.get(root) is None: 60 | return root 61 | proof = [root] 62 | 63 | def follow_path(node, total_offset, target): 64 | node_value = db.get(node) 65 | if is_json(node_value): 66 | return node_value 67 | left_sum = bytes_to_int(node_value[32:40]) 68 | if left_sum + total_offset > target: 69 | proof.insert(0, ('left', node_value[40:])) 70 | return follow_path(node_value[0:40], total_offset, target) 71 | else: 72 | proof.insert(0, ('right', node_value[0:40])) 73 | return follow_path(node_value[40:], total_offset + left_sum, target) 74 | follow_path(root, 0, coin_id) 75 | return proof 76 | 77 | # TODO: Clean this up! Eww! 78 | def get_txs_at_index(db, root, range_start): 79 | if db.get(root) is None: 80 | return root 81 | root = db.get(root) 82 | 83 | def follow_path(node, total_offset, target): 84 | print(node) 85 | if is_json(node): 86 | return node 87 | left_sum = bytes_to_int(node[32:40]) 88 | if left_sum + total_offset > target: 89 | return follow_path(db.get(node[0:40]), total_offset, target) 90 | else: 91 | return follow_path(db.get(node[40:]), total_offset + left_sum, target) 92 | 93 | return follow_path(root, 0, range_start) 94 | 95 | def get_sum_hash_of_tx(tx): 96 | offset = int_to_bytes(tx['contents']['offset']).rjust(8, b"\x00") 97 | tx_hash = sha3(json.dumps(tx)) 98 | return b''.join([tx_hash, offset]) 99 | 100 | def make_block_from_txs(db, txs): 101 | merkle_leaves = [] 102 | for t in txs: 103 | offset = int_to_bytes(t['contents']['offset']).rjust(8, b"\x00") 104 | tx_hash = sha3(json.dumps(t)) 105 | leaf = b''.join([tx_hash, offset]) # hash = leaf[:32] & sum = leaf[32:] 106 | db.put(leaf, json.dumps(t)) 107 | merkle_leaves.append(leaf) 108 | merkle_root = construct_tree(db, merkle_leaves) 109 | return merkle_root 110 | 111 | 112 | # tx = ['send', [[[parent_hash, parent_block],...], start, offset, recipient], signature] 113 | def generate_dummy_txs(num_txs, random_interval, total_deposits): 114 | txs = [] 115 | last_start = 0 116 | for i in range(num_txs): 117 | next_offset = random.randint(1, random_interval) 118 | if last_start + next_offset > total_deposits: 119 | break 120 | if bool(random.getrandbits(1)): 121 | last_start += next_offset 122 | continue 123 | txs.append({'type:': 'send', 'contents': {'start': last_start, 'offset': next_offset, 'owner': 'alice'}, 'sig': 'sig'}) 124 | last_start += next_offset 125 | return txs 126 | 127 | def generate_dummy_block(db, num_txs, random_interval, total_deposits): 128 | full_tx_list = fill_tx_list_with_notxs(generate_dummy_txs(num_txs, random_interval, total_deposits)) 129 | merkle_leaves = [] 130 | for t in full_tx_list: 131 | offset = int_to_bytes(t['contents']['offset']).rjust(8, b"\x00") 132 | tx_hash = sha3(json.dumps(t)) 133 | leaf = b''.join([tx_hash, offset]) # hash = leaf[:32] & sum = leaf[32:] 134 | db.put(leaf, json.dumps(t)) 135 | merkle_leaves.append(leaf) 136 | merkle_root = construct_tree(db, merkle_leaves) 137 | return merkle_root 138 | -------------------------------------------------------------------------------- /plasma0/plasmalib/operator/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karlfloersch/research/3270735bb3e3d317bb21ec4a3256d952cbdc9c7c/plasma0/plasmalib/operator/__init__.py -------------------------------------------------------------------------------- /plasma0/plasmalib/operator/block_generator.py: -------------------------------------------------------------------------------- 1 | from plasmalib.utils import NullTx, bytes_to_int 2 | from eth_utils import int_to_big_endian 3 | from web3 import Web3 4 | 5 | class EphemDB(): 6 | def __init__(self, kv=None): 7 | self.kv = kv or {} 8 | 9 | def get(self, k): 10 | return self.kv.get(k, None) 11 | 12 | def put(self, k, v): 13 | self.kv[k] = v 14 | 15 | def delete(self, k): 16 | del self.kv[k] 17 | 18 | class TxBucket(): 19 | def __init__(self, db, start, offset, txs): 20 | self.start = start 21 | self.txs = txs 22 | self.hashes = [] 23 | for tx in txs: 24 | self.hashes.append(tx.h) 25 | db.put(tx.h, tx.plaintext()) 26 | if len(self.hashes) > 0: 27 | root_hash = merklize(db, self.hashes) 28 | self.tx_merkle_tree_root_hash = add_sum_to_hash(root_hash, offset) 29 | 30 | def add_sum_to_hash(raw_hash, int_sum): 31 | return b''.join([raw_hash[0:24], int_to_big_endian(int_sum).rjust(8, b"\x00")]) 32 | 33 | def create_tx_buckets(db, txs): 34 | starts_and_ends = set() 35 | starts_and_ends.add(0) 36 | txs_by_start = dict() 37 | # Compute the bucket boundaries based on where txs start & end 38 | for tx in txs: 39 | starts_and_ends.add(tx.start) 40 | starts_and_ends.add(tx.start+tx.offset) 41 | if tx.start not in txs_by_start: 42 | txs_by_start[tx.start] = [] 43 | txs_by_start[tx.start].append(tx) 44 | 45 | list_of_starts_and_ends = sorted(starts_and_ends) 46 | active_txs = [] 47 | buckets = [] 48 | # Iterate over txs, adding them to their respective buckets 49 | for idx, i in enumerate(list_of_starts_and_ends): 50 | if i in txs_by_start: 51 | active_txs = active_txs + txs_by_start[i] 52 | # Remove all active txs which end here 53 | active_txs = [tx for tx in active_txs if tx.start + tx.offset != i] 54 | if len(active_txs) == 0 and idx+1 == len(list_of_starts_and_ends): 55 | continue 56 | bucket_offset = list_of_starts_and_ends[idx+1] - i 57 | if len(active_txs) == 0 and idx+1 != len(list_of_starts_and_ends): 58 | buckets.append(TxBucket(db, i, bucket_offset, [NullTx(i, list_of_starts_and_ends[idx+1] - i)])) 59 | else: 60 | buckets.append(TxBucket(db, i, bucket_offset, active_txs)) 61 | return buckets 62 | 63 | def set_first_bit(byte_value, one_or_zero): 64 | if one_or_zero == 0: 65 | return b"\x00" 66 | if one_or_zero == 1: 67 | return b"\x01" 68 | 69 | def construct_tree(db, nodes): 70 | if len(nodes) == 1: 71 | return nodes[0] 72 | remaining_nodes = [] 73 | for i in range(0, len(nodes), 2): 74 | if i+1 == len(nodes): 75 | remaining_nodes.append(nodes[i]) 76 | break 77 | left_value = nodes[i][1:].rjust(32, set_first_bit(nodes[i][0], 0)) 78 | right_value = nodes[i+1][1:].rjust(32, set_first_bit(nodes[i+1][0], 1)) 79 | new_value = b''.join([left_value, right_value]) 80 | new_sum = bytes_to_int(nodes[i+1][24:]) + bytes_to_int(nodes[i][24:]) 81 | new_hash = add_sum_to_hash(Web3.sha3(new_value), new_sum) 82 | db.put(new_hash, new_value) 83 | remaining_nodes.append(new_hash) 84 | return construct_tree(db, remaining_nodes) 85 | 86 | def merklize(db, nodes): 87 | if len(nodes) == 1: 88 | return nodes[0] 89 | remaining_nodes = [] 90 | for i in range(0, len(nodes), 2): 91 | if i+1 == len(nodes): 92 | remaining_nodes.append(nodes[i]) 93 | break 94 | new_value = b''.join([nodes[i], nodes[i+1]]) 95 | new_hash = Web3.sha3(new_value) 96 | db.put(new_hash, new_value) 97 | remaining_nodes.append(new_hash) 98 | return merklize(db, remaining_nodes) 99 | -------------------------------------------------------------------------------- /plasma0/plasmalib/operator/rsa_accumulator.py: -------------------------------------------------------------------------------- 1 | from hashlib import blake2s 2 | 3 | def hash(x): 4 | return blake2s(x).digest()[:32] 5 | 6 | def get_primes(givenNumber): 7 | # Initialize a list 8 | primes = [] 9 | for possiblePrime in range(2, givenNumber + 1): 10 | # Assume number is prime until shown it is not. 11 | isPrime = True 12 | for num in range(2, int(possiblePrime ** 0.5) + 1): 13 | if possiblePrime % num == 0: 14 | isPrime = False 15 | break 16 | if isPrime: 17 | primes.append(possiblePrime) 18 | 19 | return(primes) 20 | 21 | def get_B_value(base, result): 22 | return int.from_bytes( 23 | hash(base.to_bytes(1024, 'big') + result.to_bytes(1024, 'big')), 24 | 'big' 25 | ) 26 | 27 | def prove_exponentiation(base, exponent, result): 28 | B = get_B_value(base, result) 29 | b = pow(base, exponent // B, mod) 30 | remainder = exponent % B 31 | return (b, remainder) 32 | 33 | def verify_proof(base, result, b, remainder): 34 | B = get_B_value(base, result) 35 | return pow(b, B, mod) * pow(base, remainder, mod) % mod == result 36 | 37 | mod = 25195908475657893494027183240048398571429282126204032027777137836043662020707595556264018525880784406918290641249515082189298559149176184502808489120072844992687392807287776735971418347270261896375014971824691165077613379859095700097330459748808428401797429100642458691817195118746121515172654632282216869987549182422433637259085141865462043576798423387184774447920739934236584823824281198163815010674810451660377306056201619676256133844143603833904414952634432190114657544454178424020924616515723350778707749817125772467962926386356373289912154831438167899885040445364023527381951378636564391212010397122822120720357 38 | 39 | acc_values = [] 40 | 41 | g = 3 42 | acc = g 43 | full_exponent = 1 44 | 45 | for v in get_primes(100): 46 | acc_values.append(v) 47 | full_exponent = full_exponent * v 48 | acc = pow(acc, v, mod) 49 | prime_to_prove = acc_values[8] 50 | 51 | b, remainder = prove_exponentiation(g, full_exponent, acc) 52 | print(verify_proof(g, acc, b, remainder)) 53 | -------------------------------------------------------------------------------- /plasma0/plasmalib/operator/transaction_validator.py: -------------------------------------------------------------------------------- 1 | import bisect 2 | 3 | def subtract_range(range_list, start, end): 4 | affected_range = None 5 | for i in range(0, len(range_list), 2): 6 | r_start = range_list[i] 7 | r_end = range_list[i+1] 8 | if r_start <= start and end <= r_end: 9 | affected_range = i 10 | break 11 | if affected_range is None: 12 | return False 13 | # Remove the effected range 14 | del range_list[i:i + 2] 15 | # Create new sub-ranges based on what we deleted 16 | if r_start < start: 17 | # range_list += [r_start, start - 1] 18 | insertion_point = bisect.bisect_left(range_list, r_start) 19 | range_list[insertion_point:insertion_point] = [r_start, start - 1] 20 | if r_end > end: 21 | # range_list += [end + 1, r_end] 22 | insertion_point = bisect.bisect_left(range_list, end + 1) 23 | range_list[insertion_point:insertion_point] = [end + 1, r_end] 24 | return True 25 | 26 | 27 | def add_range(range_list, start, end): 28 | # Find left_range (a range which ends at the start of our tx) and right_range (a range which starts at the end of our tx) 29 | left_range = None 30 | right_range = None 31 | insertion_point = bisect.bisect_left(range_list, start) 32 | if insertion_point > 0 and range_list[insertion_point - 1] == start - 1: 33 | left_range = insertion_point - 2 34 | if insertion_point < len(range_list) and range_list[insertion_point] == end + 1: 35 | right_range = insertion_point 36 | # Set the start and end of our new range based on the deleted ranges 37 | if left_range is not None: 38 | start = range_list[left_range] 39 | if right_range is not None: 40 | end = range_list[right_range + 1] 41 | # Delete the left_range and right_range if we found them 42 | if left_range is not None and right_range is not None: 43 | del range_list[left_range + 1:right_range + 1] 44 | return 45 | elif left_range is not None: 46 | del range_list[left_range:left_range + 2] 47 | insertion_point -= 2 48 | elif right_range is not None: 49 | del range_list[right_range:right_range + 2] 50 | range_list[insertion_point:insertion_point] = [start, end] 51 | 52 | def add_tx(db, tx): 53 | # Now make sure the range is owned by the sender 54 | sender_ranges = db.get(tx.sender) 55 | tx_start = tx.start 56 | tx_end = tx.start + tx.offset - 1 57 | # Subtract ranges from sender range list and store 58 | if not subtract_range(sender_ranges, tx_start, tx_end): 59 | return False 60 | db.put(tx.sender, sender_ranges) 61 | # After having deleted the sender ranges, 62 | # Add ranges to recipient range list and store 63 | recipient_ranges = db.get(tx.recipient) 64 | add_range(recipient_ranges, tx_start, tx_end) 65 | db.put(tx.recipient, recipient_ranges) 66 | return sender_ranges 67 | 68 | def add_deposit(db, owner, amount): 69 | total_deposits = db.get('total_deposits') 70 | if total_deposits is None: 71 | total_deposits = 0 72 | owner_ranges = db.get(owner) 73 | if owner_ranges is None: 74 | owner_ranges = [] 75 | owner_ranges += [total_deposits, total_deposits + amount - 1] 76 | total_deposits += amount 77 | db.put(owner, owner_ranges) 78 | db.put('total_deposits', total_deposits) 79 | return owner_ranges 80 | 81 | def validate_tx_sig(tx): 82 | # TODO: Actually verify the signature 83 | return True 84 | -------------------------------------------------------------------------------- /plasma0/plasmalib/operator/transactions.py: -------------------------------------------------------------------------------- 1 | from eth_hash.auto import keccak 2 | from web3 import Web3 3 | from eth_utils import ( 4 | int_to_big_endian, 5 | big_endian_to_int, 6 | ) 7 | 8 | ''' 9 | Transaction format: 10 | list_len + TransferRecord_1 + ... + TransferRecord_n + list_len + Signature_1 + ... + Signature_n 11 | 12 | - list_len [4 bytes]: 1 byte describing how many elements in the list, 3 bytes describing the length of each element. 13 | - TransferRecord [152 bytes]: 152 bytes per TransferRecord, up to `x` TransferRecords in each TX. 14 | - Signature [96 bytes]: 96 bytes per signature. 15 | 16 | transfer_fields = [ 17 | ('sender', Web3.isAddress), 18 | ('recipient', Web3.isAddress), 19 | ('token_id', is_bytes8_int), 20 | ('start', is_bytes32_int), 21 | ('offset', is_bytes32_int), 22 | ('max_block', is_bytes32_int), 23 | ('nonce', is_bytes8_int), 24 | ('prime', is_bytes8_int), 25 | ] 26 | sig_fields = [ 27 | ('v', is_bytes32_int), 28 | ('r', is_bytes32_int), 29 | ('s', is_bytes32_int), 30 | ] 31 | ''' 32 | 33 | def is_bytes32_int(i): 34 | b = int_to_big_endian(i) 35 | if len(b) <= 32: 36 | return True 37 | else: 38 | return False 39 | 40 | def is_bytes8_int(i): 41 | b = int_to_big_endian(i) 42 | if len(b) <= 8: 43 | return True 44 | else: 45 | return False 46 | 47 | def get_field_bytes(field): 48 | if field == Web3.isAddress: 49 | return 20 50 | elif field == is_bytes32_int: 51 | return 32 52 | elif field == is_bytes8_int: 53 | return 8 54 | else: 55 | raise 'No field type checker recognized' 56 | 57 | def get_fields_total_bytes(fields): 58 | fields_bytes = 0 59 | for f in fields: 60 | fields_bytes += get_field_bytes(f[1]) 61 | return fields_bytes 62 | 63 | def get_null_tx(token_id, start, offset): 64 | return TransferRecord(b'\00'*20, b'\00'*20, token_id, start, offset, 0, 0) 65 | 66 | 67 | class SimpleSerializableElement: 68 | def __init__(self, *args): 69 | assert self.fields is not None 70 | assert len(self.fields) == len(args) 71 | for i, arg in enumerate(args): 72 | assert self.fields[i][1](arg) 73 | setattr(self, self.fields[i][0], arg) 74 | 75 | def encode(self): 76 | encoding = b'' 77 | for f in self.fields: 78 | field_type = f[1] 79 | field_value = getattr(self, f[0]) 80 | if field_type == Web3.isAddress: 81 | encoding += field_value 82 | elif field_type == is_bytes32_int: 83 | encoding += int_to_big_endian(field_value).rjust(32, b'\0') 84 | elif field_type == is_bytes8_int: 85 | encoding += int_to_big_endian(field_value).rjust(8, b'\0') 86 | return encoding 87 | 88 | @classmethod 89 | def decode(cls, encoding, element_type): 90 | assert issubclass(element_type, cls) 91 | assert len(encoding) == get_fields_total_bytes(element_type.fields) 92 | args = () 93 | byte_pos = 0 94 | for f in element_type.fields: 95 | field_type = f[1] 96 | field_bytes_len = get_field_bytes(field_type) 97 | if field_type == is_bytes8_int or field_type == is_bytes32_int: 98 | args += (big_endian_to_int(encoding[byte_pos:byte_pos+field_bytes_len]),) 99 | else: 100 | args += (encoding[byte_pos:byte_pos+field_bytes_len],) 101 | 102 | byte_pos += field_bytes_len 103 | return element_type(*args) 104 | 105 | 106 | class SimpleSerializableList: 107 | def __init__(self, serializableElements): 108 | assert len(serializableElements) > 0 and len(serializableElements) < 16 109 | # Check that all elements are of the same class 110 | assert all(isinstance(e, type(serializableElements[0])) for e in serializableElements) 111 | self.serializableElements = serializableElements 112 | self.field_total_bytes = get_fields_total_bytes(serializableElements[0].fields) 113 | 114 | def encode(self): 115 | num_elements = int_to_big_endian(len(self.serializableElements)) 116 | element_len = int_to_big_endian(self.field_total_bytes).rjust(3, b'\0') 117 | header = num_elements + element_len 118 | encoding = header 119 | for ele in self.serializableElements: 120 | encoding += ele.encode() 121 | return encoding 122 | 123 | @property 124 | def hash(self) -> bytes: 125 | return keccak(self.encode()) 126 | 127 | @staticmethod 128 | def decode(encoding, element_type): 129 | num_elements = encoding[0] 130 | element_len = big_endian_to_int(encoding[1:4]) 131 | elements = [] 132 | for i in range(num_elements): 133 | start_pos = 4 + i*element_len 134 | end_pos = start_pos+element_len 135 | elements.append(SimpleSerializableElement.decode(encoding[start_pos:end_pos], element_type)) 136 | return elements 137 | 138 | 139 | class TransferRecord(SimpleSerializableElement): 140 | fields = [ 141 | ('sender', Web3.isAddress), 142 | ('recipient', Web3.isAddress), 143 | ('token_id', is_bytes8_int), 144 | ('start', is_bytes32_int), 145 | ('offset', is_bytes32_int), 146 | ('max_block', is_bytes32_int), 147 | ('nonce', is_bytes8_int), 148 | ('prime', is_bytes8_int), 149 | ] 150 | 151 | @property 152 | def hash(self) -> bytes: 153 | return keccak(self.encode()) 154 | 155 | 156 | class Signature(SimpleSerializableElement): 157 | fields = [ 158 | ('v', is_bytes32_int), 159 | ('r', is_bytes32_int), 160 | ('s', is_bytes32_int), 161 | ] 162 | -------------------------------------------------------------------------------- /plasma0/plasmalib/state.py: -------------------------------------------------------------------------------- 1 | import plyvel 2 | from web3 import Web3 3 | from plasmalib.utils import ( 4 | int_to_big_endian8, 5 | int_to_big_endian32, 6 | ) 7 | from eth_utils import ( 8 | big_endian_to_int, 9 | decode_hex, 10 | ) 11 | import time 12 | import os 13 | import rlp 14 | 15 | 16 | TOTAL_DEPOSITS_PREFIX = b'total_deposits-' 17 | OFFSET_SUFFIX = b'-0offset' 18 | OWNER_SUFFIX = b'-1owner' 19 | NONCE_SUFFIX = b'-nonce' 20 | 21 | def get_total_deposits_key(token_id): 22 | assert type(token_id) == bytes 23 | return TOTAL_DEPOSITS_PREFIX + token_id 24 | 25 | def get_start_to_offset_key(token_id, start): 26 | assert type(token_id) == bytes and type(start) == bytes 27 | return token_id + start + OFFSET_SUFFIX 28 | 29 | def get_start_to_owner_key(token_id, start): 30 | assert type(token_id) == bytes and type(start) == bytes 31 | return token_id + start + OWNER_SUFFIX 32 | 33 | def get_owner_to_nonce_key(owner): 34 | assert type(owner) == bytes 35 | return owner + NONCE_SUFFIX 36 | 37 | def get_owner_to_start_key(owner, token_id, start): 38 | assert type(owner) == bytes and type(token_id) == bytes and type(start) == bytes 39 | return owner + token_id + start 40 | 41 | class FileLog: 42 | def __init__(self, log_dir, backup_timeout): 43 | self.log_dir = log_dir 44 | self.tmp_log_path = os.path.join(log_dir, "tmp_log") 45 | try: 46 | os.remove(self.tmp_log_path) 47 | except Exception: 48 | print('No tmp log found') 49 | assert not os.path.isfile(self.tmp_log_path) 50 | os.makedirs(os.path.dirname(self.tmp_log_path), exist_ok=True) 51 | self.tmp_log = open(self.tmp_log_path, 'a') 52 | self.last_backup = time.time() 53 | self.backup_timeout = backup_timeout 54 | 55 | def add_record(self, record): 56 | self.tmp_log.write(rlp.encode(record)) 57 | if time.time() - self.backup_timeout > self.last_backup: 58 | self.backup() 59 | 60 | def backup(self): 61 | self.tmp_log.flush() 62 | self.tmp_log.close() 63 | os.rename(self.tmp_log_path, os.path.join(self.log_dir, int(time.time()))) 64 | self.tmp_log = open(self.tmp_log_path, 'a') 65 | 66 | 67 | class State: 68 | ''' 69 | State object accepts txs, verifies that they are valid, and updates 70 | the leveldb database. 71 | ''' 72 | def __init__(self, db_dir, tx_log_dir, create_if_missing=True, backup_timeout=60): 73 | self.db = plyvel.DB(db_dir, create_if_missing=create_if_missing) 74 | # self.wb = self.db.write_batch() 75 | # self.db = EphemDB() 76 | self.file_log = FileLog(tx_log_dir, backup_timeout) 77 | 78 | def add_deposit(self, recipient, token_id, amount): 79 | # Update total deposits 80 | total_deposits = self.db.get(get_total_deposits_key(int_to_big_endian8(token_id))) 81 | if total_deposits is None: 82 | total_deposits = b'\x00'*32 83 | deposit_start = big_endian_to_int(total_deposits) 84 | total_deposits = int_to_big_endian32(deposit_start + amount) 85 | # Begin write batch--data written to DB during write batch is all or nothing 86 | wb = self.db.write_batch() 87 | # Store the range 88 | self.store_range(recipient, token_id, deposit_start, amount) 89 | # Update total deposits 90 | self.db.put(b'total_deposits-' + int_to_big_endian8(token_id), total_deposits) 91 | # End write batch 92 | wb.write() 93 | return True 94 | 95 | def store_range(self, owner, token_id, start, offset): 96 | ''' Stores a range with the specified parameters. 97 | Note that all params other than `owner` are ints. 98 | ''' 99 | owner, token_id, start, offset = self.get_converted_parameters(addresses=(owner,), bytes8s=(token_id,), bytes32s=(start, offset)) 100 | # Put everything into the db 101 | if self.db.get(get_owner_to_nonce_key(owner)) is None: 102 | # If there's no nonce for the owner, add one 103 | self.db.put(get_owner_to_nonce_key(owner), int_to_big_endian8(0)) 104 | self.db.put(get_owner_to_start_key(owner, token_id, start), b'1') 105 | self.db.put(get_start_to_offset_key(token_id, start), offset) 106 | self.db.put(get_start_to_owner_key(token_id, start), owner) 107 | 108 | def get_converted_parameters(self, addresses=(), bytes8s=(), bytes32s=()): 109 | converted = () 110 | for a in addresses: 111 | if type(a) != bytes: 112 | a = decode_hex(a) 113 | assert Web3.isAddress(a) 114 | converted += (a,) 115 | for b8 in bytes8s: 116 | if type(b8) != bytes: 117 | b8 = int_to_big_endian8(b8) 118 | assert len(b8) == 8 119 | converted += (b8,) 120 | for b32 in bytes32s: 121 | if type(b32) != bytes: 122 | b32 = int_to_big_endian32(b32) 123 | assert len(b32) == 32 124 | converted += (b32,) 125 | return converted 126 | 127 | def get_ranges(self, token_id, start, end): 128 | token_id, start, end = self.get_converted_parameters(bytes8s=(token_id,), bytes32s=(start, end)) 129 | it = self.db.iterator(include_value=False) 130 | token_lookup_key = token_id + start 131 | it.seek(token_lookup_key) 132 | last_key = next(it) 133 | # Move the iterator to the previous range 134 | while last_key[0:40] > token_lookup_key or OFFSET_SUFFIX not in last_key[40:]: 135 | last_key = it.prev() 136 | if last_key[0:40] != token_lookup_key: 137 | # If we entered the while loop, then we need to call next() to change the direction of the iterator 138 | last_key = next(it) 139 | affected_ranges = [] 140 | end = token_id + end + OWNER_SUFFIX 141 | while end >= last_key: 142 | # Append to the affected_ranges list 143 | affected_ranges.append(last_key) 144 | last_key = next(it) 145 | return affected_ranges 146 | 147 | def verify_ranges_owner(self, ranges, owner): 148 | owner, = self.get_converted_parameters(addresses=(owner,)) 149 | for r in ranges: 150 | r_owner = self.db.get(r) 151 | if owner != r_owner: 152 | return False 153 | return True 154 | 155 | def delete_ranges(self, ranges): 156 | for r in ranges: 157 | self.db.delete(r) 158 | 159 | def add_transaction(self, txs): 160 | affected_ranges = [] 161 | for tx in txs: 162 | tx_ranges = self.get_ranges(tx.token_id, tx.start, tx.start + tx.offset) 163 | assert self.verify_ranges_owner(tx_ranges[1::2], tx.sender) 164 | print([big_endian_to_int(r[8:40]) for r in tx_ranges]) 165 | affected_ranges.append(tx_ranges) 166 | print(affected_ranges) 167 | for i, tx in enumerate(txs): 168 | self.add_transfer(tx, affected_ranges[i]) 169 | 170 | def add_transfer(self, transfer, affected_ranges): 171 | sender, recipient, token_id, start, offset = self.get_converted_parameters(addresses=(transfer.sender, transfer.recipient), bytes8s=(transfer.token_id,), bytes32s=(transfer.start, transfer.offset)) 172 | end = token_id + int_to_big_endian32(big_endian_to_int(start) + big_endian_to_int(offset)) + OFFSET_SUFFIX 173 | # Shorten first range if needed 174 | if get_start_to_offset_key(token_id, start) != affected_ranges[0]: 175 | # TODO: Add 176 | self.db.put(affected_ranges[0], int_to_big_endian32(big_endian_to_int(start) - big_endian_to_int(affected_ranges[0][8:40]))) 177 | print('setting new end offset to:', big_endian_to_int(start) - big_endian_to_int(affected_ranges[0][8:40])) 178 | del affected_ranges[0:2] 179 | # Shorten last range if needed 180 | if len(affected_ranges) != 0 and end != affected_ranges[-1]: 181 | self.db.put(affected_ranges[-2], start + offset) 182 | print('setting new start to:', big_endian_to_int(start) + big_endian_to_int(offset)) 183 | del affected_ranges[-2:] 184 | 185 | # # Check that affected ranges are owned by the sender 186 | # assert self.validate_range_owner(affected_ranges, tx.sender) 187 | # # Shorten first range if needed 188 | # if new_db_entry.start_to_offset_key != affected_ranges[0]: 189 | # # TODO: Add 190 | # self.db.put(affected_ranges[0], int_to_big_endian32(tx.start - big_endian_to_int(affected_ranges[0][8:40]))) 191 | # print('setting new end offset to:', tx.start - big_endian_to_int(affected_ranges[0][8:40])) 192 | # del affected_ranges[0:2] 193 | # # Shorten last range if needed 194 | # if len(affected_ranges) != 0 and new_db_entry.end != affected_ranges[-1]: 195 | # self.db.put(affected_ranges[-2], int_to_big_endian32(tx.start + tx.offset)) 196 | # print('setting new start to:', tx.start + tx.offset) 197 | # del affected_ranges[-2:] 198 | # print('Final Affected range start pos:', [big_endian_to_int(r[8:40]) for r in affected_ranges]) 199 | -------------------------------------------------------------------------------- /plasma0/plasmalib/utils.py: -------------------------------------------------------------------------------- 1 | from web3 import Web3 2 | from web3.contract import ConciseContract 3 | from eth_tester import EthereumTester, PyEVMBackend 4 | from vyper import compiler 5 | from math import floor, ceil, log 6 | from hexbytes import HexBytes 7 | from plasmalib.constants import * 8 | from eth_utils import encode_hex as encode_hex_0x 9 | from eth_utils import ( 10 | int_to_big_endian, 11 | ) 12 | # import json 13 | 14 | from pprint import PrettyPrinter 15 | PP = PrettyPrinter(indent=4) 16 | 17 | 18 | class MST: 19 | def __init__(self, l, r): 20 | self.l = l 21 | self.r = r 22 | self.h = Web3.sha3(l.h + r.h) 23 | 24 | class Leaf: 25 | def __init__(self, tx): 26 | self.tx = tx 27 | self.h = tx.h 28 | 29 | class Msg: 30 | def __init__(self, sender, recipient, start, offset): 31 | self.sender = sender 32 | self.recipient = recipient 33 | self.start = start 34 | self.offset = offset 35 | self.h = Web3.sha3( 36 | addr_to_bytes(self.sender) + 37 | addr_to_bytes(self.recipient) + 38 | to_bytes32(self.start) + 39 | to_bytes32(self.offset) 40 | ) 41 | 42 | def plaintext(self): 43 | return addr_to_bytes(self.sender) + addr_to_bytes(self.recipient) + to_bytes32(self.start) + to_bytes32(self.offset) + self.h 44 | 45 | 46 | class Swap: 47 | def __init__(self, msgs): 48 | self.msgs = msgs 49 | self.raw_hashes = b'' 50 | for m in msgs: 51 | self.raw_hashes += m.h 52 | self.h = Web3.sha3( 53 | self.raw_hashes 54 | ) 55 | 56 | class Deposit: 57 | # TODO: Add multi-asset support 58 | def __init__(self, owner, amount): 59 | self.owner = owner 60 | self.offset = amount 61 | 62 | class Tx: 63 | # TODO: Add tx timeout to avoid free option problem 64 | def __init__(self, msg, swap, signer): 65 | self.msg = msg 66 | self.sender = msg.sender 67 | self.recipient = msg.recipient 68 | self.start = msg.start 69 | self.offset = msg.offset 70 | self.swap = swap 71 | if swap is not None: 72 | assert msg.h in swap.raw_hashes 73 | self.full_msg_hash = swap.h 74 | self.is_swap = True 75 | else: 76 | self.full_msg_hash = msg.h 77 | self.is_swap = False 78 | # self.sig = signer(msg.h) 79 | sig = signer(self.full_msg_hash) 80 | self.sigv = sig.v 81 | self.sigr = sig.r 82 | self.sigs = sig.s 83 | self.h = Web3.sha3( 84 | self.full_msg_hash + 85 | to_bytes32(self.sigv) + 86 | to_bytes32(self.sigr) + 87 | to_bytes32(self.sigs) 88 | ) 89 | 90 | def plaintext(self): 91 | plaintext = b'' 92 | if self.swap is not None: 93 | for msg in self.swap.msgs: 94 | plaintext += msg.plaintext() 95 | else: 96 | plaintext += self.msg.plaintext() 97 | plaintext += self.h + to_bytes32(self.sigv) + to_bytes32(self.sigr) + to_bytes32(self.sigs) 98 | return plaintext 99 | 100 | # def json(self): 101 | # return json.dumps(self, default=lambda o: o.__dict__) 102 | 103 | class NullTx: 104 | def __init__(self, start, offset): 105 | self.sender = '0xdead' 106 | self.recipient = '0xdead' 107 | self.start = start 108 | self.offset = offset 109 | self.is_swap = False 110 | self.h = Web3.sha3( 111 | addr_to_bytes(self.sender) + 112 | addr_to_bytes(self.recipient) + 113 | to_bytes32(self.start) + 114 | to_bytes32(self.offset) 115 | ) 116 | 117 | def plaintext(self): 118 | return b'null' 119 | 120 | def pairs(l): 121 | return [(l[i], l[i + 1]) for i in range(0, len(l), 2)] 122 | 123 | # assumes no overlapping txs 124 | def construct_tree(txs): 125 | depth = ceil(log(len(txs), 2)) 126 | assert depth <= MAX_TREE_DEPTH 127 | 128 | leaves = [Leaf(tx) for tx in txs] 129 | leaves.sort(key = lambda leaf: leaf.tx.msg.start) 130 | leaves += [Leaf(NullTx())] * (2 ** depth - len(leaves)) 131 | 132 | fst = lambda x: x[0] 133 | snd = lambda x: x[1] 134 | nodes = leaves 135 | # depth = 1 136 | for i in range(depth): 137 | nodes = list(map(lambda x: MST(fst(x), snd(x)), pairs(nodes))) 138 | return nodes[0] 139 | 140 | 141 | def int_to_big_endian32(val): 142 | return int_to_big_endian(val).rjust(32, b'\0') 143 | 144 | def int_to_big_endian8(val): 145 | return int_to_big_endian(val).rjust(8, b'\0') 146 | 147 | def addr_to_bytes(addr): 148 | return bytes.fromhex(addr[2:]) 149 | 150 | def to_bytes32(i): 151 | return i.to_bytes(32, byteorder='big') 152 | 153 | def bytes_to_int(value): 154 | return int.from_bytes(value, byteorder='big') 155 | 156 | def encode_hex(n): 157 | if isinstance(n, str): 158 | return encode_hex(n.encode('ascii')) 159 | return encode_hex_0x(n)[2:] 160 | 161 | def contract_factory(w3, source): 162 | bytecode = '0x' + compiler.compile(source).hex() 163 | abi = compiler.mk_full_signature(source) 164 | return w3.eth.contract(abi=abi, bytecode=bytecode) 165 | -------------------------------------------------------------------------------- /plasma0/requirements.txt: -------------------------------------------------------------------------------- 1 | web3==4.4.1 2 | py-evm==0.2.0a42 3 | eth-tester[py-evm]==0.1.0b33 4 | vyper==0.1.0b4 5 | pytest 6 | -------------------------------------------------------------------------------- /plasma0/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name="plasmalib", 5 | version="0.1", 6 | packages=['plasmalib'], 7 | 8 | author="River Keefer", 9 | author_email="river.keefer@gmail.com", 10 | ) 11 | -------------------------------------------------------------------------------- /plasma0/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import os 3 | import time 4 | 5 | from web3 import Web3 6 | from web3.contract import ConciseContract 7 | from eth_tester import EthereumTester, PyEVMBackend 8 | from eth_account import Account 9 | from plasmalib.utils import contract_factory 10 | from plasmalib.constants import PLASMA_BLOCK_INTERVAL 11 | from plasmalib.state import State 12 | from random import randrange, seed 13 | 14 | 15 | @pytest.fixture(scope="session") 16 | def tester(): 17 | return EthereumTester(backend=PyEVMBackend()) 18 | 19 | @pytest.fixture(scope="session") 20 | def w3(tester): 21 | w3 = Web3(Web3.EthereumTesterProvider(tester)) 22 | w3.eth.setGasPriceStrategy(lambda web3, params: 0) 23 | w3.eth.defaultAccount = w3.eth.accounts[0] 24 | return w3 25 | 26 | @pytest.fixture 27 | def acct(w3): 28 | seed() 29 | STARTING_VALUE = Web3.toWei(100, 'ether') 30 | PASSPHRASE = 'not a real passphrase' 31 | acct = Account.create(randrange(10**32)) 32 | w3.eth.sendTransaction({'to': acct.address, 'value': STARTING_VALUE}) 33 | w3.personal.importRawKey(acct.privateKey, PASSPHRASE) 34 | w3.personal.unlockAccount(acct.address, PASSPHRASE) 35 | return acct 36 | 37 | @pytest.fixture 38 | def accts(w3): 39 | seed() 40 | STARTING_VALUE = Web3.toWei(100, 'ether') 41 | PASSPHRASE = 'not a real passphrase' 42 | num_accts = 10 43 | accts = [Account.create(randrange(10**32)) for i in range(num_accts)] 44 | for i in range(len(accts)): 45 | w3.eth.sendTransaction({'to': accts[i].address, 'value': STARTING_VALUE}) 46 | w3.personal.importRawKey(accts[i].privateKey, PASSPHRASE) 47 | w3.personal.unlockAccount(accts[i].address, PASSPHRASE) 48 | return accts 49 | 50 | class MockAccount: 51 | def __init__(self, address): 52 | self.address = address 53 | 54 | @pytest.fixture 55 | def mock_accts(w3): 56 | num_accts = 100 57 | accts = [MockAccount(Web3.toHex(Web3.sha3(i))[:42]) for i in range(num_accts)] 58 | return accts 59 | 60 | @pytest.fixture 61 | def blank_state(w3): 62 | db_path = '/tmp/plasma_prime_blank_test_db/' + str(time.time()) 63 | file_log_path = '/tmp/plasma_prime_blank_test_tx_log/' + str(time.time()) 64 | state = State(db_path, file_log_path, backup_timeout=60) 65 | return state 66 | 67 | @pytest.fixture 68 | def pp(w3): 69 | wd = os.path.dirname(os.path.realpath(__file__)) 70 | with open(os.path.join(wd, os.pardir, 'contracts/plasmaprime.vy')) as f: 71 | source = f.read() 72 | factory = contract_factory(w3, source) 73 | tx_hash = factory.constructor().transact() 74 | tx_receipt = w3.eth.waitForTransactionReceipt(tx_hash) 75 | return ConciseContract(w3.eth.contract( 76 | address=tx_receipt.contractAddress, 77 | abi=factory.abi, 78 | )) 79 | 80 | @pytest.fixture 81 | def pp_live(w3, tester, pp): 82 | ITERATION = 10 83 | 84 | # submit some garbage plasma blocks 85 | h = b'\x8b' * 32 86 | for i in range(10): 87 | tester.mine_blocks(num_blocks=PLASMA_BLOCK_INTERVAL) 88 | pp.publish_hash(h, transact={}) 89 | return pp 90 | -------------------------------------------------------------------------------- /plasma0/tests/contracts/test_challenges.py: -------------------------------------------------------------------------------- 1 | from pytest import raises 2 | from eth_tester.exceptions import TransactionFailed 3 | from plasmalib.constants import CHALLENGE_PERIOD, PLASMA_BLOCK_INTERVAL 4 | from plasmalib.utils import * 5 | 6 | 7 | def test_challenges(w3, tester, pp, acct): 8 | # make a deposit 9 | DEPOSIT_VALUE = 333 10 | pp.deposit(transact={'from': acct.address, 'value': DEPOSIT_VALUE}) 11 | 12 | # publish a plasma block 13 | tester.mine_blocks(num_blocks=PLASMA_BLOCK_INTERVAL) 14 | pp.publish_hash(b'\x3b' * 32, transact={}) 15 | 16 | # submit an exit 17 | EXIT_START = 50 18 | EXIT_OFFSET = 34 19 | pp.submit_exit(0, EXIT_START, EXIT_OFFSET, transact={'from': acct.address}) 20 | 21 | # submit a challenge 22 | CHALLENGE_TOKEN_INDEX = EXIT_START + 1 23 | pp.challenge_completeness(0, CHALLENGE_TOKEN_INDEX, transact={'from': acct.address}) 24 | assert pp.challenge_nonce() == 1 25 | assert pp.challenges__exit_id(0) == 0 26 | assert pp.challenges__ongoing(0) == True 27 | assert pp.challenges__token_index(0) == CHALLENGE_TOKEN_INDEX 28 | -------------------------------------------------------------------------------- /plasma0/tests/contracts/test_compilation.py: -------------------------------------------------------------------------------- 1 | from pytest import raises 2 | from eth_tester.exceptions import TransactionFailed 3 | from plasmalib.constants import CHALLENGE_PERIOD, PLASMA_BLOCK_INTERVAL 4 | 5 | 6 | def test_compilation(pp): 7 | assert pp 8 | 9 | -------------------------------------------------------------------------------- /plasma0/tests/contracts/test_deposits.py: -------------------------------------------------------------------------------- 1 | from pytest import raises 2 | from eth_tester.exceptions import TransactionFailed 3 | from plasmalib.constants import CHALLENGE_PERIOD, PLASMA_BLOCK_INTERVAL 4 | 5 | 6 | def test_deposits(w3, tester, pp): 7 | DEPOSIT_VALUE = 5551234 8 | for account in w3.eth.accounts: 9 | tx_hash = pp.deposit(transact={'from': account, 'value': DEPOSIT_VALUE}) 10 | assert pp.deposits(account) == DEPOSIT_VALUE 11 | assert pp.total_deposits() == DEPOSIT_VALUE * len(w3.eth.accounts) 12 | -------------------------------------------------------------------------------- /plasma0/tests/contracts/test_exits.py: -------------------------------------------------------------------------------- 1 | from pytest import raises 2 | from eth_tester.exceptions import TransactionFailed 3 | from plasmalib.constants import CHALLENGE_PERIOD, PLASMA_BLOCK_INTERVAL 4 | 5 | 6 | def test_exits(w3, tester, pp_live): 7 | pp = pp_live 8 | 9 | # exit params 10 | PLASMA_BLOCK = 0 11 | START = 0 12 | OFFSET = 55 13 | 14 | # make a deposit 15 | DEPOSIT_VALUE = 500 16 | pp.deposit(transact={'value': DEPOSIT_VALUE}) 17 | 18 | # confirm we can't submit exits for future plasma blocks 19 | bn = pp.plasma_block_number() 20 | with raises(TransactionFailed): 21 | pp.submit_exit(bn, START, 1, transact={}) 22 | 23 | # confirm we can't request exits for > total_deposit_value 24 | dv = pp.total_deposits() 25 | with raises(TransactionFailed): 26 | pp.submit_exit(PLASMA_BLOCK, START, dv + 1, transact={}) 27 | 28 | # submit exit 29 | exit_id = pp.submit_exit(PLASMA_BLOCK, START, OFFSET) 30 | pp.submit_exit(PLASMA_BLOCK, START, OFFSET, transact={}) 31 | 32 | # confirm on-chain exit data is correct 33 | assert pp.exits__owner(exit_id) == w3.eth.defaultAccount 34 | assert pp.exits__plasma_block(exit_id) == PLASMA_BLOCK 35 | assert pp.exits__start(exit_id) == START 36 | assert pp.exits__offset(exit_id) == OFFSET 37 | assert pp.exits__challenge_count(exit_id) == 0 38 | assert pp.exit_nonce() == exit_id + 1 39 | 40 | # try to finalize exit before challenge period is over 41 | with raises(TransactionFailed): 42 | tx_hash = pp.finalize_exit(exit_id, transact={}) 43 | 44 | # mine blocks until the challenge period is over 45 | tester.mine_blocks(num_blocks=CHALLENGE_PERIOD) 46 | 47 | # confirm we can successfully exit now 48 | start_balance = w3.eth.getBalance(w3.eth.defaultAccount) 49 | tx_hash = pp.finalize_exit(exit_id, transact={}) 50 | end_balance = w3.eth.getBalance(w3.eth.defaultAccount) 51 | assert end_balance - start_balance == OFFSET 52 | -------------------------------------------------------------------------------- /plasma0/tests/contracts/test_publication.py: -------------------------------------------------------------------------------- 1 | from pytest import raises 2 | from eth_tester.exceptions import TransactionFailed 3 | from plasmalib.constants import CHALLENGE_PERIOD, PLASMA_BLOCK_INTERVAL 4 | 5 | 6 | def test_publication(w3, tester, pp): 7 | NEW_BLOCKS = 10 8 | for i in range(NEW_BLOCKS): 9 | # mine enough ethereum blocks to satisfy the minimum interval between plasma blocks 10 | tester.mine_blocks(num_blocks=PLASMA_BLOCK_INTERVAL) 11 | 12 | # publish some example hash 13 | h = w3.eth.getBlock('latest').hash 14 | bn = pp.plasma_block_number() 15 | tx_hash = pp.publish_hash(h, transact={}) 16 | 17 | # check the correct hash was published 18 | assert h == pp.hash_chain(bn) 19 | 20 | # confirm we can't immediately publish a new hash 21 | with raises(TransactionFailed): 22 | tx_hash = pp.publish_hash(h, transact={}) 23 | -------------------------------------------------------------------------------- /plasma0/tests/contracts/test_responses.py: -------------------------------------------------------------------------------- 1 | from pytest import raises 2 | from eth_tester.exceptions import TransactionFailed 3 | from plasmalib.constants import * 4 | from plasmalib.utils import * 5 | from random import randrange 6 | 7 | 8 | def test_responses(w3, tester, pp, accts): 9 | print("") 10 | # submit deposits 11 | for acct in accts: 12 | pp.deposit(transact={'from': acct.address, 'value': 1000}) 13 | 14 | # create 2**8 random transactions 15 | TX_VALUE = 50 16 | txs = [] 17 | for i in range(4): 18 | sender = accts[randrange(10)] 19 | recipient = accts[randrange(10)] 20 | msg = Msg(sender.address, recipient.address, TX_VALUE * i, TX_VALUE) 21 | txs.append(Tx(msg, sender.signHash)) 22 | 23 | # publish plasma block 24 | tester.mine_blocks(num_blocks=PLASMA_BLOCK_INTERVAL) 25 | root = construct_tree(txs) 26 | pp.publish_hash(root.h, transact={}) 27 | 28 | # params 29 | sender = txs[0].msg.sender 30 | recipient = txs[0].msg.recipient 31 | sigv, sigr, sigs = txs[0].sigv, txs[0].sigr, txs[0].sigs 32 | proof = [bytes(root.l.r.h), bytes(root.r.h)] + ([b'\x00'] * 6) 33 | 34 | # submit exit 35 | pp.submit_exit(0, 0, TX_VALUE, transact={'from': recipient}) 36 | 37 | # submit challenge 38 | pp.challenge_completeness(0, 20, transact={}) 39 | 40 | # confirm we can't exit while challenge is open 41 | tester.mine_blocks(num_blocks=CHALLENGE_PERIOD) 42 | with raises(TransactionFailed): 43 | pp.finalize_exit(0, transact={'from': recipient}) 44 | 45 | # respond to challenge 46 | pp.respond_completeness(0, sender, recipient, 0, TX_VALUE, sigv, sigr, sigs, proof, transact={'from': recipient}) 47 | 48 | print(w3.eth.getBalance(recipient)) 49 | pp.finalize_exit(0, transact={'from': recipient}) 50 | print(w3.eth.getBalance(recipient)) 51 | 52 | -------------------------------------------------------------------------------- /plasma0/tests/contracts/test_tx_hash.py: -------------------------------------------------------------------------------- 1 | from pytest import raises 2 | from eth_tester.exceptions import TransactionFailed 3 | from eth_account import Account 4 | from plasmalib.constants import CHALLENGE_PERIOD, PLASMA_BLOCK_INTERVAL 5 | from plasmalib.utils import * 6 | 7 | 8 | def test_tx_hash(w3, tester, pp, acct): 9 | # plasma message info 10 | SENDER = w3.eth.defaultAccount 11 | RECIPIENT = w3.eth.defaultAccount 12 | START = 111323111 13 | OFFSET = 30 14 | 15 | # compute signature to be included in plasma tx 16 | msg = Msg(SENDER, RECIPIENT, START, OFFSET) 17 | message_hash = msg.h 18 | 19 | # compute expected tx hash 20 | tx = Tx(msg, acct.signHash) 21 | expected_tx_hash = tx.h 22 | 23 | # confirm tx hashes match 24 | tx_hash = pp.tx_hash( 25 | SENDER, 26 | RECIPIENT, 27 | START, 28 | OFFSET, 29 | tx.sigv, 30 | tx.sigr, 31 | tx.sigs, 32 | ) 33 | assert tx_hash == expected_tx_hash 34 | -------------------------------------------------------------------------------- /plasma0/tests/operator/test_block_generator.py: -------------------------------------------------------------------------------- 1 | from plasmalib.block_generator import create_tx_buckets, EphemDB, construct_tree 2 | from plasmalib.transaction_validator import add_tx, add_deposit, subtract_range, add_range 3 | from plasmalib.utils import Msg, Tx, Swap 4 | from random import randrange 5 | import pickle 6 | import time 7 | import os 8 | 9 | class MockSig: 10 | def __init__(self): 11 | self.v = 0 12 | self.r = 0 13 | self.s = 0 14 | 15 | class TestNode: 16 | def __init__(self, db, account, friend_list): 17 | self.db = db 18 | self.account = account 19 | self.friend_list = friend_list 20 | 21 | def handle_response(self, responses): 22 | if self.account.address not in responses: 23 | return 24 | response = responses[self.account.address] 25 | # See if our tx went through! 26 | if response == 'FAILED': 27 | print('Oh no!') 28 | return 29 | 30 | def add_random_tx(self, msg_queue, send_full_range): 31 | ranges = self.db.get(self.account.address) 32 | if len(ranges) == 0: 33 | return 'No money!' 34 | tx_range_index = randrange(len(ranges)//2)*2 35 | if ranges[tx_range_index] == ranges[tx_range_index + 1] or send_full_range: 36 | start = ranges[tx_range_index] 37 | else: 38 | start = randrange(ranges[tx_range_index], ranges[tx_range_index + 1]) 39 | max_offset = ranges[tx_range_index + 1] - start + 1 40 | if max_offset == 1: 41 | offset = 1 42 | elif send_full_range: 43 | offset = max_offset 44 | else: 45 | offset = randrange(1, max_offset) 46 | raw_send = Msg(self.account.address, self.friend_list[randrange(len(self.friend_list))].address, start, offset) 47 | tx = Tx(raw_send, None, mock_signer) 48 | msg_queue.append(tx) 49 | 50 | 51 | def mock_signer(msg): 52 | return MockSig() 53 | 54 | # Generates a swap which swaps a bunch of coins in a contiguous range. eg. <----$$&&&@@@((----> 55 | def generate_swap(num_txs, total_deposits, max_range, accts): 56 | # Make sure that the swap doesn't try to swap more tokens than exist. 57 | assert total_deposits - max_range*num_txs > 0 58 | swap_msgs = [] 59 | start_counter = randrange(0, total_deposits - max_range*num_txs) 60 | for i in range(num_txs): 61 | sender = accts[randrange(10)] 62 | recipient = accts[randrange(10)] 63 | offset = randrange(1, max_range) 64 | swap_msgs.append(Msg(sender.address, recipient.address, start_counter, offset)) 65 | start_counter += offset 66 | swap = Swap(swap_msgs) 67 | txs = [] 68 | for msg in swap_msgs: 69 | txs.append(Tx(msg, swap, mock_signer)) 70 | return txs 71 | 72 | def generate_txs(num_txs, num_swaps, total_deposits, max_range, accts): 73 | assert max_range < total_deposits 74 | txs = [] 75 | while num_swaps > 8: 76 | # Generate some swaps 77 | swap_count = randrange(2, 8) 78 | txs += generate_swap(swap_count, total_deposits, max_range, accts) 79 | num_swaps -= swap_count 80 | for i in range(num_txs): 81 | # Generate normal txs 82 | start = randrange(total_deposits - max_range) 83 | offset = randrange(1, max_range) 84 | sender = accts[randrange(10)] 85 | recipient = accts[randrange(10)] 86 | msg = Msg(sender.address, recipient.address, start, offset) 87 | txs.append(Tx(msg, None, mock_signer)) 88 | return txs 89 | 90 | def process_tx(db, write_file, tx): 91 | add_tx_result = add_tx(db, tx) 92 | if add_tx_result is False: 93 | return tx.msg.sender, 'FAILED' 94 | write_file.write(str(tx.plaintext()) + '\n') 95 | return tx.msg.sender, add_tx_result 96 | 97 | # def test_tx_pickle(accts): 98 | # total_deposits = 1000 99 | # max_range = 30 100 | # start = randrange(total_deposits - max_range) 101 | # offset = randrange(1, max_range) 102 | # sender = accts[randrange(10)] 103 | # recipient = accts[randrange(10)] 104 | # msg = Msg(sender.address, recipient.address, start, offset) 105 | # tx = Tx(msg, None, mock_signer) 106 | # print(tx.plaintext()) 107 | 108 | 109 | # def add_deposit(db, owner, amount, total_deposits): 110 | def test_tx_validator(w3, tester, mock_accts): 111 | file_path = os.path.dirname(os.path.realpath(__file__)) + '/test.txt' 112 | test_write = open(file_path, 'w') 113 | 114 | accts = mock_accts 115 | print([a.address for a in accts]) 116 | print('starting...') 117 | db = EphemDB() 118 | nodes = [] 119 | for a in accts: 120 | nodes.append(TestNode(db, a, accts)) 121 | # Add a bunch of deposits 122 | max_deposit = 10 123 | for n in nodes: 124 | db.put(n.account.address, []) 125 | for i in range(1000): 126 | # Submit a deposit for a random node 127 | add_deposit(db, nodes[randrange(len(nodes))].account.address, randrange(1, max_deposit)) 128 | # Tell the nodes what ranges they have 129 | for n in nodes: 130 | n.ranges = db.get(n.account.address) 131 | print(db.get('total_deposits')) 132 | print([(n.account.address, n.ranges) for n in nodes]) 133 | responses = {} 134 | start_time = time.time() 135 | # We have 100 accounts 136 | total_txs = 10000 137 | for i in range(total_txs // len(mock_accts)): 138 | txs = [] 139 | for n in nodes: 140 | n.handle_response(responses) 141 | n.add_random_tx(txs, False) 142 | responses = {} 143 | for t in txs: 144 | recipient, response = process_tx(db, test_write, t) 145 | responses[recipient] = response 146 | test_write.close() 147 | end_time = time.time() - start_time 148 | print('\n\n') 149 | print([(n.account.address, n.ranges) for n in nodes]) 150 | print('Processed', total_txs, 'transactions') 151 | print("--- in %s seconds ---" % (end_time)) 152 | 153 | 154 | # db.put(accts[0].address, [0, 3, 7, 8, 10, 11]) 155 | # db.put(accts[1].address, [4, 6, 9, 9, 12, 15]) 156 | # valid_tx = Tx(Msg(accts[0].address, accts[1].address, 10, 2), None, mock_signer) 157 | # print('done') 158 | # print(db.get(accts[0].address)) 159 | # print(db.get(accts[1].address)) 160 | # print(add_tx(db, valid_tx)) 161 | # print(db.get(accts[0].address)) 162 | # print(db.get(accts[1].address)) 163 | 164 | def test_subtract_range(): 165 | # Test subtracting a bunch of ranges 166 | range_list = [0, 3, 6, 10, 15, 17, 18, 18] 167 | subtract_range(range_list, 0, 3) 168 | assert range_list == [6, 10, 15, 17, 18, 18] 169 | subtract_range(range_list, 18, 18) 170 | assert range_list == [6, 10, 15, 17] 171 | subtract_range(range_list, 7, 7) 172 | assert range_list == [6, 6, 8, 10, 15, 17] 173 | subtract_range(range_list, 15, 17) 174 | assert range_list == [6, 6, 8, 10] 175 | subtract_range(range_list, 6, 6) 176 | assert range_list == [8, 10] 177 | subtract_range(range_list, 9, 9) 178 | assert range_list == [8, 8, 10, 10] 179 | subtract_range(range_list, 8, 8) 180 | assert range_list == [10, 10] 181 | subtract_range(range_list, 10, 10) 182 | assert range_list == [] 183 | 184 | def test_add_range(): 185 | # Test adding a bunch of ranges 186 | range_list = [0, 1, 6, 10, 15, 17, 20, 20] 187 | add_range(range_list, 5, 5) 188 | assert range_list == [0, 1, 5, 10, 15, 17, 20, 20] 189 | add_range(range_list, 3, 3) 190 | assert range_list == [0, 1, 3, 3, 5, 10, 15, 17, 20, 20] 191 | add_range(range_list, 2, 2) 192 | assert range_list == [0, 3, 5, 10, 15, 17, 20, 20] 193 | add_range(range_list, 4, 4) 194 | assert range_list == [0, 10, 15, 17, 20, 20] 195 | add_range(range_list, 18, 19) 196 | assert range_list == [0, 10, 15, 20] 197 | add_range(range_list, 11, 14) 198 | assert range_list == [0, 20] 199 | 200 | def test_generate_block(w3, tester, accts): 201 | db = EphemDB() 202 | # Generate transactions 203 | total_deposits = 1000 204 | total_txs = 1000 205 | txs = generate_txs(total_txs, 500, total_deposits, 10, accts) 206 | 207 | start_time = time.time() 208 | buckets = create_tx_buckets(db, txs) 209 | root_hash = construct_tree(db, [bucket.tx_merkle_tree_root_hash for bucket in buckets]) 210 | end_time = time.time() - start_time 211 | 212 | print('~~~\nTxs:') 213 | print([(tx.start, tx.offset, tx.is_swap) for tx in txs]) 214 | print('~~~\nBuckets:') 215 | 216 | for bucket in buckets: 217 | print(bucket.start, [(tx.start, tx.offset, tx.is_swap) for tx in bucket.txs]) 218 | print('Committing block root hash:', root_hash) 219 | print(int.from_bytes(root_hash[24:], byteorder='big')) 220 | print('Processed', total_txs, 'transactions') 221 | print("--- in %s seconds ---" % (end_time)) 222 | 223 | print('done') 224 | -------------------------------------------------------------------------------- /plasma0/tests/operator/test_signature_validation_performance.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | def test_signature_validation_performance(w3, accts): 4 | raw_txs = [] 5 | num_sigs = 1000 6 | print('Starting test') 7 | for i in range(num_sigs): 8 | test_tx = { 9 | 'to': accts[0].address, 10 | 'value': 1000000000, 11 | 'gas': 2000000, 12 | 'gasPrice': 234567897654321, 13 | 'nonce': (i % len(accts)), 14 | 'chainId': 1 15 | } 16 | raw_txs.append(accts[i % len(accts)].signTransaction(test_tx)['rawTransaction']) 17 | 18 | start_time = time.time() 19 | for r in raw_txs: 20 | w3.eth.account.recoverTransaction(r) 21 | print('Total time to recover', num_sigs, 'sigs:', time.time() - start_time) # On my macbook I was processing 100 sigs per second. 22 | -------------------------------------------------------------------------------- /plasma0/tests/operator/test_state.py: -------------------------------------------------------------------------------- 1 | from random import randrange 2 | import time 3 | from eth_utils import ( 4 | big_endian_to_int, 5 | ) 6 | from plasmalib.state import get_total_deposits_key, State 7 | from plasmalib.operator.transactions import TransferRecord, SimpleSerializableList 8 | from plasmalib.utils import ( 9 | int_to_big_endian8, 10 | ) 11 | 12 | 13 | def test_deposit(blank_state, mock_accts): 14 | state = blank_state 15 | for a in mock_accts: 16 | state.add_deposit(a.address, 0, 10) 17 | # Check that total deposits was incremented correctly 18 | end_total_deposits = big_endian_to_int(state.db.get(get_total_deposits_key(int_to_big_endian8(0)))) 19 | assert end_total_deposits == 1000 20 | print('End total deposits:', end_total_deposits) 21 | 22 | def make_simple_state(state, accts): 23 | for i in range(5): 24 | # Fill up tokens 0-99, alternating between two owners every 10 tokens 25 | state.add_deposit(accts[0].address, 0, 10) 26 | state.add_deposit(accts[1].address, 0, 10) 27 | return state 28 | 29 | def test_add_transaction(blank_state, mock_accts): 30 | state = make_simple_state(blank_state, mock_accts) 31 | tr1 = TransferRecord(mock_accts[0].address, mock_accts[1].address, 0, 0, 9, 0, 0, 3) 32 | tr2 = TransferRecord(mock_accts[1].address, mock_accts[0].address, 0, 10, 9, 0, 0, 3) 33 | tr_list = SimpleSerializableList([tr1, tr2]) 34 | print(tr_list) 35 | print(tr_list.serializableElements) 36 | state.add_transaction(tr_list.serializableElements) 37 | 38 | def test_get_ranges(blank_state, mock_accts): 39 | state = make_simple_state(blank_state, mock_accts) 40 | assert [0, 0] == [big_endian_to_int(r[8:40]) for r in state.get_ranges(0, 0, 9)] 41 | assert [10, 10, 20, 20] == [big_endian_to_int(r[8:40]) for r in state.get_ranges(0, 10, 29)] 42 | assert [0, 0, 10, 10] == [big_endian_to_int(r[8:40]) for r in state.get_ranges(0, 5, 19)] 43 | assert [0, 0, 10, 10, 20, 20] == [big_endian_to_int(r[8:40]) for r in state.get_ranges(0, 5, 29)] 44 | 45 | def test_delete_ranges(blank_state, mock_accts): 46 | state = make_simple_state(blank_state, mock_accts) 47 | assert [0, 0] == [big_endian_to_int(r[8:40]) for r in state.get_ranges(0, 0, 9)] 48 | ranges_to_delete = state.get_ranges(0, 10, 29) 49 | assert [10, 10, 20, 20] == [big_endian_to_int(r[8:40]) for r in ranges_to_delete] 50 | state.delete_ranges(ranges_to_delete) 51 | new_ranges = state.get_ranges(0, 10, 29) 52 | assert [0, 0] == [big_endian_to_int(r[8:40]) for r in new_ranges] 53 | 54 | def test_check_ranges_owner(blank_state, mock_accts): 55 | state = make_simple_state(blank_state, mock_accts) 56 | state.add_deposit(mock_accts[1].address, 0, 10) 57 | test_range = state.get_ranges(0, 0, 9) 58 | assert state.verify_ranges_owner(test_range[1::2], mock_accts[0].address) 59 | test_range = state.get_ranges(0, 0, 19) 60 | assert not state.verify_ranges_owner(test_range[1::2], mock_accts[0].address) 61 | assert not state.verify_ranges_owner(test_range[1::2], mock_accts[1].address) 62 | test_range = state.get_ranges(0, 90, 109) 63 | assert state.verify_ranges_owner(test_range[1::2], mock_accts[1].address) 64 | 65 | 66 | def test_performace_get_ranges(blank_state, mock_accts): 67 | state = blank_state 68 | total_deposits = 1000000 69 | start_time = time.time() 70 | # Deposit a bunch of tokens 71 | for i in range(total_deposits//10000): 72 | for a in mock_accts: 73 | # This will run 10000 deposits because there are 100 accts & each deposits 10 times 74 | for i in range(10): 75 | state.add_deposit(a.address, 0, 10) 76 | added_deposits_time = time.time() 77 | # Select a bunch of ranges 78 | for i in range(100000): 79 | dist_from_middle = randrange(1, 500) 80 | middle_select = randrange(dist_from_middle, total_deposits-dist_from_middle) 81 | state.get_ranges(0, middle_select - dist_from_middle, middle_select + dist_from_middle) 82 | get_ranges_time = time.time() 83 | 84 | print('~~~~~~~~~~~~~~ RESULTS ~~~~~~~~~~~~~~') 85 | print('Deposit time:', added_deposits_time - start_time) 86 | print('Get ranges time:', get_ranges_time - added_deposits_time) 87 | 88 | 89 | # def test_large_state_get_ranges(): 90 | # db_path = '/tmp/plasma_prime_blank_test_db/1543988143.476207' 91 | # file_log_path = '/tmp/plasma_prime_blank_test_tx_log/1543988143.476207' 92 | # state = State(db_path, file_log_path, backup_timeout=60) 93 | # total_deposits = 100000000 94 | # start_time = time.time() 95 | # for i in range(100000): 96 | # dist_from_middle = randrange(1, 500) 97 | # middle_select = randrange(dist_from_middle, total_deposits-dist_from_middle) 98 | # state.get_ranges(0, middle_select - dist_from_middle, middle_select + dist_from_middle) 99 | # get_ranges_time = time.time() 100 | # print('Get ranges time:', get_ranges_time - start_time) 101 | -------------------------------------------------------------------------------- /plasma0/tests/operator/test_transactions.py: -------------------------------------------------------------------------------- 1 | from web3 import Web3 2 | from plasmalib.operator.transactions import ( 3 | SimpleSerializableElement, 4 | SimpleSerializableList, 5 | TransferRecord, 6 | Signature, 7 | ) 8 | 9 | def test_transactions(): 10 | sender = Web3.sha3(1)[2:22] 11 | recipient = Web3.sha3(2)[2:22] 12 | tr1 = TransferRecord(sender, recipient, 0, 300, 300, 10, 0, 3) 13 | tr2 = TransferRecord(recipient, sender, 0, 100, 100, 10, 0, 3) 14 | sig1 = Signature(0, 1, 2) 15 | sig2 = Signature(3, 4, 5) 16 | tr1_encoding = tr1.encode() 17 | tr1_decoded = SimpleSerializableElement.decode(tr1_encoding, TransferRecord) 18 | # Verify that the decoded tx has the same hash 19 | assert tr1_decoded.hash == tr1.hash 20 | tr_list = SimpleSerializableList([tr1, tr2]) 21 | sig_list = SimpleSerializableList([sig1, sig2]) 22 | tr_list_encoding = tr_list.encode() 23 | sig_list_encoding = sig_list.encode() 24 | tr_decoded_list = SimpleSerializableList.decode(tr_list_encoding, TransferRecord) 25 | sig_decoded_list = SimpleSerializableList.decode(sig_list_encoding, Signature) 26 | # Verify that the hash & start pos seem to be correct 27 | assert tr_decoded_list[0].hash == tr1.hash 28 | assert tr_decoded_list[1].start == tr2.start 29 | assert sig_decoded_list[1].v == sig2.v 30 | 31 | def test_transaction_type_checking(): 32 | sender = Web3.sha3(1)[2:22] 33 | recipient = Web3.sha3(2)[2:22] 34 | TransferRecord(sender, recipient, 0, 300, 300, 10, 0, 3) 35 | -------------------------------------------------------------------------------- /rollup-block-gen/block_gen.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | class Event: 4 | data: str 5 | 6 | class Transaction: 7 | data: str 8 | 9 | class Block: 10 | block_hash: str 11 | base_fee: int 12 | block_number: int 13 | timestamp: int 14 | events: List[Event] 15 | txs: List[Transaction] 16 | 17 | def gen_dummy_block(i: int) -> Block: 18 | events: List[Event] = [{ "data": "event 1 data" }, { "data": "event 2 data" }] 19 | txs: List[Event] = [{ "data": "tx 1 data" }] 20 | block: Block = { 21 | "block_hash": 'blockhash' + str(i), 22 | "base_fee": 'basefee' + str(i), 23 | "block_number": i, 24 | "timestamp": i, 25 | "events": events, 26 | "txs": txs 27 | } 28 | return block -------------------------------------------------------------------------------- /rollup-block-gen/sequencer_rollup.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | from enum import Enum 3 | from typing import List 4 | from block_gen import Block 5 | 6 | # A more complex rollup that allows for privilaged block 7 | # production. This is achieved by allowing a "sequencer" 8 | # to squeeze blocks into historical L1 blocks after the fact. 9 | 10 | # This expands on the "simplest possible rollup" by, in addition 11 | # to blocks being generated based on the first event, the sequencer 12 | # may submit transactions in later blocks which contain many L2 blocks. 13 | 14 | #################### Types #################### 15 | 16 | # The sequencer timeout is the number of L1 blocks that the sequencer 17 | # has to submit their blocks before they can no longer add sequencer 18 | # blocks in between L1 blocks. 19 | sequencer_timeout = 10 # AKA force inclusion period 20 | 21 | # Batch Element Types 22 | class BatchElementType(Enum): 23 | # Sequencer blocks are of type Block 24 | SEQUENCER_BLOCK = 1 25 | # L1 Block Numbers are of type L1BlockNumber 26 | L1_BLOCK_NUMBER = 2 27 | 28 | class L1BlockNumber: 29 | block_number: int 30 | 31 | class BatchElement: 32 | type: BatchElementType 33 | data: str 34 | 35 | class SequencerBatch: 36 | elements: List[BatchElement] 37 | 38 | #################### Functions #################### 39 | 40 | # Generate the full rullup chain 41 | def generate_rollup(l1_chain: List[Block]) -> List[Block]: 42 | # Simplest version of this is to repeatedly call 43 | # `generate_rollup_chain_subset` with block_n through block_n+sequencer_timeout 44 | pass 45 | 46 | # Generate a subset of the rollup chain, used for fraud proofs. 47 | # All blocks from `start_block` to `start_block+sequencer_timeout` must 48 | # be supplied. This is how we can calculate both the deposit block and all the\ 49 | # sequencer blocks as well. 50 | def generate_rollup_chain_subset(l1_chain_subset: List[Block]) -> List[Block]: 51 | # This will iterate through all blocks within this range and generate rollup blocks 52 | # which are found. 53 | pass -------------------------------------------------------------------------------- /rollup-block-gen/simple_rollup.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | from typing import List 3 | from block_gen import Block 4 | 5 | # The simplest possible rollup where the rollup blocks are 6 | # encoded as the first event of every L1 block. 7 | 8 | def generate_rollup(l1_chain: List[Block]) -> List[Block]: 9 | l2_chain: List[Block] = [] 10 | for block in l1_chain: 11 | l2_block = generate_rollup_block(block) 12 | l2_chain.append(l2_block) 13 | return l2_chain 14 | 15 | # This function is used in the fraud proof. It allows for an 16 | # easy stateless transformation that determines the L2 blocks 17 | # at a particular L1 block number. 18 | def generate_rollup_block(l1_block: Block) -> Block: 19 | first_tx = l1_block["events"][0]["data"] 20 | l2_block = pickle.loads(first_tx) 21 | return l2_block -------------------------------------------------------------------------------- /rollup-block-gen/test_sequencer_rollup.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import pickle 3 | from block_gen import Block, Event, gen_dummy_block 4 | from typing import List 5 | from sequencer_rollup import ( 6 | generate_rollup, 7 | sequencer_timeout, 8 | SequencerBatch, 9 | BatchElement, 10 | BatchElementType, 11 | L1BlockNumber 12 | ) 13 | 14 | def test_generate_rollup(): 15 | l1_chain: List[Block] = [] 16 | 17 | for i in range(100): 18 | block = gen_dummy_block(i) 19 | encoded_l2_deposit = pickle.dumps(block) 20 | block["events"][0]["data"] = encoded_l2_deposit 21 | # Add sequencer batches 22 | if i > sequencer_timeout: 23 | batch_elements: BatchElement = [] 24 | if i % 2 != 0: 25 | be = gen_l1_block_batch_element(i, sequencer_timeout) 26 | batch_elements.append(be) 27 | seq_be = gen_sequencer_block_batch_element(l1_chain[i - sequencer_timeout]) 28 | batch_elements.append(seq_be) 29 | # Now that we have the batch elements, let's encode and add it as a tx! 30 | encoded_batch_elements = pickle.dumps(batch_elements) 31 | block["txs"][0]["data"] = encoded_batch_elements 32 | 33 | 34 | l1_chain.append(block) 35 | 36 | print("Generated the L1 chain successfully!") 37 | # l2_chain = generate_rollup(l1_chain) 38 | 39 | def gen_l1_block_batch_element(i: int, seq_timeout: int) -> BatchElement: 40 | int_l1_block_num: int = i - seq_timeout 41 | l1_block_num: L1BlockNumber = { 42 | "block_number": int_l1_block_num 43 | } 44 | encoded_l1_block_num = pickle.dumps(l1_block_num) 45 | be: BatchElement = { 46 | "type": BatchElementType.L1_BLOCK_NUMBER, 47 | "data": encoded_l1_block_num 48 | } 49 | return be 50 | 51 | def gen_sequencer_block_batch_element(seq_block: Block) -> BatchElement: 52 | encoded_sequencer_block = pickle.dumps(seq_block) 53 | be: BatchElement = { 54 | "type": BatchElementType.L1_BLOCK_NUMBER, 55 | "data": encoded_sequencer_block 56 | } 57 | return be -------------------------------------------------------------------------------- /rollup-block-gen/test_simple_rollup.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import pickle 3 | from block_gen import Block, Event, gen_dummy_block 4 | from typing import List 5 | from simple_rollup import generate_rollup 6 | 7 | def test_generate_rollup(): 8 | l1_chain: List[Block] = [] 9 | expected_l2_chain: List[Block] = [] 10 | 11 | for i in range(100): 12 | block = gen_dummy_block(i) 13 | expected_l2_chain.append(copy.deepcopy(block)) 14 | l2_block_encoded = pickle.dumps(block) 15 | block["events"][0]["data"] = l2_block_encoded 16 | l1_chain.append(block) 17 | l2_chain = generate_rollup(l1_chain) 18 | 19 | for i in range(len(expected_l2_chain)): 20 | assert l2_chain[i]["timestamp"] == expected_l2_chain[i]["timestamp"] 21 | 22 | # Woot! We pulled the rollup out of the L1 chain! --------------------------------------------------------------------------------