Merge ElementsProject/elements#1038: PSET: Fix blinded coinjoins with 3 or more parties

3ac7e78a84 pset, rpc: Better error messages for imbalance failure conditions (Andrew Chow)
b2a7007961 pset: verify blind value and asset proofs when signing (Andrew Chow)
a7ef9f325d pset, test: Test a PSET coinjoin workflow (Andrew Chow)
ac580db4ec pset, rpc: Do not fail walletprocesspsbt if missing utxos when blinding (Andrew Chow)
9f94966581 pset: Allow GetUnsignedTx to force unblinded values (Andrew Chow)
f9f084f785 pset: Add missing fields to merging and fix combinepsbt (Andrew Chow)
59f65a3f73 rpc, pset: Include blinded value and asset proofs in decodepsbt (Andrew Chow)
941c54fcda pset: Create explicit value and asset proofs during blinding (Andrew Chow)
63c007dd6b pset: blind commitment proofs de/ser (Andrew Chow)
b9a08f3773 doc, pset: Add blind commitment proofs (Andrew Chow)
9856f2caa2 pset, doc: Remove requirement to remove blinded amounts and assets (Andrew Chow)
37c925f472 pset: Do not remove amounts after blinding (Andrew Chow)

Pull request description:

  In order for blinded coinjoins with 3 or more parties to work, some fields need to be added, amounts cannot be removed, and some bugs need fixing.

  First and foremost is to no longer remove amounts after blinding. Due to a miscommunication, I had believed that part of the goal of PSET was to hide semi private information (such as output amounts) from other parties in the transaction. However this causes the combiner to fail because the unique ID is dependent on those amounts and their commitments. If multiple parties had blinded just their own outputs, then the resulting PSETs would not combine because the amounts had be removed and so the unique ID calculated was incorrect. In order for this combining to work, amounts must be kept after blinding and the unique ID calculation must only use the computed unblinded transaction. This change has also been made to the spec document.

  Second is the addition of explicit value and asset proofs. In order to prove that the commitments commit to the given explicit value or asset, explicit value rangeproofs and explicit asset surjection proofs are added to PSET. Each blinded output must have an explicit value rangeproof and explict asset surjection proof after blinding. For issuances, explicit issuance value proofs and explicit reissuance value proofs must be added after blinding. These proofs are verified prior to signing.

  Thirdly, in order to make the coinjoin workflow work, a `blind` option is added `walletprocesspsbt` so that users can tell `walletprocesspsbt` to not attempt to blind. This is important because blinding requires all UTXOs to be present, and UTXOs can only be added via a call to `walletprocesspsbt`. Error messages have been added and improved so that users who do things in the wrong order will be less likely to end up with an unusable PSET.

  Lastly, a test case has been added for a 3 party coinjoin workflow.

  Fixes #1037

ACKs for top commit:
  apoelstra:
    ACK 3ac7e78a84

Tree-SHA512: b1fd848c72d8dd779b0f0640d9321b8085d62db494d25f81e3ec90e43c4a4edbe537dec404d8170a7ad057027c07f2b948980907d0849da693b2ca1bf0cf81f6
This commit is contained in:
Andrew Poelstra 2021-09-14 14:57:18 +00:00
commit 1240172eaa
No known key found for this signature in database
GPG key ID: C588D63CE41B97C1
11 changed files with 392 additions and 66 deletions

View file

