spacemeshos / go-spacemesh

Go Implementation of the Spacemesh protocol full node. 💾⏰💪
https://spacemesh.io
MIT License
746 stars 210 forks source link

Improve mempool eviction policy #5317

Open arc0035 opened 8 months ago

arc0035 commented 8 months ago

I'm using Smapp 1.2.9 on Mac. Steps to produce: 1)I create a fully new address with no gas in it, then I build a spawn transaction to test my sdk. the transaction is broadcast successfully to the node and the SubmitTransactionResponse tells me that the state is in mempool However, the transaction is still in the mempool and seems not removed. 2) I transfer some smidge to this account ,after the tx is applied, the transaction is still in the mempool, neither removed or mined. 3) I try to build another spawn tx, but its nonce from projected state is still 0, causing this tx fails, reporting: "Failed to verify transaction: tx already exists" 4) Even if I use TransactionState api to check this transaction, it still show state "TRANSACTION_STATE_MEMPOOL" .I think if the transaction does not have enough fund to pay balance/gas, it should be removed immediately and reporting states like "TRANSACTION_STATE_REJECTED"

Is this expected behavior? How to bypass it?

lrettig commented 7 months ago

I've seen some similar behavior, not sure if it's the same issue or different:

There are definitely many such transactions in the mempool today. But I don't think they're blocking following txs.

lrettig commented 7 months ago

I'm beginning to understand what's going on here - although I'm also beginning to suspect that these are two separate (possibly related?) issues and that we might need more information to troubleshoot OP's issue.

The issue as I understand it is that transactions at present can only have three possible internal states in the node:

https://github.com/spacemeshos/go-spacemesh/blob/92b7762a6b4ce854af08c8cf6e2473442b5764b9/common/types/transaction.go#L123-L134

We may want to add an INEFFECTIVE, IGNORED, DROPPED, EXCLUDED_FROM_MEMPOOL etc. state.

