fix(p2pk): allow listing and spending p2pk utxos created with uncompressed pubkeys#2410
fix(p2pk): allow listing and spending p2pk utxos created with uncompressed pubkeys#2410mariocynicys wants to merge 18 commits intodevfrom
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 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.
…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.
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.
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).
| _ => 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.
not an InternalError more like UnspendableUTXO or UnsupportedScript
There was a problem hiding this comment.
ummmm, none of these are error variants 🤔
oh i guess u meant it was removed like this #2410 (comment)
ok, will return it back 👍
| 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.
why can't output_scripts_p2pk be used here?
There was a problem hiding this comment.
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.
| 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.
pretty sure new rustc will complain and suggest similar code
There was a problem hiding this comment.
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.
| 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.
It's better to have this as doc comment i guess.
There was a problem hiding this comment.
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.
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.
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.
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.
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