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

LFM Phase-1 #30

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open

LFM Phase-1 #30

wants to merge 16 commits into from

Conversation

giladHaimov
Copy link

@giladHaimov giladHaimov commented Jun 12, 2024

Local-Fee Market phase1: Native-token discount

Design-doc

Target

Provide gas-price discount for native-token transfer txs. his discount will only be applied for EOA targeted transfer so to avoid discounting potentially complex receive() logic at the destination contract.

The statistical calculation of the discount is provided at locaFeeMarket.AdjustGasPrice and relies on the historical mean and standard-deviation Core values calculated over the last year.

Interleaving into main tx flow

In order to do this two major problems needed to be addressed after the discount has been introduced into the tx:

  1. tx validation logic failing with an 'underpriced' error
  2. the recovery of the orig tx signer failed due to the (orig) gasPrice being interleaved in the R, S, V, signer calculation

To handle both these issues the original gas price needed to be accessible and these points of the calculation so to avoid these problems. Since this price is determined per-tx, a new field - OrigGasPrice - has been added to the internal transaction struct and is initialized right before discount introduction for future processing.
It should be noted that this field is geth-internal and is not visible to users via the tx receipt or any other mean.

Account-Abstraction support

Since this feature was designed to only work between EOAs and since ERC-4337 always relies on smart-contract logic rather than private-key, no discount will be provided for AA accounts. While I could thinks of ways around this limitation, mostly involving creating some sort of detection-mechanism for paymasters, these will involve an amount of complexion we're currently better off without.

Valid native-transfer detection

The following conditions are used to detect a native-transfer tx which is applicable for discount:

  1. Data size = 0 so to omit transfer commands issued from a smart contract
  2. Value > 0 so not to deal with non-transfer txs
  3. Gas units = 21k so to omit transfer txs aimed to a non-EOA (i.e. a smart contract)

It should be noted that condition #3 also allows us to omit txs issued from a contract's constructor, where data size is still zero due to the contract not yet being deployed.

Limitations

  1. (by design) Only applicable to EOA target so not to discount potentially costly contract logic
  2. (by design) Applicable only for EOA i.e. transferring Core to a smart-contract - even if it's receive() logic is empty - will still be under old prices
  3. (by design) Discounts will not be applied for 'internal txs' where a smart contract issues a transfer command as part of its internal logic
  4. Discount will only be provided if the transaction data field is empty
  5. Not applicable to txs issued directly from a node terminal: since the vast majority of the txs are sent via RPC, we decided to apply the Pareto principle here
  6. Burning addresses (0x00, 0xDEAD etc.) were not ruled out and will be discounted as well. Again Pareto principle.

Executed tests

  1. send native tx to EOA via RPC to the miner node ==> discount
  2. send native tx to EOA via RPC to a non-miner node ==> discount
  3. send native tx to contract destination via RPC to a miner node
  4. send native tx to contract destination via RPC to a non-miner node
  5. send a contract function internally containing a non-bounded native transfer call to an EOA to a non-miner node
  6. send a contract function internally containing a non-bounded native transfer call to an EOA to a miner node
  7. send a contract function internally containing a native transfer call bounded to 21k gas units to an EOA to a miner node
  8. send a contract function internally containing a native transfer call bounded to 21k gas units to an EOA to a non-miner node

Only (1) and (2) have resulted in a discount, as should be.

Copy link

@atasktos atasktos left a comment

Choose a reason for hiding this comment

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

Great work on this. Given the huge surface area of core-chain my review was not exhaustive - I mostly focused on the code you did change as opposed to the related code sections you chose not to modify. An auditor familiar with geth, such as Gokberk, is certainly needed here.

We are intentionally handling transactions originating from node terminals differently by not discounting them, as noted in Limitations: #4. Are there any implications of doing so that render certain node terminal operations obsolete? Does a differentiation of this type already exist between the two transaction categories (RPC, node terminal), or are we creating a novel differentiation?

@@ -269,7 +269,7 @@ func New(
candidateHubABI: cABI,
signer: types.NewEIP155Signer(chainConfig.ChainID),
}

Copy link