As soon as a tx has been parsed (its header has been read, i.e., it's syntactically valid), it's assigned state MEMPOOL:

https://github.com/spacemeshos/go-spacemesh/blob/92b7762a6b4ce854af08c8cf6e2473442b5764b9/sql/transactions/transactions.go#L171-L177

This doesn't mean it's contextually valid, e.g., its counter may be wrong. It also doesn't mean the tx is actually in the mempool. It may only be in the database -- and I guess we save all txs in the database, even ones that will never be effective. So there's also a question of whether we should drop these txs from the database, and on what logic. (Even a tx that appears that it'll never be effective because, e.g., a tx from the same principal with a higher counter has already been processed, could theoretically appear in a valid older block that a node missed, or due to a reorg.)

brusherru commented 1 month ago

This issue is still actual and should be solved. I've published some transactions which cannot be processed (except maybe the first one):

  1. I've published self-spawn with nonce 0, but had zero balance
  2. When I trying to publish again it returns "tx already exist"
  3. So I've published a couple of self spawn transactions with other nonces (going out of order, e.g. 2, 100, 22222)

All of them has mempool status, does not change account nonce, and blocks from submitting tx (with the same nonce). The status is not changing over the time.

And if they actually persist in a mempool, then it might be an attack vector.

Here is one of my transactions:

curl -d "{\"txid\":[\"Qdke0v/mYF5XZSuFQAKlTvQir2PpViEqHq6i8YtCttI=\"], \"include_state\": true, \"include_result\": true,\"limit\":1 }" -X POST https://wallet-api.spacemesh.network/spacemesh.v2alpha1.TransactionService/List

{"transactions":[{"tx":{"id":"Qdke0v/mYF5XZSuFQAKlTvQir2PpViEqHq6i8YtCttI=","principal":"sm1qqqqqq89p5d8jxyyrp0jgfvfldcdu4d8vpgnclcqcvr47","template":"sm1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqg56ypy7","method":0,"nonce":{"counter":"2222"},"maxGas":"100432","gasPrice":"1","maxSpend":"0","raw":"AAAAAADlDRp5GIQYXyQliftw3lWnYFE8fwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAG5IgTMkR8HdG1+KGb+Mclb5PwOdyHXo5ING3vu66DFeG63YoNkH5Vwri7llqIyQf5rwJCG/SSdPP6IBf4/gLFmMz1zPRo+DABnFO9+uXOCBK+UTu3RqSUzbohqOOAmwRL9rQc=","type":"TRANSACTION_TYPE_UNSPECIFIED","contents":{"singleSigSpawn":{"pubkey":"cc911f07746d7e2866fe31c95be4fc0e7721d7a3920d1b7beeeba0c5786eb762"}}},"txResult":null,"txState":"TRANSACTION_STATE_MEMPOOL"}],"total":1}%        
brusherru commented 1 month ago

Okay, I run another test on standalone network and published 2 self spawn transactions (with nonces 0 and 1) with zero balance. Once I got positive balance this transactions were applied, one processed and one rejected. So I guess they are actually in the mempool...

curl -d "{\"txid\":[\"X9YwBi7EoA5g5Ro7rfhsKzvzsgiYM99rE6v8GYB5XA0=\"], \"include_state\": true, \"include_result\": true,\"limit\":1 }" -X POST http://127.0.0.1:8080/127.0.0.1:9095/spacemesh.v2alpha1.TransactionService/List

{"transactions":[{"tx":{"id":"X9YwBi7EoA5g5Ro7rfhsKzvzsgiYM99rE6v8GYB5XA0=", "principal":"standalone1qqqqqq89p5d8jxyyrp0jgfvfldcdu4d8vpgnclcwe3cnj", "template":"standalone1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqg6me6zj", "method":0, "nonce":{"counter":"1"}, "maxGas":"100432", "gasPrice":"1", "maxSpend":"0", "raw":"AAAAAADlDRp5GIQYXyQliftw3lWnYFE8fwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEEBMyRHwd0bX4oZv4xyVvk/A53Idejkg0be+7roMV4brdiEw511ZDZXJFeLIQCcg66tiFDbllS0aA1jjvKRzg0iwO3BMNq9t36BNc/ff6DI39UlFT5++fFHRrg7NKQua6DAQ==", "type":"TRANSACTION_TYPE_UNSPECIFIED", "contents":{"singleSigSpawn":{"pubkey":"cc911f07746d7e2866fe31c95be4fc0e7721d7a3920d1b7beeeba0c5786eb762"}}}, "txResult":{"status":"TRANSACTION_STATUS_FAILURE", "message":"account already spawned", "gasConsumed":"100432", "fee":"100432", "block":"CUezL8kHOkimH00ipFJSwlnQOvE=", "layer":22, "touchedAddresses":["standalone1qqqqqq89p5d8jxyyrp0jgfvfldcdu4d8vpgnclcwe3cnj"]}, "txState":"TRANSACTION_STATE_PROCESSED"}], "total":1}%                                

curl -d "{\"txid\":[\"mpPfyNOSyXdtdBkGMPK7R3pY4zBzaDoQv9s2NuCfhZs=\"], \"include_state\": true, \"include_result\": true,\"limit\":1 }" -X POST http://127.0.0.1:8080/127.0.0.1:9095/spacemesh.v2alpha1.TransactionService/List

{"transactions":[{"tx":{"id":"mpPfyNOSyXdtdBkGMPK7R3pY4zBzaDoQv9s2NuCfhZs=", "principal":"standalone1qqqqqq89p5d8jxyyrp0jgfvfldcdu4d8vpgnclcwe3cnj", "template":"standalone1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqg6me6zj", "method":0, "nonce":{"counter":"0"}, "maxGas":"100432", "gasPrice":"1", "maxSpend":"0", "raw":"AAAAAADlDRp5GIQYXyQliftw3lWnYFE8fwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEABMyRHwd0bX4oZv4xyVvk/A53Idejkg0be+7roMV4brdimN8FJS5OKvkrGE9r5HK5JQYGeFDYnBwr1Yfl52zMbdGVbJriA2dOuIkgjV3Kyh99BjqrGEQbitZcyMZ9TMhuAA==", "type":"TRANSACTION_TYPE_UNSPECIFIED", "contents":{"singleSigSpawn":{"pubkey":"cc911f07746d7e2866fe31c95be4fc0e7721d7a3920d1b7beeeba0c5786eb762"}}}, "txResult":{"status":"TRANSACTION_STATUS_SUCCESS", "message":"", "gasConsumed":"100432", "fee":"100432", "block":"CUezL8kHOkimH00ipFJSwlnQOvE=", "layer":22, "touchedAddresses":["standalone1qqqqqq89p5d8jxyyrp0jgfvfldcdu4d8vpgnclcwe3cnj"]}, "txState":"TRANSACTION_STATE_PROCESSED"}], "total":1}
lrettig commented 1 month ago

A few points. First off, there are three related but distinct issues/questions here:

It seems like our goals for the third are:

Roughly speaking, once a user submits a tx to a node, these are the possible outcomes:

Am I missing anything?

I'll do a little more homework into how other nodes/protocols handle this.

brusherru commented 1 week ago

One another case:

On TestNet 13. The tx that stuck in the mempool:

curl -d "{\"txid\":[\"4H3xp37Pq2YHDxiN6VVqwO6TDDC1VV6Q4SsXopkkiYU=\"], \"include_state\": true, \"include_result\": true,\"limit\":1 }" -X POST https://testnet-13-api.spacemesh.network/spacemesh.v2alpha1.TransactionService/List

{"transactions":[{"tx":{"id":"4H3xp37Pq2YHDxiN6VVqwO6TDDC1VV6Q4SsXopkkiYU=", "principal":"stest1qqqqqq89p5d8jxyyrp0jgfvfldcdu4d8vpgnclcad4mm8", "template":"stest1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqgf0ae28", "method":16, "nonce":{"counter":"19"}, "maxGas":"36218", "gasPrice":"1", "maxSpend":"99999999000000000", "raw":"AAAAAADlDRp5GIQYXyQliftw3lWnYFE8f0BMBAAAAADlDRp5GIQYXyQliftw3lWnYFE8fxMANu8heEVjAY+ppGvPx1hjTo0KGySkuhRvQc1iNVHkrxPg5yDouhH4vz5zhJdHilglCTpSVyEt0j/qHU7K3AD+vNPMKjcYVAI=", "type":"TRANSACTION_TYPE_SINGLE_SIG_SEND", "contents":{"send":{"destination":"stest1qqqqqq89p5d8jxyyrp0jgfvfldcdu4d8vpgnclcad4mm8", "amount":"99999999000000000"}}}, "txResult":null, "txState":"TRANSACTION_STATE_MEMPOOL"}]}

Processed one: https://testnet-13-explorer.spacemesh.network/txs/0xb1ce27d3ffd99b2ddeb99b4d8f7d70851fdf93246f132d2a22a0a7c47e56ad18

lrettig commented 1 week ago

I spoke to this in my previous comment. This is an antipattern and wallets shouldn't allow this - you should not be able to submit a spend for more than your account contains. If you do, the correct thing to do is RBF with a valid transaction.

Having said that, we still need to understand how other nodes/protocols handle this and we may just want to evict bad txs after some time, as I also said in my previous comment.

pigmej commented 1 week ago

Completely agree. On the Wallet side we should definitely add validation for "not possible to send more than the current balance".

We definitely should do RBF, just because people can do mistakes. And one of the easiest thing to do when you want to cancel, is to replace it with transaction to yourself.

But additionally on top of that I think we need some auto expiry for mempool transactions. But it's my gut feeling not an educated suggestion.

acud commented 1 week ago

@pigmej @lrettig I've added a PR #6164 to add a unit test for RBF which appears to be already possible so we can at least strike that one off the list (at least to the best of my understanding). As for the rest of the issues described above I'll have another look tomorrow.

lrettig commented 1 week ago

That's good news. Thanks for taking a look.

brusherru commented 4 days ago

Btw, another inconsistency in the behavior:

I believe either both txs should be rejected or both should be accepted into the mempool.