Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/init.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,7 @@ void SetupServerArgs(ArgsManager& argsman, bool can_listen_ipc)
strprintf("Relay and mine non-OP_RETURN datacarrier injection (default: %u)",
DEFAULT_ACCEPT_NON_STD_DATACARRIER),
ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY);
argsman.AddArg("-rejectbpub", strprintf("Count the witness scripts of inputs that carry a BPUB file as non-standard data carrier bytes (default: %u)", DEFAULT_REJECT_BPUB), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY);
argsman.AddArg("-acceptnonstdtxn", strprintf("Relay and mine \"non-standard\" transactions (default: %u)", DEFAULT_ACCEPT_NON_STD_TXN), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY);
argsman.AddArg("-acceptunknownwitness",
strprintf("Relay transactions sending to unknown/future witness script versions (default: %u)", DEFAULT_ACCEPTUNKNOWNWITNESS),
Expand Down Expand Up @@ -1081,6 +1082,7 @@ bool AppInitParameterInteraction(const ArgsManager& args)
if (auto parsed = args.GetFixedPointArg("-datacarriercost", 2)) {
g_weight_per_data_byte = ((*parsed * WITNESS_SCALE_FACTOR) + 99) / 100;
}
g_reject_bpub = args.GetBoolArg("-rejectbpub", DEFAULT_REJECT_BPUB);

nBytesPerSigOp = args.GetIntArg("-bytespersigop", nBytesPerSigOp);
nBytesPerSigOpStrict = args.GetIntArg("-bytespersigopstrict", nBytesPerSigOpStrict);
Expand Down
73 changes: 73 additions & 0 deletions src/policy/policy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@
#include <consensus/amount.h>
#include <consensus/consensus.h>
#include <consensus/validation.h>
#include <crypto/common.h>
#include <kernel/mempool_options.h>
#include <policy/feerate.h>
#include <primitives/transaction.h>
#include <pubkey.h>
#include <script/interpreter.h>
#include <script/script.h>
#include <script/solver.h>
Expand Down Expand Up @@ -444,6 +446,7 @@ std::pair<size_t, size_t> DatacarrierBytes(const CTransaction& tx, const CCoinsV
ret.first += dcb.first;
ret.second += dcb.second;
}
if (::g_reject_bpub) ret.second += BPUBBytes(tx);
for (size_t i{tx.vout.size()}; i; ) {
const CTxOut& txout = tx.vout[--i];
const auto dcb = txout.scriptPubKey.DatacarrierBytes(tx.vout.size() - i);
Expand All @@ -468,6 +471,7 @@ int32_t CalculateExtraTxWeight(const CTransaction& tx, const CCoinsViewCache& vi
mod_weight += int64_t(dcb.first + dcb.second) * (weight_per_data_byte - consensus_weight_per_byte);
}
}
if (::g_reject_bpub) mod_weight += int64_t(BPUBBytes(tx)) * (weight_per_data_byte - 1);
if (weight_per_data_byte > WITNESS_SCALE_FACTOR) {
for (size_t i{tx.vout.size()}; i; ) {
const CTxOut& txout = tx.vout[--i];
Expand All @@ -479,3 +483,72 @@ int32_t CalculateExtraTxWeight(const CTransaction& tx, const CCoinsViewCache& vi

return int32_t(std::min(mod_weight, int64_t{std::numeric_limits<int32_t>::max()}));
}

bool g_reject_bpub{DEFAULT_REJECT_BPUB};

static bool IsBPUBHeader(const std::vector<unsigned char>& head)
{
if (head.size() < 10) return false;
if (std::equal(head.begin(), head.begin() + 4, "BPUB")) return head[4] == 1;
if (head[4] != 4 && head[4] != 5) return false;
// The body opens with a 44 byte header and fits in a block, and DEFLATE inflates at most
// 1032 times, so the high half of its 64 bit uncompressed size is always zero.
const uint32_t body_len{ReadBE32(head.data())};
return body_len >= 44 && body_len <= MAX_BLOCK_SERIALIZED_SIZE && ReadBE32(head.data() + 6) == 0;
}

size_t BPUBBytes(const CTransaction& tx)
{
// A BPUB reader joins, in input order, each input whose witness ends in a 1-of-N multisig
// with the same last key, taking each key but the last, less its first and last bytes, and
// reads the result as a stream that opens with a header. Nothing checks those keys, since the
// last one signs first, so one reader takes pushes of any size but OP_0, and N as any opcode
// valued 0x50 + N; another takes only compressed keys and OP_N, skipping any other input.
struct Input {
std::vector<unsigned char> control, head;
bool compressed_keys;
size_t bytes;
};
std::vector<Input> inputs;
std::vector<unsigned char> push_data;
for (const CTxIn& txin : tx.vin) {
const auto& stack{txin.scriptWitness.stack};
if (stack.size() < 2) continue;
const CScript script(stack.back().begin(), stack.back().end());
CScript::const_iterator it{script.begin()};
opcodetype opcode;
if (!script.GetOp(it, opcode) || opcode != OP_1) continue;
Input input{{}, {}, true, script.size()};
size_t keys{0};
bool parsed;
while ((parsed = script.GetOp(it, opcode, push_data)) && opcode > OP_0 && opcode <= OP_PUSHDATA4) {
if (keys++ && input.head.size() < 10 && input.control.size() > 2) {
input.head.insert(input.head.end(), input.control.begin() + 1, input.control.begin() + 1 + std::min(10 - input.head.size(), input.control.size() - 2));
}
input.compressed_keys &= push_data.size() == CPubKey::COMPRESSED_SIZE;
input.control.swap(push_data);
}
if (!parsed || keys < 2 || static_cast<size_t>(opcode) != 0x50 + keys) continue;
input.compressed_keys &= keys <= 16;
if (!script.GetOp(it, opcode) || opcode != OP_CHECKMULTISIG || it != script.end()) continue;
inputs.push_back(std::move(input));
}

std::stable_sort(inputs.begin(), inputs.end(), [](const Input& a, const Input& b) { return a.control < b.control; });
size_t bytes{0};
for (auto stream{inputs.begin()}; stream != inputs.end();) {
const auto end{std::find_if(stream, inputs.end(), [&](const Input& input) { return input.control != stream->control; })};
std::vector<unsigned char> head, compressed_head;
size_t stream_bytes{0};
for (auto input{stream}; input != end; ++input) {
if (head.size() < 10) head.insert(head.end(), input->head.begin(), input->head.end());
if (input->compressed_keys && compressed_head.size() < 10) {
compressed_head.insert(compressed_head.end(), input->head.begin(), input->head.end());
}
stream_bytes += input->bytes;
}
if (IsBPUBHeader(head) || IsBPUBHeader(compressed_head)) bytes += stream_bytes;
stream = end;
}
return bytes;
}
6 changes: 6 additions & 0 deletions src/policy/policy.h
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,12 @@ static inline int64_t GetVirtualTransactionInputSize(const CTxIn& tx)
return GetVirtualTransactionInputSize(tx, 0, 0);
}

/** Default for -rejectbpub */
static constexpr bool DEFAULT_REJECT_BPUB{true};
extern bool g_reject_bpub;
/** Witness script bytes of the inputs that carry a BPUB stream. */
size_t BPUBBytes(const CTransaction& tx);

std::pair<CScript, unsigned int> GetScriptForTransactionInput(CScript prevScript, const CTxIn&);

std::pair<size_t, size_t> DatacarrierBytes(const CTransaction& tx, const CCoinsViewCache& view);
Expand Down
159 changes: 159 additions & 0 deletions src/test/transaction_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1053,4 +1053,163 @@ BOOST_AUTO_TEST_CASE(test_IsStandard)
CheckIsNotStandard(t, "dust");
}

/** Spend 1-of-N inputs whose keys carry `stream` in `chunk` bytes each, as a BPUB encoder does. */
static size_t AddBpubInputs(CMutableTransaction& tx, const std::vector<unsigned char>& stream, const size_t chunk, const size_t per_input, const std::vector<unsigned char>& control)
{
std::vector<std::vector<unsigned char>> keys;
for (size_t i{0}; i < stream.size(); i += chunk) {
std::vector<unsigned char> key(chunk + 2);
key[0] = 0x02;
std::copy_n(stream.begin() + i, std::min(chunk, stream.size() - i), key.begin() + 1);
keys.push_back(key);
}
size_t bytes{0};
for (size_t i{0}; i < keys.size(); i += per_input) {
const size_t n{std::min(per_input, keys.size() - i)};
CScript script{CScript() << OP_1};
for (size_t j{i}; j < i + n; ++j) script << keys[j];
script << control << CScript::EncodeOP_N(n + 1) << OP_CHECKMULTISIG;
tx.vin.emplace_back().scriptWitness.stack = {{}, std::vector<unsigned char>(72, 1), {script.begin(), script.end()}};
bytes += script.size();
}
return bytes;
}

BOOST_AUTO_TEST_CASE(bpub_bytes)
{
// A BPUB v5 stream, as its encoder writes it
const auto v5{ParseHex("000000ca05030000000000000180b25870e5538d2124919e1345dcb4674cab44d42456ad164038eb93f5e4f9969d006976a654abd02a15e1b2c15177f6e73f72ca9c2b339a651509ebf29bf1299d3bcf8c6df6aa2695cd184ad61eb1dd462f000a40ec28b0c273677fa0ef8b8a49540319ab20d0912a5ee05e445f98f078087d5532c24bb4c1e2704c913bcc6801ad6464d35d4fae82e95a5020a035e89da5d512011a4da91b2734737ea3cea48260b798faa33ce4aa13806859a490644a60d834964fb01829605702b9c3b3bf53")};
const std::vector<unsigned char> control(33, 3), other(33, 2);
const auto bytes_of = [&](const std::vector<unsigned char>& stream, size_t chunk, size_t per_input) {
CMutableTransaction tx;
const size_t bytes{AddBpubInputs(tx, stream, chunk, per_input, control)};
return std::pair{BPUBBytes(CTransaction{tx}), bytes};
};
const auto counted = [&](const std::vector<unsigned char>& stream, size_t chunk = 31, size_t per_input = 14) {
const auto [got, bytes] = bytes_of(stream, chunk, per_input);
return got == bytes && bytes > 0;
};
const auto ignored = [&](const std::vector<unsigned char>& stream) { return bytes_of(stream, 31, 14).first == 0; };

// As deployed: 1-of-15 of compressed keys
BOOST_CHECK(counted(v5));
// Any 1-of-N reads the same
BOOST_CHECK(counted(v5, 31, 10));
BOOST_CHECK(counted(v5, 31, 1));
// Readers take keys of any size, and the header can span inputs
BOOST_CHECK(counted(v5, 518, 6));
BOOST_CHECK(counted(v5, 3, 1));
BOOST_CHECK(counted(v5, 200000, 1));

auto stream{v5};
stream[4] = 4;
BOOST_CHECK(counted(stream));
stream = v5;
std::copy_n("BPUB\x01", 5, stream.begin());
BOOST_CHECK(counted(stream));
stream[4] = 2; // unknown legacy version
BOOST_CHECK(ignored(stream));
stream = v5;
stream[4] = 6; // unknown version
BOOST_CHECK(ignored(stream));
stream = v5;
stream[3] = 43; // body shorter than its header
BOOST_CHECK(ignored(stream));
stream[3] = 44;
BOOST_CHECK(counted(stream));
stream = v5;
stream[1] = 0x3d; stream[2] = 0x09; stream[3] = 0x01; // body larger than a block
BOOST_CHECK(ignored(stream));
stream[3] = 0x00;
BOOST_CHECK(counted(stream));
stream = v5;
stream[9] = 1; // uncompressed size no body can inflate to
BOOST_CHECK(ignored(stream));
stream = v5;
stream.insert(stream.begin(), 0); // header not where a reader looks
BOOST_CHECK(ignored(stream));

// Inputs signed by another key are another stream, so they neither hide the header nor count
CMutableTransaction tx;
AddBpubInputs(tx, std::vector<unsigned char>(62, 0), 31, 2, other);
const size_t bytes{AddBpubInputs(tx, v5, 31, 14, control)};
BOOST_CHECK_EQUAL(BPUBBytes(CTransaction{tx}), bytes);
// ...nor when interleaved with it
CMutableTransaction real, decoy, mixed;
const size_t real_bytes{AddBpubInputs(real, v5, 31, 2, control)};
AddBpubInputs(decoy, std::vector<unsigned char>(124, 0), 31, 2, other);
for (size_t i{0}; i < real.vin.size(); ++i) {
if (i < decoy.vin.size()) mixed.vin.push_back(decoy.vin[i]);
mixed.vin.push_back(real.vin[i]);
}
BOOST_CHECK_EQUAL(BPUBBytes(CTransaction{mixed}), real_bytes);

// An input only one reader takes cannot hide the header from the other
CMutableTransaction decoyed;
const CScript decoy_script{CScript() << OP_1 << std::vector<unsigned char>(34, 0) << control << OP_2 << OP_CHECKMULTISIG};
decoyed.vin.emplace_back().scriptWitness.stack = {{}, std::vector<unsigned char>(72, 1), {decoy_script.begin(), decoy_script.end()}};
const size_t decoyed_bytes{decoy_script.size() + AddBpubInputs(decoyed, v5, 31, 14, control)};
BOOST_CHECK_EQUAL(BPUBBytes(CTransaction{decoyed}), decoyed_bytes);
// Neither reader takes an input with an OP_0 key, so it cannot hide the header either
CMutableTransaction zero_key;
const CScript zero_key_script{CScript() << OP_1 << OP_0 << std::vector<unsigned char>(34, 0) << control << OP_3 << OP_CHECKMULTISIG};
zero_key.vin.emplace_back().scriptWitness.stack = {{}, std::vector<unsigned char>(72, 1), {zero_key_script.begin(), zero_key_script.end()}};
BOOST_CHECK_EQUAL(BPUBBytes(CTransaction{zero_key}), 0U);
const size_t zero_key_bytes{AddBpubInputs(zero_key, v5, 3, 1, control)};
BOOST_CHECK_EQUAL(BPUBBytes(CTransaction{zero_key}), zero_key_bytes);
// One reader takes N as any opcode valued 0x50 + N, so 17 keys end in OP_NOP
CScript nop_script{CScript() << OP_1};
for (size_t i{0}; i < 16; ++i) {
std::vector<unsigned char> key(13);
key[0] = 0x02;
std::copy_n(v5.begin() + 12 * i, 12, key.begin() + 1);
nop_script << key;
}
nop_script << std::vector<unsigned char>{0x11} << OP_NOP << OP_CHECKMULTISIG;
CMutableTransaction nop;
nop.vin.emplace_back().scriptWitness.stack = {{}, std::vector<unsigned char>(72, 1), {nop_script.begin(), nop_script.end()}};
BOOST_CHECK_EQUAL(BPUBBytes(CTransaction{nop}), nop_script.size());
nop.vin[0].scriptWitness.stack.back()[nop_script.size() - 2] = OP_16;
BOOST_CHECK_EQUAL(BPUBBytes(CTransaction{nop}), 0U);
// A key count the script does not have, or a witness that is only the script, is not BPUB
CMutableTransaction miscounted;
AddBpubInputs(miscounted, v5, 31, 14, control);
auto& miscounted_script{miscounted.vin[0].scriptWitness.stack.back()};
miscounted_script[miscounted_script.size() - 2] = OP_14;
BOOST_CHECK_EQUAL(BPUBBytes(CTransaction{miscounted}), 0U);
CMutableTransaction bare;
AddBpubInputs(bare, v5, 31, 14, control);
for (auto& txin : bare.vin) txin.scriptWitness.stack.erase(txin.scriptWitness.stack.begin(), txin.scriptWitness.stack.end() - 1);
BOOST_CHECK_EQUAL(BPUBBytes(CTransaction{bare}), 0U);

// It counts as non-standard datacarrier, and as data for -datacarriercost
CCoinsView coins_dummy;
CCoinsViewCache coins{&coins_dummy};
for (uint32_t i{0}; i < tx.vin.size(); ++i) {
tx.vin[i].prevout = COutPoint{Txid::FromUint256(uint256::ONE), i};
const auto& script{tx.vin[i].scriptWitness.stack.back()};
coins.AddCoin(tx.vin[i].prevout, Coin{CTxOut{1000, GetScriptForDestination(WitnessV0ScriptHash{CScript(script.begin(), script.end())})}, 1, false}, false);
}
BOOST_CHECK_EQUAL(DatacarrierBytes(CTransaction{tx}, coins).second, bytes);
BOOST_CHECK_EQUAL(CalculateExtraTxWeight(CTransaction{tx}, coins, 4), int32_t(bytes * 3));
// -rejectbpub=0 counts none of it
g_reject_bpub = false;
BOOST_CHECK_EQUAL(DatacarrierBytes(CTransaction{tx}, coins).second, 0U);
BOOST_CHECK_EQUAL(CalculateExtraTxWeight(CTransaction{tx}, coins, 4), 0);
g_reject_bpub = true;

// Other multisig, and other spends, are not BPUB
for (const opcodetype m : {OP_1, OP_2}) {
CMutableTransaction honest;
const CScript multisig{CScript() << m << other << control << std::vector<unsigned char>(33, 4) << OP_3 << OP_CHECKMULTISIG};
honest.vin.emplace_back().scriptWitness.stack = {{}, std::vector<unsigned char>(72, 1), {multisig.begin(), multisig.end()}};
honest.vin.emplace_back().scriptWitness.stack = {std::vector<unsigned char>(72, 1), control};
BOOST_CHECK_EQUAL(BPUBBytes(CTransaction{honest}), 0U);
}
CMutableTransaction two_of_n;
AddBpubInputs(two_of_n, v5, 31, 14, control);
for (auto& txin : two_of_n.vin) txin.scriptWitness.stack.back()[0] = OP_2;
BOOST_CHECK_EQUAL(BPUBBytes(CTransaction{two_of_n}), 0U);
}

BOOST_AUTO_TEST_SUITE_END()