-
Notifications
You must be signed in to change notification settings - Fork 409
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
[WIP_DO_NOT_MERGE] Add outpoint subscribe method #446
Conversation
Many thanks for the contribution, will review this week :)
Good catch - the code in
|
I changed the search heuristic and fix errors I found after testing with logging. There is a difficult tradeoff between searching the spending transaction by scripthash and searching by block height first. I had to use both for reasonnable speed. So the current code first looks at transactions with same scripthash and then filter them to match the block height of spending transaction we can found in the index. This choice is optimal for addresses with small history but my be bad if the address' history is larger than a block. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The method is supposed to subscribe the caller but this change doesn't seem to subscribe anything. Am I missing something? Besides, there are several DoS vectors which I believe really should be fixed.
src/electrum.rs
Outdated
} | ||
|
||
let funding_inputs = &funding_tx.as_ref().unwrap().input; | ||
let outpoint_script = &funding_tx.as_ref().unwrap().output.get(vout as usize).unwrap().script_pubkey; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lines 286-293 are not Rust-idiomatic and it looks incorrect. If fetching the transaction fails it will pretend everything is OK and not subscribe. Also the client could DoS the server by sending out-of-bounds vout. I think this would be more appropriate (notice question mark at the end of zeroth line):
let funding_tx = self.daemon.get_transaction(&funding.txid, funding_blockhash)?;
let funding_inputs = &funding_tx.input;
let outpoint_script = &funding_tx.output.get(vout as usize).ok_or_else(|| anyhow::anyhow!("vout out-of-bounds")).script_pubkey;
This suggestion reports the error to the client. Perhaps it'd be better to still subscribe the client and only send notifications when it works again.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will not hide that I am terrible at Rust coding since I am litteraly doing it for the first time while working on this PR x')
I need to return an empty json if the funding transaction doesn't exist according to Electrum 1.5 specifications. Is it handled with your suggested code ?
Good catch for the possible out-of-bounds vout, thanks !
src/electrum.rs
Outdated
} | ||
|
||
|
||
let scripthash = ScriptHash::new(&outpoint_script); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I find let scripthash = outpoint_script.script_hash();
a bit nicer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree but new_status
wants a ScriptHash struct defined in types.rs
and not from rust-bitcoin (which is what you obtain when taking .script_hash()
)
Is it a problem with import lib rust-bitcoin ?
src/electrum.rs
Outdated
let scripthash = ScriptHash::new(&outpoint_script); | ||
let outpoint_scripthash_status = self.new_status(scripthash); | ||
|
||
let funding_height = self.tracker.chain().get_block_height(&funding_blockhash.unwrap()).unwrap(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will crash if funding transaction i unconfirmed -another DoS vector. I suggest this:
let funding_height = match &funding_blockhash {
Some(funding_blockhash) => self.tracker.chain().get_block_height(funding_blockhash)?,
None => -1,
};
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have to return 0 in the None case or the compiler complain that Neg is not implemented for usize. This is fine :) thanks for the catch !
src/electrum.rs
Outdated
return Ok(json!({"height": funding_height})); | ||
} | ||
|
||
let spending_height = self.tracker.chain().get_block_height(&spending_blockhash.unwrap()).unwrap(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another unidiomatic code. This one is a bit tricky but I think let mut iter = match spending_blockhash { ... };
Should do the trick. Either
from crate either
will need to be used. This can prevent allocation. You can then check for double spends:
let spender_txid = iter.next().ok_or_else(|| anyhow::anyhow!("WTF spending tx not found but exist ?"));
if let Some(double_spending_txid) = iter.next() {
// This will probably never happen so allocating is OK.
let mut txids = vec![spender_txid, double_spending_txid];
txids.extend(iter);
bail!("double spend of {}: {:?}", txid, txids);
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I understand what your suggestion is doing.
Notice that it is totally normal if we don't find any spending txid. The method can be called on a outpoint that is unspend and we have to return a JSON with only the height of the funding transaction. So I guess I have to change the argument in ok_or_else
method of your suggestion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, right then instead of ok_or_else
probably match
and None
should return the height as success?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I end up refactoring most of the code, it should be more idiomatic now ! Feel free to tell me if there is something else ! :)
Many thanks for your review @Kixunil ! I will answer to your suggestions. Yes my code doesn't implement the subscription. In fact I just tried to "merge" the work done in #444 in branch outpoint-RPC which didn't implement the subscription to notifications. I don't know how to do it for now so it only returns the expected outpoint, which is what I needed to display spending transaction in BTC-RPC-Explorer. The current code is really terrible with outpoints for which the scriptPubKey has a big history. |
I think the right way to implement subscriptions is to send them together with client channel over a bounded channel ( |
In the last commit I revert back to first getting the transactions of the block where the output is spend than searching which one is indeed spending the outpoint. My first try was too slow because I fetched transactions one by one with rpc call to Core, which is very slow. Now electrs makes only one call for the whole block where the spending transaction is and then search which one is spending the outpoint. This is a lot faster and electrs is not stalling anymore when the scriptPubKey of outpoint has a big history ! When the spending transaction is in the mempool, I had to use @Kixunil code changed a lot since scripthash is not needed anymore. I still have one or two of your suggestions I will include soon. I really don't know how to implement subscriptions as you describe, I don't think I have the skill to do it from scratch, so it could be in another PR or you could edit mine to include your implementation of subscriptions. |
I refactored the unidiomatic code to follow @Kixunil suggestions ! I learn a lot by doing it so thank you :) The only case not covered in the current code is if a spending transaction is not in the block at the height given by the index. It returns the height of funding transaction as success while it means the electrs index of spending transactions is wrong (so it should panic instead...) I will squash the commits once @romanz reviewed the PR ! I will soon publish a PR in BTC-RPC-Explorer repo to display the spending transaction of STXOs in the explorer using the outpoint.subscribe method this is really a great thing to have :P |
Lol, was just writing reply. I wonder if this could be sped up even more by optionally using I will have to look at the code later. I may also try implement subscriptions, shouldn't be too hard, I hope.
I'm not sure if panicking in case of DB corruption is the right approach. Panics are intended for programmer errors (like index out of bounds), database could be corrupted due to disk problem, so not necessarily programmer bug. |
I don't think it is the case. The ElectRS DB only tell you which block you should look at. So you must always load all the transactions of the block where the outpoint is spent to find which one is indeed spending it. So the most optimal thing you can do is asking to Core the whole block to immediatly get all the transactions in it and start exhaustive search in it.
I think we should wait for @romanz opinion on this PR before adding subscriptions anyway so no hurry :)
I don't know any :/. If I had to bet on the first client to use them, I would bet on Sparrow. Maybe @craigraw would quickly implement them on his client on regtest if you ask him.
Yeah you are right. If this case happens we would have much bigger issues somewhere else anyway, so I guess it is fine to not catch it. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wanted to go AFK but got a few more ideas. 😆
src/electrum.rs
Outdated
|
||
let funding_tx = match self.daemon.get_transaction(&funding.txid, funding_blockhash) { | ||
Ok(tx) => tx, | ||
Err(_) => return Ok(json!({})), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should somehow distinguish transaction not found from other errors so we can log them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here I return an empty JSON to follow the Electrum 1.5 spec.
get_transaction
is defined here:
Lines 139 to 147 in d153bfa
pub(crate) fn get_transaction( | |
&self, | |
txid: &Txid, | |
blockhash: Option<BlockHash>, | |
) -> Result<Transaction> { | |
self.rpc | |
.get_raw_transaction(txid, blockhash.as_ref()) | |
.context("failed to get transaction") | |
} |
There is a context defined in case of error. Is it sufficient ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, anyhow
is screwing us. I think this should work:
let funding_tx = match self.daemon.get_raw_transaction(&funding.txid, funding_blockhash.as_ref()) {
Ok(tx) => tx,
Err(bitcoincore_rpc::Error::JsonRpc(jsonrpc::Error::Rpc(jsonrpc::error::RpcError { code: -5, .. }))) => return Ok(json!({})),
Err(error) => return Err(error),
};
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I found anyhow::Context
weird to handle. I had to keep the same match but I add a new match in error case.
In the error case, I downcast anyhow
to a bitcoinrpc::Error
to remove the context and match its type as you did. If it does, I return the empty JSON, else I just return the error.
Is it the right way to do it ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I personally find it messy even though not wrong. That's why I suggested calling get_raw_transaction
directly instead of via get_transaction
which does the same thing except messing error type. If @romanz has a different opinion, I don't mind.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would calling get_raw_transaction
require the rpc field of daemon to be made public ? I don't know what it would imply...
It looks like a matter of preferences here, I don't mind either haha
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know what it would imply...
AFAIK nobody uses electrs
as a library so nothing substantial. I think whole daemon
struct may be just relic of the past when there was manual implementation.
Thinking about it, maybe the correct way to do it would be to change get_transaction
to return Result<Option<Transaction>>
instead of Result<Transaction>
and then convert not found errors to None
similarly to how e.g. HashMap
works except fallible.
src/electrum.rs
Outdated
if is_spending(&tx, funding) { | ||
txids.push(tx.txid()) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this:
let iter = block.txdata.into_iter().filter(|tx| is_spending(&tx, funding)).map(|tx| tx.txid());
txids.extend(iter);
may be more efficient. Anyway, I 'm not happy the whole thing can't be iterator, so maybe I'll look into it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed !
I tried to make it an iterator too but I had great trouble because of the match
:/
Ah, I see, we don't know txid.
I think we should handle it but log it and return |
Second round of @Kixunil suggestions included ! (Including error handling in case of bad indexing !) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As far as this incomplete change goes it looks good. I would personally prefer not doing downcast for stylistic reasons, but logically it's sound.
Subscription is obviously missing, that will be reviewed separately or I will look into it eventually. :)
Sorry for the delay, hopefully will more available in the next week :( |
Closing in favor of #454. |
Solve #444
Do not merge before spesmilo/electrumx#90 is merged.
Not tested properly because I have problems with electrs logging.
I can't find how to increase electrs verbosity, I use a .conf file and I set "verbose=4 timestamp=true" and "rust_backtrack = true" but p2p banch doesn't seem to take it into account (it only shows me the very first line of log at launch with config parameters, and no logging parameters are displayed).
@romanz how can I test that the request is working in command line ? (assuming electrs listen to port 50001 on localhost)