Merge ElementsProject/elements#1050: wallet: fix a pile of bugs in transaction funding logic

a67a2df330 pset: remove one more intermediate-zero check from the blinding logic (Andrew Poelstra)
9eb285c19d test: add test for confidential non-wallet-owned change (Andrew Poelstra)
185d473fe8 walletcreatefundedpsbt: add functional test for blinding edge cases (Andrew Poelstra)
533da12c2c wallet: make sure extra OP_RETURN output is blinded when called from fundraw (Andrew Poelstra)
b09b63bd1b pset: allow input blinding factors to sum to zero, or value to be 0 (Andrew Poelstra)
9afcb83baf wallet: correctly handle blinding of manually-set change addresses (Andrew Poelstra)
9813c3e74a wallet: fix "cannot unblind IsMine output" check in SignPSBT (Andrew Poelstra)
7103471fd5 walletcreatefundedpsbt: signal blinding data correctly to `FundTransaction` (Andrew Poelstra)

Pull request description:

  Fixes #1049

  Needs backport to 0.21 (and a new rc).

  Although there are several bugs here, none affect the functionaries. There is a more thorough summary in the second-to-last commit message.

ACKs for top commit:
  achow101:
    ACK a67a2df330

Tree-SHA512: 25066c29f080e43cd00c5b33c60a986a8cb5bbf4ca01ceb3b4182c5b8f61979a1d6d946b8f28fa871ec90f44bdb6fc22014b5c11280cf081181db102cd588cdf
This commit is contained in:
Andrew Poelstra 2021-10-02 15:10:42 +00:00
commit 6eed792d43
No known key found for this signature in database
GPG key ID: C588D63CE41B97C1
6 changed files with 178 additions and 13 deletions

View file

@ -263,6 +263,7 @@ bool SubtractScalars(uint256& a, const uint256& b)
// Compute the scalar offset used for the final blinder computation
// value * asset_blinder + value_blinder
// FIXME this method should be in libsecp, as should `ComputeAndAddToScalarOffset`
bool CalculateScalarOffset(uint256& out, CAmount value, const uint256& asset_blinder, const uint256& value_blinder)
{
// If the asset_blinder is 0, then the equation resolves to just the value_blinder
@ -276,8 +277,24 @@ bool CalculateScalarOffset(uint256& out, CAmount value, const uint256& asset_bli
// tweak_mul expects a 32 byte, big endian tweak.
// We need to pack the 8 byte CAmount into a uint256 with the correct padding, so start it at 24 bytes from the front
WriteBE64(val.begin() + 24, value);
if (secp256k1_ec_privkey_tweak_mul(secp256k1_blind_context, out.begin(), val.begin()) != 1) return false;
if (!value_blinder.IsNull() && secp256k1_ec_privkey_tweak_add(secp256k1_blind_context, out.begin(), value_blinder.begin()) != 1) return false;
if (value > 0) {
if (secp256k1_ec_privkey_tweak_mul(secp256k1_blind_context, out.begin(), val.begin()) != 1) return false;
} else {
out = value_blinder;
return true;
}
if (!value_blinder.IsNull()) {
uint256 value_negated = value_blinder;
if (secp256k1_ec_seckey_negate(secp256k1_blind_context, value_negated.begin()) != 1) {
return false;
}
// Special-case zero, which would otherwise cause `secp256k1_ec_privkey_tweak_add` to fail
if (value_negated == out) {
out = uint256{};
return true;
}
if (secp256k1_ec_privkey_tweak_add(secp256k1_blind_context, out.begin(), value_blinder.begin()) != 1) return false;
}
return true;
}
@ -294,8 +311,17 @@ bool ComputeAndAddToScalarOffset(uint256& a, CAmount value, const uint256& asset
if (a.IsNull()) {
a = scalar;
} else {
// If we have a, then add the scalar to it.
if (secp256k1_ec_privkey_tweak_add(secp256k1_blind_context, a.begin(), scalar.begin()) != 1) return false;
uint256 scalar_negated = scalar;
if (secp256k1_ec_seckey_negate(secp256k1_blind_context, scalar_negated.begin()) != 1) {
return false;
}
// Special-case zero, which would otherwise cause `secp256k1_ec_privkey_tweak_add` to fail
if (scalar_negated == a) {
a = uint256{};
} else {
// If we have a, then add the scalar to it.
if (secp256k1_ec_privkey_tweak_add(secp256k1_blind_context, a.begin(), scalar.begin()) != 1) return false;
}
}
return true;
}

View file

@ -41,6 +41,10 @@ bilingual_str TransactionErrorString(const TransactionError err)
return Untranslated("Proof of blinded value is invalid");
case TransactionError::INVALID_ASSET_PROOF:
return Untranslated("Proof of blinded asset is invalid");
case TransactionError::MISSING_BLINDING_KEY:
return Untranslated("Wallet does not have necessary blinding key");
case TransactionError::MISSING_SIDECHANNEL_DATA:
return Untranslated("A rangeproof did not encode necessary blinding data");
// no default case, so the compiler can warn about missing cases
}
assert(false);

