-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathassembler.cpp
More file actions
248 lines (220 loc) · 11.5 KB
/
Copy pathassembler.cpp
File metadata and controls
248 lines (220 loc) · 11.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
// assembler: combines ring.json (decoy_tool output, supports multiple
// "sources", PUBLIC data) with selected.json (select_transfers output,
// the offline side's own private fields: real_out_tx_key,
// real_output_in_tx_index, amount - PRIVATE, never comes from LoRa) and
// signs the transaction using the real, unmodified wallet2 function.
// Supports multiple inputs (multiple tx_source_entry).
#include "cryptonote_core/cryptonote_tx_utils.h"
#include "cryptonote_basic/account.h"
#include "wallet/wallet2.h"
#include "storages/portable_storage.h"
#include "string_tools.h"
#include <fstream>
#include <sstream>
#include <cstring>
#include <iostream>
#include <nlohmann/json.hpp>
using namespace cryptonote;
using json = nlohmann::json;
static crypto::public_key hex2pub(const std::string& h) {
crypto::public_key p; epee::string_tools::hex_to_pod(h, p); return p;
}
static rct::key hex2key(const std::string& h) {
rct::key k; epee::string_tools::hex_to_pod(h, k); return k;
}
int main(int argc, char** argv) {
el::Loggers::setLoggingLevel(el::Level::Global);
mlog_set_log_level(4);
if (argc != 7) {
std::cerr << "usage: assembler <wallet_file> <password> <ring.json> <selected.json> <send_amount> <dest_address>\n"
" - ring.json: decoy_tool output (PUBLIC, may contain multiple 'sources')\n"
" - selected.json: select_transfers output (PRIVATE: real_out_tx_key, real_output_in_tx_index, amount)\n"
" - send_amount: the amount YOU want to send (atomic units)\n";
return 1;
}
std::string wallet_file = argv[1], password = argv[2];
std::ifstream rf(argv[3]); std::stringstream rss; rss << rf.rdbuf();
json rj = json::parse(rss.str());
std::ifstream sf(argv[4]); std::stringstream sss; sss << sf.rdbuf();
json sj = json::parse(sss.str());
uint64_t send_amount = std::stoull(argv[5]);
std::string dest_address_str = argv[6];
// --- open the real wallet file (real, unmodified wallet2::load) ---
tools::wallet2 wallet(cryptonote::STAGENET, 1, true /* unattended */);
wallet.load(wallet_file, password);
const cryptonote::account_keys& keys = wallet.get_account().get_keys();
// For each output in selected.json, find the matching source in
// ring.json and build a tx_source_entry using the key/mask we
// compute ourselves.
std::vector<tx_source_entry> sources;
uint64_t total_amount = 0;
std::unordered_map<crypto::public_key, subaddress_index> subaddr;
subaddr[keys.m_account_address.m_spend_public_key] = {0, 0};
for (auto& sel : sj["selected"]) {
uint64_t global_index = sel["global_index"].get<uint64_t>();
uint64_t amount = sel["amount"].get<uint64_t>();
crypto::public_key real_out_tx_key = hex2pub(sel["real_out_tx_key"].get<std::string>());
uint64_t real_output_in_tx_index = sel["real_output_in_tx_index"].get<uint64_t>();
// find the matching source in ring.json's sources array by global_index
json* matched_source = nullptr;
for (auto& src_j : rj["sources"]) {
if (src_j["real_output_global_index"].get<uint64_t>() == global_index) {
matched_source = &src_j; break;
}
}
if (!matched_source) {
std::cerr << "ERROR: no matching source in ring.json for global_index=" << global_index << "\n";
return 1;
}
uint32_t subaddr_major = sel.value("subaddr_major", 0u);
uint32_t subaddr_minor = sel.value("subaddr_minor", 0u);
cryptonote::account_public_address spend_addr =
(subaddr_major == 0 && subaddr_minor == 0)
? keys.m_account_address
: wallet.get_subaddress({subaddr_major, subaddr_minor});
subaddr[spend_addr.m_spend_public_key] = {subaddr_major, subaddr_minor};
crypto::key_derivation D;
crypto::generate_key_derivation(real_out_tx_key, keys.m_view_secret_key, D);
crypto::public_key P;
crypto::derive_public_key(D, real_output_in_tx_index, spend_addr.m_spend_public_key, P);
crypto::ec_scalar shared_secret;
crypto::derivation_to_scalar(D, real_output_in_tx_index, shared_secret);
rct::key shared_secret_key; memcpy(shared_secret_key.bytes, &shared_secret, 32);
rct::key blind = rct::genCommitmentMask(shared_secret_key);
rct::key real_commitment = rct::commit(amount, blind);
tx_source_entry src;
src.amount = amount;
src.rct = true;
src.mask = blind;
src.real_output = (*matched_source)["real_output_position_in_ring"].get<size_t>();
src.real_out_tx_key = real_out_tx_key;
src.real_output_in_tx_index = real_output_in_tx_index;
for (auto& m : (*matched_source)["ring"]) {
uint64_t idx = m["global_index"].get<uint64_t>();
bool is_real = (idx == global_index);
rct::key pk = is_real ? rct::pk2rct(P) : hex2key(m["public_key"].get<std::string>());
rct::key commitment = is_real ? real_commitment : hex2key(m["commitment"].get<std::string>());
// Same defense as wallet2::tx_add_fake_output: check that
// every pk/commitment (whether it came from the node or was
// computed by us) actually lies in the main elliptic curve
// subgroup, before signing. A malicious or broken node
// injecting an invalid point gets caught here instead of
// silently making it into the signature.
if (!rct::isInMainSubgroup(pk) || !rct::isInMainSubgroup(commitment)) {
std::cerr << "ERROR: ring member is not in the main subgroup (global_index="
<< idx << ", real_output_global_index=" << global_index << "). "
<< "The node may be returning untrusted data; aborting.\n";
return 1;
}
src.outputs.push_back({idx, {pk, commitment}});
}
sources.push_back(src);
total_amount += amount;
std::cerr << "Added source: global_index=" << global_index << ", amount=" << amount << "\n";
}
cryptonote::address_parse_info addr_info;
if (!cryptonote::get_account_address_from_str(addr_info, cryptonote::STAGENET, dest_address_str)) {
std::cerr << "invalid destination address\n"; return 1;
}
// Fee estimate: fee_per_byte (from get_fee_estimate, fetched on the
// online side) times an estimated weight. Weight grows with input
// count (each extra input adds its own ring/CLSAG/pseudoOut) -
// roughly +900 bytes per input.
const uint64_t BASE_WEIGHT = 1500, PER_EXTRA_INPUT_WEIGHT = 900;
uint64_t estimated_weight = BASE_WEIGHT + (sources.size() > 1 ? (sources.size() - 1) * PER_EXTRA_INPUT_WEIGHT : 0);
uint64_t fee_per_byte = rj.value("fee_per_byte", 20000ULL);
uint64_t qmask = rj.value("fee_quantization_mask", 1ULL);
uint64_t FEE = fee_per_byte * estimated_weight;
if (qmask > 1) FEE = ((FEE + qmask - 1) / qmask) * qmask;
std::cerr << "Estimated fee: " << FEE << " atomic units (fee_per_byte=" << fee_per_byte
<< ", " << sources.size() << " input(s), estimated weight=" << estimated_weight << ")\n";
if (total_amount <= FEE) { std::cerr << "total amount is less than or equal to the fee\n"; return 1; }
uint64_t total_out = total_amount - FEE;
if (send_amount > total_out) {
std::cerr << "ERROR: send_amount (" << send_amount << ") exceeds what's available after fee (" << total_out << ").\n";
return 1;
}
uint64_t change_amount = total_out - send_amount;
tx_destination_entry dst1;
dst1.amount = send_amount;
dst1.addr = addr_info.address;
dst1.is_subaddress = addr_info.is_subaddress;
tx_destination_entry dst2;
dst2.amount = change_amount;
dst2.addr = keys.m_account_address;
dst2.is_subaddress = false;
std::vector<tx_destination_entry> destinations{dst1, dst2};
transaction tx;
crypto::secret_key tx_key;
std::vector<crypto::secret_key> additional_tx_keys;
const boost::optional<cryptonote::account_public_address> change_addr = keys.m_account_address;
bool ok = construct_tx_and_get_tx_key(
keys, subaddr, sources, destinations, change_addr, {}, tx, tx_key,
additional_tx_keys, true, {rct::RangeProofPaddedBulletproof, 4}, true);
// --- two-pass fee correction ---
// The first pass was built with an estimated weight (the static FEE
// above). Now we measure the ACTUAL built transaction's weight,
// compute the real fee from that, and if it differs, rebuild once
// more with the corrected change amount. sources gets sorted
// deterministically by key image inside construct_tx_with_tx_key
// (not shuffled), so calling it again with the same sources is
// safe. destinations, on the other hand, genuinely gets shuffled
// in place (std::shuffle, by reference) inside the function - that's
// why we look up the change entry by address rather than by index.
if (ok) {
std::stringstream first_pass_ss;
binary_archive<true> first_pass_ar(first_pass_ss);
::serialization::serialize(first_pass_ar, tx);
size_t blob_size = first_pass_ss.str().size();
uint64_t real_weight = cryptonote::get_transaction_weight(tx, blob_size);
uint64_t needed_fee = real_weight * fee_per_byte;
if (qmask > 1) needed_fee = ((needed_fee + qmask - 1) / qmask) * qmask;
std::cerr << "First-pass fee: " << FEE << ", actual weight: " << real_weight
<< ", actual fee needed: " << needed_fee << "\n";
if (needed_fee != FEE) {
if (total_amount <= needed_fee) {
std::cerr << "ERROR: total amount is insufficient after the real fee.\n";
return 1;
}
uint64_t new_total_out = total_amount - needed_fee;
if (send_amount > new_total_out) {
std::cerr << "ERROR: send_amount exceeds what's available after the real fee.\n";
return 1;
}
uint64_t new_change_amount = new_total_out - send_amount;
for (auto& d : destinations) {
if (!d.is_subaddress && d.addr == keys.m_account_address)
d.amount = new_change_amount;
}
transaction tx2;
crypto::secret_key tx_key2;
std::vector<crypto::secret_key> additional_tx_keys2;
bool ok2 = construct_tx_and_get_tx_key(
keys, subaddr, sources, destinations, change_addr, {}, tx2, tx_key2,
additional_tx_keys2, true, {rct::RangeProofPaddedBulletproof, 4}, true);
if (!ok2) {
std::cerr << "ERROR: rebuilding the transaction after the fee correction failed.\n";
return 1;
}
tx = tx2;
FEE = needed_fee;
ok = ok2;
}
}
std::cerr << "success: " << ok << std::endl;
if (ok) {
std::cerr << "tx version: " << tx.version << ", vin: " << tx.vin.size()
<< ", vout: " << tx.vout.size()
<< ", rct type: " << (int)tx.rct_signatures.type
<< ", fee: " << FEE << std::endl;
std::stringstream txss;
binary_archive<true> ar(txss);
::serialization::serialize(ar, tx);
std::string hexstr = epee::string_tools::buff_to_hex_nodelimer(txss.str());
std::ofstream out("signed_tx.hex", std::ios::trunc);
out << hexstr;
out.close();
std::cerr << "Signed tx written to signed_tx.hex (" << hexstr.size() << " hex chars)\n";
}
return ok ? 0 : 1;
}