Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Standard user account #230

Merged
merged 20 commits into from
Jan 19, 2018
Merged

Conversation

jannikluhn
Copy link
Contributor

@jannikluhn jannikluhn commented Dec 19, 2017

The standard user account is a contract parametrized by an address that

  • Validates that the transaction is signed with the private key corresponding to the address,
  • validates that the block number is in a certain range (given by the transaction),
  • stores a nonce that is incremented at every call,
  • pays for gas, and
  • calls another contract.

It also has additional methods to

  • extract the nonce and to
  • validate an arbitrary signed message against the owner address.

Once the PR is finished it should contain

  • the contract code both as Vyper LLL and compiled bytecode
  • extensive tests for the contract code
  • utility functions to construct transactions in a format understood by the contract
  • utility functions to deploy such a contract (which might be split into a main and a validation contract)

References:

Copy link
Member

@pipermerriam pipermerriam left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good.

#

@property
def signature(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what do you think about renaming this to vrs instead of signature? I think that naming convention would match better with other portions of both this codebase and others (web3.py and eth-keys).

With the conventions we've been establishing, I would expect this to return a eth_keys.Signature object.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you prefer renaming it to vrs or making it return a Signature object? I have no preference in that regard.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think vrs is my preference since this is a low-level API

self.data = self.data[:VALUE_SLICE.start] + b + self.data[VALUE_SLICE.stop:]

@property
def receiver(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fragments the terminology that we use for the destination of a transaction. Elsewhere we're using to. Any thoughts on whether it'd be better to conform to this naming convention?

Copy link
Contributor Author

@jannikluhn jannikluhn Dec 20, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure... We used to have to and sender. Now we have target, sender and receiver.

  • to and target refer to more or less the same thing I guess (the first account that is called)
  • sender changes meaning: it used to be the one who signed the transaction, now it's the "owner" of the target account (and just happens to put a signature in the data field)
  • receiver is the second hop (the account that's called by the target account)

So from a technical standpoint, I don't see a lot of overlap between original and new terms (sender and receiver are new concepts, and don't live at the protocol level at all). I could imagine renaming target to to for consistency (although I'd prefer target in a vacuum). Renaming receiver to to could be misleading on the protocol level, but would make sense from a high level point of view in most cases. I don't see a good alternative to sender.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good explanation. I think you are right to leave this as-is and not change the names from target/sender/receiver

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the name destination better capture the concept that receiver currently represents?



def sighash(computation):
pass
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thoughts on changing this to a raise NotImplementedError?



def paygas(computation):
pass
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thoughts on changing this to a raise NotImplementedError?

mnemonic=mnemonics.PAYGAS,
gas_cost=0,
),

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

whitespace


def test_valid_transfer(chain, tx1):
vm = chain.get_vm()
vm.apply_transaction(tx1)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there any assertions that we could make here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely. That's one of the reasons why it's still WiP (this test doesn't even pass yet)



USER_ACCOUNT_CODE = decode_hex(
"0x3f595260805903608020600052602060006080600060006001610bb8f1506001600051"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will you add a comment referencing the source code that produced this bytecode and how it can be reproduced/verified.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is not much more than a placeholder for now. Ideally I'd like to put the source code there directly and compile it in a test for comparison, but this will probably not work because Viper needs Python 3.6.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#206 has the same issue, it's better not import Viper even in Python 3.6.

A (little complicated) suggestion of verifying Viper code:

  1. A command for compiling Viper source code to bytecode.
  2. If someone want to update the code, they have to upload source code and bytecode (can be in separate file or assigned to a global variable in this case)
  3. Install Viper in CI testing.
  4. Write a test for auto-compiling the source code and then comparing the compiled bytecode with evm.auxiliary.user_account.USER_ACCOUNT_CODE.

By the way, @pipermerriam what do you think about upgrading to Python 3.6?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps the cleanest solution would be to put the code plus tests in a separate repository. The repository would contain packages with the bytecode for different languages that can be imported into client implementations such as Py-EVM.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By the way, @pipermerriam what do you think about upgrading to Python 3.6?

I'm in favor: ethereum/py-trie#18

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm indifferent to the actual solution for the bytecode creation as long as it's documented and independently verifiable.

@jannikluhn
Copy link
Contributor Author

I've given up on testing the contract code for now, as it's not really possible at the moment. I'll come back to it once #238 is done, but for now I'd like to put this PR on hold.

Copy link
Member

@pipermerriam pipermerriam left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you mind taking a minute to flesh out the main description for this pull request.

@pipermerriam
Copy link
Member

I think that a large part of this diff will go away once this is rebased.

@jannikluhn
Copy link
Contributor Author

Updated the description. Sorry for the current state of the PR. Now that #238 is merged I'm back at writing tests/debugging the contract. In the meantime I've made a couple of changes in the contract. I'm going to push something later today.

@jannikluhn
Copy link
Contributor Author

Still not completely finished, but almost there I hope.

To run the tests, cherry-pick 90d155d, resolve the merge conflicts (accept both), and replace this line with pass.

@pipermerriam
Copy link
Member

Sighash is now merged so this can be rebased (as I assume it needs sighash in it to work)

@pipermerriam
Copy link
Member

eh, should have said sighash is ready for merge :P

@jannikluhn
Copy link
Contributor Author

Merged the sighash PR and rebased. This should be ready for review now.

I'm not particularly happy with some names in this one, especially ForwardingTransaction. I called it forwarding because the "payload" data is passed through to the actual "destination" contract. But it's specific to one particular user contract, others might require a different format.

Test-wise I'd like to see tests that make sure that everything works as expected even if the gas runs out at different points during the execution of the contract. But this can only be done once PAYGAS is properly implemented.

@jannikluhn
Copy link
Contributor Author

jannikluhn commented Jan 11, 2018

Ooh, I missed one thing: access_list is left empty all the time. It should default to the whole storage space of the destination account and to the required parts of user and validation accounts (as suggested here). Tests are not failing because of #263 .

Copy link
Member

@pipermerriam pipermerriam left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a few suggestions for cleanup/readability/opinionated-code-style

EMPTY_DATA = b'\x00' * DESTINATION_SLICE.stop


class ForwardingTransaction(ShardingTransaction):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other name ideas

  • StandardAccountTransaction
  • ProxyTransaction
  • EIP86Transaction (or whatever the appropriate EIP number is)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

StandardAccountTransaction was the name before I switched to ForwardingTransaction. I dismissed it because I'm not sure if "standard account" is a good term either. It should probably be rather called "standard user account", and even that assumes that this contract will actually become "the" standard. But StandardUserAccountTransaction strikes me as too long.

General note on EIPs: As far as I know there are no EIPs for the sharding protocol itself. At least at the moment, I don't know if that will change.

self.sig_hash,
])

def sign(self, private_key):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was it intentional to deviate from the existing API for transaction signing which uses as_signed_transaction which returns a new object rather than mutating the existing object?

This question is from a broader question about the use of property setters.

  • if the use of property setters was to embed validation as part of setting the value, then I would suggest changing to use a single validate() method which is inline with the other transaction classes.
  • if the use of property setters was to allow mutation of the object, I would like to understand why since we've trended away from most forms of object mutation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the setters to allow mutation. The other transaction objects are technically mutable as well and I wanted to mirror that. But if they aren't mutated in practice I'll be happy to remove them because all these setters look quite ugly. I'll also try to implement the as_signed_transaction pattern.

"code": b"",
}

DEFAULT_TX_PARAMS = {**DEFAULT_BASE_TX_PARAMS, **{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method of dictionary merging has always bugged me, mostly because it takes advantage of the implementation rather than using formal semantics or which keys take precedence. I admit it could be argued that the way key precedence is handled in this case is part of the formal semantics for how this works, but either way, it's always felt a bit like magic.

What do you think about making use of something like the following. (If you haven't figured it out by now, I have great love for the toolz/cytoolz library)

from cytoolz import (
    merge,
    dissoc,
)

DEFAUT_TX_PARAMS = merge(
    dissoc(DEFAULT_BASE_TX_PARAMS, 'code'),
    {...<the overrides>},
}

Zero mutation and explicit about dictionary merging.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally I don't prefer one way over the other, so I don't mind adopting this style.

(v, r, s - 1),
(v, r, 0),

(27 if v == 28 else 28, r, SECPK1_N - s)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we parametrize these using pytest.mark.parametrize so that they each run in isolation?


logs = computation.get_log_entries()
assert len(logs) == 1
assert big_endian_to_int(logs[0][-1]) > 900 * 1000 # some gas will have been consumed earlier
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A slightly easier to understand version of this assertion that I've used elsewhere is to assert that the delta is smaller than a certain value.

gas_used = big_endian_to_int(logs[0][-1])
assert transaction.gas - gas_used < 1000

block_number=10,
gas_limit=3141592,
timestamp=1422494849,
parent_hash=decode_hex("0000000000000000000000000000000000000000000000000000000000000000"),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use evm.constants.ZERO_HASH32.

gas=gas,
gas_price=gas_price,
access_list=access_list,
code=b'',
)

def create_unsigned_transaction(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To stick with the existing transaction API this should be a classmethod. This API exists to reduce the cumbersome import of both the signed and unsigned classes, allowing you to create unsigned transactions from the main transaction class. I'd be fine with this just being an *args, **kwargs passthrough to reduce size since there are 10+ arguments.

s=s,
)
# don't detect invalidity before transaction execution
transaction.validate = lambda: None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a more appropriate way to check this would be to assemble a Message and then to run vm.get_computation(message).apply_message().

This should execute the transaction below the place where the call to transaction.validate() occurs, allowing you to directly test that the EVM code appropriately validates the signature without having to do any monkeypatching of the transaction object.

@hwwhww
Copy link
Contributor

hwwhww commented Jan 13, 2018

I was checking with @vbuterin about renaming some parameters and also mentioned about
ShardingTransaction.to or ShardingTransaction.target, he clarified his intention:

so the reason I wanted to rename it to target is that in real life, the transaction.to field will be the sender of the money (and/or function call)
as it would be the forwarding contract that controls the account from which the funds get sent and the receiver would be specified in the tx.data under some encoding
I think it would cause too much long-term risk of devs getting confused for transaction.to to be the sender
hence my idea to rename it target

@jannikluhn jannikluhn force-pushed the standard-user-account branch 2 times, most recently from f7897d9 to 35476e8 Compare January 15, 2018 13:36
@jannikluhn
Copy link
Contributor Author

Addressed comments and rebased.

@vbuterin Do you have time to review the contract code? https://github.com/ethereum/py-evm/pull/230/files#diff-05178e170e27782a10a1498ec2aefdef

['if',
['eq',
['calldataload', CALLDATA_FUNCTION_ID],
NONCE_GETTER_ID],

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't NONCE_GETTER_ID be multiplied by 2^224? It's the first four bytes that are the function sig, so getting the first 32 bytes would return that with 28 zero bytes at the end.

BTW are there tests for all of this? If so, the tests should catch this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, will fix this.

Tests are here: https://github.com/jannikluhn/py-evm/blob/07587ac50734c62a7d8862c072fbcaeedaa93c97/tests/auxiliary/user-account/test_contract.py

They cheat a bit by importing the function ids, that's why they didn't catch this. Will add a test comparing against a manually generated one (using the Vyper code).

['calldataload', CALLDATA_FUNCTION_ID],
VALIDATION_CODE_GETTER_ID,
# return validation code address
['mstore', 0, validation_code_address],

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that I think about it, what would the validation code address be? We don't want to create two contracts per account. I would actually recommend just not bothering with this get_validation_code feature at this point to keep it simpler.

# output data (discarded)
0,
0
]],

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not do things this way. This would require calling 2 accounts to send from this account, which would blow up witness size by a factor of 2. IMO just do ecrecover here.

['sstore', STORAGE_NONCE, ['add', ['calldataload', CALLDATA_NONCE], 1]],

# Assert that we won't run out of gas from here on
['assert', ['gt', 'gas', GAS_RESERVE + GAS_RESERVE_OFFSET]],

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this line still necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Depends: If we run out of gas after the PAYGAS call, the transaction can be replayed infinitely and the account would get drained. This line makes sure that this can never happen. If we trust the user to never create transactions with not enough gas, the check isn't necessary. But at least I would feel much safer with it.

@jannikluhn
Copy link
Contributor Author

Anything else or can we merge?

@jannikluhn
Copy link
Contributor Author

I'm also in favor of renaming to to target, but I think this should be done in a separate pull request.

@jannikluhn
Copy link
Contributor Author

Rebased. Is this Ready-for-merge when tests have passed?

VALIDATION_CODE_GAS = 3500
GAS_RESERVE = 4500 # amount of gas reserved for returning
GAS_RESERVE_OFFSET = 200
NONCE_GETTER_ID = 0x141b5b48
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe add a comment to describe
# NONCE_GETTER_ID = big_endian_to_int(keccak(b"get_nonce()")[:4]) ?

Copy link
Member

@pipermerriam pipermerriam left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good to merge (when tests are green)

@hwwhww
Copy link
Contributor

hwwhww commented Jan 19, 2018

They are green now 🎉

@jannikluhn jannikluhn merged commit 081a851 into ethereum:sharding Jan 19, 2018
@hwwhww hwwhww mentioned this pull request Jan 20, 2018
2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants