From 84c8db7756e2c37f97ea7381aca011e8068007dc Mon Sep 17 00:00:00 2001 From: Paul Shapiro Date: Thu, 23 Aug 2018 19:25:52 -0400 Subject: [PATCH] wallet and fake addr for rct tx method integrations; moved estimatedRctSize; moved tx parsing fns out of cryptonote_utils --- cryptonote_utils/cryptonote_utils.js | 251 +++++++++++++--------- monero_utils/monero_sendingFunds_utils.js | 52 ++++- monero_utils/monero_txParsing_utils.js | 55 ++++- monero_utils/monero_wallet_utils.js | 204 ++++++------------ 4 files changed, 312 insertions(+), 250 deletions(-) diff --git a/cryptonote_utils/cryptonote_utils.js b/cryptonote_utils/cryptonote_utils.js index edad47e..94543e3 100644 --- a/cryptonote_utils/cryptonote_utils.js +++ b/cryptonote_utils/cryptonote_utils.js @@ -127,6 +127,10 @@ var cnUtil = function(currencyConfig) } throw "ret_val_boolstring_to_bool given illegal input" } + function api_safe_wordset_name(wordset_name) + { + return wordset_name.charAt(0).toUpperCase() + wordset_name.substr(1) // capitalizes first letter + } // var config = {}; // shallow copy of initConfig for (var key in currencyConfig) { @@ -149,6 +153,50 @@ var cnUtil = function(currencyConfig) return mnemonic.mn_random(64); }; + this.is_subaddress = function(addr, nettype) { + const args = + { + address: addr, + nettype_string: nettype_utils.nettype_to_API_string(nettype) + }; + const args_str = JSON.stringify(args); + const CNCrypto = loaded_CNCrypto(); + const ret_string = CNCrypto.is_subaddress(args_str); + const ret = JSON.parse(ret_string); + if (typeof ret.err_msg !== 'undefined' && ret.err_msg) { + throw ret.err_msg // TODO: maybe return this somehow + } + return ret_val_boolstring_to_bool(ret.retVal); + }; + + this.is_integrated_address = function(addr, nettype) { + const args = + { + address: addr, + nettype_string: nettype_utils.nettype_to_API_string(nettype) + }; + const args_str = JSON.stringify(args); + const CNCrypto = loaded_CNCrypto(); + const ret_string = CNCrypto.is_integrated_address(args_str); + const ret = JSON.parse(ret_string); + if (typeof ret.err_msg !== 'undefined' && ret.err_msg) { + throw ret.err_msg // TODO: maybe return this somehow + } + return ret_val_boolstring_to_bool(ret.retVal); + }; + + this.new_payment_id = function() { + const args = {}; + const args_str = JSON.stringify(args); + const CNCrypto = loaded_CNCrypto(); + const ret_string = CNCrypto.new_payment_id(args_str); + const ret = JSON.parse(ret_string); + if (typeof ret.err_msg !== 'undefined' && ret.err_msg) { + throw ret.err_msg // TODO: maybe return this somehow + } + return ret.retVal; + }; + this.new__int_addr_from_addr_and_short_pid = function( address, short_pid, @@ -174,9 +222,17 @@ var cnUtil = function(currencyConfig) return ret.retVal; }; - this.create_address = function(seed, nettype) - { - // TODO: + this.new_fake_address_for_rct_tx = function(nettype) + { // TODO: possibly support sending random_scalar from JS to emscripten to avoid emscripten random + const args = { nettype_string: nettype_utils.nettype_to_API_string(nettype) }; + const args_str = JSON.stringify(args); + const CNCrypto = loaded_CNCrypto(); + const ret_string = CNCrypto.new_fake_address_for_rct_tx(args_str); + const ret = JSON.parse(ret_string); + if (typeof ret.err_msg !== 'undefined' && ret.err_msg) { + throw ret.err_msg // TODO: maybe return this somehow + } + return ret.retVal; }; this.decode_address = function(address, nettype) @@ -201,20 +257,106 @@ var cnUtil = function(currencyConfig) } }; - this.is_subaddress = function(addr, nettype) { + this.newly_created_wallet = function( + wordset_name, + nettype + ) { const args = { - address: addr, + wordset_name: api_safe_wordset_name(wordset_name), nettype_string: nettype_utils.nettype_to_API_string(nettype) }; const args_str = JSON.stringify(args); const CNCrypto = loaded_CNCrypto(); - const ret_string = CNCrypto.is_subaddress(args_str); + const ret_string = CNCrypto.newly_created_wallet(args_str); const ret = JSON.parse(ret_string); if (typeof ret.err_msg !== 'undefined' && ret.err_msg) { throw ret.err_msg // TODO: maybe return this somehow } - return ret_val_boolstring_to_bool(ret.retVal) + return { // calling these out so as to provide a stable ret val interface + mnemonic_string: ret.mnemonic_string, + sec_seed_string: ret.sec_seed_string, + address_string: ret.address_string, + pub_viewKey_string: ret.pub_viewKey_string, + sec_viewKey_string: ret.sec_viewKey_string, + pub_spendKey_string: ret.pub_spendKey_string, + sec_spendKey_string: ret.sec_spendKey_string + }; + }; + + this.mnemonic_from_seed = function( + seed_string, + wordset_name + ) { + const args = + { + seed_string: seed_string, + wordset_name: api_safe_wordset_name(wordset_name) + }; + const args_str = JSON.stringify(args); + const CNCrypto = loaded_CNCrypto(); + const ret_string = CNCrypto.mnemonic_from_seed(args_str); + const ret = JSON.parse(ret_string); + if (typeof ret.err_msg !== 'undefined' && ret.err_msg) { + throw ret.err_msg // TODO: maybe return this somehow + } + return ret.retVal; + }; + + this.seed_and_keys_from_mnemonic = function( + mnemonic_string, + wordset_name + ) { + const args = + { + mnemonic_string: mnemonic_string, + wordset_name: api_safe_wordset_name(wordset_name) + }; + const args_str = JSON.stringify(args); + const CNCrypto = loaded_CNCrypto(); + const ret_string = CNCrypto.seed_and_keys_from_mnemonic(args_str); + const ret = JSON.parse(ret_string); + if (typeof ret.err_msg !== 'undefined' && ret.err_msg) { + throw ret.err_msg // TODO: maybe return this somehow + } + return { // calling these out so as to provide a stable ret val interface + sec_seed_string: ret.sec_seed_string, + address_string: ret.address_string, + pub_viewKey_string: ret.pub_viewKey_string, + sec_viewKey_string: ret.sec_viewKey_string, + pub_spendKey_string: ret.pub_spendKey_string, + sec_spendKey_string: ret.sec_spendKey_string + }; + }; + + this.validate_components_for_login = function( + address_string, + sec_viewKey_string, + sec_spendKey_string, + seed_string, + nettype + ) { + const args = + { + address_string: address_string, + sec_viewKey_string: sec_viewKey_string, + sec_spendKey_string: sec_spendKey_string, + seed_string: seed_string, + nettype_string: nettype_utils.nettype_to_API_string(nettype) + }; + const args_str = JSON.stringify(args); + const CNCrypto = loaded_CNCrypto(); + const ret_string = CNCrypto.validate_components_for_login(args_str); + const ret = JSON.parse(ret_string); + if (typeof ret.err_msg !== 'undefined' && ret.err_msg) { + throw ret.err_msg // TODO: maybe return this somehow + } + return { // calling these out so as to provide a stable ret val interface + isValid: ret_val_boolstring_to_bool(ret.isValid), + isInViewOnlyMode: ret_val_boolstring_to_bool(ret.isInViewOnlyMode), + pub_viewKey_string: ret.pub_viewKey_string, + pub_spendKey_string: ret.pub_spendKey_string + }; }; this.generate_key_image = function( @@ -322,101 +464,6 @@ var cnUtil = function(currencyConfig) throw "Not enough outputs to mix with"; } } - - // TODO - - }; - - this.estimateRctSize = function(inputs, mixin, outputs, extra_size, bulletproof) - { - // keeping this in JS instead of C++ for now b/c it's much faster to access, and we don't have to make it asynchronous by waiting for the module to load - bulletproof = bulletproof == true ? true : false - extra_size = extra_size || 40 - // - var size = 0; - // tx prefix - // first few bytes - size += 1 + 6; - size += inputs * (1 + 6 + (mixin + 1) * 2 + 32); - // vout - size += outputs * (6 + 32); - // extra - size += extra_size; - // rct signatures - // type - size += 1; - // rangeSigs - if (bulletproof) - size += ((2*6 + 4 + 5)*32 + 3) * outputs; - else - size += (2*64*32+32+64*32) * outputs; - // MGs - size += inputs * (64 * (mixin + 1) + 32); - // mixRing - not serialized, can be reconstructed - /* size += 2 * 32 * (mixin+1) * inputs; */ - // pseudoOuts - size += 32 * inputs; - // ecdhInfo - size += 2 * 32 * outputs; - // outPk - only commitment is saved - size += 32 * outputs; - // txnFee - size += 4; - // const logStr = `estimated rct tx size for ${inputs} at mixin ${mixin} and ${outputs} : ${size} (${((32 * inputs/*+1*/) + 2 * 32 * (mixin+1) * inputs + 32 * outputs)}) saved)` - // console.log(logStr) - - return size; - }; - - this.is_tx_unlocked = function(unlock_time, blockchain_height) { - if (!config.maxBlockNumber) { - throw "Max block number is not set in config!"; - } - if (unlock_time < config.maxBlockNumber) { - // unlock time is block height - return blockchain_height >= unlock_time; - } else { - // unlock time is timestamp - var current_time = Math.round(new Date().getTime() / 1000); - return current_time >= unlock_time; - } - }; - - this.tx_locked_reason = function(unlock_time, blockchain_height) { - if (unlock_time < config.maxBlockNumber) { - // unlock time is block height - var numBlocks = unlock_time - blockchain_height; - if (numBlocks <= 0) { - return "Transaction is unlocked"; - } - var unlock_prediction = moment().add( - numBlocks * config.avgBlockTime, - "seconds", - ); - return ( - "Will be unlocked in " + - numBlocks + - " blocks, ~" + - unlock_prediction.fromNow(true) + - ", " + - unlock_prediction.calendar() + - "" - ); - } else { - // unlock time is timestamp - var current_time = Math.round(new Date().getTime() / 1000); - var time_difference = unlock_time - current_time; - if (time_difference <= 0) { - return "Transaction is unlocked"; - } - var unlock_moment = moment(unlock_time * 1000); - return ( - "Will be unlocked " + - unlock_moment.fromNow() + - ", " + - unlock_moment.calendar() - ); - } }; function assert(stmt, val) { diff --git a/monero_utils/monero_sendingFunds_utils.js b/monero_utils/monero_sendingFunds_utils.js index bf51973..63b1573 100644 --- a/monero_utils/monero_sendingFunds_utils.js +++ b/monero_utils/monero_sendingFunds_utils.js @@ -69,7 +69,46 @@ function default_priority() { } // aka .low exports.default_priority = default_priority; // -// +function estimateRctSize(inputs, mixin, outputs, extra_size, bulletproof) +{ + // keeping this in JS instead of C++ for now b/c it's much faster to access, and we don't have to make it asynchronous by waiting for the module to load + bulletproof = bulletproof == true ? true : false + extra_size = extra_size || 40 + // + var size = 0; + // tx prefix + // first few bytes + size += 1 + 6; + size += inputs * (1 + 6 + (mixin + 1) * 2 + 32); + // vout + size += outputs * (6 + 32); + // extra + size += extra_size; + // rct signatures + // type + size += 1; + // rangeSigs + if (bulletproof) + size += ((2*6 + 4 + 5)*32 + 3) * outputs; + else + size += (2*64*32+32+64*32) * outputs; + // MGs + size += inputs * (64 * (mixin + 1) + 32); + // mixRing - not serialized, can be reconstructed + /* size += 2 * 32 * (mixin+1) * inputs; */ + // pseudoOuts + size += 32 * inputs; + // ecdhInfo + size += 2 * 32 * outputs; + // outPk - only commitment is saved + size += 32 * outputs; + // txnFee + size += 4; + // const logStr = `estimated rct tx size for ${inputs} at mixin ${mixin} and ${outputs} : ${size} (${((32 * inputs/*+1*/) + 2 * 32 * (mixin+1) * inputs + 32 * outputs)}) saved)` + // console.log(logStr) + + return size; +}; function calculate_fee(fee_per_kb_JSBigInt, numberOf_bytes, fee_multiplier) { const numberOf_kB_JSBigInt = new JSBigInt( (numberOf_bytes + 1023.0) / 1024.0, @@ -114,7 +153,7 @@ function EstimatedTransaction_networkFee( 1 /*dest*/ + 1 /*change*/ + 0; /*no mymonero fee presently*/ // TODO: update est tx size for bulletproofs // TODO: normalize est tx size fn naming - const estimated_txSize = monero_utils.estimateRctSize( + const estimated_txSize = estimateRctSize( numberOf_inputs, nonZero_mixin_int, numberOf_outputs, @@ -422,7 +461,7 @@ function SendFunds( if (/*usingOuts.length > 1 &&*/ isRingCT) { var newNeededFee = calculate_fee( feePerKB_JSBigInt, - monero_utils.estimateRctSize(usingOuts.length, mixin, 2), + estimateRctSize(usingOuts.length, mixin, 2), fee_multiplier_for_priority(simple_priority), ); // if newNeededFee < neededFee, use neededFee instead (should only happen on the 2nd or later times through (due to estimated fee being too low)) @@ -475,7 +514,7 @@ function SendFunds( // and recalculate invalidated values newNeededFee = calculate_fee( feePerKB_JSBigInt, - monero_utils.estimateRctSize( + estimateRctSize( usingOuts.length, mixin, 2, @@ -587,10 +626,9 @@ function SendFunds( // this should always fire when sweeping if (isRingCT) { // then create random destination to keep 2 outputs always in case of 0 change - var fakeAddress = monero_utils.create_address( - monero_utils.random_scalar(), + const fakeAddress = monero_utils.new_fake_address_for_rct_tx( nettype, - ).public_addr; + ); console.log( "Sending 0 XMR to a fake address to keep tx uniform (no change exists): " + fakeAddress, diff --git a/monero_utils/monero_txParsing_utils.js b/monero_utils/monero_txParsing_utils.js index 7be647b..08b721d 100644 --- a/monero_utils/monero_txParsing_utils.js +++ b/monero_utils/monero_txParsing_utils.js @@ -37,14 +37,57 @@ function IsTransactionConfirmed(tx, blockchain_height) { exports.IsTransactionConfirmed = IsTransactionConfirmed; // function IsTransactionUnlocked(tx, blockchain_height) { - return monero_utils.is_tx_unlocked(tx.unlock_time || 0, blockchain_height); + const unlock_time = tx.unlock_time || 0; + if (!monero_config.maxBlockNumber) { + throw "Max block number is not set in config!"; + } + if (unlock_time < monero_config.maxBlockNumber) { + // unlock time is block height + return blockchain_height >= unlock_time; + } else { + // unlock time is timestamp + var current_time = Math.round(new Date().getTime() / 1000); + return current_time >= unlock_time; + } } exports.IsTransactionUnlocked = IsTransactionUnlocked; // -function TransactionLockedReason(tx, blockchain_height) { - return monero_utils.tx_locked_reason( - tx.unlock_time || 0, - blockchain_height, - ); +function TransactionLockedReason(tx, blockchain_height) +{ + const unlock_time = tx.unlock_time || 0; + if (unlock_time < monero_config.maxBlockNumber) { + // unlock time is block height + var numBlocks = unlock_time - blockchain_height; + if (numBlocks <= 0) { + return "Transaction is unlocked"; + } + var unlock_prediction = moment().add( + numBlocks * monero_config.avgBlockTime, + "seconds" + ); + return ( + "Will be unlocked in " + + numBlocks + + " blocks, ~" + + unlock_prediction.fromNow(true) + + ", " + + unlock_prediction.calendar() + + "" + ); + } else { + // unlock time is timestamp + var current_time = Math.round(new Date().getTime() / 1000); + var time_difference = unlock_time - current_time; + if (time_difference <= 0) { + return "Transaction is unlocked"; + } + var unlock_moment = moment(unlock_time * 1000); + return ( + "Will be unlocked " + + unlock_moment.fromNow() + + ", " + + unlock_moment.calendar() + ); + } } exports.TransactionLockedReason = TransactionLockedReason; diff --git a/monero_utils/monero_wallet_utils.js b/monero_utils/monero_wallet_utils.js index 867b050..3a56e80 100644 --- a/monero_utils/monero_wallet_utils.js +++ b/monero_utils/monero_wallet_utils.js @@ -158,14 +158,25 @@ exports.DefaultWalletMnemonicWordsetName = // Wallet creation: // function NewlyCreatedWallet(mnemonic_wordsetName, nettype) { - const seed = monero_utils.random_scalar(); // to generate a 32-byte (25-word) but reduced seed - const mnemonicString = mnemonic.mn_encode(seed, mnemonic_wordsetName); - const keys = monero_utils.create_address(seed, nettype); - // + // TODO: possibly deprecate this function now that it's basically a passthrough (it existed so as to avoid modifying cryptonote_utils) + const ret = monero_utils.newly_created_wallet( + mnemonic_wordsetName, + nettype + ); return { - seed: seed, - mnemonicString: mnemonicString, - keys: keys, + seed: ret.sec_seed_string, + mnemonicString: ret.mnemonic_string, + keys: { + public_addr: ret.address_string, + view: { + sec: ret.sec_viewKey_string, + pub: ret.pub_viewKey_string + }, + spend: { + sec: ret.sec_spendKey_string, + pub: ret.pub_spendKey_string + } + } }; } exports.NewlyCreatedWallet = NewlyCreatedWallet; @@ -175,12 +186,8 @@ exports.NewlyCreatedWallet = NewlyCreatedWallet; // Wallet login: // function MnemonicStringFromSeed(account_seed, mnemonic_wordsetName) { - const mnemonicString = mnemonic.mn_encode( - account_seed, - mnemonic_wordsetName, - ); - // - return mnemonicString; + // TODO: possibly deprecate this function as it now merely wraps another + return monero_utils.mnemonic_from_seed(account_seed, mnemonic_wordsetName); } exports.MnemonicStringFromSeed = MnemonicStringFromSeed; // @@ -192,37 +199,25 @@ function SeedAndKeysFromMnemonic_sync( // -> {err_str?, seed?, keys?} mnemonicString = mnemonicString.toLowerCase() || ""; try { - var seed = null; - var keys = null; - switch (mnemonic_wordsetName) { - case "english": - try { - seed = mnemonic.mn_decode(mnemonicString); - } catch (e) { - // Try decoding as an electrum seed, on failure throw the original exception - try { - seed = mnemonic.mn_decode(mnemonicString, "electrum"); - } catch (ee) { - throw e; - } + const ret = monero_utils.seed_and_keys_from_mnemonic( + mnemonicString, + mnemonic_wordsetName + ); + return { + err_str: null, + seed: ret.sec_seed_string, + keys: { + public_addr: ret.address_string, + view: { + sec: ret.sec_viewKey_string, + pub: ret.pub_viewKey_string + }, + spend: { + sec: ret.sec_spendKey_string, + pub: ret.pub_spendKey_string } - break; - default: - seed = mnemonic.mn_decode(mnemonicString, mnemonic_wordsetName); - break; - } - if (seed === null) { - return { err_str: "Unable to derive seed", seed: null, keys: null }; - } - keys = monero_utils.create_address(seed, nettype); - if (keys === null) { - return { - err_str: "Unable to derive keys from seed", - seed: seed, - keys: null, - }; - } - return { err_str: null, seed: seed, keys: keys }; + } + }; } catch (e) { console.error("Invalid mnemonic!"); return { @@ -257,93 +252,37 @@ function VerifiedComponentsForLogIn_sync( address, nettype, view_key, - spend_key_orUndefinedForViewOnly, - seed_orUndefined, - wasAGeneratedWallet, + spend_key__orZero, + seed__orZero, ) { - var spend_key = - typeof spend_key_orUndefinedForViewOnly !== "undefined" && - spend_key_orUndefinedForViewOnly != null && - spend_key_orUndefinedForViewOnly != "" - ? spend_key_orUndefinedForViewOnly - : null; - var isInViewOnlyMode = spend_key == null; - if ( - !view_key || - view_key.length !== 64 || - (isInViewOnlyMode ? false : spend_key.length !== 64) - ) { - return { err_str: "invalid secret key length" }; - } - if ( - !monero_utils.valid_hex(view_key) || - (isInViewOnlyMode ? false : !monero_utils.valid_hex(spend_key)) - ) { - return { err_str: "invalid hex formatting" }; - } - var public_keys; + var spend_key__orEmpty = spend_key__orZero || ""; + var seed__orEmpty = seed__orZero || ""; try { - public_keys = monero_utils.decode_address(address, nettype); - } catch (e) { - return { err_str: "invalid address" }; - } - var expected_view_pub; - try { - expected_view_pub = monero_utils.sec_key_to_pub(view_key); - } catch (e) { - return { err_str: "invalid view key" }; - } - var expected_spend_pub; - if (spend_key.length === 64) { - try { - expected_spend_pub = monero_utils.sec_key_to_pub(spend_key); - } catch (e) { - return { err_str: "invalid spend key" }; - } - } - if (public_keys.view !== expected_view_pub) { - return { err_str: "invalid view key" }; - } - if (!isInViewOnlyMode && public_keys.spend !== expected_spend_pub) { - return { err_str: "invalid spend key" }; - } - const private_keys = { - view: view_key, - spend: spend_key, - }; - var account_seed = null; // default - if ( - typeof seed_orUndefined !== "undefined" && - seed_orUndefined && - seed_orUndefined.length != 0 - ) { - var expected_account; - try { - expected_account = monero_utils.create_address( - seed_orUndefined, - nettype, - ); - } catch (e) { - return { err_str: "invalid seed" }; - } - if ( - expected_account.view.sec !== view_key || - expected_account.spend.sec !== spend_key || - expected_account.public_addr !== address - ) { - return { err_str: "invalid seed" }; + const ret = monero_utils.validate_components_for_login( + address, + view_key, + spend_key__orEmpty, + seed__orEmpty, + nettype + ); + if (ret.isValid == false) { // actually don't think we're expecting this.. + return { + err_str: "Invalid input" + } } - account_seed = seed_orUndefined; + return { + err_str: null, + public_keys: { + view: ret.pub_viewKey_string, + spend: ret.pub_spendKey_string + }, + isInViewOnlyMode: ret.isInViewOnlyMode // should be true "if(spend_key__orZero)" + }; + } catch (e) { + return { + err_str: typeof e === "string" ? e : "" + e + }; } - const payload = { - err_str: null, // err - address: address, - account_seed: account_seed !== "" ? account_seed : null, - public_keys: public_keys, - private_keys: private_keys, - isInViewOnlyMode: isInViewOnlyMode, - }; - return payload; } exports.VerifiedComponentsForLogIn_sync = VerifiedComponentsForLogIn_sync; // @@ -351,9 +290,8 @@ function VerifiedComponentsForLogIn( address, nettype, view_key, - spend_key_orUndefinedForViewOnly, - seed_orUndefined, - wasAGeneratedWallet, + spend_key_orUndef, + seed_orUndef, fn, ) { // fn: (err?, address, account_seed, public_keys, private_keys, isInViewOnlyMode) -> Void @@ -361,16 +299,12 @@ function VerifiedComponentsForLogIn( address, nettype, view_key, - spend_key_orUndefinedForViewOnly, - seed_orUndefined, - wasAGeneratedWallet, + spend_key_orUndef, + seed_orUndef ); fn( payload.err_str ? new Error(payload.err_str) : null, - payload.address, - payload.account_seed, payload.public_keys, - payload.private_keys, payload.isInViewOnlyMode, ); }