upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/rpc.cpp101
1 files changed, 99 insertions, 2 deletions
diff --git a/src/rpc.cpp b/src/rpc.cpp
index 310c0d4..be6c80d 100644
--- a/src/rpc.cpp
+++ b/src/rpc.cpp
@@ -776,6 +776,29 @@ static void PublishPendingPayouts(const VersionedBlob& v)
776 gPayoutLedgerPublishedSeq, "pending payout ledger"); 776 gPayoutLedgerPublishedSeq, "pending payout ledger");
777} 777}
778 778
779// Parse a persisted block-hash field, refusing to guess. SetHex() reads
780// whatever hex prefix it finds and stops at the first character it does not
781// recognise, so a corrupt value does not fail to parse -- it parses as a
782// DIFFERENT hash. Both shapes of that are wrong in a dangerous direction: a
783// value with a hex-looking prefix ("deadbeef...") becomes a hash no block
784// ever had, which PayoutBlockStillOnChain reads as an orphaned block and
785// FlushMaturePayouts then VOIDS the debt; a value with no hex prefix at all
786// becomes 0, which is grandfathered past the orphan check on the strength
787// of garbage. A field that is not the 64-digit hex string ToString() wrote
788// is corruption, and corruption is a load failure like any other parse
789// error -- the file is quarantined, not read around.
790static uint256 ParsePayoutBlockHash(const std::string& str)
791{
792 if (str.size() != 64 ||
793 str.find_first_not_of("0123456789abcdefABCDEF") != std::string::npos)
794 throw std::runtime_error(
795 strprintf("blockHash \"%s\" is not a 64-digit hex string",
796 str.c_str()));
797 uint256 h;
798 h.SetHex(str);
799 return h;
800}
801
779static void LoadPendingPayouts() 802static void LoadPendingPayouts()
780{ 803{
781 std::string path = PayoutFilePath(); 804 std::string path = PayoutFilePath();
@@ -797,7 +820,8 @@ static void LoadPendingPayouts()
797 // field. Leave blockHash zero and they are grandfathered past the 820 // field. Leave blockHash zero and they are grandfathered past the
798 // orphan check rather than voided on a guess. 821 // orphan check rather than voided on a guess.
799 if (obj.contains("blockHash") && obj["blockHash"].is_string()) 822 if (obj.contains("blockHash") && obj["blockHash"].is_string())
800 pp.blockHash.SetHex(obj["blockHash"].get<std::string>()); 823 pp.blockHash =
824 ParsePayoutBlockHash(obj["blockHash"].get<std::string>());
801 if (obj.contains("blockHeight") && obj["blockHeight"].is_number()) 825 if (obj.contains("blockHeight") && obj["blockHeight"].is_number())
802 pp.blockHeight = obj["blockHeight"].get<int>(); 826 pp.blockHeight = obj["blockHeight"].get<int>();
803 // Upstream's tolerance for an entry with no usable height, kept 827 // Upstream's tolerance for an entry with no usable height, kept
@@ -976,7 +1000,8 @@ static void LoadPaying()
976 r.amount = obj["amount"].get<int64>(); 1000 r.amount = obj["amount"].get<int64>();
977 r.matureAtHeight = obj["matureAtHeight"].get<int>(); 1001 r.matureAtHeight = obj["matureAtHeight"].get<int>();
978 if (obj.contains("blockHash") && obj["blockHash"].is_string()) 1002 if (obj.contains("blockHash") && obj["blockHash"].is_string())
979 r.blockHash.SetHex(obj["blockHash"].get<std::string>()); 1003 r.blockHash =
1004 ParsePayoutBlockHash(obj["blockHash"].get<std::string>());
980 if (obj.contains("blockHeight") && obj["blockHeight"].is_number()) 1005 if (obj.contains("blockHeight") && obj["blockHeight"].is_number())
981 r.blockHeight = obj["blockHeight"].get<int>(); 1006 r.blockHeight = obj["blockHeight"].get<int>();
982 if (obj.contains("retries") && obj["retries"].is_number()) 1007 if (obj.contains("retries") && obj["retries"].is_number())
@@ -4681,6 +4706,78 @@ int RunPayoutLedgerSelfTest()
4681 strPoolRoundsFile = savedRoundsFile; 4706 strPoolRoundsFile = savedRoundsFile;
4682 } 4707 }
4683 4708
4709 // -- 16. A malformed block hash is corruption, not a hash ----------------
4710 //
4711 // SetHex() parses whatever hex prefix it finds and stops, so a corrupt
4712 // blockHash did not fail to load -- it loaded as a DIFFERENT hash.
4713 // "deadbeef..." became 0xdeadbeef, a hash no block ever had, which the
4714 // orphan check reads as an orphaned block and the next flush VOIDS the
4715 // debt; a value with no hex prefix became 0, which is grandfathered past
4716 // the orphan check instead. Either way a corrupt file silently changed
4717 // what the pool owed. It is a load failure now, handled like every other
4718 // parse error: quarantine, and never rewrite.
4719 {
4720 PayoutTestResetState();
4721 remove(ledgerPath.c_str());
4722 remove(payingPath.c_str());
4723
4724 const std::string badLedger = strprintf(
4725 "[{\"amounts\":{\"%s\":100000000},"
4726 "\"blockHash\":\"deadbeef oops, not a hash\","
4727 "\"blockHeight\":10,\"matureAtHeight\":10}]", addrA.c_str());
4728 check(PayoutTestWriteFile(ledgerPath, badLedger),
4729 "wrote a ledger whose blockHash is not a 64-digit hex string");
4730 LoadPendingPayouts();
4731 {
4732 std::lock_guard<std::mutex> lk(gPayoutLedgerMutex);
4733 check(gPendingPayouts.empty(),
4734 "no entry is loaded with a guessed hash");
4735 }
4736 std::string gone;
4737 check(!PayoutTestReadFile(ledgerPath, gone),
4738 "the ledger with the corrupt hash is quarantined");
4739
4740 const std::string badPaying = strprintf(
4741 "[{\"address\":\"%s\",\"amount\":100000000,"
4742 "\"blockHash\":\"00000000cafe\",\"blockHeight\":10,"
4743 "\"matureAtHeight\":10}]", addrA.c_str());
4744 check(PayoutTestWriteFile(payingPath, badPaying),
4745 "wrote an in-flight list whose blockHash is truncated");
4746 LoadPaying();
4747 {
4748 std::lock_guard<std::mutex> lk(gPayoutExecMutex);
4749 check(gPaying.empty(),
4750 "no in-flight record is loaded with a guessed hash");
4751 }
4752 check(!PayoutTestReadFile(payingPath, gone),
4753 "the in-flight list with the corrupt hash is quarantined");
4754
4755 // The two spellings every real file uses still load: the 64-digit
4756 // hash of a keyed round, and the 64 zeros this build writes for a
4757 // grandfathered entry.
4758 PayoutTestResetState();
4759 remove(ledgerPath.c_str());
4760 const std::string goodLedger = strprintf(
4761 "[{\"amounts\":{\"%s\":100000000},"
4762 "\"blockHash\":\"00000000000000000000000000000000000000000000000000000000000000cd\","
4763 "\"blockHeight\":10,\"matureAtHeight\":10},"
4764 "{\"amounts\":{\"%s\":200000000},"
4765 "\"blockHash\":\"%s\","
4766 "\"blockHeight\":20,\"matureAtHeight\":20}]",
4767 addrA.c_str(), addrB.c_str(), uint256(0).ToString().c_str());
4768 check(PayoutTestWriteFile(ledgerPath, goodLedger),
4769 "wrote a ledger in both real hash spellings");
4770 LoadPendingPayouts();
4771 {
4772 std::lock_guard<std::mutex> lk(gPayoutLedgerMutex);
4773 check(gPendingPayouts.size() == 2 &&
4774 gPendingPayouts[0].blockHash != 0 &&
4775 gPendingPayouts[1].blockHash == 0,
4776 "a real hash and a serialised zero hash both still load");
4777 }
4778 remove(ledgerPath.c_str());
4779 }
4780
4684 // -- clean up ------------------------------------------------------------ 4781 // -- clean up ------------------------------------------------------------
4685 PayoutTestResetState(); 4782 PayoutTestResetState();
4686 CRITICAL_BLOCK(cs_mapWallet) 4783 CRITICAL_BLOCK(cs_mapWallet)