View file

@ -35,6 +35,8 @@ enum class TransactionError {
UTXOS_MISSING_BALANCE_CHECK,
INVALID_VALUE_PROOF,
INVALID_ASSET_PROOF,
MISSING_BLINDING_KEY,
MISSING_SIDECHANNEL_DATA,
};
bilingual_str TransactionErrorString(const TransactionError error);

View file

@ -4957,8 +4957,16 @@ static RPCHelpMan walletcreatefundedpsbt()
// Automatically select coins, unless at least one is manually selected. Can
// be overridden by options.add_inputs.
coin_control.m_add_inputs = rawTx.vin.size() == 0;
// FundTransaction expects blinding keys, if present, to appear in the output nonces
for (CTxOut& txout : rawTx.vout) {
auto search_it = psbt_outs.find(txout);
assert (search_it != psbt_outs.end());
CPubKey& blind_pub = search_it->second.m_blinding_pubkey;
if (blind_pub.IsFullyValid()) {
txout.nNonce.vchCommitment = std::vector<unsigned char>(blind_pub.begin(), blind_pub.end());
}
}
FundTransaction(pwallet, rawTx, fee, change_position, request.params[3], coin_control, /* solving_data */ request.params[5], /* override_min_fee */ true);
PartiallySignedTransaction psbtx(rawTx, psbt_version);
// Find an input that is ours
unsigned int blinder_index = 0;
{
@ -4971,6 +4979,17 @@ static RPCHelpMan walletcreatefundedpsbt()
}
}
assert(blinder_index < rawTx.vin.size()); // We added inputs, or existing inputs are ours, we should have a blinder index at this point.
// It may add outputs (change, and in some edge case OP_RETURN) which need to be
// blinded. So pull these into `psbt_outs`.
for (const CTxOut& txout : rawTx.vout) {
if (!txout.nNonce.IsNull() && !psbt_outs.count(txout)) {
PSBTOutput new_out{2}; // psbtv2 output
new_out.m_blinding_pubkey.Set(txout.nNonce.vchCommitment.begin(), txout.nNonce.vchCommitment.end());
new_out.m_blinder_index = blinder_index;
psbt_outs.insert(std::make_pair(txout, new_out));
}
}
PartiallySignedTransaction psbtx(rawTx, psbt_version);
for (unsigned int i = 0; i < rawTx.vout.size(); ++i) {
PSBTOutput& output = psbtx.outputs[i];
auto it = psbt_outs.find(rawTx.vout.at(i));

View file

@ -19,6 +19,7 @@
#include <policy/policy.h>
#include <primitives/block.h>
#include <primitives/transaction.h>
#include <rpc/util.h> // for GetDestinationBlindingKey and IsBlindDestination
#include <script/descriptor.h>
#include <script/pegins.h>
#include <script/script.h>
@ -2859,7 +2860,7 @@ TransactionError CWallet::SignPSBT(PartiallySignedTransaction& psbtx, bool& comp
CConfidentialNonce nonce;
nonce.vchCommitment.insert(nonce.vchCommitment.end(), o.m_ecdh_pubkey.begin(), o.m_ecdh_pubkey.end());
if (!UnblindConfidentialPair(blinding_key, o.m_value_commitment, o.m_asset_commitment, nonce, o.script.get(), o.m_value_rangeproof, value, value_factor, asset, asset_factor)) {
if (UnblindConfidentialPair(blinding_key, o.m_value_commitment, o.m_asset_commitment, nonce, o.script.get(), o.m_value_rangeproof, value, value_factor, asset, asset_factor)) {
// These assertions are cryptographically impossible to trigger, as we
// checked the proofs above, and then `UnblindConfidentialPair` checks
// the extracted value/asset against the commitments.
@ -2869,10 +2870,11 @@ TransactionError CWallet::SignPSBT(PartiallySignedTransaction& psbtx, bool& comp
if (!o.m_asset.IsNull()) {
assert(CAsset(o.m_asset) == asset);
}
return TransactionError::INVALID_ASSET_PROOF; // FIXME
} else {
return TransactionError::MISSING_SIDECHANNEL_DATA;
}
} else {
return TransactionError::INVALID_ASSET_PROOF; // FIXME
return TransactionError::MISSING_BLINDING_KEY;
}
}
}
@ -3232,6 +3234,8 @@ bool fillBlindDetails(BlindDetails* det, CWallet* wallet, CMutableTransaction& t
// We need to make sure to dupe an asset that is in input set
//TODO Have blinding do some extremely minimal rangeproof
CTxOut newTxOut(det->o_assets.back(), 0, CScript() << OP_RETURN);
CPubKey blind_pub = wallet->GetBlindingPubKey(newTxOut.scriptPubKey); // irrelevent, just needs to be non-null
newTxOut.nNonce.vchCommitment = std::vector<unsigned char>(blind_pub.begin(), blind_pub.end());
txNew.vout.push_back(newTxOut);
det->o_pubkeys.push_back(wallet->GetBlindingPubKey(newTxOut.scriptPubKey));
det->o_amount_blinds.push_back(uint256());
@ -3352,6 +3356,9 @@ bool CWallet::CreateTransactionInternal(
// A map that keeps track of the change script for each asset and also
// the index of the reservedest used for that script (-1 if none).
std::map<CAsset, std::pair<int, CScript>> mapScriptChange;
// For manually set change, we need to use the blinding pubkey associated
// with the manually-set address rather than generating one from the wallet
std::map<CAsset, Optional<CPubKey> > mapBlindingKeyChange;
LOCK(cs_wallet);
txNew.nLockTime = GetLocktimeForNewTransaction(chain(), GetLastBlockHash(), GetLastBlockHeight());
@ -3366,6 +3373,11 @@ bool CWallet::CreateTransactionInternal(
for (const auto& dest : coin_control.destChange) {
// No need to test we cover all assets. We produce error for that later.
mapScriptChange[dest.first] = std::pair<int, CScript>(-1, GetScriptForDestination(dest.second));
if (IsBlindDestination(dest.second)) {
mapBlindingKeyChange[dest.first] = GetDestinationBlindingKey(dest.second);
} else {
mapBlindingKeyChange[dest.first] = nullopt;
}
}
} else { // no coin control: send change to newly generated address
// Note: We use a new key here to keep it from being obvious which side is the change.
@ -3638,15 +3650,36 @@ bool CWallet::CreateTransactionInternal(
}
std::vector<CTxOut>::iterator position = txNew.vout.begin()+vChangePosInOut[assetChange.first];
Optional<CPubKey> blind_pub;
if (blind_details) {
CPubKey blind_pub = GetBlindingPubKey(itScript->second.second);
blind_details->o_pubkeys.insert(blind_details->o_pubkeys.begin() + vChangePosInOut[assetChange.first], blind_pub);
assert(blind_pub.IsFullyValid());
const auto itBlindingKey = mapBlindingKeyChange.find(assetChange.first);
if (itBlindingKey != mapBlindingKeyChange.end()) {
// If the change output was specified, use the blinding key that
// came with the specified address (if any)
blind_pub = itBlindingKey->second;
} else {
// Otherwise, we generated it from our own wallet, so get the
// blinding key from our own wallet.
blind_pub = GetBlindingPubKey(itScript->second.second);
}
} else {
// ...and if we are not blinding at all, use no blinding key. (This
// assignment is technically unnecessary as `blind_pub` was initialized
// above to nullopt, but we leave it here for clarity.)
blind_pub = nullopt;
}
if (blind_pub) {
blind_details->o_pubkeys.insert(blind_details->o_pubkeys.begin() + vChangePosInOut[assetChange.first], *blind_pub);
assert(blind_pub->IsFullyValid());
blind_details->num_to_blind++;
blind_details->change_to_blind++;
blind_details->only_change_pos = vChangePosInOut[assetChange.first];
// Place the blinding pubkey here in case of fundraw calls
newTxOut.nNonce.vchCommitment = std::vector<unsigned char>(blind_pub.begin(), blind_pub.end());
newTxOut.nNonce.vchCommitment = std::vector<unsigned char>(blind_pub->begin(), blind_pub->end());
} else if (blind_details) {
// Insert placeholder
blind_details->o_pubkeys.insert(blind_details->o_pubkeys.begin() + vChangePosInOut[assetChange.first], CPubKey{});
}
txNew.vout.insert(position, newTxOut);
}

View file

@ -24,6 +24,17 @@ import os
MAX_BIP125_RBF_SEQUENCE = 0xfffffffd
# Utility function to extract info from outputs for use in assert_equal
# Returns a set of tuples (address, is blinded, is OP_RETURN). (It would
# feel nicer to return a set of dicts, but you can't do that in Python.)
def outputs_info(outputs):
return {(
"" if x["script"].get("addresses") is None else x["script"]["addresses"][0],
x.get("blinding_pubkey") is not None,
x["script"]["asm"] == "OP_RETURN",
) for x in outputs}
# Create one-input, one-output, no-fee transaction:
class PSBTTest(BitcoinTestFramework):
@ -316,13 +327,49 @@ class PSBTTest(BitcoinTestFramework):
assert_raises_rpc_error(-4, msg, self.nodes[1].walletcreatefundedpsbt, inputs, outputs_array, 0, {"feeRate": 1, "add_inputs": bool_add})
self.log.info("Test various PSBT operations")
addr = self.get_address(confidential, 1)
unconf_addr = self.nodes[1].getaddressinfo(addr)['unconfidential']
change_addr = self.nodes[1].getrawchangeaddress()
conf_change_addr = self.nodes[1].getaddressinfo(change_addr)['confidential']
unconf_change_addr = self.nodes[1].getaddressinfo(change_addr)['unconfidential']
# partially sign multisig things with node 1
psbtx = wmulti.walletcreatefundedpsbt(inputs=[{"txid":txid,"vout":p2wsh_pos},{"txid":txid,"vout":p2sh_pos},{"txid":txid,"vout":p2sh_p2wsh_pos}], outputs=[{self.get_address(confidential, 1):29.99}], options={'changeAddress': self.nodes[1].getrawchangeaddress()})['psbt']
psbtx = wmulti.walletcreatefundedpsbt(inputs=[{"txid":txid,"vout":p2wsh_pos},{"txid":txid,"vout":p2sh_pos},{"txid":txid,"vout":p2sh_p2wsh_pos}], outputs=[{addr:29.99}], options={'changeAddress': unconf_change_addr})['psbt']
filled = wmulti.walletprocesspsbt(psbtx)
# have both nodes fill before we try to blind and sign
walletprocesspsbt_out = self.nodes[1].walletprocesspsbt(filled["psbt"])
psbtx = walletprocesspsbt_out['psbt']
assert_equal(walletprocesspsbt_out['complete'], False)
# check that the unblinded change address led to unblinded change
assert_equal(
outputs_info(self.nodes[1].decodepsbt(psbtx)["outputs"]),
{
(unconf_addr, confidential, False),
(unconf_change_addr, False, False),
("", False, False), # fee
},
)
# Repeat the above, with a confidential change address
psbtx = wmulti.walletcreatefundedpsbt(inputs=[{"txid":txid,"vout":p2wsh_pos},{"txid":txid,"vout":p2sh_pos},{"txid":txid,"vout":p2sh_p2wsh_pos}], outputs=[{addr:29.99}], options={'changeAddress': conf_change_addr})['psbt']
filled = wmulti.walletprocesspsbt(psbtx)
# have both nodes fill before we try to blind and sign
walletprocesspsbt_out = self.nodes[1].walletprocesspsbt(filled["psbt"])
psbtx = walletprocesspsbt_out['psbt']
assert_equal(walletprocesspsbt_out['complete'], False)
# check that the blinded change address led to blinded change (and below,
# when we call `walletprocesspsbt` with nodes[2], it will make sure that
# node 2 is able to unblind this change, even though wmulti created it).
# Notice that if `confidential` is False, the change is not blinded. This
# is a quirk of the wallet.cpp blinding logic and will go away when we
# overhaul this.
assert_equal(
outputs_info(self.nodes[1].decodepsbt(psbtx)["outputs"]),
{
(unconf_addr, confidential, False),
(unconf_change_addr, confidential, False),
("", False, False), # fee
},
)
# Unload wmulti, we don't need it anymore
wmulti.unloadwallet()
@ -716,6 +763,40 @@ class PSBTTest(BitcoinTestFramework):
self.nodes[0].generate(1)
self.sync_all()
# Regression for #1049
# 1. Create a one-blinded-output PSET and check that it is blinded correctly
addr = self.nodes[0].getnewaddress()
conf_addr = self.nodes[0].getaddressinfo(addr)['confidential']
unconf_addr = self.nodes[0].getaddressinfo(addr)['unconfidential']
# 1a. Funding should succeed and *not* add a OP_RETURN output
funded = self.nodes[1].walletcreatefundedpsbt([], [{conf_addr: self.nodes[1].getbalance()['bitcoin']}], 0, {"subtractFeeFromOutputs": [0]})["psbt"]
assert_equal(
outputs_info(self.nodes[1].decodepsbt(funded)["outputs"]),
{
(unconf_addr, True, False),
("", False, False), # fee
},
)
# 1b. `walletprocesspsbt` should then succeed in creating a full transaction
signed = self.nodes[1].walletprocesspsbt(funded)["psbt"]
tx = self.nodes[1].finalizepsbt(signed)["hex"]
assert self.nodes[1].testmempoolaccept([tx])[0]['allowed']
# 2. Create a one-unblinded-output PSET and check that it is blinded correctly
# 2a. Funding should succeed and add a OP_RETURN output
funded = self.nodes[1].walletcreatefundedpsbt([], [{unconf_addr: self.nodes[1].getbalance()['bitcoin']}], 0, {"subtractFeeFromOutputs": [0]})["psbt"]
assert_equal(
outputs_info(self.nodes[1].decodepsbt(funded)["outputs"]),
{
(unconf_addr, False, False),
("", True, True), # blinded OP_RETURN
("", False, False), # fee
},
)
# 2b. `walletprocesspsbt` should then succeed in creating a full transaction
signed = self.nodes[1].walletprocesspsbt(funded)["psbt"]
tx = self.nodes[1].finalizepsbt(signed)["hex"]
assert self.nodes[1].testmempoolaccept([tx])[0]['allowed']
def pset_confidential_proofs(self):
UNBLINDED = "cHNldP8BAgQCAAAAAQMEAAAAAAEEAQEBBQECAfsEAgAAAAABAP1UAQIAAAAAASopobdl5W15RSedscp/8bxEXKuKIMOZw+JTqgD8qJEKBAAAAAD9////Awrye7Xu4kI5VnpTDeGaq8sYdXP3qdzYaHrLDRzaC8y51ggl1U8hJxSo+8GcTzHv926wsqTTkOrdBnJo8qcLwLQauQKktt71EJU7HTH5HsgG4kJV/tC32F992/WgieIPRkUkmxYAFPrs/iioimRS5hoJKl/hua83d7rwC1uuuLvfuQh38wHS+0Vg2ecXzypsUabYofOFaGSrICByCKvjgTF6TdHNp2el7Cwi+94dy4qMDrEh/25Aqnc+5qABAqWPEY9ZNCz7m64pANrr04bVgPxaWCr7LvvWGH5FLzvRFgAU96wAzcLFRah7B8gq17sVY9Uso18BIw9PXUt8b6hFgG7k9ncTRZ4baejmD87i5JQMeg1d4bIBAAAAAAAAKfQAAAAAAAABAXoK8nu17uJCOVZ6Uw3hmqvLGHVz96nc2Gh6yw0c2gvMudYIJdVPIScUqPvBnE8x7/dusLKk05Dq3QZyaPKnC8C0GrkCpLbe9RCVOx0x+R7IBuJCVf7Qt9hffdv1oIniD0ZFJJsWABT67P4oqIpkUuYaCSpf4bmvN3e68CIGA3pgD7iheh1WkyCWvviXQBa9KOJk6JBeYxEpPuxiRBOvEElHIxkAAACAAQAAgAMAAIABDiB25bQww62kp1L1uQVb7MxEVoem8kCzSmM5DW09I9V6DQEPBAAAAAABEAT/////B/wEcHNldA79CwFgAgAHY7+9IRzxAXWemL7C9M7CBAqQoSrXRoxI5/YnMLV6nV/GBMEhmvoDFJcNzRXI/LrIRMLZFvNrP5IupN8OZ+4q+++aJTnuYCZIDR1pssb0JHA0z2UXkEYdHv26qoW26RbLf2LNh29yVIOHG3jqqc7+L7F4UELZmjlEs6R1sulqQ0ePCUUgAsqURkdnNKtl0nORiyLN/9JfqGGTC30WhsdXifWRmqOfkWil0Va1bDYumMU7zJdW/go83ODuZ5VZVWFsBLFSn9HxF1SaFCGt197qo8dr+vhPZwb72k13A72D+5Lx7UKoYqamRJsoAZdUZ/oVd9GRlPbAmRPV7iOxmPYf+t9AQiEd0Z4AIgICuujF5+Lk/uCeX9+RWtJ8ioG51rogGduwt+iY1tZFtjUQSUcjGQAAAIAAAACACwAAgAEEFgAUg+8ATSQ8VvNg+WJAuweXm6kXlFkBAwgAv3xIGAkAAAf8BHBzZXQCICMPT11LfG+oRYBu5PZ3E0WeG2no5g/O4uSUDHoNXeGyB/wEcHNldAYhAwxmNPa94Vg9u/nZBWC/8IYTgnp85V5TMOEFWTTAcF2pB/wEcHNldAgEAAAAAAABBAABAwgA4fUFAAAAAAf8BHBzZXQCICMPT11LfG+oRYBu5PZ3E0WeG2no5g/O4uSUDHoNXeGyB/wEcHNldAgEAAAAAAA="
BLINDED = "cHNldP8BAgQCAAAAAQMEAAAAAAEEAQEBBQECAfsEAgAAAAABAP1UAQIAAAAAASopobdl5W15RSedscp/8bxEXKuKIMOZw+JTqgD8qJEKBAAAAAD9////Awrye7Xu4kI5VnpTDeGaq8sYdXP3qdzYaHrLDRzaC8y51ggl1U8hJxSo+8GcTzHv926wsqTTkOrdBnJo8qcLwLQauQKktt71EJU7HTH5HsgG4kJV/tC32F992/WgieIPRkUkmxYAFPrs/iioimRS5hoJKl/hua83d7rwC1uuuLvfuQh38wHS+0Vg2ecXzypsUabYofOFaGSrICByCKvjgTF6TdHNp2el7Cwi+94dy4qMDrEh/25Aqnc+5qABAqWPEY9ZNCz7m64pANrr04bVgPxaWCr7LvvWGH5FLzvRFgAU96wAzcLFRah7B8gq17sVY9Uso18BIw9PXUt8b6hFgG7k9ncTRZ4baejmD87i5JQMeg1d4bIBAAAAAAAAKfQAAAAAAAABAXoK8nu17uJCOVZ6Uw3hmqvLGHVz96nc2Gh6yw0c2gvMudYIJdVPIScUqPvBnE8x7/dusLKk05Dq3QZyaPKnC8C0GrkCpLbe9RCVOx0x+R7IBuJCVf7Qt9hffdv1oIniD0ZFJJsWABT67P4oqIpkUuYaCSpf4bmvN3e68CIGA3pgD7iheh1WkyCWvviXQBa9KOJk6JBeYxEpPuxiRBOvEElHIxkAAACAAQAAgAMAAIABDiB25bQww62kp1L1uQVb7MxEVoem8kCzSmM5DW09I9V6DQEPBAAAAAABEAT/////B/wEcHNldA79CwFgAgAHY7+9IRzxAXWemL7C9M7CBAqQoSrXRoxI5/YnMLV6nV/GBMEhmvoDFJcNzRXI/LrIRMLZFvNrP5IupN8OZ+4q+++aJTnuYCZIDR1pssb0JHA0z2UXkEYdHv26qoW26RbLf2LNh29yVIOHG3jqqc7+L7F4UELZmjlEs6R1sulqQ0ePCUUgAsqURkdnNKtl0nORiyLN/9JfqGGTC30WhsdXifWRmqOfkWil0Va1bDYumMU7zJdW/go83ODuZ5VZVWFsBLFSn9HxF1SaFCGt197qo8dr+vhPZwb72k13A72D+5Lx7UKoYqamRJsoAZdUZ/oVd9GRlPbAmRPV7iOxmPYf+t9AQiEd0Z4AIgICuujF5+Lk/uCeX9+RWtJ8ioG51rogGduwt+iY1tZFtjUQSUcjGQAAAIAAAACACwAAgAEEFgAUg+8ATSQ8VvNg+WJAuweXm6kXlFkH/ARwc2V0ASEIBoHxCnQKKMcpdKYCHdu36jzQ0zSc49oGuDQl7Nvus3gBAwgAv3xIGAkAAAf8BHBzZXQDIQsuVWSYT/UkUbq/hYsdWuoo3ARSy5K7e//36h8QhjdKRAf8BHBzZXQCICMPT11LfG+oRYBu5PZ3E0WeG2no5g/O4uSUDHoNXeGyB/wEcHNldAT9CwFgAgAACRhIfL75AJaUOCJ2q+YnbnYTFqluECvtDoJFGcrYvu5VsxPdASJNduFIJRBglnPdW73QRjqt+r3KlxBQ3XUWTce6is6cGED9eySEVJwBXz4Mt8SjqM2GsyUfqC+Ey3+APGgh54MYLt+HHKmt6ibcvE1DDU/UGpVo+I3cY/kgKJzrWMG6y/jDm/CHcF49L8EBtYC7iSrBhwzmDk7DmiViiQFCTUDfIqilX/piqS9ZlO4JNydA5kmLqXkj/xtR2hKt57wknqqvM7/car1S4Do8VljtG9lCzvSOBtBvijSwpFY1KaVFjpj0UZI9XJQ2eEbMrqC0qygNBi1f+ULyZFccNSGpXaZnrZAH/ARwc2V0BUMBAAECnwdoJ4rVnGgLT0He5GaLEhDnGqCKcH0nlTi1T53tBYMI8InonQGT61IAjoLcRxOqzMLgEC3KXg7yW8x6d6VmB/wEcHNldAYhAwxmNPa94Vg9u/nZBWC/8IYTgnp85V5TMOEFWTTAcF2pB/wEcHNldAchAitGVbG/bZNcV2ifjimuh04FOwRlxNrNPva66U6/RiHFB/wEcHNldAgEAAAAAAf8BHBzZXQJSSAAAAkYSHy/AIN6lvAUJ1o6ZQK5i/ewcpqRz4eW8zMzXFO/ZlNvAomxweIBD8YyywTguhBMI0BdLs2VeS5mc5e1oR0R27YAUccH/ARwc2V0CkMBAAGJm91DfvVBUOaEFZ0uH1RbT2cgI9MN9k1lE1hlWc2AtALpMJ17khkivt8F7dgCAVdBvcHFaw138ZsVfiD7g480AAEEAAEDCADh9QUAAAAAB/wEcHNldAIgIw9PXUt8b6hFgG7k9ncTRZ4baejmD87i5JQMeg1d4bIH/ARwc2V0CAQAAAAAAA=="