├── .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!
--------------------------------------------------------------------------------