Choose a reason for hiding this comment

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

Suggested change

Remove leftover new line

Copy link
Author

Choose a reason for hiding this comment

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

removed

@@ -0,0 +1,318 @@
package locaFeeMarket
Copy link

Choose a reason for hiding this comment

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

Suggested change
package locaFeeMarket
package lfm
  1. "local" is misspelled
  2. Package names in Golang should be lowercase, see here

Rename the package to either lfm or localfeemarket

Copy link
Author

Choose a reason for hiding this comment

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

renamed 'lfm'

Comment on lines 14 to 16
historicalMeanGasPrice = 35783571428 //wei
historicalSDGasPrice = 849870638 //wei
maxDiscountedGasPrice = historicalMeanGasPrice + historicalSDGasPrice/2 // =36208506747wei
Copy link

Choose a reason for hiding this comment

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

Will we want to update historicalMeanGasPrice and historicalSDGasPrice as part of each future hardfork, with the values determined by the entire network history? Or will they be fixed and constant as of the introduction of lfm?

Copy link
Author

@giladHaimov giladHaimov Sep 10, 2024

Choose a reason for hiding this comment

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

{length warning followed by a general discussion of external network config}

One that I want to address here is regarding our ability to update the historical stats values and, in general, LFM data used by the new gas calc. model.

Indeed we can update these values on each hardfork point and this will allow is not to lag too much behind real world network stats, but there is a better way that will to update the LFM values in an ongoing manner without relying on hardforks.

To do this we will need to create a governance-managed smart contract that will contain said values, have its data periodically updates by the governance body, which may or may not be identcal to the genesis contract governance and basically make sure the stats used are always kept up-to date.

The problem with this approach is that content of this governance contract will need to be periodically read and agreed upon by all network nodes, which is tricky to achieve.

The simplest way to go would be to read the latest data upon each tx processing but that would impose a significant performance cost that we probably will not want to pay. remember that a 3-second block may contains thousands of txs.

The other route would be to cache and periodically refresh the contarct data on each occurrence ('period') agreed upon by all nodes say each block start or epoch or (block_number%1000 == 0) or something.
This approach will most likely reduce the performance impact to an acceptable value, but will introduce other problems
Data context: data regarding block number, current epoch etc. will need to be propagated to the LFM function which may result in additional code complexity
Node synch: since it typically takes some time for all nodes to agree on the current block number, we could run into data mismatch between different network validators

Solving these problems is a major step in moving towards the next steps of LFM.
I'm currently investigating several ways for solving these problems. Regardless, I prefer to keep the current, simpler, approach for the 1st delivery (KISS).

Once we have a solution that allows for such governance-managed contract update we will need to run extensive tests to verify that performance impact is acceptable and that no node synch problem has been introduced.
Note that such a solution will also be a solid baseline for the network subsidy-fund (aka LFM/v2) which, in fact, relies on such a contract for fund data synchronization.

)


func AdjustGasPriceForEstimation(_networkGasPrice *hexutil.Big, _gasAmount *hexutil.Uint64, _value *hexutil.Big, to *common.Address, data []byte) *hexutil.Big {
Copy link

Choose a reason for hiding this comment

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

Why are some function arguments prefixed with _ here?

Copy link
Author

Choose a reason for hiding this comment

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

to allow using non-underscored names inside function

}