@ -84,7 +84,7 @@ The currently defined elements per-input proprietary types are as folows:
| None
| No key data
| <tt><64-bit int></tt>
| The explicit little endian 64-bit integer for the value of this issuance. This is mutually exclusive with <tt>PSBT_ELEMENTS_IN_ISSUANCE_VALUE_COMMITMENT</tt>
| The explicit little endian 64-bit integer for the value of this issuance.
|
| 0
| 2
@ -94,7 +94,7 @@ The currently defined elements per-input proprietary types are as folows:
| None
| No key data
| <tt><33 byte commitment></tt>
| The 33 byte Value Commitment. This is mutually exclusive with <tt>PSBT_IN_ISSUANCE_VALUE</tt>.
| The 33 byte Value Commitment. If provided, <tt>PSBT_ELEMENTS_IN_ISSUANCE_BLIND_VALUE_PROOF</tt> must be provided too.
|
| 0
| 2
@ -184,7 +184,7 @@ The currently defined elements per-input proprietary types are as folows:
| None
| No key data
| <tt><64-bit int></tt>
| The value for the inflation keys output to set in this issuance. This is mutually exclusive with <tt>PSBT_ELEMENTS_IN_ISSUANCE_INFLATION_KEYS</tt>.
| The value for the inflation keys output to set in this issuance.
|
| 0
| 2
@ -194,7 +194,7 @@ The currently defined elements per-input proprietary types are as folows:
| None
| No key data
| <tt><33 byte commitment></tt>
| The 33 byte commitment to the inflation keys output value in this issuance. This is mutually exclusive with <tt>PSBT_ELEMENTS_IN_ISSUANCE_INFLATION_KEYS</tt>
| The 33 byte commitment to the inflation keys output value in this issuance. If provided, <tt>PSBT_ELEMENTS_IN_ISSUANCE_BLIND_INFLATION_KEYS_PROOF</tt> must be provided too.
|
| 0
| 2
@ -228,6 +228,26 @@ The currently defined elements per-input proprietary types are as folows:
|
| 0
| 2
|-
| Issuance Blind Value Proof
| <tt>PSBT_ELEMENTS_IN_ISSUANCE_BLIND_VALUE_PROOF = 0x0f</tt>
| None
| No key data
| <tt><rangeproof></tt>
| An explicit value rangeproof that proves that the value commitment in <tt>PSBT_ELEMENTS_IN_ISSUANCE_VALUE_COMMITMENT</tt> matches the explicit value in <tt>PSBT_ELEMENTS_IN_ISSUANCE_VALUE</tt>. If provided, <tt>PSBT_ELEMENTS_IN_ISSUANCE_VALUE_COMMITMENT</tt> must be provided too.
|
| 0
| 2
|-
| Issuance Inflation Keys Blind Value Proof
| <tt>PSBT_ELEMENTS_IN_ISSUANCE_BLIND_INFLATION_KEYS_PROOF = 0x10</tt>
| None
| No key data
| <tt><rangeproof></tt>
| An explicit value rangeproof that proves that the value commitment in <tt>PSBT_ELEMENTS_IN_ISSUANCE_INFLATION_KEYS_COMMITMENT</tt> matches the explicit value in <tt>PSBT_ELEMENTS_IN_ISSUANCE_INFLATION_KEYS</tt>. If provided, <tt>PSBT_ELEMENTS_IN_ISSUANCE_INFLATION_KEYS_COMMITMENT</tt> must be provided too.
|
| 0
| 2
|}
The currently defined elements per-output proprietary types are as follows:
@ -248,7 +268,7 @@ The currently defined elements per-output proprietary types are as follows:
| None
| No key data
| <tt><33 byte commitment></tt>
| The 33 byte Value Commitment for this output. This is mutually exclusive with <tt>PSBT_OUT_VALUE</tt>.
| The 33 byte Value Commitment for this output. If provided, <tt>PSBT_ELEMENTS_OUT_BLIND_VALUE_PROOF</tt> must be provided too.
|
| 0
| 2
@ -258,7 +278,7 @@ The currently defined elements per-output proprietary types are as follows:
| None
| No key data
| <tt><32 byte asset tag></tt>
| The explicit 32 byte asset tag for this output. This is mutually exclusive with <tt>PSBT_ELEMENTS_OUT_ASSET_COMMITMENT</tt>.
| The explicit 32 byte asset tag for this output.
|
| 0
| 2
@ -268,7 +288,7 @@ The currently defined elements per-output proprietary types are as follows:
| None
| No key data
| <tt><33 byte commitment></tt>
| The 33 byte Asset Commitment for this output. This is mutually exclusive with <tt>PSBT_ELEMENTS_OUT_ASSET</tt>.
| The 33 byte Asset Commitment for this output. If provided, <tt>PSBT_ELEMENTS_OUT_BLIND_ASSET_PROOF</tt> must be provided too.
|
| 0
| 2
@ -322,10 +342,27 @@ The currently defined elements per-output proprietary types are as follows:
|
| 0
| 2
|-
| Blind Value Proof
| <tt>PSBT_ELEMENTS_OUT_BLIND_VALUE_PROOF = 0x09</tt>
| None
| No key data
| <tt><rangeproof></tt>
| An explicit value rangeproof that proves that the value commitment in <tt>PSBT_ELEMENTS_OUT_VALUE_COMMITMENT</tt> matches the explicit value in <tt>PSBT_OUT_VALUE</tt>. If provided, <tt>PSBT_ELEMENTS_OUT_VALUE_COMMITMENT</tt> must be provided too.
|
| 0
| 2|-
| Blind Asset Proof
| <tt>PSBT_ELEMENTS_OUT_BLIND_ASSET_PROOF = 0x0a</tt>
| None
| No key data
| <tt><proof></tt>
| An asset surjection proof with this output's asset as the only asset in the input set in order to prove that the asset commitment in <tt>PSBT_ELEMENTS_OUT_ASSET_COMMITMENT</tt> matches the explicit asset in <tt>PSBT_ELEMENTS_OUT_ASSET</tt>. If provided, <tt>PSBT_ELEMENTS_OUT_ASSET_COMMITMENT</tt> must be provided too.
|
| 0
| 2
|}
In addition to these new types, the <tt>PSBT_OUT_AMOUNT</tt> field is no longer required so long as <tt>PSBT_ELEMENTS_OUT_VALUE_COMMITMENT</tt> is present.
The PSET Magic Bytes are <tt>0x70736574</tt>
===Handling Duplicated Keys===
@ -373,16 +410,15 @@ A single entity is likely to be both a Creator and Updater.
PSET requires a role not present in PSBT, the Blinder. Blinders are similar to Signers and own inputs.
The Blinder adds the blinding data to a transaction.
If Bit 0 of <tt>PSBT_ELEMENTS_GLOBAL_TX_MODIFIABLE</tt> is 0, the Blinder must do nothing.
For issuance inputs that belong to the Blinder, the Blinder should generate a random blinding factor and create a value commitment for the issuance value.
It will then add the value commitment in the <tt>PSBT_ELEMENTS_IN_ISSUANCE_VALUE_COMMITMENT</tt>. When it does so, it must remove the <tt>PSBT_ELEMENTS_IN_ISSUANCE_VALUE</tt> field.
It will then add the value commitment in the <tt>PSBT_ELEMENTS_IN_ISSUANCE_VALUE_COMMITMENT</tt>.
The blinder will also add the issuance value rangeproof and the issuance keys rangeproof in their respective fields.
The blinder will also add the issuance blind value and issuance keys blind value proofs in their respective fields.
For ease of identifying the blinder for an issuance, the input the issuance is attached to must belong to the blinder for the issuance.
For the Blinder's outputs that are to be blinded (i.e. they have a blinding pubkey), the Blinder will create value and asset commitments and put them in their respective fields.
When they do so, the <tt>PSBT_ELEMENTS_OUT_VALUE</tt> and <tt>PSBT_ELEMENTS_OUT_ASSET</tt> fields must be removed.
The Blinder will create the Value Rangeproof and Asset Surjection Proof and put them in their respective fields.
The Blinder will create the blind value and blind asset proofs and put them in their respective fields.
It will also add the ephemeral pubkey used for ECDH of the nonce for the rangeproof to the <tt>PSBT_ELEMENTS_OUT_ECDH_PUBKEY</tt> field.
The blinder will then compute a scalar offset that will be added as a <tt>PSBT_ELEMENTS_GLOBAL_SCALAR</tt>.
@ -400,17 +436,13 @@ It will then compute a final scalar offset.
Then it will subtract all of the scalar offsets from the value blinding factor for the last output and the result is the value blinding factor to be used for that last output.
The creation of the commitments, proofs, and other fields proceeds as usual.
Once all outputs are blinded, all <tt>PSBT_ELEMENTS_GLOBAL_SCALAR</tt> fields must be removed from the PSET.
Once all outputs are blinded, Bit 0 of <tt>PSBT_ELEMENTS_GLOBAL_TX_MODIFIABLE</tt> must be set to 0.
A single entity is likely to be a Creator, Updater, and Blinder.
In that case, the PSET should never be output with <tt>PSBT_ELEMENTS_IN_ISSUANCE_VALUE</tt>, <tt>PSBT_ELEMENTS_IN_ISSUANCE_INFLATION_KEYS</tt>, <tt>PSBT_ELEMENTS_OUT_VALUE</tt>, or <tt>PSBT_ELEMENTS_OUT_ASSET</tt> except for unblinded issuances and unblinded outputs.
===Signer===
In addition to the BIP 370 PSBT Signer behavior, PSET specifies some addtional constraints.
Before signing, the Signer must check whether blinding is complete. If any output contains a blinding pubkey but no commitments or proofs, then it must not sign.
This is easily done by checking whether Bit 0 of <tt>PSBT_ELEMENTS_GLOBAL_TX_MODIFIABLE</tt> is 1.
If so, the Signer must do nothing.
===Combiner===

View file

