-
Notifications
You must be signed in to change notification settings - Fork 113
fix(p2pk): allow listing and spending p2pk utxos created with uncompressed pubkeys #2410
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
base: dev
Are you sure you want to change the base?
Conversation
…ressed pubkeys previously we did only one of them depending on the stored Public object (which apparently always seem to be compressed based on how we initialize it)
allow both compressed and uncompressed pubkeys in scriptPubkey of the utxo being spent
5e309e2 to
7366240
Compare
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 you mind to add some test coverage on this logic if possible?
there seem to be no way to consume the vec to only take one value out of it. this way looks the cleanst. indexing the first element by ref and cloning it.
borngraced
left a comment
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.
lgtm 🚀
…rom a single function
|
@onur-ozkan added some tests for balance and list unspents that interact with the uncompressed pubkey. |
the address hash should dhash160 the compressed pubkey and not the uncompressed one. without this, 65-byte p2pk outputs show incorrect addresses in tx_history (address for a p2pk is just the p2pkh of the same pubkey, to get the hash, one should hash the compressed pubkey rather than the full 65-byte uncompressed one)
mm2src/coins/utxo_signer/src/lib.rs
Outdated
| UtxoSignWithKeyPairError::InputIndexOutOfBound { .. } => UtxoSignTxError::Internal(error), | ||
| UtxoSignWithKeyPairError::UnspendableUTXO { script } => UtxoSignTxError::UnspendableUTXO { script }, | ||
| UtxoSignWithKeyPairError::ErrorSigning(sign) => UtxoSignTxError::ErrorSigning(sign), | ||
| UtxoSignWithKeyPairError::InternalError(internal) => UtxoSignTxError::Internal(internal), |
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 as a general approach we should try to avoid using such general and unspecific errors like InternalError, with attached long string messages (which bloats code and are harder to document), in favour of specific error valiants which can be converted into documented top-level RPC errors.
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.
agree. the prompt for me to compress the unspendableutxo into internal error though was the fact that it is only spitted out once in a specific place. and with the addition of pubkey conversion, another error would be spitted out once in another specific place. maybe 2 specific errors are OK, but i feel if we keep adding such small errors variants that aren't generic and used in only one or two places, we will end up with large error structs, and subsequently large error conversion code (which will compress everything to internal error at the later stages :/, as they are too specific to have their own error variants on user-facing/RPC level - though we could have error variants containing other errors, that's more clean and information preserving).
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.
borngraced
left a comment
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.
some notes
| _ => MmError::err(UtxoSignWithKeyPairError::InternalError(format!( | ||
| "Can't spend the UTXO with script = '{}'. This script format isn't supported", | ||
| input.prev_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.
not an InternalError more like UnspendableUTXO or UnsupportedScript
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.
ummmm, none of these are error variants 🤔
oh i guess u meant it was removed like this #2410 (comment)
ok, will return it back 👍
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.
| let pubkey = key_pair.public().to_secp256k1_pubkey().map_err(|e| { | ||
| UtxoSignWithKeyPairError::InternalError(format!("Couldn't get secp256k1 pubkey from keypair: {}", e)) | ||
| })?; | ||
| // Build the scriptPubKey for both compressed and uncompressed public keys. | ||
| let possible_script_pubkeys = vec![ | ||
| Builder::build_p2pk(&keys::Public::Compressed(pubkey.serialize().into())), | ||
| Builder::build_p2pk(&keys::Public::Normal(pubkey.serialize_uncompressed().into())), | ||
| ]; |
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.
why can't output_scripts_p2pk be used here?
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.
because it's in another crate the depends on this one. but i prefer the verbosity here anyway tbh.
| pub fn to_secp256k1_pubkey(&self) -> Result<PublicKey, SecpError> { | ||
| match self { | ||
| Public::Compressed(public) => PublicKey::from_slice(&**public), | ||
| Public::Normal(public) => PublicKey::from_slice(&**public), | ||
| } | ||
| } |
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.
| pub fn to_secp256k1_pubkey(&self) -> Result<PublicKey, SecpError> { | |
| match self { | |
| Public::Compressed(public) => PublicKey::from_slice(&**public), | |
| Public::Normal(public) => PublicKey::from_slice(&**public), | |
| } | |
| } | |
| pub fn to_secp256k1_pubkey(&self) -> Result<PublicKey, SecpError> { PublicKey::from_slice(self) } |
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.
pretty sure new rustc will complain and suggest similar code
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 can let this throw for now and the other PR will fix it. but i can fix it here if u want too.
| pub fn address_hash(&self) -> H160 { | ||
| match self { | ||
| Public::Compressed(public) => dhash160(public.as_slice()), | ||
| // If the public key isn't compressed, we wanna compress it then get the hash. | ||
| // No body uses the uncompressed form to get an address hash. | ||
| Public::Normal(public) => match PublicKey::from_slice(public.as_slice()) { | ||
| Ok(public) => dhash160(&public.serialize()), | ||
| // This should never happen, as then the public key would be invalid. If so, return a dummy value. | ||
| Err(_) => H160::default(), | ||
| }, | ||
| } | ||
| } |
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.
| pub fn address_hash(&self) -> H160 { | |
| match self { | |
| Public::Compressed(public) => dhash160(public.as_slice()), | |
| // If the public key isn't compressed, we wanna compress it then get the hash. | |
| // No body uses the uncompressed form to get an address hash. | |
| Public::Normal(public) => match PublicKey::from_slice(public.as_slice()) { | |
| Ok(public) => dhash160(&public.serialize()), | |
| // This should never happen, as then the public key would be invalid. If so, return a dummy value. | |
| Err(_) => H160::default(), | |
| }, | |
| } | |
| } | |
| pub fn address_hash(&self) -> H160 { | |
| match self { | |
| Public::Compressed(public) => dhash160(public.as_slice()), | |
| // If the public key isn't compressed, we wanna compress it then get the hash. | |
| // No body uses the uncompressed form to get an address hash. | |
| Public::Normal(public) => PublicKey::from_slice(public.as_slice()) | |
| .map(|public| dhash160(&public.serialize())) | |
| .unwrap_or_default(), | |
| } | |
| } |
| // If the public key isn't compressed, we wanna compress it then get the hash. | ||
| // No body uses the uncompressed form to get an address hash. |
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.
It's better to have this as doc comment i guess.
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 it's gonna be confusing, esp that this is a very legacy edge case that doesn't happen in day to day.
| // This should never happen, as then the public key would be invalid. If so, return a dummy value. | ||
| Err(_) => H160::default(), |
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 looks fishy. If this should never happen then put unreachable!() here. Let's say it did happen, with the current approach we will most likely miss it. If it never happens, then unreachable will not be harmful anyway.
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.
it indeed looks fishy 😂
i just prefer errors over panics, but this one is a utility func that's used everywhere and hard to be made fallible.
returning such invalid/dummy data tho will screw whatever operation being done down the call stack.
there is only this place https://github.com/KomodoPlatform/komodo-defi-framework/blob/5e697997585e70a8bbc01dbc756d330f7b1e5f68/mm2src/mm2_bitcoin/script/src/script.rs#L415
where a public key is provided not by us, but from the blockchain (or a lying rpc server), so it's not completely safe to panic given that there is an exterior entity that could provide that pubkey. otherwise, all pubkeys delt with within mm2 should be always valid.
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.
On KDF I do prefer errors over panics as well, but the comment says this should never happen, so either the comment is wrong or the code needs to change.
I would create 2 functions instead of trying to handle infallible/fallible calls from a single function: address_hash_unchecked that panics and address_hash with Result (or address_hash that panics try_address_hash with Result.).
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.
seams reasonable 👍
A p2pk utxo could have the scriptPub created with either a compressed 33-byte pubkey or an uncompressed 65-byte pubkey.
We store pubkeys internally in compressed format, so when we query electrum for unspent utxos, we construct the output script using that compressed 33-byte pubkey. But a p2pk output script could very well be constructed using an uncompressed 65-byte pubkey (e.g.). Both are valid and accepted by the blockchain.
Also while spending, we need to construct the output script as it's used in the sig_hash calcuation. We used to always construct the p2pk output script as
OP_PUSHBYTES_33 33-BYTE-PUBKEY OP_CHECKSIGbut now we also construct theOP_PUSHBYTES_65 65-BYTE-PUBKEY OP_CHECKSIGas well, and check which of them matches the previous script and that's what's used to sign the transaction.p.s. lingo might be confusing, output_script = scriptPub = scriptPubkey = locking_script