Files
nerbus-wallet/core/nerbus-wallet-core.cpp

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;
}
}