@ -29,16 +29,18 @@ std::string GetBlindingStatusError(const BlindingStatus& status)
return "Computed blinding factor is invalid";
case BlindingStatus::ASP_UNABLE:
return "Unable to create an asset surjection proof";
case BlindingStatus::NO_BLIND_OUTPUTS:
return "Transaction has blind inputs belonging to this blinder but does not have outputs to blind";
}
assert(false);
}
// Create surjection proof
bool CreateAssetSurjectionProof(std::vector<unsigned char>& output_proof, const std::vector<secp256k1_fixed_asset_tag>& fixed_input_tags, const std::vector<secp256k1_generator>& ephemeral_input_tags, const std::vector<uint256>& input_asset_blinders, const uint256& output_asset_blinder, const secp256k1_generator& output_asset_tag, const CAsset& asset)
bool CreateAssetSurjectionProof(std::vector<unsigned char>& output_proof, const std::vector<secp256k1_fixed_asset_tag>& fixed_input_tags, const std::vector<secp256k1_generator>& ephemeral_input_tags, const std::vector<uint256>& input_asset_blinders, const uint256& output_asset_blinder, const secp256k1_generator& output_asset_tag, const CAsset& asset, size_t num_targets)
{
int ret;
// 1 to 3 targets
size_t inputs_to_select = std::min(MAX_SURJECTION_TARGETS, fixed_input_tags.size());
size_t inputs_to_select = std::min(num_targets, fixed_input_tags.size());
unsigned char randseed[32];
GetStrongRandBytes(randseed, 32);
size_t input_index;
@ -64,6 +66,21 @@ bool CreateAssetSurjectionProof(std::vector<unsigned char>& output_proof, const
return true;
}
bool VerifyBlindAssetProof(const std::vector<unsigned char>& proof, const CConfidentialAsset& conf_asset)
{
secp256k1_surjectionproof surj_proof;
if (secp256k1_surjectionproof_parse(secp256k1_blind_context, &surj_proof, proof.data(), proof.size()) == 0) {
return false;
}
secp256k1_generator gen;
if (secp256k1_generator_parse(secp256k1_blind_context, &gen, conf_asset.vchCommitment.data()) == 0) {
return false;
}
return secp256k1_surjectionproof_verify(secp256k1_blind_context, &surj_proof, &gen, 1, &gen) == 0;
}
uint256 GenerateRangeproofECDHKey(CPubKey& ephemeral_pubkey, const CPubKey blinding_pubkey)
{
// Generate ephemeral key for ECDH nonce generation
@ -98,6 +115,43 @@ bool CreateValueRangeProof(std::vector<unsigned char>& rangeproof, const uint256
return (res == 1);
}
// Create an explicit value rangeproof which proves that the commitment commits to an explicit value
bool CreateBlindValueProof(std::vector<unsigned char>& rangeproof, const uint256& value_blinder, const CAmount amount, const secp256k1_pedersen_commitment& value_commit, const secp256k1_generator& gen)
{
// Prep rangeproof
size_t rangeproof_len = 5134;
rangeproof.resize(rangeproof_len);
// Generate a new random nonce
uint256 nonce;
GetStrongRandBytes(nonce.begin(), nonce.size());
// Make the rangeproof
int res = secp256k1_rangeproof_sign(secp256k1_blind_context, rangeproof.data(), &rangeproof_len, /* min_value */ amount, &value_commit, value_blinder.begin(), nonce.begin(), /* exp */ -1, /* min_bits */ 0, amount, /* message */ nullptr, /* message_len */ 0, /* extra_commit */ nullptr, /* extra_commit_len */ 0, &gen);
rangeproof.resize(rangeproof_len);
return res == 1;
}
bool VerifyBlindValueProof(CAmount value, const CConfidentialValue& conf_value, const std::vector<unsigned char>& proof, const CConfidentialAsset& conf_asset)
{
secp256k1_pedersen_commitment value_commit;
if (secp256k1_pedersen_commitment_parse(secp256k1_blind_context, &value_commit, conf_value.vchCommitment.data()) == 0) {
return false;
}
secp256k1_generator gen;
if (secp256k1_generator_parse(secp256k1_blind_context, &gen, conf_asset.vchCommitment.data()) == 0) {
return false;
}
uint64_t min_value;
uint64_t max_value;
if (secp256k1_rangeproof_verify(secp256k1_blind_context, &min_value, &max_value, &value_commit, proof.data(), proof.size(), /* extra_commit */ nullptr, /* extra_commit_len */ 0, &gen) == 0) {
return false;
}
return min_value == (uint64_t)value;
}
void CreateAssetCommitment(CConfidentialAsset& conf_asset, secp256k1_generator& asset_gen, const CAsset& asset, const uint256& asset_blinder)
{
conf_asset.vchCommitment.resize(CConfidentialAsset::nCommittedSize);
@ -310,14 +364,19 @@ BlindingStatus BlindPSBT(PartiallySignedTransaction& psbt, std::map<uint32_t, st
bool rangeresult = CreateValueRangeProof(rangeproof, value_blinder, nonce, value, CScript(), value_commit, asset_gen, asset, asset_blinder);
assert(rangeresult);
// Create explicit value rangeproofs
std::vector<unsigned char> blind_value_proof;
rangeresult = CreateBlindValueProof(blind_value_proof, value_blinder, value, value_commit, asset_gen);
assert(rangeresult);
if (blind_value) {
input.m_issuance_value_commitment = conf_value;
input.m_issuance_rangeproof = rangeproof;
input.m_issuance_value = nullopt;
input.m_blind_issuance_value_proof = blind_value_proof;
} else {
input.m_issuance_inflation_keys_commitment = conf_value;
input.m_issuance_inflation_keys_rangeproof = rangeproof;
input.m_issuance_inflation_keys_amount = nullopt;
input.m_blind_issuance_inflation_keys_proof = blind_value_proof;
}
}
}
@ -327,10 +386,14 @@ BlindingStatus BlindPSBT(PartiallySignedTransaction& psbt, std::map<uint32_t, st
uint256 output_scalar;
bool did_last_blind = false;
int our_blinds = 0;
for (uint32_t i : to_blind) {
PSBTOutput& output = psbt.outputs[i];
if (output.IsFullyBlinded()) continue;
if (output.IsFullyBlinded()) {
our_blinds++;
continue;
}
// Check this is our output to blind
if (output.m_blinder_index == nullopt || our_input_data.count(*output.m_blinder_index) == 0) continue;
@ -396,30 +459,45 @@ BlindingStatus BlindPSBT(PartiallySignedTransaction& psbt, std::map<uint32_t, st
bool rangeresult = CreateValueRangeProof(rangeproof, value_blinder, nonce, *output.amount, *output.script, value_commit, asset_generator, asset, asset_blinder);
assert(rangeresult);
// Create explicit value rangeproof
std::vector<unsigned char> blind_value_proof;
rangeresult = CreateBlindValueProof(blind_value_proof, value_blinder, *output.amount, value_commit, asset_generator);
assert(rangeresult);
// Create surjection proof for this output
if (!CreateAssetSurjectionProof(asp, fixed_input_tags, ephemeral_input_tags, input_asset_blinders, asset_blinder, asset_generator, asset)) {
return BlindingStatus::ASP_UNABLE;
}
// Create explicit asset surjection proof
std::vector<unsigned char> blind_asset_proof;
if (!CreateAssetSurjectionProof(blind_asset_proof, fixed_input_tags, ephemeral_input_tags, input_asset_blinders, asset_blinder, asset_generator, asset, /* num_targets */ 1)) {
return BlindingStatus::ASP_UNABLE;
}
// Fill output
output.m_asset_commitment = asset_commitment;
output.m_value_commitment = value_commitment;
output.m_ecdh_pubkey = ecdh_key;
output.m_value_rangeproof = rangeproof;
output.m_asset_surjection_proof = asp;
output.m_blind_value_proof = blind_value_proof;
output.m_blind_asset_proof = blind_asset_proof;
// Drop explicit value and asset
output.amount = nullopt;
output.m_asset.SetNull();
our_blinds++;
}
if (!did_last_blind) {
// Compute scalar and add to PSBT if it isn't null
if (!did_last_blind && !output_scalar.IsNull()) {
// Subtract input scalar from output scalar
if (!SubtractScalars(output_scalar, input_scalar)) return BlindingStatus::SCALAR_UNABLE;
// Now add the scalar to the PSBT if it isn't null
if (!output_scalar.IsNull()) {
psbt.m_scalar_offsets.insert(output_scalar);
}
// Add to PSBT
psbt.m_scalar_offsets.insert(output_scalar);
}
// Make sure that we blinded some outputs if we have blinded inputs
if (our_input_data.size() > 0 && our_blinds == 0) {
return BlindingStatus::NO_BLIND_OUTPUTS;
}
return BlindingStatus::OK;

View file

@ -26,13 +26,17 @@ enum class BlindingStatus
SCALAR_UNABLE,
INVALID_BLINDER,
ASP_UNABLE,
NO_BLIND_OUTPUTS,
};
std::string GetBlindingStatusError(const BlindingStatus& status);
bool CreateAssetSurjectionProof(std::vector<unsigned char>& output_proof, const std::vector<secp256k1_fixed_asset_tag>& fixed_input_tags, const std::vector<secp256k1_generator>& ephemeral_input_tags, const std::vector<uint256>& input_asset_blinders, const uint256& output_asset_blinder, const secp256k1_generator& output_asset_tag, const CAsset& asset);
bool CreateAssetSurjectionProof(std::vector<unsigned char>& output_proof, const std::vector<secp256k1_fixed_asset_tag>& fixed_input_tags, const std::vector<secp256k1_generator>& ephemeral_input_tags, const std::vector<uint256>& input_asset_blinders, const uint256& output_asset_blinder, const secp256k1_generator& output_asset_tag, const CAsset& asset, size_t num_targets = MAX_SURJECTION_TARGETS);
bool VerifyBlindAssetProof(const std::vector<unsigned char>& proof, const CConfidentialAsset& conf_asset);
uint256 GenerateRangeproofECDHKey(CPubKey& ephemeral_pubkey, const CPubKey blinding_pubkey);
bool CreateValueRangeProof(std::vector<unsigned char>& rangeproof, const uint256& value_blinder, const uint256& nonce, const CAmount amount, const CScript& scriptPubKey, const secp256k1_pedersen_commitment& value_commit, const secp256k1_generator& gen, const CAsset& asset, const uint256& asset_blinder);
bool CreateBlindValueProof(std::vector<unsigned char>& rangeproof, const uint256& value_blinder, const CAmount amount, const secp256k1_pedersen_commitment& value_commit, const secp256k1_generator& gen);
bool VerifyBlindValueProof(CAmount value, const CConfidentialValue& conf_value, const std::vector<unsigned char>& proof, const CConfidentialAsset& conf_asset);
void CreateAssetCommitment(CConfidentialAsset& conf_asset, secp256k1_generator& asset_gen, const CAsset& asset, const uint256& asset_blinder);
void CreateValueCommitment(CConfidentialValue& conf_value, secp256k1_pedersen_commitment& value_commit, const uint256& value_blinder, const secp256k1_generator& asset_gen, const CAmount amount);
BlindingStatus BlindPSBT(PartiallySignedTransaction& psbt, std::map<uint32_t, std::tuple<CAmount, CAsset, uint256, uint256>> our_input_data, std::map<uint32_t, std::pair<CKey, CKey>> our_issuances_to_blind);

View file

@ -59,6 +59,9 @@ bool PartiallySignedTransaction::Merge(const PartiallySignedTransaction& psbt)
m_xpubs[xpub_pair.first].insert(xpub_pair.second.begin(), xpub_pair.second.end());
}
}
for (auto& scalar : psbt.m_scalar_offsets) {
m_scalar_offsets.insert(scalar);
}
if (fallback_locktime == nullopt && psbt.fallback_locktime != nullopt) fallback_locktime = psbt.fallback_locktime;
if (m_tx_modifiable == nullopt && psbt.m_tx_modifiable != nullopt) m_tx_modifiable = psbt.m_tx_modifiable;
unknown.insert(psbt.unknown.begin(), psbt.unknown.end());
@ -101,7 +104,7 @@ bool PartiallySignedTransaction::ComputeTimeLock(uint32_t& locktime) const
return true;
}
CMutableTransaction PartiallySignedTransaction::GetUnsignedTx() const
CMutableTransaction PartiallySignedTransaction::GetUnsignedTx(bool force_unblinded) const
{
if (tx != nullopt) {
return *tx;
@ -119,7 +122,7 @@ CMutableTransaction PartiallySignedTransaction::GetUnsignedTx() const
txin.nSequence = input.sequence.value_or(max_sequence);
txin.assetIssuance.assetBlindingNonce = input.m_issuance_blinding_nonce;
txin.assetIssuance.assetEntropy = input.m_issuance_asset_entropy;
if (input.m_issuance_value != nullopt && input.m_issuance_inflation_keys_amount != nullopt) {
if (input.m_issuance_value != nullopt && input.m_issuance_inflation_keys_amount != nullopt && !force_unblinded) {
txin.assetIssuance.nAmount.SetToAmount(*input.m_issuance_value);
txin.assetIssuance.nInflationKeys.SetToAmount(*input.m_issuance_inflation_keys_amount);
} else {
@ -131,7 +134,7 @@ CMutableTransaction PartiallySignedTransaction::GetUnsignedTx() const
for (const PSBTOutput& output : outputs) {
CTxOut txout;
txout.scriptPubKey = *output.script;
if (output.IsFullyBlinded()) {
if (output.IsFullyBlinded() && !force_unblinded) {
txout.nValue = output.m_value_commitment;
txout.nAsset = output.m_asset_commitment;
txout.nNonce.vchCommitment.insert(txout.nNonce.vchCommitment.end(), output.m_ecdh_pubkey.begin(), output.m_ecdh_pubkey.end());
@ -153,7 +156,7 @@ uint256 PartiallySignedTransaction::GetUniqueID() const
}
// Get the unsigned transaction
CMutableTransaction mtx = GetUnsignedTx();
CMutableTransaction mtx = GetUnsignedTx(/* force_unblinded */ true);
// Set the locktime to 0
mtx.nLockTime = 0;
// Set the sequence numbers to 0
@ -413,6 +416,8 @@ bool PSBTInput::Merge(const PSBTInput& input)
}
if (m_issuance_blinding_nonce.IsNull() && !input.m_issuance_blinding_nonce.IsNull()) m_issuance_blinding_nonce = input.m_issuance_blinding_nonce;
if (m_issuance_asset_entropy.IsNull() && !input.m_issuance_asset_entropy.IsNull()) m_issuance_asset_entropy = input.m_issuance_asset_entropy;
if (m_blind_issuance_value_proof.empty() && !input.m_blind_issuance_value_proof.empty()) m_blind_issuance_value_proof = input.m_blind_issuance_value_proof;
if (m_blind_issuance_inflation_keys_proof.empty() && !input.m_blind_issuance_inflation_keys_proof.empty()) m_blind_issuance_inflation_keys_proof = input.m_blind_issuance_inflation_keys_proof;
if (m_peg_in_tx.which() == 0 && input.m_peg_in_tx.which() != 0) m_peg_in_tx = input.m_peg_in_tx;
if (m_peg_in_txout_proof.which() == 0 && input.m_peg_in_txout_proof.which() != 0) m_peg_in_txout_proof = input.m_peg_in_txout_proof;
@ -421,6 +426,8 @@ bool PSBTInput::Merge(const PSBTInput& input)
if (m_peg_in_value == nullopt && input.m_peg_in_value != nullopt) m_peg_in_value = input.m_peg_in_value;
if (m_peg_in_witness.IsNull() && !input.m_peg_in_witness.IsNull()) m_peg_in_witness = input.m_peg_in_witness;
if (m_utxo_rangeproof.empty() && !input.m_utxo_rangeproof.empty()) m_utxo_rangeproof = input.m_utxo_rangeproof;
return true;
}
@ -459,8 +466,7 @@ bool PSBTOutput::Merge(const PSBTOutput& output)
{
assert(amount == output.amount);
assert(script == output.script);
assert(m_value_commitment == output.m_value_commitment);
assert(m_asset_commitment == output.m_asset_commitment);
assert(m_asset == output.m_asset);
hd_keypaths.insert(output.hd_keypaths.begin(), output.hd_keypaths.end());
unknown.insert(output.unknown.begin(), output.unknown.end());
@ -481,7 +487,6 @@ bool PSBTOutput::Merge(const PSBTOutput& output)
if (!m_asset_commitment.IsNull() && !output.m_asset_commitment.IsNull() && (m_asset_commitment != output.m_asset_commitment)) return false;
if (!m_value_rangeproof.empty() && !output.m_value_rangeproof.empty() && (m_value_rangeproof != output.m_value_rangeproof)) return false;
if (!m_asset_surjection_proof.empty() && !output.m_asset_surjection_proof.empty() && (m_asset_surjection_proof != output.m_asset_surjection_proof)) return false;
if (amount|| output.amount || !m_asset.IsNull() || !output.m_asset.IsNull()) return false;
}
// If output IsFullyBlinded and this is not, copy the blinding data and remove the explicits
@ -491,6 +496,8 @@ bool PSBTOutput::Merge(const PSBTOutput& output)
m_value_rangeproof = output.m_value_rangeproof;
m_asset_surjection_proof = output.m_asset_surjection_proof;
m_ecdh_pubkey = output.m_ecdh_pubkey;
m_blind_value_proof = output.m_blind_value_proof;
m_blind_asset_proof = output.m_blind_asset_proof;
}
return true;
@ -514,10 +521,9 @@ bool PSBTOutput::IsBlinded() const
bool PSBTOutput::IsPartiallyBlinded() const
{
return IsBlinded() && (!amount ||
return IsBlinded() && (
!m_value_commitment.IsNull() ||
!m_asset_commitment.IsNull() ||
m_asset.IsNull() ||
!m_value_rangeproof.empty() ||
!m_asset_surjection_proof.empty() ||
m_ecdh_pubkey.IsValid());
@ -525,10 +531,9 @@ bool PSBTOutput::IsPartiallyBlinded() const
bool PSBTOutput::IsFullyBlinded() const
{
return IsBlinded() && !amount &&
return IsBlinded() &&
!m_value_commitment.IsNull() &&
!m_asset_commitment.IsNull() &&
m_asset.IsNull() &&
!m_value_rangeproof.empty() &&
!m_asset_surjection_proof.empty() &&
m_ecdh_pubkey.IsValid();

View file

@ -70,6 +70,8 @@ static constexpr uint8_t PSBT_ELEMENTS_IN_ISSUANCE_INFLATION_KEYS_COMMITMENT = 0
static constexpr uint8_t PSBT_ELEMENTS_IN_ISSUANCE_BLINDING_NONCE = 0x0c;
static constexpr uint8_t PSBT_ELEMENTS_IN_ISSUANCE_ASSET_ENTROPY = 0x0d;
static constexpr uint8_t PSBT_ELEMENTS_IN_UTXO_RANGEPROOF = 0x0e;
static constexpr uint8_t PSBT_ELEMENTS_IN_ISSUANCE_BLIND_VALUE_PROOF = 0x0f;
static constexpr uint8_t PSBT_ELEMENTS_IN_ISSUANCE_BLIND_INFLATION_KEYS_PROOF = 0x10;
// Output types
static constexpr uint8_t PSBT_OUT_REDEEMSCRIPT = 0x00;
@ -87,6 +89,8 @@ static constexpr uint8_t PSBT_ELEMENTS_OUT_ASSET_SURJECTION_PROOF = 0x05;
static constexpr uint8_t PSBT_ELEMENTS_OUT_BLINDING_PUBKEY = 0x06;
static constexpr uint8_t PSBT_ELEMENTS_OUT_ECDH_PUBKEY = 0x07;
static constexpr uint8_t PSBT_ELEMENTS_OUT_BLINDER_INDEX = 0x08;
static constexpr uint8_t PSBT_ELEMENTS_OUT_BLIND_VALUE_PROOF = 0x09;
static constexpr uint8_t PSBT_ELEMENTS_OUT_BLIND_ASSET_PROOF = 0x0a;
// Proprietary type identifer string
static const std::vector<unsigned char> PSBT_ELEMENTS_ID = {'p', 's', 'e', 't'};
@ -238,6 +242,8 @@ struct PSBTInput
CConfidentialValue m_issuance_inflation_keys_commitment;
uint256 m_issuance_blinding_nonce;
uint256 m_issuance_asset_entropy;
std::vector<unsigned char> m_blind_issuance_value_proof;
std::vector<unsigned char> m_blind_issuance_inflation_keys_proof;
// Peg-in
boost::variant<boost::blank, Sidechain::Bitcoin::CTransactionRef, CTransactionRef> m_peg_in_tx;
@ -337,11 +343,11 @@ struct PSBTInput
// Elements proprietary fields are only allowed with v2
// Issuance value
// We shouldn't have both value and value commitment. If we do, ignore the explicit value
if (!m_issuance_value_commitment.IsNull()) {
SerializeToVector(s, CompactSizeWriter(PSBT_IN_PROPRIETARY), PSBT_ELEMENTS_ID, CompactSizeWriter(PSBT_ELEMENTS_IN_ISSUANCE_VALUE_COMMITMENT));
SerializeToVector(s, m_issuance_value_commitment);
} else if (m_issuance_value != nullopt) {
}
if (m_issuance_value != nullopt) {
SerializeToVector(s, CompactSizeWriter(PSBT_IN_PROPRIETARY), PSBT_ELEMENTS_ID, CompactSizeWriter(PSBT_ELEMENTS_IN_ISSUANCE_VALUE));
SerializeToVector(s, *m_issuance_value);
}
@ -451,6 +457,18 @@ struct PSBTInput
SerializeToVector(s, CompactSizeWriter(PSBT_OUT_PROPRIETARY), PSBT_ELEMENTS_ID, CompactSizeWriter(PSBT_ELEMENTS_IN_UTXO_RANGEPROOF));
s << m_utxo_rangeproof;
}
// Blind issuance value proof
if (!m_blind_issuance_value_proof.empty()) {
SerializeToVector(s, CompactSizeWriter(PSBT_OUT_PROPRIETARY), PSBT_ELEMENTS_ID, CompactSizeWriter(PSBT_ELEMENTS_IN_ISSUANCE_BLIND_VALUE_PROOF));
s << m_blind_issuance_value_proof;
}
// Blind issuance inflation keys value proof
if (!m_blind_issuance_inflation_keys_proof.empty()) {
SerializeToVector(s, CompactSizeWriter(PSBT_OUT_PROPRIETARY), PSBT_ELEMENTS_ID, CompactSizeWriter(PSBT_ELEMENTS_IN_ISSUANCE_BLIND_INFLATION_KEYS_PROOF));
s << m_blind_issuance_inflation_keys_proof;
}
}
// Write proprietary things
@ -844,6 +862,26 @@ struct PSBTInput
s >> m_utxo_rangeproof;
break;
}
case PSBT_ELEMENTS_IN_ISSUANCE_BLIND_VALUE_PROOF:
{
if (!m_blind_issuance_value_proof.empty()) {
throw std::ios_base::failure("Duplicate Key, input blind issuance value proof already provided");
} else if (subkey_len != 1) {
throw std::ios_base::failure("Input blind issuance value key is more than one byte type");
}
s >> m_blind_issuance_value_proof;
break;
}
case PSBT_ELEMENTS_IN_ISSUANCE_BLIND_INFLATION_KEYS_PROOF:
{
if (!m_blind_issuance_inflation_keys_proof.empty()) {
throw std::ios_base::failure("Duplicate Key, input blind issuance inflation keys value proof already provided");
} else if (subkey_len != 1) {
throw std::ios_base::failure("Input blind issuance inflation keys value proof key is more than one byte type");
}
s >> m_blind_issuance_inflation_keys_proof;
break;
}
default:
{
known = false;
@ -888,17 +926,23 @@ struct PSBTInput
if (prev_out == nullopt) {
throw std::ios_base::failure("Previous output's index is required in PSBTv2");
}
if (m_issuance_value != nullopt && !m_issuance_value_commitment.IsNull()) {
throw std::ios_base::failure("Both issuance value and issuance value commitment cannot be provided at the same time");
if (!m_issuance_value_commitment.IsNull() && m_issuance_value == nullopt) {
throw std::ios_base::failure("Explicit issuance value must be provided if its commitment is provided too");
}
if (m_issuance_inflation_keys_amount != nullopt && !m_issuance_inflation_keys_commitment.IsNull()) {
throw std::ios_base::failure("Both issuance inflations keys amount and issuance inflations keys commitment cannot be provided at the same time");
if (!m_issuance_value_commitment.IsNull() && m_blind_issuance_value_proof.empty()) {
throw std::ios_base::failure("Blind issuance value proof must be provided if its commitment is provided too");
}
if (!m_issuance_inflation_keys_commitment.IsNull() && m_issuance_inflation_keys_amount == nullopt) {
throw std::ios_base::failure("Explicit issuance inflation keys amount must be provided if its commitment is provided too");
}
if (!m_issuance_inflation_keys_commitment.IsNull() && m_blind_issuance_inflation_keys_proof.empty()) {
throw std::ios_base::failure("Blind issuance inflation keys value proof must be provided if its commitment is provided too");
}
if (!m_issuance_value_commitment.IsNull() && m_issuance_rangeproof.empty()) {
throw std::ios_base::failure("Issuance value commitment provided without value rangeproof");
}
if (!m_issuance_inflation_keys_commitment.IsNull() && m_issuance_inflation_keys_rangeproof.empty()) {
throw std::ios_base::failure("Issuance inflatio nkeys commitment provided without inflation keys rangeproof");
throw std::ios_base::failure("Issuance inflation keys commitment provided without inflation keys rangeproof");
}
}
}
@ -931,6 +975,8 @@ struct PSBTOutput
CPubKey m_ecdh_pubkey;
CPubKey m_blinding_pubkey;
Optional<uint32_t> m_blinder_index{nullopt};
std::vector<unsigned char> m_blind_value_proof;
std::vector<unsigned char> m_blind_asset_proof;
bool IsNull() const;
void FillSignatureData(SignatureData& sigdata) const;
@ -968,21 +1014,21 @@ struct PSBTOutput
// Elements proprietary fields are v2 only
// Amount
// We shouldn't have both amount and amount commitment. If we do, only write the amount commitment
if (!m_value_commitment.IsNull()) {
SerializeToVector(s, CompactSizeWriter(PSBT_OUT_PROPRIETARY), PSBT_ELEMENTS_ID, CompactSizeWriter(PSBT_ELEMENTS_OUT_VALUE_COMMITMENT));
SerializeToVector(s, m_value_commitment);
} else if (amount != nullopt) {
}
if (amount != nullopt) {
SerializeToVector(s, CompactSizeWriter(PSBT_OUT_AMOUNT));
SerializeToVector(s, *amount);
}
// Asset
// We shouldn't have both asset and asset commitment, but if we do, write only the asset commitment
if (!m_asset_commitment.IsNull()) {
SerializeToVector(s, CompactSizeWriter(PSBT_OUT_PROPRIETARY), PSBT_ELEMENTS_ID, CompactSizeWriter(PSBT_ELEMENTS_OUT_ASSET_COMMITMENT));
SerializeToVector(s, m_asset_commitment);
} else if (!m_asset.IsNull()) {
}
if (!m_asset.IsNull()) {
SerializeToVector(s, CompactSizeWriter(PSBT_OUT_PROPRIETARY), PSBT_ELEMENTS_ID, CompactSizeWriter(PSBT_ELEMENTS_OUT_ASSET));
SerializeToVector(s, m_asset);
}
@ -1016,6 +1062,18 @@ struct PSBTOutput
SerializeToVector(s, CompactSizeWriter(PSBT_OUT_PROPRIETARY), PSBT_ELEMENTS_ID, CompactSizeWriter(PSBT_ELEMENTS_OUT_BLINDER_INDEX));
SerializeToVector(s, *m_blinder_index);
}
// BLind value proof
if (!m_blind_value_proof.empty()) {
SerializeToVector(s, CompactSizeWriter(PSBT_OUT_PROPRIETARY), PSBT_ELEMENTS_ID, CompactSizeWriter(PSBT_ELEMENTS_OUT_BLIND_VALUE_PROOF));
s << m_blind_value_proof;
}
// BLind asset proof
if (!m_blind_asset_proof.empty()) {
SerializeToVector(s, CompactSizeWriter(PSBT_OUT_PROPRIETARY), PSBT_ELEMENTS_ID, CompactSizeWriter(PSBT_ELEMENTS_OUT_BLIND_ASSET_PROOF));
s << m_blind_asset_proof;
}
}
// Write proprietary things
@ -1202,6 +1260,31 @@ struct PSBTOutput
m_blinder_index = i;
break;
}
case PSBT_ELEMENTS_OUT_BLIND_VALUE_PROOF:
{
if (!m_blind_value_proof.empty()) {
throw std::ios_base::failure("Duplicate Key, output blind value proof already provided");
} else if (subkey_len != 1) {
throw std::ios_base::failure("Output blind value proof key is more than one byte type");
}
s >> m_blind_value_proof;
break;
}
case PSBT_ELEMENTS_OUT_BLIND_ASSET_PROOF:
{
if (!m_blind_asset_proof.empty()) {
throw std::ios_base::failure("Duplicate Key, output blind asset proof already provided");
} else if (subkey_len != 1) {
throw std::ios_base::failure("Output blind asset proof key is more than one byte type");
}
s >> m_blind_asset_proof;
break;
}
default:
{
known = false;
break;
}
}
}
@ -1236,20 +1319,20 @@ struct PSBTOutput
// Make sure required PSBTv2 fields are present
if (m_psbt_version >= 2) {
if (amount == nullopt && m_value_commitment.IsNull()) {
if (amount == nullopt) {
throw std::ios_base::failure("Output amount is required in PSBTv2");
}
if (script == nullopt) {
throw std::ios_base::failure("Output script is required in PSBTv2");
}
if (amount != nullopt && !m_value_commitment.IsNull()) {
throw std::ios_base::failure("Both output amount and output value commitment cannot be specified at the same time");
}
if (m_asset.IsNull() && m_asset_commitment.IsNull()) {
if (m_asset.IsNull()) {
throw std::ios_base::failure("Output asset is required in PSET");
}
if (!m_asset.IsNull() && !m_asset_commitment.IsNull()) {
throw std::ios_base::failure("Both output asset and output asset commitment cannot be specified at the same time");
if (!m_value_commitment.IsNull() && m_blind_value_proof.empty()) {
throw std::ios_base::failure("Blind value proof must be provided if value commitment is provided");
}
if (!m_asset_commitment.IsNull() && m_blind_asset_proof.empty()) {
throw std::ios_base::failure("Blind asset proof must be provided if asset commitment is provided");
}
if (m_blinding_pubkey.IsValid() && m_blinder_index == nullopt) {
throw std::ios_base::failure("Output is blinded but does not have a blinder index");
@ -1296,7 +1379,7 @@ struct PartiallySignedTransaction
void SetupFromTx(const CMutableTransaction& tx);
void CacheUnsignedTxPieces();
bool ComputeTimeLock(uint32_t& locktime) const;
CMutableTransaction GetUnsignedTx() const;
CMutableTransaction GetUnsignedTx(bool foce_unblinded=false) const;
uint256 GetUniqueID() const;
PartiallySignedTransaction() {}
PartiallySignedTransaction(uint32_t version);

View file

@ -1156,9 +1156,11 @@ static RPCHelpMan decodepsbt()
{RPCResult::Type::NUM, "issuance_value", "The explicit value of the issuance in this input in " + CURRENCY_UNIT},
{RPCResult::Type::STR_HEX, "issuance_value_commitment", "The commitment of the value of the issuance in this input."},
{RPCResult::Type::STR_HEX, "issuance_value_rangeproof", "The rangeproof for the value commitment of the issuance in this input."},
{RPCResult::Type::STR_HEX, "blind_issuance_value_proof", "Explicit value rangeproof that proves the issuance value commitment matches the value"},
{RPCResult::Type::NUM, "issuance_reissuance_amount", "The explicit amount available for the reissuance output."},
{RPCResult::Type::STR_HEX, "issuance_reissuance_amount_commitment", "The commitment of the reissuance amount."},
{RPCResult::Type::STR_HEX, "issuance_reissuance_amount_rangeproof", "The rangeproof for the amount commitment of the reissuance amount."},
{RPCResult::Type::STR_HEX, "blind_reissuance_amount_proof", "Explicit value rangeproof that proves the reissuance value commitment matches the reissuance value"},
{RPCResult::Type::STR_HEX, "issuance_blinding_nonce", "The blinding nonce for the issuance in this input."},
{RPCResult::Type::STR_HEX, "issuance_asset_entropy", "The asset entropy for the issuance in this input."},
{RPCResult::Type::STR_HEX, "pegin_bitcoin_tx", "The tx providing the peg-in in the format of the getrawtransaction RPC"},
@ -1223,6 +1225,8 @@ static RPCHelpMan decodepsbt()
{RPCResult::Type::STR_HEX, "surjection_proof", "The surjection proof for the output"},
{RPCResult::Type::STR_HEX, "ecdh_pubkey", "The ecdh pubkey for the output"},
{RPCResult::Type::STR_HEX, "blinding_pubkey", "The blinding pubkey for the output"},
{RPCResult::Type::STR_HEX, "blind_value_proof", "Explicit value rangeproof that proves the value commitment matches the value"},
{RPCResult::Type::STR_HEX, "blind_asset_proof", "Assert surjection proof that proves the assert commitment matches the asset"},
{RPCResult::Type::OBJ_DYN, "unknown", "The unknown global fields",
{
{RPCResult::Type::STR_HEX, "key", "(key-value pair) An unknown key-value pair"},
@ -1458,6 +1462,11 @@ static RPCHelpMan decodepsbt()
in.pushKV("issuance_value_rangeproof", HexStr(input.m_issuance_rangeproof));
}
// Issuance blind value proof
if (!input.m_blind_issuance_value_proof.empty()) {
in.pushKV("blind_issuance_value_proof", HexStr(input.m_blind_issuance_value_proof));
}
// Issuance inflation keys amount
if (input.m_issuance_inflation_keys_amount != nullopt) {
in.pushKV("issuance_reissuance_amount", ValueFromAmount(*input.m_issuance_inflation_keys_amount));
@ -1473,6 +1482,11 @@ static RPCHelpMan decodepsbt()
in.pushKV("issuance_reissuance_amount_rangeproof", HexStr(input.m_issuance_inflation_keys_rangeproof));
}
// Issuance blind inflation keys value proof
if (!input.m_blind_issuance_value_proof.empty()) {
in.pushKV("blind_reissuance_amount_proof", HexStr(input.m_blind_issuance_inflation_keys_proof));
}
// Issuance blinding nonce
if (!input.m_issuance_blinding_nonce.IsNull()) {
in.pushKV("issuance_blinding_nonce", input.m_issuance_blinding_nonce.GetHex());
@ -1649,6 +1663,16 @@ static RPCHelpMan decodepsbt()
out.pushKV("blinder_index", (int64_t)*output.m_blinder_index);
}
// Blind value proof
if (!output.m_blind_value_proof.empty()) {
out.pushKV("blind_value_proof", HexStr(output.m_blind_value_proof));
}
// Blind asset proof
if (!output.m_blind_asset_proof.empty()) {
out.pushKV("blind_asset_proof", HexStr(output.m_blind_asset_proof));
}
// Proprietary
if (!output.m_proprietary.empty()) {
UniValue proprietary(UniValue::VARR);
@ -1761,8 +1785,8 @@ static RPCHelpMan combinepsbt()
is_fully_blinded &= psbt_out.IsFullyBlinded();
}
}
if (!is_fully_blinded) {
throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "Cannot combine PSETs");
if (is_fully_blinded) {
throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "Cannot combine PSETs as the values and blinders would become imbalanced");
}
}

View file

@ -37,6 +37,10 @@ bilingual_str TransactionErrorString(const TransactionError err)
return Untranslated("Transaction values or blinders are not balanced");
case TransactionError::UTXOS_MISSING_BALANCE_CHECK:
return Untranslated("Missing UTXOs that are needed to check transaction balance");
case TransactionError::INVALID_VALUE_PROOF:
return Untranslated("Proof of blinded value is invalid");
case TransactionError::INVALID_ASSET_PROOF:
return Untranslated("Proof of blinded asset is invalid");
// no default case, so the compiler can warn about missing cases
}
assert(false);

View file

@ -33,6 +33,8 @@ enum class TransactionError {
BLINDING_REQUIRED,
VALUE_IMBALANCE,
UTXOS_MISSING_BALANCE_CHECK,
INVALID_VALUE_PROOF,
INVALID_ASSET_PROOF,
};
bilingual_str TransactionErrorString(const TransactionError error);

View file

@ -4749,7 +4749,8 @@ static RPCHelpMan walletprocesspsbt()
}
if (needs_blinding) {
BlindingStatus status = pwallet->WalletBlindPSBT(psbtx);
if (status != BlindingStatus::OK) {
// Fail if we couldn't blind, but only if it is for reasons other than needing UTXOs
if (status != BlindingStatus::OK && status != BlindingStatus::NEEDS_UTXOS) {
throw JSONRPCError(RPC_WALLET_ERROR, GetBlindingStatusError(status));
}
}

View file

@ -2830,10 +2830,21 @@ BlindingStatus CWallet::WalletBlindPSBT(PartiallySignedTransaction& psbtx) const
TransactionError CWallet::SignPSBT(PartiallySignedTransaction& psbtx, bool& complete, int sighash_type, bool sign, bool imbalance_ok, bool bip32derivs, size_t* n_signed) const
{
// If we're signing, check that the transaction is not still in need of blinding
// Also check that the amount and asset proofs are valid
if (sign) {
for (const PSBTOutput& o : psbtx.outputs) {
if (o.IsBlinded() && !o.IsFullyBlinded()) {
return TransactionError::BLINDING_REQUIRED;
if (o.IsBlinded()) {
if (!o.IsFullyBlinded()) {
return TransactionError::BLINDING_REQUIRED;
}
assert(!o.m_blind_value_proof.empty());
assert(!o.m_blind_asset_proof.empty());
if (!VerifyBlindValueProof(*o.amount, o.m_value_commitment, o.m_blind_value_proof, o.m_asset_commitment)) {
return TransactionError::INVALID_VALUE_PROOF;
}
if (!VerifyBlindAssetProof(o.m_blind_asset_proof, o.m_asset_commitment)) {
return TransactionError::INVALID_ASSET_PROOF;
}
}
}
}

View file

@ -12,6 +12,7 @@ from test_framework.util import (
assert_greater_than,
assert_raises_rpc_error,
find_output,
find_vout_for_address,
)
from decimal import Decimal
@ -616,6 +617,9 @@ class PSBTTest(BitcoinTestFramework):
conf_addr_4 = self.get_address(True, 0)
psbt = self.nodes[2].createpsbt([{"txid": txid_conf_2, "vout": 1}], [{conf_addr_4: 24.998, "blinder_index": 0}, {"fee": 0.001}])
psbt = self.nodes[2].walletprocesspsbt(psbt)['psbt']
decoded = self.nodes[1].decodepsbt(psbt)
assert "blind_value_proof" in decoded["outputs"][0]
assert "blind_asset_proof" in decoded["outputs"][0]
hex_tx = self.nodes[2].finalizepsbt(psbt)['hex']
assert_equal(self.num_blinded_outputs(hex_tx), 1)
self.nodes[2].sendrawtransaction(hex_tx)
@ -634,6 +638,84 @@ class PSBTTest(BitcoinTestFramework):
self.nodes[0].generate(1)
self.sync_all()
# Try a multiparty blinded tx
# Prepare wallets and UTXOs for inputs
self.nodes[2].createwallet("w1")
w1 = self.nodes[2].get_wallet_rpc("w1")
self.nodes[2].createwallet("w2")
w2 = self.nodes[2].get_wallet_rpc("w2")
self.nodes[2].createwallet("w3")
w3 = self.nodes[2].get_wallet_rpc("w3")
w1_addr = w1.getaddressinfo(w1.getnewaddress())["confidential"]
w2_addr = w2.getaddressinfo(w2.getnewaddress())["confidential"]
w3_addr = w3.getaddressinfo(w3.getnewaddress())["confidential"]
txid1 = self.nodes[0].sendtoaddress(w1_addr, 10)
txid2 = self.nodes[0].sendtoaddress(w2_addr, 10)
txid3 = self.nodes[0].sendtoaddress(w3_addr, 10)
self.sync_all()
vout1 = find_vout_for_address(self.nodes[2], txid1, w1_addr)
vout2 = find_vout_for_address(self.nodes[2], txid2, w2_addr)
vout3 = find_vout_for_address(self.nodes[2], txid3, w3_addr)
self.nodes[0].generate(1)
self.sync_all()
# Check that a walletprocesspsbt fails if the wallet has a blind input but no blind outputs
created_psbt = self.nodes[0].createpsbt(
[
{"txid": txid1, "vout": vout1},
{"txid": txid2, "vout": vout2},
],
[
{self.get_address(True, 0): Decimal("19.999"), "blinder_index": 0},
{"fee": Decimal("0.001")}
]
)
up_psbt1 = w1.walletprocesspsbt(psbt=created_psbt, sign=False)["psbt"]
assert_raises_rpc_error(-4, "Transaction has blind inputs belonging to this blinder but does not have outputs to blind", w2.walletprocesspsbt, up_psbt1, False)
# Make the PSBT
created_psbt = self.nodes[0].createpsbt(
[
{"txid": txid1, "vout": vout1},
{"txid": txid2, "vout": vout2},
{"txid": txid3, "vout": vout3},
],
[
{self.get_address(True, 0): Decimal("9.999"), "blinder_index": 0},
{self.get_address(True, 0): Decimal("9.999"), "blinder_index": 1},
{self.get_address(True, 0): Decimal("9.999"), "blinder_index": 2},
{"fee": Decimal("0.003")}
]
)
# Update all but don't blind
up_psbt1 = w1.walletprocesspsbt(psbt=created_psbt, sign=False)["psbt"]
up_psbt2 = w2.walletprocesspsbt(psbt=created_psbt, sign=False)["psbt"]
up_psbt3 = w3.walletprocesspsbt(psbt=created_psbt, sign=False)["psbt"]
# Combine updated
comb_psbt1 = self.nodes[0].combinepsbt([up_psbt1, up_psbt2, up_psbt3])
# 1 and 2 blind
blind_psbt1 = w1.walletprocesspsbt(psbt=comb_psbt1, sign=False)["psbt"]
blind_psbt2 = w2.walletprocesspsbt(psbt=comb_psbt1, sign=False)["psbt"]
# Check that trying to blind a PSET where our inputs are already blinded results in no change
re_blind_psbt2 = w2.walletprocesspsbt(psbt=blind_psbt2, sign=False)["psbt"]
assert_equal(blind_psbt2, re_blind_psbt2)
# Make sure combinepsbt does not work if the result would have imbalanced values and blinders
blind_psbt3 = w3.walletprocesspsbt(psbt=comb_psbt1, sign=False)["psbt"]
assert_raises_rpc_error(-22, "Cannot combine PSETs as the values and blinders would become imbalanced", self.nodes[0].combinepsbt, [blind_psbt1, blind_psbt2, blind_psbt3])
# Combine 1 and 2 blinded
comb_psbt2 = self.nodes[0].combinepsbt([blind_psbt1, blind_psbt2])
# 3 Updates and blinds combined
blind_psbt = w3.walletprocesspsbt(psbt=comb_psbt2, sign=False)["psbt"]
# All sign
sign_psbt1 = w1.walletprocesspsbt(psbt=blind_psbt)["psbt"]
sign_psbt2 = w2.walletprocesspsbt(psbt=blind_psbt)["psbt"]
sign_psbt3 = w3.walletprocesspsbt(psbt=blind_psbt)["psbt"]
# Combine sigs
comb_psbt2 = self.nodes[0].combinepsbt([sign_psbt1, sign_psbt2, sign_psbt3])
# Finalize and send
tx = self.nodes[0].finalizepsbt(comb_psbt2)["hex"]
self.nodes[0].sendrawtransaction(tx)
self.nodes[0].generate(1)
self.sync_all()
def run_test(self):
self.nodes[0].generate(200)
self.sync_all()