func AdjustGasPrice(gasAmount uint64, value *big.Int, to *common.Address, data []byte, networkGasPrice *big.Int) *big.Int {
// apply gas-price discount if tx is a 'native' transfer of either Core or whitelisted erc20
Copy link

Choose a reason for hiding this comment

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

Suggested change
// apply gas-price discount if tx is a 'native' transfer of either Core or whitelisted erc20
// apply gas-price discount if tx is a 'native' transfer of either CORE or whitelisted erc20

*/
func internalCalcDiscountedGasPrice(networkGasPrice *big.Int) *big.Int {
networkIntPrice := int(networkGasPrice.Uint64())
MEAN := historicalMeanGasPrice
Copy link

Choose a reason for hiding this comment

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

Technically an unnecessary var assignment here, although I'm okay with it. Guess the syntax looks cool

return internalCalcDiscountedGasPrice(networkGasPrice)
}

func obsolete__internalCalcDiscountedGasPrice(networkGasPrice *big.Int) *big.Int {
Copy link

Choose a reason for hiding this comment

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

obsolete__internalCalcDiscountedGasPrice has 0 references, what is the justification for not removing it? If it's just here for documentation purposes let's move it to a dedicated testing or documentation file. If this information is important enough to be colocated with the actual internalCalcDiscountedGasPrice function then we should condense the information and add it as inline comments to the internalCalcDiscountedGasPrice function.

Copy link
Author

Choose a reason for hiding this comment

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

yep I left is for reviewers to diff. now removed


errNotFunctionInvocation = errors.New("tx is not a function invocation")

//-------
Copy link

Choose a reason for hiding this comment

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

Suggested change
//-------

Copy link
Author

Choose a reason for hiding this comment

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

ok

coreTransferGasAmount = 21000 // hard-coded gas amount

functionSelectorSize = 4
transferDataLen = 4+32+32 // transfer(address,uint256): 4 bytes for func ID, 32 bytes for address (20 bytes padded to 32), 32 bytes for amount
Copy link

Choose a reason for hiding this comment

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

Technically this approach can be extrapolated to any contract address/function signature, correct? For example, we could make staking CORE on the Staking.sol contract a fee-reduced transaction type

Copy link

Choose a reason for hiding this comment

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

If it's possible we'll probably end up going in that direction for some key core-genesis-contracts actions. Which begs the question if we should already rearchitect the list of ERC20 token addresses + list of function signatures into a 1-to-1 structure such that each fee-reduced contract is a struct containing:

  • an address
  • a list of valid function signatures, where each signature has a data length

The above struct is probably off by a bit, but you get the idea. Thoughts?

Copy link
Author

Choose a reason for hiding this comment

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

Technically this approach can be extrapolated to any contract address/function signature, correct? For example, we could make staking CORE on the Staking.sol contract a fee-reduced transaction type

correct and part of our design goals. we want this mechanism to be easily extendable.

Copy link
Author

Choose a reason for hiding this comment

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

If it's possible we'll probably end up going in that direction for some key core-genesis-contracts actions. Which begs the question if we should already rearchitect the list of ERC20 token addresses + list of function signatures into a 1-to-1 structure such that each fee-reduced contract is a struct containing:

  • an address
  • a list of valid function signatures, where each signature has a data length

The above struct is probably off by a bit, but you get the idea. Thoughts?

see response above as to the erc20-compliant contract address. I would not add an additional 'discounted-functions' column due to KISS as well as having no immediate value

@@ -332,6 +347,10 @@ func (tx *Transaction) GasTipCapCmp(other *Transaction) int {

// GasTipCapIntCmp compares the gasTipCap of the transaction against the given gasTipCap.
func (tx *Transaction) GasTipCapIntCmp(other *big.Int) int {
legacyTx, isLegacy := tx.inner.(*LegacyTx) //@lfm
if isLegacy && legacyTx.origGasPrice() != nil {
return legacyTx.origGasPrice().Cmp(other) // tx gas price might have been reduced by the lfm module
Copy link

Choose a reason for hiding this comment

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

👍

@giladHaimov
Copy link
Author

Great work on this. Given the huge surface area of core-chain my review was not exhaustive - I mostly focused on the code you did change as opposed to the related code sections you chose not to modify. An auditor familiar with geth, such as Gokberk, is certainly needed here.

We are intentionally handling transactions originating from node terminals differently by not discounting them, as noted in Limitations: #4. Are there any implications of doing so that render certain node terminal operations obsolete? Does a differentiation of this type already exist between the two transaction categories (RPC, node terminal), or are we creating a novel differentiation?

N

ng transactions originating from node terminals differently by not discounting them

... transactions originating from node terminals differently by not discounting them

correct, that'sconcise decision made at the beginning due to reasons of complexity and value.
99.x% of the txs are rpc txs, terminal are mainly used for developer playing their games and, when they do, the tx will indeed not be discounted.

note that there's no attack angle here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants