171 lines
7.7 KiB
C++
171 lines
7.7 KiB
C++
// Offline NRB key generation and transparent transaction signing.
|
|
// Build with the same flags and libraries as nerbus-tx. No RPC or node startup.
|
|
#define main NerbusLegacyTransactionMain
|
|
#include "nerbus-original-tx.inc"
|
|
#undef main
|
|
#include <sodium.h>
|
|
#include <set>
|
|
|
|
static const int64_t WALLET_MAX_MONEY = 209000000LL * COIN;
|
|
|
|
static CAmount WalletAmount(const UniValue &v)
|
|
{
|
|
if (!v.isStr()) throw std::runtime_error("Amount must be a decimal string");
|
|
CAmount n;
|
|
if (!ParseMoney(v.get_str(), n) || n < 0 || n > WALLET_MAX_MONEY)
|
|
throw std::runtime_error("Invalid NRB amount");
|
|
return n;
|
|
}
|
|
|
|
static UniValue WalletKey(const CKey &key)
|
|
{
|
|
if (!key.IsValid() || !key.IsCompressed())
|
|
throw std::runtime_error("A compressed NRB private key is required");
|
|
UniValue result(UniValue::VOBJ);
|
|
result.pushKV("address", EncodeDestination(key.GetPubKey().GetID()));
|
|
result.pushKV("privateKey", EncodeSecret(key));
|
|
return result;
|
|
}
|
|
|
|
static UniValue WalletPayment(const UniValue &request)
|
|
{
|
|
const auto &inputs = request["inputs"];
|
|
const auto &keys = request["privateKeys"];
|
|
if (!inputs.isArray() || inputs.empty() || inputs.size() > 100 ||
|
|
!keys.isArray() || keys.empty() || keys.size() > 1000)
|
|
throw std::runtime_error("Invalid payment inputs");
|
|
int height = request["height"].get_int();
|
|
if (height < 1 || height > 400000000)
|
|
throw std::runtime_error("Invalid chain height");
|
|
CAmount amount = WalletAmount(request["amount"]);
|
|
CAmount fee = WalletAmount(request["fee"]);
|
|
if (amount <= 0 || fee < 10000 || fee > COIN)
|
|
throw std::runtime_error("Invalid payment amount or fee");
|
|
CTxDestination to = DecodeDestination(request["to"].get_str());
|
|
CTxDestination change = DecodeDestination(request["change"].get_str());
|
|
if (!boost::get<CKeyID>(&to) || !boost::get<CKeyID>(&change))
|
|
throw std::runtime_error("A transparent NRB address is required");
|
|
CBasicKeyStore keystore;
|
|
for (size_t i = 0; i < keys.size(); ++i) {
|
|
CKey key = DecodeSecret(keys[i].get_str());
|
|
if (!key.IsValid() || !key.IsCompressed())
|
|
throw std::runtime_error("Invalid NRB private key");
|
|
keystore.AddKey(key);
|
|
}
|
|
if (!keystore.HaveKey(*boost::get<CKeyID>(&change)))
|
|
throw std::runtime_error("Change address is not owned by this wallet");
|
|
|
|
CMutableTransaction tx;
|
|
tx.fOverwintered = true;
|
|
tx.nVersion = SAPLING_TX_VERSION;
|
|
tx.nVersionGroupId = SAPLING_VERSION_GROUP_ID;
|
|
tx.nExpiryHeight = height + 40;
|
|
tx.nLockTime = 0;
|
|
CAmount total = 0;
|
|
std::vector<CTxOut> previous;
|
|
std::set<std::pair<uint256, unsigned int>> seen;
|
|
for (size_t i = 0; i < inputs.size(); ++i) {
|
|
const auto &in = inputs[i];
|
|
std::string raw = in["raw"].get_str();
|
|
if (raw.size() > 4000000) throw std::runtime_error("Previous transaction is too large");
|
|
CTransaction prev;
|
|
if (!DecodeHexTx(prev, raw)) throw std::runtime_error("Invalid previous transaction");
|
|
uint256 txid = ParseHashUV(in["txid"], "txid");
|
|
int index = in["vout"].get_int();
|
|
if (prev.GetHash() != txid || index < 0 || (size_t)index >= prev.vout.size())
|
|
throw std::runtime_error("Previous transaction does not match the selected input");
|
|
if (!seen.emplace(txid, (unsigned int)index).second)
|
|
throw std::runtime_error("Duplicate payment input");
|
|
const CTxOut &out = prev.vout[index];
|
|
CTxDestination dest;
|
|
if (!ExtractDestination(out.scriptPubKey, dest) || !boost::get<CKeyID>(&dest) ||
|
|
out.scriptPubKey != GetScriptForDestination(dest) ||
|
|
!keystore.HaveKey(*boost::get<CKeyID>(&dest)))
|
|
throw std::runtime_error("Input is not a standard address owned by this wallet");
|
|
if (out.nValue != WalletAmount(in["amount"]) || out.nValue <= 0 ||
|
|
total > WALLET_MAX_MONEY - out.nValue)
|
|
throw std::runtime_error("Previous output amount does not match");
|
|
total += out.nValue;
|
|
previous.push_back(out);
|
|
tx.vin.emplace_back(txid, (unsigned int)index);
|
|
}
|
|
if (total < amount + fee) throw std::runtime_error("Insufficient funds");
|
|
CAmount changeAmount = total - amount - fee;
|
|
if (changeAmount != 0 && changeAmount < 1000)
|
|
throw std::runtime_error("Change is too small; adjust the amount or fee");
|
|
tx.vout.emplace_back(amount, GetScriptForDestination(to));
|
|
if (changeAmount) tx.vout.emplace_back(changeAmount, GetScriptForDestination(change));
|
|
const auto branch = CurrentEpochBranchId(height, Params().GetConsensus());
|
|
for (size_t i = 0; i < previous.size(); ++i) {
|
|
SignatureData sig;
|
|
const auto &prev = previous[i];
|
|
if (!ProduceSignature(MutableTransactionSignatureCreator(&keystore, &tx, i,
|
|
prev.nValue, prev.scriptPubKey, height, SIGHASH_ALL),
|
|
prev.scriptPubKey, sig, branch))
|
|
throw std::runtime_error("Unable to sign payment input");
|
|
UpdateTransaction(tx, i, sig);
|
|
if (!VerifyScript(tx.vin[i].scriptSig, prev.scriptPubKey, STANDARD_SCRIPT_VERIFY_FLAGS,
|
|
MutableTransactionSignatureChecker(&tx, i, prev.nValue), branch))
|
|
throw std::runtime_error("Payment signature verification failed");
|
|
}
|
|
CTransaction completed(tx);
|
|
UniValue result(UniValue::VOBJ);
|
|
result.pushKV("hex", EncodeHexTx(completed));
|
|
result.pushKV("txid", completed.GetHash().GetHex());
|
|
result.pushKV("fee", FormatMoney(fee));
|
|
result.pushKV("amount", FormatMoney(amount));
|
|
result.pushKV("change", FormatMoney(changeAmount));
|
|
return result;
|
|
}
|
|
|
|
int main(int argc, char *argv[])
|
|
{
|
|
// The only supported interface is a single bounded JSON request on stdin.
|
|
// Private keys never appear in command-line arguments or temporary files.
|
|
if (argc != 2 || std::string(argv[1]) != "-wallet-json") return EXIT_FAILURE;
|
|
try {
|
|
SetupEnvironment();
|
|
ParseParameters(argc, argv);
|
|
SelectParams(CBaseChainParams::MAIN);
|
|
if (sodium_init() < 0) throw std::runtime_error("Secure randomness is unavailable");
|
|
Secp256k1Init ellipticCurve;
|
|
std::string raw;
|
|
char buffer[4096];
|
|
while (!feof(stdin)) {
|
|
size_t n = fread(buffer, 1, sizeof(buffer), stdin);
|
|
if (raw.size() + n > 16 * 1024 * 1024)
|
|
throw std::runtime_error("Request is too large");
|
|
raw.append(buffer, n);
|
|
if (n < sizeof(buffer)) break;
|
|
}
|
|
if (ferror(stdin)) throw std::runtime_error("Unable to read request");
|
|
UniValue request;
|
|
if (!request.read(raw) || !request.isObject())
|
|
throw std::runtime_error("Invalid request");
|
|
sodium_memzero(&raw[0], raw.size());
|
|
sodium_memzero(buffer, sizeof(buffer));
|
|
UniValue result;
|
|
std::string action = request["action"].get_str();
|
|
if (action == "newKey") {
|
|
CKey key;
|
|
key.MakeNewKey(true);
|
|
result = WalletKey(key);
|
|
} else if (action == "importKey") {
|
|
result = WalletKey(DecodeSecret(request["privateKey"].get_str()));
|
|
} else if (action == "payment") {
|
|
result = WalletPayment(request);
|
|
} else throw std::runtime_error("Unknown wallet operation");
|
|
std::string response = result.write();
|
|
fwrite(response.data(), 1, response.size(), stdout);
|
|
fputc('\n', stdout);
|
|
fflush(stdout);
|
|
sodium_memzero(&response[0], response.size());
|
|
return EXIT_SUCCESS;
|
|
} catch (const std::exception &e) {
|
|
UniValue error(UniValue::VOBJ);
|
|
error.pushKV("error", e.what());
|
|
fprintf(stdout, "%s\n", error.write().c_str());
|
|
return EXIT_FAILURE;
|
|
}
|
|
}
|