diff --git a/README_offline_transaction_signing.md b/README_offline_transaction_signing.md new file mode 100644 index 00000000..79227d81 --- /dev/null +++ b/README_offline_transaction_signing.md @@ -0,0 +1,288 @@ +## Sign a transaction using an offline wallet +This README matches the behaviour of a modified release 140 + +## Introduction +The problem with traditional wallets and how Dero addresses these +shortfalls: +1. How traditional wallets work
+ A wallet file (wallet.db) stores your addresses. Each address consists of a public + and secret part. The public part is what you share with people, so they can + make payments to you. The secret key is required to unlock and spend the funds + associated with that address. + +2. Risk associated with traditional wallets
+ If somebody obtains your secret key they can spend your funds. The network will + process the transaction if the cryptographic signature is correct. The network + doesn't validate who sent the transaction or from where. + An attacker will aim to obtain your wallet file in order to steal your secret + keys, and per extension, your funds. Malware hidden in applications is an easy + way for hackers to scout your hard drive for wallet files, which they send to + themselves over your internet link.
+ + Apart from the risk of loosing your funds to an attacker it's also much more likely + you'll loose your wallet file due to neglect. There are countless horror stories + of people who have lost billions (yes, with a 'B') worth of Bitcoin due to + hard drive crashes, hard drives that were formatted to make room for a new + O/S or game installation or simply old machines that were thrown away.
+ + If you were diligent and made a backup of the wallet file, you have to remember + which addresses it contains. Each time you create a new addresses it is not + automatically added to the backup copy. + Upon restoring of an old backup you might discover that it doesn't contain all + the new addresses created since the backup. Access to those funds are lost forever.
+ +3. BIP39: Mnemonic phrase improvement
+ The problem of loosing addresses and their private keys were mitigated with the + implementation of BIP39 mnemonic seed phrases. A single master key is created. + All the addresses in that wallet are derived from the master key. To the user + the master key is presented as a random 25 word (natural language) phrase, + called a mnemonic. + + When the wallet is created this phrase must be written down and stored in a secure + manner. The storage medium must protect against fire and moisture damage. + + Do not use your phone to take a picture of the phrase. Synchronisation software + could upload your images to the 'cloud' where it can be intercepted in transit + or on the remote storage. Do not print it out either. In rare cases malware in + printer firmware has been found to recognise these seed phrases and send it of + to the hackers. + + Paranoia regarding protection of your seed phrase is not unwarrented. + + When it's time to restore your addresses in a new wallet all you have to do is + to type in the mnemonic. The application will convert it to the master key. + No software backup of the wallet file is required. By initialising the wallet + with a valid seed phrase all the addresses derived from it can be recreated. + + If you have funds in a traditional wallet thats not based on BIP39, you can + setup a second wallet that uses the BIP39 technology. You can pay the funds + from the old wallet addresses to the new wallet, thereby transferring the + funds to the new wallet. + +4. Encrypted wallets + As seen above, BIP39 guards against loosing access to your individual addresses. + It however does not provide additional protection of loosing the actual wallet + file to an attacker. + On Dero the wallet file is encrypted with a password. If an attacker obtains + the wallet file, they will not be able to open it. Your security is based on + choosing a long, secure password which can withstand a brute force attack. + +5. Offline cold storage
+ Two computers are required for this setup. One is connected to the internet + while the other has no network connectivity. + + The machine without network access is called the offline machine. On it you + setup an encrypted wallet with mnemonic seed phrase. The machine doesn't have + a copy of the blockchain. This wallet will perform transaction signing (authorisation). + + The machine with internet access is called the online machine. This machine + is synchronised with the blockchain. This wallet is called the viewing wallet. It displays + your balance and transaction history and can compile a transaction, but not authorise it.
+ + This process adds two levels of complexity:
+ * How do you view the transactions & balance of an address located in the offline wallet?
+ * How do you spend the funds of an address located in the offline wallet?
+
+ The Dero command line (CLI) wallet overcomes these obstacles as follow:
+ + Viewing the transaction history
+ The Dero blockchain is encrypted. None of the transaction data is visible on + the blockchain. You can't import an address into the online wallet and view + the activity that occurred on that address. The secret key is required to + decrypt the data that contains your transactions. + + Dero requires that your register your address on the network. This allows an + initial filter of the data so that your wallet client only receives data that + is related to its address. + + As the online wallet receives the data it will create a separate data file, which + needs to be copied to the offline wallet for decryption. The decrypted data must + be copied back to the online wallet where it is imported. The decrypted data + enables the wallet to extract the transaction data and to compile your account balance. + + If the online wallet is somehow stolen it's (almost) no deal - information + is leaked regarding the balance & transaction history of your address. This is + clearly not a desireable thing, but at least the attacker will not be able to spend + the funds, like they would have if the wallet contained the secret (private) + key as well. + + Spend the funds
+ The online wallet can construct a transaction. All the required inputs are contained in this transaction. + The transaction is stored in a data file. The file must be copied to the offline machine. There the secret + key authorises (signs) the transaction. The authorised transaction is copied back to the online wallet and + submitted to the network for processing. + +6. Summary + The Dero CLI wallet gives you access to these features today: + * BIP39 master seed + * Split addresses between two wallets. The offline contains the private keys + and the online the viewing and spending keys. + * Encrypted storage of the wallet on disk and encrypted communication + * All running on an encrypted blockchain + +## Software setup +1. You require two PCs, preferrably Intel i5 or faster. About 200mb of hard drive space + is required if you plan to connect to a remote node. The internet link speed isn't + very important if you connect to a remote node. 1mbps or faster is sufficient. + The machines can run Linux, Mac OS X or MS Windows. For this tutorial Linux is used. + +2. You can obtain the software from the official Dero website: + https://dero.io/download.html or a source copy from GitHUB at + https://github.com/deroproject/derohe + + At the moment only the cli (command line) wallet supports offline transaction signing. + + If you've downloaded the Linux CLI (command line interface) install archive, extract + it as follow:
+ $ tar -xvzf dero_linux_amd64.tar.gz
+ $ cd dero_linux_amd64
+ $ ls
+ derod-linux-amd64 dero-miner-linux-amd64 dero-wallet-cli-linux-amd64 explorer-linux-amd64 simulator-linux-amd64 Start.md
+

+ We'll use the dero-wallet-cli-* application + + To build from source, you'll need to Go language (golang) compiler on your machine + to compile the software. On a Debian based Linux installation, you can install the + package as follow:
+ $ sudo apt-get update
+ $ sudo apt-get install golang:amd64

+ + If you want to check out a copy of the github source code:
+ $ git clone https://github.com/deroproject/derohe
+ $ cd derohe/cmd/dero-wallet-cli
+ $ go build

+ The new application is called: dero-wallet-cli
+3. Offline machine with the signing wallet
+3.1. First run
+ From a terminal console, launch the application: ./dero-wallet-cli --help
+ We will use the following command line options:
+ --offline - Specify that this wallet is an offline (signing) wallet
+ --wallet-file - The name of your wallet, i.e. offline.db
+ --password - The password with which to encypt the wallet. It needs to be a strong password, + which can withstand a password attack, but note, you'll have to enter this password regularly, so it + still needs to be something practical to work with.
+ --generate-new-wallet - Let the wallet create a new mnemonic seed phrase and address
+ or
+ --restore-deterministic-wallet - You'll provide the mnemonic seed phrase
+ --electrum-seed - Here you'll provide the mnemonic phrase
+ An example will be:
+ $ ./dero-wallet-cli --offline --wallet-file=offline.db --password=someexamplepw --restore-deterministic-wallet --electrum-seed="your 25 seedphrase words here"
+ After the wallet starts up the menu will provide you with a couple of options. At the top of the menu is a greeting to show you that it is running in offline mode:
+ Offline (signing) wallet:
+   1. Exported public key to setup the online (view only) wallet
+   2. Generate a registration transaction for the online wallet
+   3. Sign spend transactions for the online wallet
+ Select '0' to exit the wallet.
+3.2. Second run
+ Now that the wallet is already created, you don't provide the restore & seed CLI options anymore:
+ $ ./dero-wallet-cli --offline --wallet-file=offline.db --password=someexamplepw
+ Menu options:
+ 1 Display account Address
+ Display your account address. Share this with people so they can send Dero to you:
+   Wallet address : dero1abcdef12345678907j0n6ft4yzlm300fxzz2sg84t28g2cp897f5yqghyx4z3
+ 2 Display seed
+ This prints your mnemonic recovery seed. If somebody obtains this seed phrase, they can restore a wallet and spend all your funds.
+ 3 Display Keys
+ This normally contains the public and secret keys. While you're running in offline mode, an additional entry is display, the 'view only' key. This key is used to set up the online (view only) wallet.
+   secret key: <Your secret key>
+   public key: <Your public key>
+   View only key - Import the complete text into the online (view only) wallet to set it up:
+   viewkey,<key parts>;20010
+ 4 Generate registration transaction
+ In order to use a remote node, i.e. running in 'light mode', where you do not download the full blockchain yourself, the node requires you to register your address on it.
+ Example output:
+    Generating registration transaction for wallet address : dero1<Your address>
+    Searched 100000 hashes
+    ...
+    ...
+    Searched 24600000 hashes
+    Found transaction:
+    Found the registration transaction. Import the complete text into the online (view only) wallet:
+    registration,dero1<registration text>;24578

+4. Online machine with the view only wallet
+4.1 First run
+ From a terminal console, launch the application: ./dero-wallet-cli --help
+ We will use the following command line options:
+ --remote - Connect to a remote node. This is often called 'light weight mode', since you do not maintain a full copy of the blockchain.
+ --wallet-file - The name of your wallet, i.e. viewonly.db
+ --password - The password with which to encypt the wallet. It needs to be a strong password, which can withstand a password attack, but note, you'll have to enter this password regularly, so it still needs to be something practical to work with.
+ --restore-viewonly-wallet - Set up the wallet with the viewing key obtained from the offline wallet
+An example will be:
+ $ ./dero-wallet-cli --remote --wallet-file=viewonly.db --password=someexamplepw --restore-viewonly-wallet
+ The software will have these 2 prompts:
+    Enter wallet filename (default viewonly.db): Just press enter to accept the default
+    Enter the view only key (obtained from the offline (signing) wallet): Paste the viewing key here
+If the key was accepted, you'll get this confirmation:
+   Successfully restored an online (view only) wallet
+   Address: dero1<Your address>
+   Public key: <Your public key>
+After the wallet starts up the menu will provide you with a couple of options. At the top of the menu is a greeting to show you that it is running in view only mode:
+ Online (view only) wallet:
+   1. Register you account, using registration transaction from the offline (signing) wallet.
+   2. View your account balance & transaction history
+   3. Generate transactions for the offline wallet to sign.
+   4. Submit the signed transactions to the network.
+ Select '0' to exit the wallet.
+4.2 Second run
+ Now that the wallet is already created, you don't provide the restore & seed CLI options anymore:
+ $ ./dero-wallet-cli --remote --wallet-file=viewonly.db --password=someexamplepw
+ Menu options:
+ 1 Display account Address
+ Display your account address. Share this with people so they can pay you. This address must match the address in the offline (signing) wallet.
+   Wallet address : dero1abcdef12345678907j0n6ft4yzlm300fxzz2sg84t28g2cp897f5yqghyx4z3
+ 2 Display seed
+ This option is not available in the view only wallet.
+ 3 Display Keys
+ Only the public key is displayed. This must match the public key in the offline wallet
+ 4 Account registration to blockchain
+ In order to use a remote node, i.e. running in 'light mode', where you do not download the full blockchain yourself, the node requires you to register your address.
+   Enter the registration transaction (obtained from the offline (signing) wallet): Paste the registration transaction here
+   Registration TXID <txid number>
+   registration tx dispatched successfully

+ Note: After the account was registered, the wallet needs to synchronise your account balance. In order to accomplish this, interaction between the online & offline wallet is required.
+## Using the split Online / Offline wallet configuration + For the demonstration to work effectively, fund your newly created address with some Dero by sending some cents (0.xx) to it, either from an online exchange or from one of your existing wallets with a balance.
+1. Balance enquiry and transaction history
+ The online (view only) wallet connects to the remote node and retrieves the blockchain data that matches your wallet address. The data needs to be decrypted before the information can be processed. The secret key, located in the offline (signing) wallet, is required to accomplish this.
+ Each time the online wallet receives a block which contains transaction information for your address, a part of the information needs to be send to the offline wallet for decryption.
+ The online wallet will automatically create a file called 'offline_request' in the prefix directory. The prefix is specified as a command line option when the application is started, i.e.: $ ./dero-wallet-cli --prefix=/tmp, along with the other command line options passed to the application.

+ For testing purposes, if you run the online & offline wallets on the same machine with the same prefix, the file created by the online wallet (offline_request) will be immediately detected by the offline (signing) wallet and automatically processed. If you run a production setup where the two wallets are on separate computers, you'll have to copy the file (offline_request) manually from the online machine to the offline machine.
+ The prompts are as follow:
+ Online wallet:
+ The blockchain interaction occurs in the background. After a transaction applicable to your address is detected this text will appear:
+   Interaction with offline wallet required. Saved request to: /prefix/offline_request
+   Waiting 60 seconds for the response at: /prefix/offline_response

+ The 'offline_request' file must be copied to the offline wallet.
+ Offline wallet:
+ After the 'offline_request' file is copied to the specified prefix directory, the wallet will detect the presense of the file automatically and process it. The wallet reports:
+   Found /prefix/offline_request -- new decryption request
+   Saved result in /prefix/offline_response

+ The 'offline_response' file must be copied back to the online wallet.
+ Online wallet:
+ As soon as the response file is detected the wallet reports:
+   Found a valid response
+ This exchange happens for each transaction that you receive or spend on your address. Your balance will be shown as part of the command prompt. You can view the transaction history under menu entry 13 Show transaction history.
+ If you use a Pirate+ hardware wallet the data exchange happens in the background. No manual copying of files are required.
+2. Spend transaction
+ In order to spend your hard earned Dero you first need to fund your address, as suggested at the top of this chapter. The online (view only) wallet will pick up the transaction from the remote node. The transaction history and account balance will be updated, after you've decrypted the data files, as described above in 5.1.
+ To create a transaction the prompts are as follow:
+ Online wallet:
+ Select option 5 Prepare (DERO) transaction (for the offline wallet to sign) to prepare the transaction.
+ Enter the destination address and amount. The destination port and comment are optional
+ After the transaction is confirmed, the wallet prepares all the data and saves it to /prefix/offline_request.
+ This file must be copied to the offline wallet and placed in its /prefix directory.
+ Offline wallet:
+ As soon as the wallet detects the offline request file and determine that it is a transaction that must be signed, a + prompt is presented:
+   Found /tmp/offline_request -- new decryption request
+   Detected new transaction sign request. Authorise the request with menu option '5: Sign'.

+ Enter '5' on the prompt to tell the wallet to sign the transaction. The expected feedback is:
+   Read XXXX bytes from /prefix/transaction
+   Saved result in /prefix/offline_response

+ Return the file to the online (view only) wallet to complete the transaction.
+ Online wallet:
+ As per instruction, return offline_response to the online wallet. When the wallet detect the file it will print this message:
+   Read XXXX bytes from /prefix/offline_response
+   Ready to broadcast the transaction
+   INFO wallet Dispatched tx {"txid": "<tx id>"}

+Congratulations, you've successfully created and send a transaction using an online/offline split wallet diff --git a/cmd/dero-wallet-cli/easymenu_post_open.go b/cmd/dero-wallet-cli/easymenu_post_open.go index 8e989c37..a9607ac6 100644 --- a/cmd/dero-wallet-cli/easymenu_post_open.go +++ b/cmd/dero-wallet-cli/easymenu_post_open.go @@ -26,7 +26,9 @@ import ( "runtime" "strings" "time" - + "strconv" + "encoding/hex" + "github.com/chzyer/readline" "github.com/deroproject/derohe/cryptography/crypto" "github.com/deroproject/derohe/globals" @@ -39,28 +41,60 @@ import ( // handle menu if a wallet is currently opened func display_easymenu_post_open_command(l *readline.Instance) { w := l.Stderr() + + bViewOnly := wallet.ViewOnly() + bOffline := globals.Arguments["--offline"].(bool) + + if (bViewOnly==true) { + io.WriteString(w, "Online (view only) wallet:\n"); + io.WriteString(w, " 1. Register you account, using registration transaction from the offline (signing) wallet.\n") + io.WriteString(w, " 2. View your account balance & transaction history\n") + io.WriteString(w, " 3. Generate transactions for the offline wallet to sign.\n") + io.WriteString(w, " 4. Submit the signed transactions to the network.\n\n") + } + if (bOffline==true) { + io.WriteString(w, "Offline (signing) wallet:\n") + io.WriteString(w, " 1. Exported public key to setup the online (view only) wallet\n") + io.WriteString(w, " 2. Generate a registration transaction for the online wallet\n"); + io.WriteString(w, " 3. Sign spend transactions for the online wallet\n"); + } + + io.WriteString(w, "Menu:\n") io.WriteString(w, "\t\033[1m1\033[0m\tDisplay account Address \n") - io.WriteString(w, "\t\033[1m2\033[0m\tDisplay Seed "+color_red+"(Please save seed in safe location)\n\033[0m") + if ( bViewOnly==false ) { + io.WriteString(w, "\t\033[1m2\033[0m\tDisplay Seed "+color_red+"(Please save seed in safe location)\n\033[0m") + } io.WriteString(w, "\t\033[1m3\033[0m\tDisplay Keys (hex)\n") - if !wallet.IsRegistered() { - io.WriteString(w, "\t\033[1m4\033[0m\tAccount registration to blockchain (registration has no fee requirement and is precondition to use the account)\n") - io.WriteString(w, "\n") - io.WriteString(w, "\n") - } else { // hide some commands, if view only wallet - io.WriteString(w, "\t\033[1m4\033[0m\tDisplay wallet pool\n") - io.WriteString(w, "\t\033[1m5\033[0m\tTransfer (send DERO) to Another Wallet\n") - io.WriteString(w, "\t\033[1m6\033[0m\tToken transfer to another wallet\n") - io.WriteString(w, "\n") + if (bOffline==true) { + io.WriteString(w, "\t\033[1m4\033[0m\tGenerate registration transaction for the online (view only) wallet\n") + io.WriteString(w, "\t\033[1m5\033[0m\tSign (DERO) transaction, prepared by the online (view only) wallet\n") + } else { + if !wallet.IsRegistered() { + io.WriteString(w, "\t\033[1m4\033[0m\tAccount registration to blockchain (registration has no fee requirement and is precondition to use the account)\n") + } else { + io.WriteString(w, "\t\033[1m4\033[0m\tDisplay wallet pool\n") + if ( bViewOnly==false ) { + io.WriteString(w, "\t\033[1m5\033[0m\tTransfer (send DERO) to Another Wallet\n") + io.WriteString(w, "\t\033[1m6\033[0m\tToken transfer to another wallet\n") + } else { + io.WriteString(w, "\t\033[1m5\033[0m\tPrepare (DERO) transaction (for the offline wallet to sign)\n") +// Not yet implemented/tested io.WriteString(w, "\t\033[1m6\033[0m\tPrepare token transaction (for the offline wallet to sign)\n") + } + io.WriteString(w, "\n") + } } io.WriteString(w, "\t\033[1m7\033[0m\tChange wallet password\n") io.WriteString(w, "\t\033[1m8\033[0m\tClose Wallet\n") - if wallet.IsRegistered() { - io.WriteString(w, "\t\033[1m12\033[0m\tTransfer all balance (send DERO) To Another Wallet\n") + if wallet.IsRegistered() && (bOffline==false) { + // Commands applicable only to online wallets + if (bViewOnly==false) { + io.WriteString(w, "\t\033[1m12\033[0m\tTransfer all balance (send DERO) To Another Wallet\n") + } io.WriteString(w, "\t\033[1m13\033[0m\tShow transaction history\n") io.WriteString(w, "\t\033[1m14\033[0m\tRescan transaction history\n") io.WriteString(w, "\t\033[1m15\033[0m\tExport all transaction history in json format\n") @@ -95,19 +129,30 @@ func handle_easymenu_post_open_command(l *readline.Instance, line string) (proce case "1": fmt.Fprintf(l.Stderr(), "Wallet address : "+color_green+"%s"+color_white+"\n", wallet.GetAddress()) - if !wallet.IsRegistered() { - reg_tx := wallet.GetRegistrationTX() - fmt.Fprintf(l.Stderr(), "Registration TX : "+color_green+"%x"+color_white+"\n", reg_tx.Serialize()) + if !wallet.IsRegistered() && globals.Arguments["--offline"].(bool)==false { + // The registration transaction to 'remote' requires some POW: First 3 bytes must be 0. + // The view only wallet doesn't generate its own registratio transaction + if (globals.Arguments["--remote"]==true) || (wallet.ViewOnly()==true) { + fmt.Fprintf(l.Stderr(), "Register your account in order to synchronise with the network\n"); + } else { + reg_tx := wallet.GetRegistrationTX() + fmt.Fprintf(l.Stderr(), "Registration TX : "+color_green+"%x"+color_white+"\n", reg_tx.Serialize()) + } + } PressAnyKey(l, wallet) case "2": // give user his seed - if !ValidateCurrentPassword(l, wallet) { - logger.Error(fmt.Errorf("Invalid password"), "") - PressAnyKey(l, wallet) - break + if (wallet.ViewOnly() == false) { + if !ValidateCurrentPassword(l, wallet) { + logger.Error(fmt.Errorf("Invalid password"), "") + PressAnyKey(l, wallet) + break + } + display_seed(l, wallet) // seed should be given only to authenticated users + } else { + fmt.Printf("This is a view only wallet. It doens't contain the seed phrase\n") } - display_seed(l, wallet) // seed should be given only to authenticated users PressAnyKey(l, wallet) case "3": // give user his keys in hex form @@ -122,52 +167,201 @@ func handle_easymenu_post_open_command(l *readline.Instance, line string) (proce PressAnyKey(l, wallet) case "4": // Registration - - if !wallet.IsRegistered() { - - // at this point we must send the registration transaction - fmt.Fprintf(l.Stderr(), "Wallet address : "+color_green+"%s"+color_white+" is going to be registered. Please wait till the account is registered.", wallet.GetAddress()) - fmt.Fprintf(l.Stderr(), "This is a pre-condition POW for using the online chain.") - fmt.Fprintf(l.Stderr(), "This will take a couple of minutes. Please wait....\n") - + // If the wallet is performing the 'offline' (sign) role, the transaction must be printed in a data format + // which can be imported in the ViewOnly 'online' wallet. + var IsOffline = globals.Arguments["--offline"].(bool) + + if (!wallet.IsRegistered()) || (IsOffline==true) { var reg_tx *transaction.Transaction + + if (wallet.ViewOnly()==true) { + // Format: [0] - preamble: registration + // [1] Address + // [2] Registration address + // [3] Hash of the registartion address + // [4] Checksum of the string + + //The view only online wallet received the registration transaction from the offline wallet. + sRegistration := read_line_with_prompt(l, fmt.Sprintf("Enter the registration transaction (obtained from the offline (signing) wallet): ")) + + if (len(sRegistration)==0) { + //No input provided + break; + } + + //Strip off any newlines or extra spaces + sTmp := strings.ReplaceAll(sRegistration,"\n","") + sRegistration = strings.ReplaceAll(sTmp," ",""); + + //Split string on ';' + saParts := strings.Split(sRegistration,";") + if (len(saParts) != 2) { + fmt.Fprintf(l.Stderr(), "Invalid number of parts. Expected 2, found %d\n\n", len(saParts)) + break; + } + + sTransaction := saParts[0] + sProtocolChecksum := saParts[1] + iTmp,err := strconv.Atoi(sProtocolChecksum) + if err!=nil { + fmt.Fprintf(l.Stderr(), "Could not convert the checksum back to an integer: "+sProtocolChecksum+"\n") + break + } + iProtocolChecksum:=uint16(iTmp); - successful_regs := make(chan *transaction.Transaction) - - counter := 0 - - for i := 0; i < runtime.GOMAXPROCS(0); i++ { - go func() { - - for counter == 0 { + //Regenerate checksum: + var iCalculatedChecksum uint16 + iCalculatedChecksum=0x01 + for t := range sTransaction { + iCalculatedChecksum = iCalculatedChecksum + (uint16)(sTransaction[t]) + } - lreg_tx := wallet.GetRegistrationTX() - hash := lreg_tx.GetHash() + // Check 1: Checksum + if (iProtocolChecksum != iCalculatedChecksum) { + fmt.Fprintf(l.Stderr(), "Checksum calculation failed. Protocol=%d, Calculated=%d. Please check if you've imported the transaction correctly\n\n", iProtocolChecksum, iCalculatedChecksum); + break + } + + saParts = strings.Split(sTransaction,",") + if (len(saParts) != 4) { + fmt.Fprintf(l.Stderr(), "Invalid number of parts. Expected 4, found %d\n\n", len(saParts)) + break + } + + if (saParts[0]!="registration") { + fmt.Fprintf(l.Stderr(), "Input doesn't start with 'registration'\n\n") + break; + } + + //Check 2: Is this transaction for our address? + sAddress := wallet.GetAddress().String() + if (sAddress != saParts[1]) { + fmt.Fprintf(l.Stderr(), "Mismatch: Our address is %s, the registration transaction contains a different address:%s\n", sAddress, saParts[1]) + break + } + + + baRegistrationTx, err1 := hex.DecodeString(saParts[2]) + if err1 != nil { + fmt.Fprintf(l.Stderr(), "Could not convert the transaction back to binary data.\n\n") + break + } + baHash, err1 := hex.DecodeString(saParts[3]) + if err1 != nil { + fmt.Fprintf(l.Stderr(), "Could not convert the hash back to binary data.\n\n") + break + } + if (baHash[0]!=0 || baHash[1]!=0 || baHash[2]!=0) { + fmt.Fprintf(l.Stderr(), "For a valid registration transaction the first 3 bytes must be 0\n\n") + break + } - if hash[0] == 0 && hash[1] == 0 && hash[2] == 0 { - successful_regs <- lreg_tx - counter++ - break + var tx2 transaction.Transaction + tx2.Deserialize( baRegistrationTx ) + + //var hash2 string + calculated_hash := fmt.Sprintf("%s", tx2.GetHash()) + PublicKey2 := fmt.Sprintf("%x", tx2.MinerAddress ) + + keys := wallet.Get_Keys() + PublicKey := fmt.Sprintf("%s", keys.Public.StringHex()) + + // Check 3 + if (calculated_hash != saParts[3]) { + fmt.Fprintf(l.Stderr(), "Mismatch: the calculated hash (of the registration transaction) and the hash provided in the input differs\n\n") + break + } + + // Check 4 + if (PublicKey != PublicKey2) { + fmt.Fprintf(l.Stderr(), "Mismatch: the registration transaction is for public key: %s, but our public key is %s\n\n", PublicKey2, PublicKey) + break + } + + //At this point the address & public key in the transaction matchs our online wallet values. + reg_tx = & tx2 + } else { + // The offline wallet generates the registration and provide the transaction text to be used + // by the online wallet to complete the registration. + // The full function wallet (view+sign) generates the registration transaction and submits it + // directy to the network to complete the registration + if IsOffline==true { + fmt.Fprintf(l.Stderr(), "Generating registration transaction for wallet address : "+color_green+"%s"+color_white+"\n", wallet.GetAddress()) + } else { + fmt.Fprintf(l.Stderr(), "Wallet address : "+color_green+"%s"+color_white+" is going to be registered. Please wait till the account is registered. ", wallet.GetAddress()) + } + fmt.Fprintf(l.Stderr(), "This is a pre-condition POW for using the online chain. ") + fmt.Fprintf(l.Stderr(), "This will take a couple of minutes. A match is usually found between 2-5 million hashes. Please wait....\n") + + + successful_regs := make(chan *transaction.Transaction) + + counter := 0 + counter2 := 0 + + for i := 0; i < runtime.GOMAXPROCS(0); i++ { + go func() { + for counter == 0 { + + lreg_tx := wallet.GetRegistrationTX() + hash := lreg_tx.GetHash() + + if hash[0] == 0 && hash[1] == 0 && hash[2] == 0 { + fmt.Printf("Found transaction:\n"); + successful_regs <- lreg_tx + counter++ + break + } else { + counter2++ + if ((counter2 % 100000) == 0) { + //Match usually found round about 2 million mark + fmt.Printf("Searched %d hashes\n",counter2) + + //FIxIT quick search +// successful_regs <- lreg_tx +// counter++ +// break; + } + } } - } - }() - } + }() + } - reg_tx = <-successful_regs + reg_tx = <-successful_regs + } - fmt.Fprintf(l.Stderr(), "Registration TXID %s\n", reg_tx.GetHash()) - err := wallet.SendTransaction(reg_tx) - if err != nil { - fmt.Fprintf(l.Stderr(), "sending registration tx err %s\n", err) + if (IsOffline==true) { + // Offline wallet prints the prepared transaction, to be used in the online wallet + fmt.Printf("Found the registration transaction. Import the complete text into the online (view only) wallet:\n"); + sTransaction := fmt.Sprintf("registration,%s,%x,%s",wallet.GetAddress().String(), reg_tx.Serialize(), reg_tx.GetHash()) + + //Append a simple checksum to the string to detect copy/paste errors + //during import into the online wallet: + var iChecksum=0x01 + for t := range sTransaction { + iChecksum = iChecksum + (int)(sTransaction[t]) + } + + fmt.Printf("%s;%d\n\n",sTransaction, iChecksum) + } else { - fmt.Fprintf(l.Stderr(), "registration tx dispatched successfully\n") + // View only online wallet & full feature wallet submits the transaction to the network + fmt.Fprintf(l.Stderr(), "Registration TXID %s\n", reg_tx.GetHash()) + err := wallet.SendTransaction(reg_tx) + if err != nil { + fmt.Fprintf(l.Stderr(), "sending registration tx err %s\n\n", err) + } else { + fmt.Fprintf(l.Stderr(), "registration tx dispatched successfully\n\n") + } } - - } else { - } case "6": + if globals.Arguments["--offline"].(bool) { + //Offline wallet can“t send tokens + break; + } + if !valid_registration_or_display_error(l, wallet) { break } @@ -213,7 +407,7 @@ func handle_easymenu_post_open_command(l *readline.Instance, line string) (proce break // invalid amount provided, bail out } - if ConfirmYesNoDefaultNo(l, "Confirm Transaction (y/N)") { + if ConfirmYesNoDefaultNo(l, color_white+"Confirm Transaction (y/N)") { tx, err := wallet.TransferPayload0([]rpc.Transfer{{SCID: scid, Amount: amount_to_transfer, Destination: a.String()}}, 0, false, rpc.Arguments{}, 0, false) // empty SCDATA if err != nil { @@ -228,6 +422,49 @@ func handle_easymenu_post_open_command(l *readline.Instance, line string) (proce } case "5": + if globals.Arguments["--offline"].(bool) { + // Offline wallet: Sign transaction + if !ValidateCurrentPassword(l, wallet) { + logger.Error(fmt.Errorf("Invalid password"), "") + break + } + + remote_request_prefix="." + if globals.Arguments["--prefix"] != nil { + remote_request_prefix = globals.Arguments["--prefix"].(string) // override with user specified settings + } + + sFileIn:=remote_request_prefix+"/transaction" + sFileOut:=remote_request_prefix+"/offline_response" + _ = os.Remove(sFileOut) + + baData, err := os.ReadFile(sFileIn) + if err!=nil { + fmt.Printf("Can't read from the transaction file: %s\n",sFileIn); + break; + } + fmt.Printf("Read %d bytes from %s\n",len(baData),sFileIn) + sTransaction := string(baData[:]) + + _ = os.Remove(sFileIn) + + baData,err=sign_remote_transaction(sTransaction); + if err!=nil { + fmt.Printf("Error signing transaction: %s\n",err) + break; + } + + err = os.WriteFile(sFileOut, baData, 0644) + if err!=nil { + err = fmt.Errorf("Error saving to %s: %s\n",sFileOut,err) + break; + } + fmt.Printf("Saved result in %s\n",sFileOut) + break; + } + + + if !valid_registration_or_display_error(l, wallet) { break } @@ -251,6 +488,7 @@ func handle_easymenu_post_open_command(l *readline.Instance, line string) (proce // { rpc.RPC_EXPIRY , rpc.DataTime, time.Now().Add(time.Hour).UTC()}, // { rpc.RPC_COMMENT , rpc.DataString, "Purchase XYZ"}, } + if a.IsIntegratedAddress() { // read everything from the address if a.Arguments.Validate_Arguments() != nil { @@ -293,28 +531,28 @@ func handle_easymenu_post_open_command(l *readline.Instance, line string) (proce arguments = append(arguments, rpc.Argument{Name: arg.Name, DataType: arg.DataType, Value: v}) } else { logger.Error(fmt.Errorf("%s could not be parsed (type %s),", arg.Name, arg.DataType), "") - return + break } case rpc.DataInt64: if v, err := ReadInt64(l, arg.Name, arg.Value.(int64)); err == nil { arguments = append(arguments, rpc.Argument{Name: arg.Name, DataType: arg.DataType, Value: v}) } else { logger.Error(fmt.Errorf("%s could not be parsed (type %s),", arg.Name, arg.DataType), "") - return + break } case rpc.DataUint64: if v, err := ReadUint64(l, arg.Name, arg.Value.(uint64)); err == nil { arguments = append(arguments, rpc.Argument{Name: arg.Name, DataType: arg.DataType, Value: v}) } else { logger.Error(fmt.Errorf("%s could not be parsed (type %s),", arg.Name, arg.DataType), "") - return + break } case rpc.DataFloat64: if v, err := ReadFloat64(l, arg.Name, arg.Value.(float64)); err == nil { arguments = append(arguments, rpc.Argument{Name: arg.Name, DataType: arg.DataType, Value: v}) } else { logger.Error(fmt.Errorf("%s could not be parsed (type %s),", arg.Name, arg.DataType), "") - return + break } case rpc.DataTime: logger.Error(fmt.Errorf("time argument is currently not supported."), "") @@ -359,27 +597,28 @@ func handle_easymenu_post_open_command(l *readline.Instance, line string) (proce arguments = append(arguments, rpc.Argument{Name: rpc.RPC_DESTINATION_PORT, DataType: rpc.DataUint64, Value: v}) } else { logger.Error(err, fmt.Sprintf("%s could not be parsed (type %s),", "Number", rpc.DataUint64)) - return + break } if v, err := ReadString(l, "Comment", ""); err == nil { arguments = append(arguments, rpc.Argument{Name: rpc.RPC_COMMENT, DataType: rpc.DataString, Value: v}) } else { logger.Error(fmt.Errorf("%s could not be parsed (type %s),", "Comment", rpc.DataString), "") - return + break } } if _, err := arguments.CheckPack(transaction.PAYLOAD0_LIMIT); err != nil { logger.Error(err, "Arguments packing err") - return + break } - if ConfirmYesNoDefaultNo(l, "Confirm Transaction (y/N)") { + if ConfirmYesNoDefaultNo(l, color_white+"Confirm Transaction (y/N)") { //src_port := uint64(0xffffffffffffffff) - tx, err := wallet.TransferPayload0([]rpc.Transfer{{Amount: amount_to_transfer, Destination: a.String(), Payload_RPC: arguments}}, 0, false, rpc.Arguments{}, 0, false) // empty SCDATA + tx, err := wallet.TransferPayload0([]rpc.Transfer{{Amount: amount_to_transfer, Destination: a.String(), Payload_RPC: arguments}}, + 0, false, rpc.Arguments{}, 0, false) // empty SCDATA if err != nil { logger.Error(err, "Error while building Transaction") @@ -418,7 +657,7 @@ func handle_easymenu_post_open_command(l *readline.Instance, line string) (proce globals.Logger.Infof("Payment ID is integrated in address ID:%x", a.PaymentID) } - if ConfirmYesNoDefaultNo(l, "Confirm Transaction to send entire balance (y/N)") { + if ConfirmYesNoDefaultNo(l, color_white+"Confirm Transaction to send entire balance (y/N)") { addr_list := []address.Address{*a} amount_list := []uint64{0} // transfer 50 dero, 2 dero diff --git a/cmd/dero-wallet-cli/easymenu_pre_open.go b/cmd/dero-wallet-cli/easymenu_pre_open.go index b56ef134..87483865 100644 --- a/cmd/dero-wallet-cli/easymenu_pre_open.go +++ b/cmd/dero-wallet-cli/easymenu_pre_open.go @@ -22,7 +22,6 @@ import "time" import "strconv" import "strings" import "encoding/hex" - import "github.com/chzyer/readline" import "github.com/deroproject/derohe/cryptography/crypto" @@ -143,7 +142,13 @@ func handle_easymenu_pre_open_command(l *readline.Instance, line string) { break } - wallett, err = walletapi.Create_Encrypted_Wallet(filename, password, new(crypto.BNRed).SetBytes(seed_raw)) + seed := new(crypto.BNRed).SetBytes(seed_raw) + account,err2 := walletapi.Generate_Account_From_Seed ( seed ) + if err2 != nil { + logger.Error(err, "Could not use the seed to create an account") + } + + wallett, err = walletapi.Create_Encrypted_Wallet(filename, password, account) if err != nil { logger.Error(err, "Error while recovering wallet using seed key") break @@ -218,7 +223,15 @@ func handle_easymenu_pre_open_command(l *readline.Instance, line string) { // sets online mode, starts RPC server etc func common_processing(wallet *walletapi.Wallet_Disk) { if globals.Arguments["--offline"].(bool) == true { - //offline_mode = true + // For an offline (signing) wallet, we require the secret key: + account := wallet.GetAccount() + sSecret := fmt.Sprintf("%x", account.Keys.Secret.BigInt() ) + if ( len(sSecret)<=1 ) { + fmt.Printf("Your wallet doesn't have a secret key. Did you specify an online (view only) wallet (%s)?\n", globals.Arguments["--wallet-file"]) + //Exit application + globals.Exit_In_Progress = true + return + } } else { wallet.SetOnlineMode() } @@ -245,7 +258,11 @@ func common_processing(wallet *walletapi.Wallet_Disk) { wallet.SetSaveDuration(time.Duration(s) * time.Second) logger.Info("Wallet changes will be saved every", "duration (seconds)", wallet.SetSaveDuration(-1)) } + } else { + //Initialise save duration to the duration in the --help menu: 300 seconds + wallet.SetSaveDuration(300 * time.Second) } + wallet.SetNetwork(!globals.Arguments["--testnet"].(bool)) diff --git a/cmd/dero-wallet-cli/main.go b/cmd/dero-wallet-cli/main.go index 1982ca6c..49971eae 100644 --- a/cmd/dero-wallet-cli/main.go +++ b/cmd/dero-wallet-cli/main.go @@ -26,32 +26,22 @@ import "sync" import "strings" import "strconv" import "runtime" - +import "encoding/hex" import "sync/atomic" - -//import "io/ioutil" -//import "bufio" -//import "bytes" -//import "net/http" +import "github.com/deroproject/derohe/transaction" import "github.com/go-logr/logr" - import "github.com/chzyer/readline" import "github.com/docopt/docopt-go" -//import "github.com/vmihailenco/msgpack" - -//import "github.com/deroproject/derosuite/address" - import "github.com/deroproject/derohe/config" - -//import "github.com/deroproject/derohe/crypto" +import "github.com/deroproject/derohe/cryptography/bn256" +import "github.com/deroproject/derohe/cryptography/crypto" import "github.com/deroproject/derohe/globals" +import "github.com/deroproject/derohe/rpc" import "github.com/deroproject/derohe/walletapi" import "github.com/deroproject/derohe/walletapi/mnemonics" -//import "encoding/json" - var command_line string = `dero-wallet-cli DERO : A secure, private blockchain with smart-contracts @@ -63,37 +53,41 @@ Usage: Options: -h --help Show this screen. --version Show version. - --wallet-file= Use this file to restore or create new wallet + --wallet-file= Use this file to restore or create new wallet --password= Use this password to unlock the wallet - --offline Run the wallet in completely offline mode - --offline_datafile= Use the data in offline mode default ("getoutputs.bin") in current dir + --offline Run the wallet in offline (signing) mode. An online (view only) wallet is required to create the transaction & sync to the network + Full wallet & offline wallet: + --viewingkey Print the viewing key and exit + --regtx Generate the registration transaction with required POW and exit + --remotetx Process a balance or transaction sign request from a view only wallet and exit + --prefix=<.> The path to search for the remotetx requests. --prompt Disable menu and display prompt - --testnet Run in testnet mode. + --testnet Run in testnet mode. --debug Debug mode enabled, print log messages --unlocked Keep wallet unlocked for cli commands (Does not confirm password before commands) - --generate-new-wallet Generate new wallet - --restore-deterministic-wallet Restore wallet from previously saved recovery seed - --electrum-seed= Seed to use while restoring wallet - --socks-proxy= Use a proxy to connect to Daemon. + --generate-new-wallet Create a new wallet, using a randomly generated seed + --restore-viewonly-wallet Restore a view only wallet. The offline (signing) wallet contains the secret key & can export the view only key + --restore-deterministic-wallet Restore wallet from previously saved recovery seed + --electrum-seed= Seed to use while restoring wallet + --socks-proxy= Use a proxy to connect to Daemon. --remote use hard coded remote daemon https://rwallet.dero.live - --daemon-address= Use daemon instance at : or https://domain - --rpc-server Run rpc server, so wallet is accessible using api - --rpc-bind=<127.0.0.1:20209> Wallet binds on this ip address and port + --daemon-address= Use daemon instance at : or https://domain + --rpc-server Run rpc server, so wallet is accessible using api + --rpc-bind=<127.0.0.1:20209> Wallet binds on this ip address and port --rpc-login= RPC server will grant access based on these credentials - --allow-rpc-password-change RPC server will change password if you send "Pass" header with new password - --scan-top-n-blocks=<100000> Only scan top N blocks - --save-every-x-seconds=<300> Save wallet every x seconds + --allow-rpc-password-change RPC server will change password if you send "Pass" header with new password + --scan-top-n-blocks=<100000> Only scan top N blocks + --save-every-x-seconds=<300> Save wallet every x seconds ` var menu_mode bool = true // default display menu mode // var account_valid bool = false // if an account has been opened, do not allow to create new account in this session var offline_mode bool // whether we are in offline mode +var remote_request_prefix string var sync_in_progress int // whether sync is in progress with daemon var wallet *walletapi.Wallet_Disk //= &walletapi.Account{} // all account data is available here // var address string var sync_time time.Time // used to suitable update prompt -var default_offline_datafile string = "getoutputs.bin" - var logger logr.Logger = logr.Discard() // default discard all logs var color_black = "\033[30m" @@ -179,6 +173,8 @@ func main() { menu_mode = false } + offline_mode = globals.Arguments["--offline"].(bool) + wallet_file := "wallet.db" //dero.wallet" if globals.Arguments["--wallet-file"] != nil { wallet_file = globals.Arguments["--wallet-file"].(string) // override with user specified settings @@ -189,7 +185,11 @@ func main() { wallet_password = globals.Arguments["--password"].(string) // override with user specified settings } - // lets handle the arguments one by one + // lets handle the arguments one by one: + // Mutually exclusive commands: + // --restore-deterministic-wallet + // --generate-new-wallet + // if globals.Arguments["--restore-deterministic-wallet"].(bool) { // user wants to recover wallet, check whether seed is provided on command line, if not prompt now seed := "" @@ -210,32 +210,46 @@ func main() { password := "" if wallet_password == "" { password = ReadConfirmedPassword(l, "Enter password", "Confirm password") + } else { + //Use provided password from the command line + password = wallet_password } - wallet, err = walletapi.Create_Encrypted_Wallet(wallet_file, password, account.Keys.Secret) + wallet, err = walletapi.Create_Encrypted_Wallet(wallet_file, password, account) if err != nil { logger.Error(err, "Error occurred while restoring wallet") return } - logger.V(1).Info("Seed Language", "language", account.SeedLanguage) - logger.Info("Successfully recovered wallet from seed") - } - - // generare new random account if requested - if globals.Arguments["--generate-new-wallet"] != nil && globals.Arguments["--generate-new-wallet"].(bool) { + if (offline_mode==false) { + logger.V(1).Info("Seed Language", "language", account.SeedLanguage) + logger.Info("Successfully recovered wallet from seed") + } else { + fmt.Printf("Successfully recovered wallet from seed. Your address: %s\n", wallet.GetAddress() ) + globals.Exit_In_Progress=true + } + } else if globals.Arguments["--generate-new-wallet"] != nil && globals.Arguments["--generate-new-wallet"].(bool) { + // generare new random account var filename string if globals.Arguments["--wallet-file"] != nil && len(globals.Arguments["--wallet-file"].(string)) > 0 { filename = globals.Arguments["--wallet-file"].(string) } else { filename = choose_file_name(l) } + // Check right at the beginning if the file exist + if _, err = os.Stat(filename); err == nil { + fmt.Printf("File '%s' already exists\n", filename) + return + } // ask user a pass, if not provided on command_line password := "" if wallet_password == "" { password = ReadConfirmedPassword(l, "Enter password", "Confirm password") - } + } else { + //Use provided password from the command line + password = wallet_password + } seed_language := choose_seed_language(l) _ = seed_language @@ -245,8 +259,104 @@ func main() { wallet = nil return } + if wallet==nil { + logger.Error(err, "Internal error: Could not initialise the wallet.") + return + } logger.V(1).Info("Seed Language", "language", account.SeedLanguage) display_seed(l, wallet) + } else if globals.Arguments["--restore-viewonly-wallet"]!=nil && globals.Arguments["--restore-viewonly-wallet"].(bool) { + // Create a 'view only' account using the details obtained from the offline (signing) wallet + + filename := choose_file_name(l) + // Check right at the beginning if the file exist + if _, err = os.Stat(filename); err == nil { + fmt.Printf("File '%s' already exists\n", filename) + return + } + + // ask user a pass, if not provided on command_line + password := "" + if wallet_password == "" { + password = ReadConfirmedPassword(l, "Enter password", "Confirm password") + } else { + password = wallet_password + } + + //Format: [0] - preamble: viewkey + // [1] - Address + // [2] - Public key + // [3] - Public key internal data + // [4] - Checksum + var sViewOnly = read_line_with_prompt(l, "Enter the view only key (obtained from the offline (signing) wallet): ") + + //Strip off any newlines or extra spaces + sTmp := strings.ReplaceAll(sViewOnly,"\n","") + sViewOnly = strings.ReplaceAll(sTmp," ",""); + + saParts := strings.Split(sViewOnly, ";") + if (len(saParts)!=2) { + fmt.Printf("Invalid number of parts in the input. Expected 2 found %d\n",len(saParts)); + return + } + + sViewKey := saParts[0] + sProtocolChecksum := saParts[1] + iTmp,err := strconv.Atoi(sProtocolChecksum) + if err!=nil { + fmt.Fprintf(l.Stderr(),"Could not convert the checksum back to an integer: "+sProtocolChecksum+"\n") + return + } + iProtocolChecksum:=uint16(iTmp) + + //Regenerate checksum: + var iCalculatedChecksum uint16 + iCalculatedChecksum=0x1 + for t := range sViewKey { + iCalculatedChecksum = iCalculatedChecksum + (uint16)(sViewKey[t]) + } + + // Check 1: Checksum + if (iProtocolChecksum != iCalculatedChecksum) { + fmt.Printf("Checksum calculation failed. Please check if you've imported the view key correctly\n"); + return + } + + saParts = strings.Split(sViewKey,",") + if (len(saParts) != 4) { + fmt.Fprintf(l.Stderr(), "Invalid number of parts. Expected 4, found %d\n", len(saParts)) + return + } + + if (saParts[0]!="viewkey") { + fmt.Fprintf(l.Stderr(), "Input doesn't start with 'viewkey'\n"); + return + } + + //Send: Public key, public key internals + account,err := walletapi.Generate_Account_From_ViewOnly_params(saParts[2],saParts[3], globals.IsMainnet() ) + if err != nil { + logger.Error(err, "Error while recovering view only parameters.") + return + } + + wallet, err = walletapi.Create_Encrypted_Wallet(filename, password, account) + + if err != nil { + logger.Error(err, "Error occurred while restoring wallet") + return + } + + //Double check that the restored public key generates the expected dero address + sAddress := fmt.Sprintf("%s",wallet.GetAddress().String() ) + if (sAddress != saParts[1]) { + logger.Error(err, "The addres containted in the viewing key (%s) doesn't match the restored address (%s)\n", saParts[1], sAddress); + return + } + + fmt.Printf("Successfully restored an online (view only) wallet\n") + fmt.Printf(" Address: %s\n",sAddress) + fmt.Printf(" Public key: %s\n", wallet.Get_Keys().Public.StringHex()) } if globals.Arguments["--rpc-login"] != nil { @@ -284,19 +394,97 @@ func main() { } } } - //globals.Logger.Debugf("Seed Language %s", account.SeedLanguage) //globals.Logger.Infof("Successfully recovered wallet from seed") - } } - // check if offline mode requested - if wallet != nil { - common_processing(wallet) + if wallet == nil { + logger.Error(err, "Error occurred while opening wallet.") + return } + common_processing(wallet) + go walletapi.Keep_Connectivity() // maintain connectivity + + bResult := globals.Arguments["--viewingkey"].(bool) + if (bResult==true) { + display_viewing_key(wallet) + return; + } + bResult = globals.Arguments["--regtx"].(bool) + if (bResult==true) { + if (wallet.ViewOnly() == true) { + fmt.Fprintf(l.Stderr(), "A view only wallet cannot generate the registration transaction\n"); + return; + } + fmt.Fprintf(l.Stderr(), "Generating registration transaction for wallet address : "+color_green+"%s"+color_white+"\n", wallet.GetAddress()) + + successful_regs := make(chan *transaction.Transaction) + counter := 0 + counter2 := 0 + var reg_tx *transaction.Transaction + for i := 0; i < runtime.GOMAXPROCS(0); i++ { + go func() { + for counter == 0 { + lreg_tx := wallet.GetRegistrationTX() + hash := lreg_tx.GetHash() + + if hash[0] == 0 && hash[1] == 0 && hash[2] == 0 { + fmt.Printf("Found transaction:\n"); + successful_regs <- lreg_tx + counter++ + break + } else { + counter2++ + if ((counter2 % 10000) == 0) { + //Match usually found round about 2 million mark + fmt.Printf("Searched %d hashes\n",counter2) + } + } + } + }() + } + + reg_tx = <-successful_regs + + // Offline wallet: print the prepared transaction, to be used in the online wallet + fmt.Printf("Found the registration transaction. Import the complete text into the online (view only) wallet:\n"); + sTransaction := fmt.Sprintf("registration,%s,%x,%s",wallet.GetAddress().String(), reg_tx.Serialize(), reg_tx.GetHash()) + + //Append a simple checksum to the string to detect copy/paste errors + //during import into the online wallet: + var iChecksum=1 + for t := range sTransaction { + iChecksum = iChecksum + (int)(sTransaction[t]) + } + + fmt.Printf("%s;%d\n\n",sTransaction, iChecksum) + return + } + + //Any remote request to decrypt/sign? + // The 'remote_request' file must already be present on the filesystem + remote_request_prefix="." + if globals.Arguments["--prefix"] != nil { + remote_request_prefix = globals.Arguments["--prefix"].(string) // override with user specified settings + } + + bResult = globals.Arguments["--remotetx"].(bool) + if (bResult==true) { + if (wallet.ViewOnly() == true) { + fmt.Fprintf(l.Stderr(), "A view only wallet cannot process remote transactions\n"); + return; + } + + err = process_remote_requests(true,remote_request_prefix) + if err!=nil { + fmt.Printf("\n\nRemote request error: %s\n",err) + } + return + } + //pipe_reader, pipe_writer = io.Pipe() // create pipes // reader ready to parse any data from the file @@ -373,6 +561,19 @@ func update_prompt(l *readline.Instance) { last_daemon_height := int64(0) daemon_online := false last_update_time := int64(0) + + // show first 8 bytes of address + address_trim := "" + if wallet != nil { + tmp_addr := wallet.GetAddress().String() + address_trim = tmp_addr[0:8] + } else { + address_trim = "DERO Wallet" + } + + + + for { time.Sleep(30 * time.Millisecond) // give user a smooth running number @@ -389,14 +590,7 @@ func update_prompt(l *readline.Instance) { prompt_mutex.Lock() // do not update if we can not lock the mutex - // show first 8 bytes of address - address_trim := "" - if wallet != nil { - tmp_addr := wallet.GetAddress().String() - address_trim = tmp_addr[0:8] - } else { - address_trim = "DERO Wallet" - } + if wallet == nil { l.SetPrompt(fmt.Sprintf("\033[1m\033[32m%s \033[0m"+color_green+"0/%d \033[32m>>>\033[0m ", address_trim, walletapi.Get_Daemon_Height())) @@ -410,8 +604,9 @@ func update_prompt(l *readline.Instance) { _ = daemon_online //fmt.Printf("chekcing if update is required\n") + // Dero blocktime ~18 seconds. Check for new blocks every 15 seconds if last_wallet_height != wallet.Get_Height() || last_daemon_height != walletapi.Get_Daemon_Height() || - /*daemon_online != wallet.IsDaemonOnlineCached() ||*/ (time.Now().Unix()-last_update_time) >= 1 { + (time.Now().Unix()-last_update_time) >= 15 { // choose color based on urgency color := "\033[32m" // default is green color if wallet.Get_Height() < wallet.Get_Daemon_Height() { @@ -453,9 +648,15 @@ func update_prompt(l *readline.Instance) { } prompt_mutex.Unlock() - + + //test for an incomming request to interact with the secret key + //The online (view only) wallet uses this to reconstruct the account balance & transaction history + var err error; + err=process_remote_requests(false,remote_request_prefix) + if err!=nil { + fmt.Printf("Remote request error: %s\n",err) + } } - } // create a new wallet from scratch from random numbers @@ -469,7 +670,7 @@ func Create_New_Wallet(l *readline.Instance) (w *walletapi.Wallet_Disk, err erro account, _ := walletapi.Generate_Keys_From_Random() account.SeedLanguage = choose_seed_language(l) - w, err = walletapi.Create_Encrypted_Wallet(walletpath, walletpassword, account.Keys.Secret) + w, err = walletapi.Create_Encrypted_Wallet(walletpath, walletpassword, account) if err != nil { return @@ -619,3 +820,550 @@ func filterInput(r rune) (rune, bool) { } return r, true } + +//Look for a request file (./offline_request) from the online wallet. The wallet with +//the secret key (full wallet or offline wallet) can process the request. +//The online (view only) wallet uses the response to reconstruct the account balance, +//transaction history and to broadcast signed transactions +//Input: bSignTransactions - If starting app with --remotetx then balance requests and +// transaction sign requests will be processed. +// - If running interactively (without --remotetx) then only +// balance requests will be processed automatically. The user +// has to choose explicitly from the menu to sign a transaction +func process_remote_requests(bSignTransactions bool,sPrefix string) (err error) { + if (wallet.ViewOnly() == false) { + sFileRequest:=sPrefix+"/offline_request" + + if _, err := os.Stat(sFileRequest); err != nil { + return nil + } + + fmt.Printf("Found %s/offline_request -- new decryption request\n",sPrefix) + + baData, err := os.ReadFile(sFileRequest) + if err!=nil { + err = fmt.Errorf("Could not read from %s. Check the file permissions.\n",sFileRequest); + return err + } + + _ = os.Remove(sFileRequest) + if _, err = os.Stat(sFileRequest); err == nil { + err = fmt.Errorf("Could not delete %s\n",sFileRequest) + return err + } + + //Parameter [0]: Project - 'dero' + // [1]: Version - Layout of the command fields + // [2]: header: scalar_mult, shared_secret, sign_offline + // Version 1: + // scalar_mult & shared_secret: + // [3] el.Right + // sign_offline + // [3]..[11] + // ; Checksum of all the characters in the data stream + sInput := string(baData[:]) + sInput = strings.TrimSpace(sInput) + saParts := strings.Split(sInput,";") + if (len(saParts) != 2) { + err = fmt.Errorf("Invalid number of parts in the transaction. Expected 2, found %d\n", len(saParts)) + return err + } + + var iCalculatedChecksum uint16 + iCalculatedChecksum=0x01; + for t := range saParts[0] { + iVal := uint16(saParts[0][t]) + iCalculatedChecksum = iCalculatedChecksum + iVal; + } + + sProtocolChecksum := saParts[1] + iTmp,err := strconv.Atoi(sProtocolChecksum) + if err!=nil { + err = fmt.Errorf("Could not convert the checksum back to an integer: "+saParts[1]+" ; "+sProtocolChecksum+"\n") + return err + } + iProtocolChecksum:=uint16(iTmp) + + + if (iProtocolChecksum!=iCalculatedChecksum) { + err = fmt.Errorf("The checksum of the request data is invalid. Protocol: %u, Calculates: %u\n", iProtocolChecksum, iCalculatedChecksum) + return err + } + + saFields := strings.Split(saParts[0]," ") + if (len(saFields) < 4) { + err = fmt.Errorf("Invalid number of parts in the transaction. Expected at least 4, found %d\n", len(saFields)) + return err + } + + if (saFields[0] != "dero") { + err = fmt.Errorf("Expected a Dero transaction, Found %s\n",saFields[0]); + return err + } + + if (saFields[1] != "1") { + err = fmt.Errorf("Only transaction version 1 supported. Found %s\n",saFields[1]) + return err + } + + if ((saFields[2] != "scalar_mult") && + (saFields[2] != "shared_secret") && + (saFields[2] != "sign_offline")) { + err = fmt.Errorf("Transaction doesn't start with 'scalar_mult', 'shared_secret' or 'sign_offline'\n") + return err + } + + keys := wallet.Get_Keys() + if (saFields[2]=="scalar_mult") { + if (len(saFields) != 4) { + err = fmt.Errorf("Invalid number of parts in the transaction. Expected 4, found %d\n", len(saFields)) + return err + } + + baData,err = hex.DecodeString(saFields[3]) + if err!=nil { + err = fmt.Errorf("Could not hex decode the data portion\n"); + return err + } + + var elRight *bn256.G1 + elRight = new(bn256.G1) + elRight.Unmarshal(baData) + + scalarMultResult := new(bn256.G1).ScalarMult(elRight, keys.Secret.BigInt()) + baData = scalarMultResult.Marshal() + + sOutput := fmt.Sprintf("dero 1 scalar_mult_result %x",baData) + var iCalculatedChecksum uint16 + iCalculatedChecksum=0x01 + for t := range sOutput { + iCalculatedChecksum = iCalculatedChecksum + (uint16)(sOutput[t]) + } + sOutput = fmt.Sprintf("%s;%d",sOutput, iCalculatedChecksum) + baData = []byte(sOutput) + } else if (saFields[2]=="shared_secret") { + if (len(saFields) != 4) { + err = fmt.Errorf("Invalid number of parts in the transaction. Expected 4, found %d\n", len(saFields)) + return err + } + + baData,err = hex.DecodeString(saFields[3]) + if err!=nil { + err = fmt.Errorf("Could not hex decode the data portion\n"); + return err + } + + fmt.Printf("processing shared_secret request\n"); + var peer_publickey *bn256.G1 + peer_publickey = new(bn256.G1) + peer_publickey.Unmarshal(baData) + + shared_key := crypto.GenerateSharedSecret(keys.Secret.BigInt(), peer_publickey) + + sOutput := fmt.Sprintf("dero 1 shared_secret_result %x",shared_key) + var iCalculatedChecksum uint16 + iCalculatedChecksum=0x01 + for t := range sOutput { + iCalculatedChecksum = iCalculatedChecksum + (uint16)(sOutput[t]) + } + sOutput = fmt.Sprintf("%s;%d",sOutput, iCalculatedChecksum) + baData = []byte(sOutput) + } else if (saFields[2]=="sign_offline") { + if (len(saFields) < 12) { + err = fmt.Errorf("Invalid number of parts in the transaction. Expected 12, found %d\n", len(saFields)) + return err + } + + if (bSignTransactions==false) { + //Process sign transaction request. If running in interactive mode, + //the user must select 'sign' from the menu to authorise the activity + //During interactive mode, the input filename is 'transaction' + err = os.WriteFile(sPrefix+"/transaction", baData, 0644) + if err!=nil { + err = fmt.Errorf("Error saving file: %s\n",err) + return err + } + + fmt.Printf("Detected new transaction sign request. Authorise the request with menu option '5: Sign'.\n"); + //User must authorise the signature through the menu system: + // 5. Sign (DERO) transaction, prepared by the online (view only) wallet + return nil + } else { + //Sign the transaction when launching app with --remotetx flag: + sOutput:=string(baData[:]) + baData,err = sign_remote_transaction(sOutput) + if err!=nil { + err = fmt.Errorf("Error signing transaction: %s\n",err) + return err; + } + } + } else { + err = fmt.Errorf("Unknown type request. Only scalar_mult and shared_secret supported\n"); + return err + } + + err = os.WriteFile(sPrefix+"/offline_response", baData, 0644) + if err!=nil { + err = fmt.Errorf("Error saving file. %s\n",err) + return err + } + fmt.Printf("Saved result in %s/offline_response\n",sPrefix) + } + + return nil +} + +func sign_remote_transaction(sTransaction string) (baData []byte, err error){ + // Transaction structure in the file: + //Parameter [0] Project - 'dero' + // [1] Version - Layout of the command fields + // [2] Command: sign_offline + // Version 1: [3] Array of transfers (outputs) + // [4] Array of ring balances + // [5] Array of rings + // [6] block_hash + // [7] height + // [8] Array of scdata + // [9] treehash + // [10] max_bits + // [11] gasstorage + // [12] account balance + // ; Checksum of all the characters in the command. + + //Split string on ';' + saParts := strings.Split(sTransaction,";") + if (len(saParts) != 2) { + err = fmt.Errorf("Invalid number of parts. Expected 2, found %d\n\n", len(saParts)) + return nil,err + } + + sTransaction = saParts[0] + sProtocolChecksum := saParts[1] + iTmp,err := strconv.Atoi(sProtocolChecksum) + if err!=nil { + err = fmt.Errorf("Could not convert the checksum back to an integer: "+sProtocolChecksum+"\n") + return nil,err + } + iProtocolChecksum := uint16(iTmp) + + //Regenerate checksum: + var iCalculatedChecksum uint16 + iCalculatedChecksum=0x01 + for t := range sTransaction { + iCalculatedChecksum = iCalculatedChecksum + (uint16)(sTransaction[t]) + } + + //fmt.Printf("Checksum input\n'%s'\n\n",sTransaction); + // Check 1: Checksum + if (iProtocolChecksum != iCalculatedChecksum) { + err = fmt.Errorf("Checksum calculation failed. Please check if you've imported the transaction correctly. Protocol: %u, calculated: %u\n\n", iProtocolChecksum, iCalculatedChecksum) + return nil,err + } + + saParts = strings.Split(sTransaction," ") + if (len(saParts)!=13) { + err = fmt.Errorf("Invalid number of parts in the transaction. Expected 13, found %d\n", len(saParts)) + return nil,err + } + + if (saParts[0] != "dero") { + err = fmt.Errorf("Expected a Dero transaction, Found %s\n",saParts[1]); + return nil,err + } + + if (saParts[1] != "1") { + err = fmt.Errorf("Only transaction version 1 supported. Found %s\n",saParts[2]) + return nil,err + } + + if (saParts[2] != "sign_offline") { + err = fmt.Errorf("Transaction doesn't start with 'sign_offline'\n") + return nil,err + } + + //sChecksumInput:="dero 1 sign_offline "; + //--------------------------------------------------------------------------------------------------------------------- + var transfers []rpc.Transfer + + sTmp := strings.ReplaceAll(saParts[3],"'","") + sTmp1:= strings.ReplaceAll(sTmp, "[","") + sTmp = strings.ReplaceAll(sTmp1,"]","") + + saTransfers := strings.SplitAfter(sTmp, "}") + //fmt.Printf("transfers:%d\n",len(saTransfers)-1); + //sChecksumInput+="transfers:"; + for t:=range saTransfers { + if len(saTransfers[t])>0 { + var transfer rpc.Transfer + + sTmp1 = strings.ReplaceAll(saTransfers[t], ",{","") + sTmp = strings.ReplaceAll(sTmp1, "{","") + sTransfer := strings.ReplaceAll(sTmp,"}","") + + saParts := strings.Split(sTransfer,",") + if (len(saParts)!=5) { + err = fmt.Errorf("Parse error. Invalid number of parts in transfers. Expected 5, found %d\n", len(saParts)); + return nil,err + } + + sTmp:=strings.ReplaceAll(saParts[0],"\"","") + saItemValue:=strings.Split(sTmp,":") + if (saItemValue[0]!="SCID") { + err = fmt.Errorf("Parse error. Could not find SCID in transfers\n"); + return nil,err + } + err = transfer.SCID.UnmarshalText([]byte(string(saItemValue[1]))) + if err != nil { + err = fmt.Errorf("Parse error. Could not assign SCID to transfers\n"); + return nil,err + } + //sChecksumInput+=" \""+saItemValue[1]+"\"" + + + sTmp=strings.ReplaceAll(saParts[1],"\"","") + saItemValue=strings.Split(sTmp,":") + if (saItemValue[0]!="Destination") { + err = fmt.Errorf("Parse error. Could not find destination in transfers\n"); + return nil,err + } + transfer.Destination = saItemValue[1] + //sChecksumInput+=" \""+saItemValue[1]+"\"" + + sTmp=strings.ReplaceAll(saParts[2],"\"","") + saItemValue=strings.Split(sTmp,":") + if (saItemValue[0]!="Amount") { + err = fmt.Errorf("Parse error. Could not find amount in transfers\n"); + return nil,err + } + transfer.Amount, err = strconv.ParseUint(saItemValue[1],10,64) + //sChecksumInput+=" "+saItemValue[1] + + sTmp=strings.ReplaceAll(saParts[3],"\"","") + saItemValue=strings.Split(sTmp,":") + if (saItemValue[0]!="Burn") { + err = fmt.Errorf("Parse error. Could not find burn in transfers\n"); + return nil,err + } + transfer.Burn, err = strconv.ParseUint(saItemValue[1],10,64) + //sChecksumInput+=" "+saItemValue[1] + + sTmp=strings.ReplaceAll(saParts[4],"\"","") + saItemValue=strings.Split(sTmp,":") + if (saItemValue[0]!="RPC") { + err = fmt.Errorf("Parse error. Could not find rpc in transfers\n"); + return nil,err + } + hexRPC, err2 := hex.DecodeString(saItemValue[1]) + if err2 != nil { + err = fmt.Errorf("Parse error. Could not hex decode the rpc field of the transfers\n"); + return nil,err + } + err2 = transfer.Payload_RPC.UnmarshalBinary(hexRPC) + if err2 != nil { + err = fmt.Errorf("Parse error. Could not assign the rpc field of the transfers\n"); + return nil,err + } + //sChecksumInput+=" \""+saItemValue[1]+"\"" + + transfers = append(transfers, transfer) + } + } + + //--------------------------------------------------------------------------------------------------------------------- + var rings_balances [][][]byte //initialize all maps + + sTmp = strings.ReplaceAll(saParts[4],"'","") + sTmp1 = strings.ReplaceAll(sTmp, "[","") + sTmp = strings.ReplaceAll(sTmp1,"]","") + + saRingBalances := strings.SplitAfter(sTmp, "}") + + //sChecksumInput+=" rings_balances:" + for t:=range saRingBalances { + if len(saRingBalances[t])>0 { + var ring_balances [][]byte + + sTmp1 = strings.ReplaceAll(saRingBalances[t], ",{","") + sTmp = strings.ReplaceAll(sTmp1, "{","") + sRingBalance := strings.ReplaceAll(sTmp,"}","") + + saRingBalances:=strings.Split(sRingBalance,",") + if ( len(saRingBalances)<16) { + err = fmt.Errorf("Expected at least 16 ring balances. Found %d\n", len(saRingBalances)) + return nil,err + } + + for t := range saRingBalances { + baData,_ := hex.DecodeString( saRingBalances[t] ) + ring_balances = append(ring_balances,baData) + //sChecksumInput+=" "+saRingBalances[t] + } + rings_balances = append(rings_balances, ring_balances) + } + } + /* + fmt.Printf("rings_balances:{%d} entries\n",len(rings_balances)) + var counter1=0 + var counter2=0 + for t := range rings_balances { + fmt.Printf("rings_balances{%d}:{%d} entries\n",counter1,len(rings_balances[t])) + counter2=0 + for u := range rings_balances[t] { + fmt.Printf(" ring balance[%d]=\"%x\"\n",counter2,rings_balances[t][u]) + counter2++ + } + counter1++ + } + */ + //--------------------------------------------------------------------------------------------------------------------- + var rings [][]*bn256.G1 + //sChecksumInput+=" rings:"; + + sTmp = strings.ReplaceAll(saParts[5],"'","") + sTmp1 = strings.ReplaceAll(sTmp, "[","") + sTmp = strings.ReplaceAll(sTmp1,"]","") + + saRings := strings.SplitAfter(sTmp, "}") + //fmt.Printf("Rings:%d\n",len(saRings)-1); + + for t:=range saRings { + if len(saRings[t])>0 { + var ring []*bn256.G1 + var oG1 *bn256.G1 + + sTmp1 = strings.ReplaceAll(saRings[t], ",{","") + sTmp = strings.ReplaceAll(sTmp1, "{","") + sRing := strings.ReplaceAll(sTmp,"}","") + + saRing:=strings.Split(sRing,",") + //fmt.Printf("saRing=%d\n",len(saRing)) + if ( len(saRing)<16) { + err = fmt.Errorf("Expected at least 16 rings. Found %d\n\n", len(saRing)) + return nil,err + } + + for t := range saRing { + //fmt.Printf("Processing: '%s'\n", saRing[t]) + baData,err := hex.DecodeString( saRing[t] ) + if err!=nil { + err = fmt.Errorf("Could not decode ring entry: %s\n\n", saRing[t]) + return nil,err + } + oG1 = new(bn256.G1); + _,err = oG1.Unmarshal(baData); + if err != nil { + err = fmt.Errorf("Could not assign ring data\n\n"); + return nil,err + }; + ring = append(ring,oG1) + + //sChecksumInput+=" "+saRing[t] + } + rings = append(rings, ring) + } + } + /* + fmt.Printf("rings:{%d} entries\n",len(rings)) + counter1=0 + counter2=0 + for t := range rings { + fmt.Printf("ring{%d}:{%d} entries\n",counter1,len(rings[t])) + counter2=0 + for u := range rings[t] { + fmt.Printf(" ring[%d]=\"%x\"\n",counter2,rings[t][u].Marshal() ) + counter2++ + } + counter1++ + } + */ + //--------------------------------------------------------------------------------------------------------------------- + //[6] block_hash + sTmp = strings.ReplaceAll(saParts[6],"\"","") + block_hash := crypto.HashHexToHash(sTmp) + //sChecksumInput+=" "+saParts[6] + + //[7] height + var height uint64 + height, err = strconv.ParseUint(saParts[7],10,64) + if err!=nil { + err = fmt.Errorf("Parse error. Could not assign height to transfers\n\n"); + return nil,err + } + //sChecksumInput+=" "+saParts[7] + + //[8] Array of scdata + var scdata rpc.Arguments + sTmp = strings.ReplaceAll(saParts[8],"'","") + sTmp1 = strings.ReplaceAll(sTmp, "[","") + sTmp = strings.ReplaceAll(sTmp1,"]","") + if (len(sTmp) > 0) { + hexSCData, err2 := hex.DecodeString(sTmp) + if err2!=nil { + err = fmt.Errorf("Parse error. Could not decode the SCData\n\n"); + return nil,err + } + if (len(hexSCData)>0) { + err = scdata.UnmarshalBinary(hexSCData) + if err!=nil { + err = fmt.Errorf("Parse error. Could not decode the SCData\n\n"); + return nil,err + } + } + } + //sChecksumInput+=" "+saParts[8] + + //[9] treehash + sTmp = strings.ReplaceAll(saParts[9],"\"","") + treehash_raw, err := hex.DecodeString(sTmp) + if err != nil { + err = fmt.Errorf("Parse error. Could not decode treehash_raw\n\n") + return nil,err + } + //sChecksumInput+=" "+saParts[9] + + //[10] max_bits + var max_bits int + max_bits, err = strconv.Atoi(saParts[10]) + if err != nil { + err = fmt.Errorf("Parse error. Could not decode max_bits\n\n") + return nil,err + } + + //[11] gasstorage + var gasstorage uint64 + gasstorage, err = strconv.ParseUint(saParts[11],16,64) + if err != nil { + err = fmt.Errorf("Parse error. Could not decode gasstorage\n\n") + return nil,err + } + + //[12] current balance + var current_balance uint64 + current_balance, err = strconv.ParseUint(saParts[12],16,64) + if err != nil { + err = fmt.Errorf("Parse error. Could not decode current_balancew\n\n") + return nil,err + } + fmt.Printf("Account balance: %s\n", globals.FormatMoney(current_balance) ) + + tx := wallet.BuildTransaction(transfers, rings_balances, rings, block_hash, height, scdata, treehash_raw, max_bits, gasstorage) + if tx == nil { + err = fmt.Errorf("The transaction could not be reconstructed, please retry\n\n") + return nil,err + } + + sTxSerialized := tx.Serialize() + sOutput := fmt.Sprintf("dero 1 signed %x",sTxSerialized) + + iCalculatedChecksum=0x01; + for t := range sOutput { + iVal := uint16(sOutput[t]) + iCalculatedChecksum = iCalculatedChecksum + iVal; + } + sCalculatedChecksum := fmt.Sprintf("%d",iCalculatedChecksum) + sOutput+=";"+sCalculatedChecksum + + baData = []byte( sOutput ) + + return baData,nil +} diff --git a/cmd/dero-wallet-cli/prompt.go b/cmd/dero-wallet-cli/prompt.go index 4ffb1442..0d2a671f 100644 --- a/cmd/dero-wallet-cli/prompt.go +++ b/cmd/dero-wallet-cli/prompt.go @@ -344,7 +344,7 @@ func handle_prompt_command(l *readline.Instance, line string) { logger.Error(err, "Error Parsing burn amount", "raw", line_parts[1]) return } - if ConfirmYesNoDefaultNo(l, "Confirm Transaction (y/N)") { + if ConfirmYesNoDefaultNo(l, color_white+"Confirm Transaction (y/N)") { //uid, err := wallet.PoolTransferWithBurn(addr, send_amount, burn_amount, data, rpc.Arguments{}) @@ -534,11 +534,13 @@ func ReadAddress(l *readline.Instance, wallet *walletapi.Wallet_Disk) (a *rpc.Ad if len(line) >= 1 { _, err := globals.ParseValidateAddress(string(line)) - if err != nil { + if err == nil { + //Verify online, once a valid address is provided if linestr, err = wallet.NameToAddress(string(strings.TrimSpace(string(line)))); err != nil { - error_message = " " //err.Error() - } else { - + sTmp := fmt.Sprintf("%s", err.Error()) + if !strings.Contains(sTmp,"leaf not found") { + error_message = " " //err.Error() + } } } } @@ -997,10 +999,17 @@ func usage(w io.Writer) { // display seed to the user in his preferred language func display_seed(l *readline.Instance, wallet *walletapi.Wallet_Disk) { - seed := wallet.GetSeed() - fmt.Fprintf(l.Stderr(), color_green+"PLEASE NOTE: the following 25 words can be used to recover access to your wallet. Please write them down and store them somewhere safe and secure. Please do not store them in your email or on file storage services outside of your immediate control."+color_white+"\n") - fmt.Fprintf(os.Stderr, color_red+"%s"+color_white+"\n", seed) - + if (wallet==nil) { + fmt.Fprintf(os.Stderr,"The wallet file is uninitialised!\n") + os.Exit(0) + } + if (wallet.ViewOnly() == false) { + seed := wallet.GetSeed() + fmt.Fprintf(l.Stderr(), color_green+"PLEASE NOTE: the following 25 words can be used to recover access to your wallet. Please write them down and store them somewhere safe and secure. Please do not store them in your email or on file storage services outside of your immediate control."+color_white+"\n") + fmt.Fprintf(os.Stderr, color_red+"%s"+color_white+"\n", seed) + } else { + fmt.Fprintf(os.Stderr,"This is a view only wallet. It doesn't contain the seed.\n") + } } // display spend key @@ -1009,10 +1018,44 @@ func display_seed(l *readline.Instance, wallet *walletapi.Wallet_Disk) { func display_spend_key(l *readline.Instance, wallet *walletapi.Wallet_Disk) { keys := wallet.Get_Keys() - h := "0000000000000000000000000000000000000000000000" + keys.Secret.Text(16) - fmt.Fprintf(os.Stderr, "secret key: "+color_red+"%s"+color_white+"\n", h[len(h)-64:]) + h := "0000000000000000000000000000000000000000000000" + keys.Secret.Text(16) + + var IsOffline = globals.Arguments["--offline"].(bool) + if (IsOffline==true) || (wallet.ViewOnly() == false) { + // Offline (signing) wallet + // Full featured online wallet + fmt.Fprintf(os.Stderr, "secret key: "+color_red+"%s"+color_white+"\n", h[len(h)-64:]) + } else { + // Online (view only) wallet + fmt.Fprintf(os.Stderr,"secret key: None -- View only wallet\n") + } + + // All wallets: + fmt.Fprintf(os.Stderr, "public key: %s\n", keys.Public.StringHex()) + + if (IsOffline==true) { + // Offline (signing) wallet + fmt.Printf("\nView only key - Import the complete text into the online (view only) wallet to set it up:\n") + display_viewing_key(wallet) + } +} + +func display_viewing_key (wallet *walletapi.Wallet_Disk) { + if (wallet.ViewOnly() == true) { + fmt.Printf("A view only wallet cannot generate the viewing key\n"); + return; + } + + keys := wallet.Get_Keys() + sViewOnlyKey := fmt.Sprintf("viewkey,%s,%s,%x",wallet.GetAddress(), keys.Public.StringHex(), keys.Public.G1().Marshal()) - fmt.Fprintf(os.Stderr, "public key: %s\n", keys.Public.StringHex()) + //Append a simple checksum to the string to detect copy/paste errors + //during import into the online wallet: + var iChecksum=1 + for t := range sViewOnlyKey { + iChecksum = iChecksum + (int)(sViewOnlyKey[t]) + } + fmt.Printf("%s;%d\n\n",sViewOnlyKey, iChecksum) } // start a rescan from block 0 diff --git a/images/0_export_view_key.jpg b/images/0_export_view_key.jpg new file mode 100644 index 00000000..e81aba42 Binary files /dev/null and b/images/0_export_view_key.jpg differ diff --git a/images/1_import_viewing_key.jpg b/images/1_import_viewing_key.jpg new file mode 100644 index 00000000..3f1e2776 Binary files /dev/null and b/images/1_import_viewing_key.jpg differ diff --git a/images/2_register_account.jpg b/images/2_register_account.jpg new file mode 100644 index 00000000..f9569561 Binary files /dev/null and b/images/2_register_account.jpg differ diff --git a/images/3_spend_transaction.jpg b/images/3_spend_transaction.jpg new file mode 100644 index 00000000..1282733c Binary files /dev/null and b/images/3_spend_transaction.jpg differ diff --git a/walletapi/balance_decoder.go b/walletapi/balance_decoder.go index 7839106a..519f56e2 100644 --- a/walletapi/balance_decoder.go +++ b/walletapi/balance_decoder.go @@ -21,7 +21,6 @@ import "fmt" import "sort" import "math/big" import "encoding/binary" - //import "github.com/mattn/go-isatty" //import "github.com/cheggaaa/pb/v3" @@ -178,9 +177,9 @@ func (t *LookupTable) Lookup(p *bn256.G1, previous_balance uint64) (balance uint } loop_counter++ - //if loop_counter >= 10 { - // break; - // } + if loop_counter >= 100 { + break; + } compressed := pcopy.EncodeCompressed() @@ -225,7 +224,7 @@ func (t *LookupTable) Lookup(p *bn256.G1, previous_balance uint64) (balance uint } - //panic(fmt.Sprintf("balance not yet found, work done %x", balance)) + panic(fmt.Sprintf("\nCould not decode the balance from the transaction data. Please verify that your secret key is correct\n\n")) //return balance } diff --git a/walletapi/daemon_communication.go b/walletapi/daemon_communication.go index cf0e9c4d..09b71232 100644 --- a/walletapi/daemon_communication.go +++ b/walletapi/daemon_communication.go @@ -23,7 +23,7 @@ package walletapi * * */ //import "io" -//import "os" +import "os" import ( "bytes" "context" @@ -32,9 +32,10 @@ import ( "math/big" "runtime/debug" "strings" + "strconv" "sync" "time" - + "github.com/creachadair/jrpc2" "github.com/deroproject/derohe/block" "github.com/deroproject/derohe/config" @@ -246,7 +247,6 @@ func IsDaemonOnline() bool { // sync the wallet with daemon, this is instantaneous and can be done with a single call // we have now the apis to avoid polling func (w *Wallet_Memory) Sync_Wallet_Memory_With_Daemon_internal(scid crypto.Hash) (err error) { - if !IsDaemonOnline() { daemon_height = 0 daemon_topoheight = 0 @@ -261,7 +261,7 @@ func (w *Wallet_Memory) Sync_Wallet_Memory_With_Daemon_internal(scid crypto.Hash //fmt.Printf("data '%s' previous '%s' scid %s\n", w.account.Balance_Result[scid].Data, previous, scid) if w.getEncryptedBalanceresult(scid).Data != previous { b := w.DecodeEncryptedBalanceNow(e) // try to decode balance - + if scid.IsZero() { w.account.Balance_Mature = b } @@ -339,12 +339,217 @@ func (w *Wallet_Memory) SendTransaction(tx *transaction.Transaction) (err error) return } + + +type OfflineOperations_t struct { + iType int + baRequest []byte + baResult []byte +} +var saOfflineOperations []OfflineOperations_t + +func (w *Wallet_Memory) OfflineOperationWithSecretKey(iType int, baData []byte) (result []byte, err error) { + if (w.ViewOnly()==false) { + err = fmt.Errorf("OfflineOperationWithSecretKey() should only be used by view only wallets\n") + return + } + + remote_request_prefix:="." + if globals.Arguments["--prefix"] != nil { + remote_request_prefix = globals.Arguments["--prefix"].(string) // override with user specified settings + } + + //Clean up temporary files: + sFileIn:=remote_request_prefix+"/offline_response" + _ = os.Remove(sFileIn) + if _, err = os.Stat(sFileIn); err == nil { + err = fmt.Errorf("Could not delete %s\n",sFileIn) + return + } + err=nil + + sFileOut:=remote_request_prefix+"/offline_request" + _ = os.Remove(sFileOut) + if _, err = os.Stat(sFileOut); err == nil { + err = fmt.Errorf("Could not delete %s\n",sFileOut) + return + } + err=nil + + var sType string + if (iType==0) { + sType = "scalar_mult" + } else if (iType==1) { + sType = "shared_secret" + } else { + err = fmt.Errorf("Unsupported type: %d\n", iType) + return + } + + //During the processing of the blockchain data, the same transaction + //will appear mutiple times. Test to see if we can simply return the + //previously obtained result: + for t := range saOfflineOperations { + if (saOfflineOperations[t].iType == iType) { + if bytes.Equal(saOfflineOperations[t].baRequest,baData) { + result = make([]byte, len(saOfflineOperations[t].baResult)) + copy (result,saOfflineOperations[t].baResult) + return; + } + } + } + + //Parameter [0] Project - 'dero' + // [1] Version - Layout of the command fields + // [2] action: 0=scalar mult, 1=shared secret + // Version 1: [3] data + // [4] Checksum of all the characters in the data stream + sOutput := fmt.Sprintf("dero 1 %s %x",sType,baData) + var iCalculatedChecksum uint16 + iCalculatedChecksum=0x01 + for t := range sOutput { + iCalculatedChecksum = iCalculatedChecksum + (uint16)(sOutput[t]) + } + sOutput = fmt.Sprintf("%s;%d",sOutput, iCalculatedChecksum) + + baOutput := []byte(sOutput) + + err = os.WriteFile(sFileOut, baOutput, 0644) + if err!=nil { + err = fmt.Errorf("Error saving file. %s\n",err) + return + } + fmt.Printf("\nInteraction with offline wallet required. Saved request to: %s\nWaiting 60 seconds for the response at: %s\n",sFileOut,sFileIn) + + bFound:=0 + counter:=0 + for counter <= 60 { + if _, err := os.Stat(sFileIn); err == nil { + bFound=1 + break; + } + counter++ + time.Sleep(1 * time.Second) + } + + if (bFound==0) { + err = fmt.Errorf("Timeout: No result from the offline wallet.\n"); + return; + } + + baInput, err := os.ReadFile(sFileIn) + if err!=nil { + err = fmt.Errorf("Could not read from %s. Check the file permissions.\n",sFileIn); + return; + } + _ = os.Remove(sFileIn) + if _, err = os.Stat(sFileIn); err == nil { + err = fmt.Errorf("Could not delete %s\n",sFileIn) + return + } + + //Parameter [0] Project - 'dero' + // [1] Version - Layout of the command fields + // [2] header - scalar_mult_result or shared_secret_result + // Version 1: [3] scalar multiply result used to decode the balance + // [4] Checksum of all the characters in the data stream + sInput := string(baInput[:]) + saParts := strings.Split(sInput,";") + if (len(saParts) != 2) { + err = fmt.Errorf("Invalid number of parts in the transaction. Expected 2, found %d\n", len(saParts)) //14 + return; + } + + sProtocolChecksum := saParts[1] + iTmp,err := strconv.Atoi(sProtocolChecksum) + if err!=nil { + err = fmt.Errorf("Could not convert the checksum back to an integer:"+sProtocolChecksum+"\n") + return + } + iProtocolChecksum:=uint16(iTmp) + + iCalculatedChecksum=0x01; + for t := range saParts[0] { + iVal := uint16(saParts[0][t]) + iCalculatedChecksum = iCalculatedChecksum + iVal; + } + + if (iProtocolChecksum!=iCalculatedChecksum) { + err = fmt.Errorf("The checksum of the signed transaction data is invalid.\n") + return + } + + saFields := strings.Split(saParts[0]," ") + if (len(saFields) != 4) { + err = fmt.Errorf("Invalid number of parts in the transaction. Expected 4, found %d\n", len(saFields)) + return; + } + + + + if (saFields[0] != "dero") { + err = fmt.Errorf("Expected Dero communication, Found %s\n",saFields[1]); + return; + } + + if (saFields[1] != "1") { + err = fmt.Errorf("Only transaction version 1 supported. Found %s\n",saFields[2]) + return + } + + sCompare := fmt.Sprintf("%s_result", sType) + if (saFields[2] != sCompare) { + err = fmt.Errorf("Requested operation: '%s'. Expected back result: '%s', found: %s\n",sType, sCompare, saFields[0]) + return + } + + result,err = hex.DecodeString(saFields[3]) + if err!=nil { + err = fmt.Errorf("Could not decode the result from the offline wallet\n"); + } + + //Reconstruct type 0=scalar mult + // result := new(bn256.G1); + // result.Unmarshal(baResult) + //Reconstruct type 1=shared secret + // The result is a 32 byte array + + //Store the entry in the cache + var sOfflineOperations OfflineOperations_t + sOfflineOperations.iType = iType; + + sOfflineOperations.baRequest = make([]byte, len(baData)) + copy (sOfflineOperations.baRequest, baData) + + sOfflineOperations.baResult = make([]byte, len(result)) + copy (sOfflineOperations.baResult, result) + + saOfflineOperations = append(saOfflineOperations, sOfflineOperations) + + fmt.Printf("\nFound a valid response\n"); + return +} + // decode encrypted balance now // it may take a long time, its currently sing threaded, need to parallelize func (w *Wallet_Memory) DecodeEncryptedBalanceNow(el *crypto.ElGamal) uint64 { + ScalarMultResult := new(bn256.G1); + if (w.ViewOnly()==false) { + ScalarMultResult = new(bn256.G1).ScalarMult(el.Right, w.account.Keys.Secret.BigInt()) + } else { + // Need to perform this operation in the offline wallet: ScalarMultResult := new(bn256.G1).ScalarMult(el.Right, Secret.BigInt()) + baData := el.Right.Marshal(); + baResult,err := w.OfflineOperationWithSecretKey(0,baData) + if err!=nil { + fmt.Printf("Could not retrieve the balance: %s\n",err) + return 0 + } + ScalarMultResult.Unmarshal(baResult) + } - balance_point := new(bn256.G1).Add(el.Left, new(bn256.G1).Neg(new(bn256.G1).ScalarMult(el.Right, w.account.Keys.Secret.BigInt()))) - return Balance_lookup_table.Lookup(balance_point, w.account.Balance_Mature) + balance_point := new(bn256.G1).Add( el.Left, new(bn256.G1).Neg( ScalarMultResult ) ) + balance := Balance_lookup_table.Lookup(balance_point, w.account.Balance_Mature) + return balance } func (w *Wallet_Memory) GetSelfEncryptedBalanceAtTopoHeight(scid crypto.Hash, topoheight int64) (r rpc.GetEncryptedBalance_Result, err error) { @@ -365,7 +570,6 @@ func (w *Wallet_Memory) GetSelfEncryptedBalanceAtTopoHeight(scid crypto.Hash, to // TODO in order to stop privacy leaks we must guess this information somehow on client side itself // maybe the server can broadcast a bloomfilter or something else from the mempool keyimages func (w *Wallet_Memory) GetEncryptedBalanceAtTopoHeight(scid crypto.Hash, topoheight int64, accountaddr string) (bits int, lastused uint64, blid crypto.Hash, e *crypto.ElGamal, err error) { - defer func() { if r := recover(); r != nil { logger.V(1).Error(nil, "Recovered while GetEncryptedBalanceAtTopoHeight", "r", r, "stack", debug.Stack()) @@ -385,12 +589,14 @@ func (w *Wallet_Memory) GetEncryptedBalanceAtTopoHeight(scid crypto.Hash, topohe //var params rpc.GetEncryptedBalance_Params var result rpc.GetEncryptedBalance_Result + + var WalletAddress = w.GetAddress().String() // Issue a call with a response. if err = rpc_client.Call("DERO.GetEncryptedBalance", rpc.GetEncryptedBalance_Params{SCID: scid, Address: accountaddr, TopoHeight: topoheight}, &result); err != nil { logger.Error(err, "DERO.GetEncryptedBalance Call failed:") - if strings.Contains(strings.ToLower(err.Error()), strings.ToLower(errormsg.ErrAccountUnregistered.Error())) && accountaddr == w.GetAddress().String() && scid.IsZero() { + if strings.Contains(strings.ToLower(err.Error()), strings.ToLower(errormsg.ErrAccountUnregistered.Error())) && accountaddr == WalletAddress && scid.IsZero() { w.Error = errormsg.ErrAccountUnregistered //fmt.Printf("setting unregisterd111 err %s scid %s topoheight %d\n",err,scid, topoheight) //fmt.Printf("debug stack %s\n",debug.Stack()) @@ -415,8 +621,8 @@ func (w *Wallet_Memory) GetEncryptedBalanceAtTopoHeight(scid crypto.Hash, topohe return } - // fmt.Printf("GetEncryptedBalance result %+v\n", result) - if scid.IsZero() && accountaddr == w.GetAddress().String() { + // fmt.Printf("GetEncryptedBalance result %+v\n", result) + if scid.IsZero() && accountaddr == WalletAddress { if result.Status == errormsg.ErrAccountUnregistered.Error() { w.Error = errormsg.ErrAccountUnregistered w.account.Registered = false @@ -425,7 +631,7 @@ func (w *Wallet_Memory) GetEncryptedBalanceAtTopoHeight(scid crypto.Hash, topohe } } - // fmt.Printf("status '%s' err '%s' %+v %+v \n", result.Status , w.Error , result.Status == errormsg.ErrAccountUnregistered.Error() , accountaddr == w.account.GetAddress().String()) + // fmt.Printf("status '%s' err '%s' %+v %+v \n", result.Status , w.Error , result.Status == errormsg.ErrAccountUnregistered.Error() , accountaddr == WalletAddress) if scid.IsZero() && result.Status == errormsg.ErrAccountUnregistered.Error() { err = fmt.Errorf("%s", result.Status) @@ -438,8 +644,8 @@ func (w *Wallet_Memory) GetEncryptedBalanceAtTopoHeight(scid crypto.Hash, topohe w.Merkle_Balance_TreeHash = result.DMerkle_Balance_TreeHash } - if topoheight == -1 && accountaddr == w.GetAddress().String() { - //fmt.Printf("topoheight %d accountaddr '%s' waddress '%s'\n ",topoheight,accountaddr,w.GetAddress().String()) + if topoheight == -1 && accountaddr == WalletAddress { + //fmt.Printf("topoheight %d accountaddr '%s' waddress '%s'\n ",topoheight,accountaddr,WalletAddress) w.setEncryptedBalanceresult(scid, result) w.account.TopoHeight = result.Topoheight @@ -455,7 +661,7 @@ func (w *Wallet_Memory) GetEncryptedBalanceAtTopoHeight(scid crypto.Hash, topohe return } - if accountaddr == w.GetAddress().String() && scid.IsZero() { + if accountaddr == WalletAddress && scid.IsZero() { w.Error = nil } @@ -466,8 +672,23 @@ func (w *Wallet_Memory) GetEncryptedBalanceAtTopoHeight(scid crypto.Hash, topohe } func (w *Wallet_Memory) DecodeEncryptedBalance_Memory(el *crypto.ElGamal, hint uint64) (balance uint64) { + + ScalarMultResult := new(bn256.G1); + if (w.ViewOnly()==false) { + ScalarMultResult = new(bn256.G1).ScalarMult(el.Right, w.account.Keys.Secret.BigInt()) + } else { + // Need to perform this operation in the offline wallet: ScalarMultResult := new(bn256.G1).ScalarMult(el.Right, Secret.BigInt()) + baData := el.Right.Marshal(); + baResult,err := w.OfflineOperationWithSecretKey(0,baData) + if err!=nil { + fmt.Printf("Could not retrieve the balance") + return 0 + } + ScalarMultResult.Unmarshal(baResult) + } + var balance_point bn256.G1 - balance_point.Add(el.Left, new(bn256.G1).Neg(new(bn256.G1).ScalarMult(el.Right, w.account.Keys.Secret.BigInt()))) + balance_point.Add(el.Left, new(bn256.G1).Neg( ScalarMultResult )) return Balance_lookup_table.Lookup(&balance_point, hint) } @@ -639,6 +860,13 @@ func (w *Wallet_Memory) synchistory_internal_binary_search(level int, scid crypt var err error //defer fmt.Printf("end %d start %d err %s\n", end_topo, start_topo, err) + +// fmt.Printf("end %d start %d err %s\n", end_topo, start_topo, err) +// _, file, no, ok := runtime.Caller(1) +// if ok { +// fmt.Printf("called from %s#%d\n", file, no) +// } + if end_topo < 0 { return fmt.Errorf("done") @@ -706,7 +934,6 @@ func (w *Wallet_Memory) synchistory_internal_binary_search(level int, scid crypt // for a particular block // for the entire chain func (w *Wallet_Memory) synchistory_block(scid crypto.Hash, topo int64) (err error) { - var local_entries []rpc.Entry compressed_address := w.account.Keys.Public.EncodeCompressed() @@ -714,7 +941,7 @@ func (w *Wallet_Memory) synchistory_block(scid crypto.Hash, topo int64) (err err var previous_balance_e, current_balance_e *crypto.ElGamal var previous_balance, current_balance, total_sent, total_received uint64 - if topo <= 0 || w.getEncryptedBalanceresult(scid).Registration == topo { + if topo <= 0 || w.getEncryptedBalanceresult(scid).Registration == topo { previous_balance_e = crypto.ConstructElGamal(w.account.Keys.Public.G1(), crypto.ElGamal_BASE_G) } else { _, _, _, previous_balance_e, err = w.GetEncryptedBalanceAtTopoHeight(scid, topo-1, w.GetAddress().String()) @@ -881,7 +1108,28 @@ func (w *Wallet_Memory) synchistory_block(scid crypto.Hash, topo int64) (err err for l := range tx.Payloads[t].Statement.Publickeylist_compressed { rinputs = append(rinputs, tx.Payloads[t].Statement.Publickeylist_compressed[l][:]...) } - rencrypted := new(bn256.G1).ScalarMult(crypto.HashToPoint(crypto.HashtoNumber(append([]byte(crypto.PROTOCOL_CONSTANT), rinputs...))), w.account.Keys.Secret.BigInt()) + + hashtonumber := crypto.HashtoNumber( append( []byte(crypto.PROTOCOL_CONSTANT), rinputs... )) + + rencrypted := new(bn256.G1); + if (w.ViewOnly()==true) { + // Need to perform this operation in the offline wallet: rencrypted := new(bn256.G1).ScalarMult(crypto.HashToPoint( hashtonumber ) , Secret.BigInt() ) + baData := crypto.HashToPoint( hashtonumber ).Marshal() + + baResult,err := w.OfflineOperationWithSecretKey(0,baData) + if err!=nil { + fmt.Printf("Could not retrieve the balance") + continue + } + rencrypted = new(bn256.G1); + rencrypted.Unmarshal(baResult) + + + } else { + rencrypted = new(bn256.G1).ScalarMult(crypto.HashToPoint( hashtonumber ) , w.account.Keys.Secret.BigInt() ) + } + + r := crypto.ReducedHash(rencrypted.EncodeCompressed()) //fmt.Printf("t %d r calculated %s value amount %d burn %d\n", t, r.Text(16), entry.Amount,entry.Burn) @@ -970,7 +1218,7 @@ func (w *Wallet_Memory) synchistory_block(scid crypto.Hash, topo int64) (err err } - case previous_balance < changed_balance: // someone sentus this amount + case previous_balance < changed_balance: // someone sent us this amount entry.Amount = changed_balance - previous_balance entry.Incoming = true @@ -981,7 +1229,23 @@ func (w *Wallet_Memory) synchistory_block(scid crypto.Hash, topo int64) (err err blinder := &x - shared_key := crypto.GenerateSharedSecret(w.account.Keys.Secret.BigInt(), tx.Payloads[t].Statement.D) + var shared_key [32]byte + if (w.ViewOnly()==true) { + baData := tx.Payloads[t].Statement.D.Marshal() + baResult,err := w.OfflineOperationWithSecretKey(1,baData) + if err!=nil { + fmt.Printf("Could not retrieve the balance") + continue + } + if (len(baResult)!= 32) { + fmt.Printf("Invalid lenght in result. Expected 32 bytes, found %d\n",len(baResult)) + } + for t := range baResult { + shared_key[t] = baResult[t] + } + } else { + shared_key = crypto.GenerateSharedSecret(w.account.Keys.Secret.BigInt(), tx.Payloads[t].Statement.D) + } // enable receiver side proofs proof := rpc.NewAddressFromKeys((*crypto.Point)(blinder)) diff --git a/walletapi/transaction_build.go b/walletapi/transaction_build.go index 572a8221..279b9357 100644 --- a/walletapi/transaction_build.go +++ b/walletapi/transaction_build.go @@ -23,8 +23,7 @@ var GenerateProoffuncptr GenerateProofFunc = crypto.GenerateProof // generate proof etc func (w *Wallet_Memory) BuildTransaction(transfers []rpc.Transfer, emap [][][]byte, rings [][]*bn256.G1, block_hash crypto.Hash, height uint64, scdata rpc.Arguments, roothash []byte, max_bits int, fees uint64) *transaction.Transaction { - - sender := w.account.Keys.Public.G1() + sender := w.account.Keys.Public.G1() sender_secret := w.account.Keys.Secret.BigInt() var retry_count int @@ -59,6 +58,10 @@ rebuild_tx: panic("currently we cannot use more than 240 bits") } + var account_balance uint64 + account_balance=0 + balance_unassigned:=false + for t, _ := range transfers { var publickeylist, C, CLn, CRn []*bn256.G1 @@ -117,8 +120,6 @@ rebuild_tx: } } - // fmt.Printf("len of publickeylist %d \n", len(publickeylist)) - // revealing r will disclose the amount and the sender and receiver and separate anonymous ring members // calculate r deterministically, so its different every transaction, in emergency it can be given to other, and still will not allows key attacks rinputs := append([]byte{}, roothash[:]...) @@ -138,6 +139,7 @@ rebuild_tx: value := transfers[t].Amount burn_value := transfers[t].Burn + if fees == 0 && asset.SCID.IsZero() && !fees_done { fees = fees + uint64(len(transfers)+2)*uint64((float64(config.FEE_PER_KB)*float64(float32(len(publickeylist)/16)+w.GetFeeMultiplier()))) if data, err := scdata.MarshalBinary(); err != nil { @@ -217,9 +219,13 @@ rebuild_tx: } // decode sender (our) balance now, it might have been updated - balance := w.DecodeEncryptedBalanceNow(ebalances_list[witness_index[0]]) - - //fmt.Printf("t %d scid %s balance %d\n", t, transfers[t].SCID, balance) + // Note: As the loop iterates over []transfers, the balance is updated to reflect each spend + balance := w.DecodeEncryptedBalanceNow(ebalances_list[witness_index[0]]) + if (balance_unassigned==false) { + balance_unassigned=true + account_balance=balance + } + //fmt.Printf("balance: %u\n", balance) // time for bullets-sigma fees_currentasset := uint64(0) @@ -231,7 +237,15 @@ rebuild_tx: copy(statement.Roothash[:], roothash[:]) statement.Bytes_per_publickey = byte(max_bits / 8) - witness := GenerateWitness(sender_secret, r, value, balance-value-fees_currentasset-burn_value, witness_index) + //Evaluate available balance: + if (balance < (value+fees_currentasset+burn_value)) { + fmt.Printf("Insufficient funds to process the transaction.\nBalance %d < sum(spend amount,fees,burn value)", account_balance) + return nil + } + + //Account balance is decreases with the amount of the transfer each time w.DecodeEncryptedBalanceNow() is called. + remaining_balance := balance-value-fees_currentasset-burn_value + witness := GenerateWitness(sender_secret, r, value, remaining_balance, witness_index) witness_list = append(witness_list, witness) @@ -250,7 +264,6 @@ rebuild_tx: balance = balance.Add(echanges) // homomorphic addition of changes umap[transfers[t].SCID.String()+publickeylist[i].String()] = balance.Serialize() // reserialize and store } - } scid_map := map[crypto.Hash]int{} diff --git a/walletapi/wallet.go b/walletapi/wallet.go index be4a6f18..1f35c463 100644 --- a/walletapi/wallet.go +++ b/walletapi/wallet.go @@ -27,6 +27,7 @@ import ( "strings" "sync" "time" + "encoding/hex" "github.com/deroproject/derohe/cryptography/bn256" "github.com/deroproject/derohe/cryptography/crypto" @@ -164,10 +165,52 @@ func Generate_Keys_From_Seed(Seed *crypto.BNRed) (keys _Keys) { // generate user account using recovery seeds func Generate_Account_From_Recovery_Words(words string) (user *Account, err error) { user = &Account{Ringsize: 16, FeesMultiplier: 2.0} - language, seed, err := mnemonics.Words_To_Key(words) - if err != nil { - return - } + + var language string + var seed *big.Int + var baData []byte + + saParts:=strings.Split(words,";") + + if (len(saParts)==1) { + //25 word mnemonic + language, seed, err = mnemonics.Words_To_Key(words) + if err != nil { + return + } + } else { + //raw 32 byte seed with a checksum + if (len(saParts)!=2) { + err = fmt.Errorf("raw seed requires 2 parts.") + return + } + if (len(saParts[0]) != 64) { + err = fmt.Errorf("raw seed must be 64 characters long") + return + } + baData, err = hex.DecodeString( saParts[0] ) + if err!=nil { + err = fmt.Errorf("Could not decode the hex seedphrase to binary") + return + } + + iChecksum:=0x01; + for t := range baData { + iVal := int(baData[t]) + iChecksum = iChecksum + iVal; + } + sCalculatedChecksum:= fmt.Sprintf("%d",iChecksum) + sProtocolChecksum:= saParts[1] + + if (sCalculatedChecksum!=sProtocolChecksum) { + err = fmt.Errorf("Checksum mismatch for the raw seed") + return + } + seed = new(big.Int) + seed.SetBytes(baData) + + language="English" + } user.SeedLanguage = language user.Keys = Generate_Keys_From_Seed(crypto.GetBNRed(seed)) @@ -184,6 +227,58 @@ func Generate_Account_From_Seed(Seed *crypto.BNRed) (user *Account, err error) { return } +func Generate_Account_From_ViewOnly_params(sPublicKey string, sPublicKeyG1 string, IsMainnet bool ) (user *Account, err error) { + user = &Account{Ringsize: 16, FeesMultiplier: 2.0} + + user.mainnet=IsMainnet + + //Secret: + var biSecret,ok = new(big.Int).SetString("0000000000000000000000000000000000000000000000000000000000000000",0) + if !ok { + err = fmt.Errorf("Cant assign secret") + return + } + user.Keys.Secret = crypto.GetBNRed(biSecret) + + //Public: + //Initialise the structure: + user.Keys.Public = crypto.GPoint.ScalarMult( user.Keys.Secret ) + + //Apply the imported public key: + baData, err := hex.DecodeString( sPublicKeyG1 ) + if err!=nil { + err = fmt.Errorf("Could not decode the hex input") + return + } + _, err = user.Keys.Public.G1().Unmarshal(baData) + if err!=nil { + err = fmt.Errorf("Could not process the public key") + return + } + + sDerivedPublic := fmt.Sprintf("%x",user.Keys.Public) + if (sDerivedPublic != sPublicKey) { + err = fmt.Errorf("The derived public key (%s) doesn't match the provided public key (%s) from the input\n", sDerivedPublic, sPublicKey) + return + } + + fmt.Printf(" Restored public key: %x\n", user.Keys.Public) + return +} + + +// Is this a 'view only' wallet? +// Return true: view only wallet with only a public key +// false: full function wallet with public & private key +func (w *Wallet_Memory) ViewOnly() bool { + if (len(w.account.Keys.Secret.String()) > 1) { + return false + } else { + return true + } +} + + // convert key to seed using language func (w *Wallet_Memory) GetSeed() (str string) { return mnemonics.Key_To_Words(w.account.Keys.Secret.BigInt(), w.account.SeedLanguage) diff --git a/walletapi/wallet_disk.go b/walletapi/wallet_disk.go index e8b059ea..6ee1eb16 100644 --- a/walletapi/wallet_disk.go +++ b/walletapi/wallet_disk.go @@ -35,11 +35,11 @@ type Wallet_Disk struct { // when smart contracts are implemented, each will have it's own universe to track and maintain transactions // this file implements the encrypted data store at rest -func Create_Encrypted_Wallet(filename string, password string, seed *crypto.BNRed) (wd *Wallet_Disk, err error) { - +// By providing the Account as input, both full and view-only wallet types are supported +func Create_Encrypted_Wallet(filename string, password string, account *Account) (wd *Wallet_Disk, err error) { if _, err = os.Stat(filename); err == nil { err = fmt.Errorf("File '%s' already exists", filename) - return + return nil,err } else if os.IsNotExist(err) { // path/to/whatever does *not* exist @@ -50,11 +50,18 @@ func Create_Encrypted_Wallet(filename string, password string, seed *crypto.BNRe wd = &Wallet_Disk{filename: filename} // generate account keys - if wd.Wallet_Memory, err = Create_Encrypted_Wallet_Memory(password, seed); err != nil { + wd.Wallet_Memory, err = Create_Encrypted_Wallet_Memory(password, account) + if err != nil { + fmt.Printf("Could not create wallet: %s\n",err); return nil, err } wd.Wallet_Memory.wallet_disk = wd + // Flush wallet to disk, otherwise wallet file will only be created once the + // user exits the wallet menu back to the shell. During that time the wallet + // file is not on the disk yet, which can lead to data loss. + wd.Save_Wallet() + return } @@ -62,28 +69,56 @@ func Create_Encrypted_Wallet(filename string, password string, seed *crypto.BNRe func Create_Encrypted_Wallet_From_Recovery_Words(filename string, password string, electrum_seed string) (wd *Wallet_Disk, err error) { wd = &Wallet_Disk{filename: filename} - language, seed, err := mnemonics.Words_To_Key(electrum_seed) + language, seed, err2 := mnemonics.Words_To_Key(electrum_seed) + if err2 != nil { + fmt.Printf("Could not convert phrase to a key: %s\n",err2); + return nil,err2 + } + + var account *Account + account,err = Generate_Account_From_Seed (crypto.GetBNRed(seed)) if err != nil { - return + fmt.Printf("Could not create account: %s\n",err) + return nil,err } - if wd.Wallet_Memory, err = Create_Encrypted_Wallet_Memory(password, crypto.GetBNRed(seed)); err != nil { + + if wd.Wallet_Memory, err = Create_Encrypted_Wallet_Memory(password, account); err != nil { + fmt.Printf("Could not create wallet:\n",err) return nil, err } wd.Wallet_Memory.account.SeedLanguage = language wd.Wallet_Memory.wallet_disk = wd + + //Save to disk + wd.Save_Wallet() + return } // create an encrypted wallet using using random data func Create_Encrypted_Wallet_Random(filename string, password string) (wd *Wallet_Disk, err error) { wd = &Wallet_Disk{filename: filename} - if wd.Wallet_Memory, err = Create_Encrypted_Wallet_Memory(password, crypto.RandomScalarBNRed()); err == nil { - return wd, nil + + var account *Account + account,err = Generate_Account_From_Seed ( crypto.RandomScalarBNRed() ) + if err != nil { + fmt.Printf("Could not generate random seed: %s\n", err) + return nil,err + } + + wd.Wallet_Memory, err = Create_Encrypted_Wallet_Memory(password, account ) + if err != nil { + fmt.Printf("Could not create wallet\n") + return nil,err } + wd.Wallet_Memory.wallet_disk = wd + + //Save to disk + wd.Save_Wallet() - return nil, err + return wd, err } // wallet must already be open diff --git a/walletapi/wallet_memory.go b/walletapi/wallet_memory.go index 37e5a147..fea9d481 100644 --- a/walletapi/wallet_memory.go +++ b/walletapi/wallet_memory.go @@ -90,7 +90,8 @@ type Wallet_Memory struct { // when smart contracts are implemented, each will have it's own universe to track and maintain transactions // this file implements the encrypted data store at rest -func Create_Encrypted_Wallet_Memory(password string, seed *crypto.BNRed) (w *Wallet_Memory, err error) { +// By providing the Account as input, both full and view-only wallet types are supported +func Create_Encrypted_Wallet_Memory(password string, account *Account) (w *Wallet_Memory, err error) { w = &Wallet_Memory{} w.Version = config.Version @@ -98,12 +99,8 @@ func Create_Encrypted_Wallet_Memory(password string, seed *crypto.BNRed) (w *Wal return } - // generate account keys - w.account, err = Generate_Account_From_Seed(seed) - if err != nil { - return - } - + w.account = account + // generate a 64 byte key to be used as master Key w.master_password = make([]byte, 32, 32) _, err = rand.Read(w.master_password) @@ -133,8 +130,14 @@ func Create_Encrypted_Wallet_From_Recovery_Words_Memory(password string, electru if err != nil { return } - w, err = Create_Encrypted_Wallet_Memory(password, crypto.GetBNRed(seed)) + //Prepare account with private&public key + account,err2 := Generate_Account_From_Seed ( crypto.GetBNRed(seed) ) + if err2 != nil { + return + } + + w, err = Create_Encrypted_Wallet_Memory(password, account) if err != nil { return } @@ -145,11 +148,16 @@ func Create_Encrypted_Wallet_From_Recovery_Words_Memory(password string, electru // create an encrypted wallet using using random data func Create_Encrypted_Wallet_Random_Memory(password string) (w *Wallet_Memory, err error) { - w, err = Create_Encrypted_Wallet_Memory(password, crypto.RandomScalarBNRed()) + account,err2 := Generate_Account_From_Seed ( crypto.RandomScalarBNRed() ) + if err2 != nil { + return + } + w, err = Create_Encrypted_Wallet_Memory(password, account) if err != nil { return } + // TODO setup seed language, default is already english return } diff --git a/walletapi/wallet_transfer.go b/walletapi/wallet_transfer.go index 170eadae..3103cf7b 100644 --- a/walletapi/wallet_transfer.go +++ b/walletapi/wallet_transfer.go @@ -19,10 +19,15 @@ package walletapi import ( "encoding/hex" "fmt" - + "os" + "time" +// "strconv" + "strings" + "github.com/deroproject/derohe/config" "github.com/deroproject/derohe/cryptography/bn256" "github.com/deroproject/derohe/cryptography/crypto" + "github.com/deroproject/derohe/globals" "github.com/deroproject/derohe/rpc" "github.com/deroproject/derohe/transaction" ) @@ -177,8 +182,8 @@ func (w *Wallet_Memory) TransferPayload0(transfers []rpc.Transfer, ringsize uint total_amount_required[transfers[i].SCID] = total_amount_required[transfers[i].SCID] + transfers[i].Amount + transfers[i].Burn } + var current_balance uint64 for i := range transfers { - var current_balance uint64 current_balance, _, err = w.GetDecryptedBalanceAtTopoHeight(transfers[i].SCID, -1, w.GetAddress().String()) if err != nil { @@ -189,7 +194,7 @@ func (w *Wallet_Memory) TransferPayload0(transfers []rpc.Transfer, ringsize uint return } } - + for t := range transfers { if transfers[t].Destination == "" { // user skipped destination @@ -257,6 +262,7 @@ func (w *Wallet_Memory) TransferPayload0(transfers []rpc.Transfer, ringsize uint // TODO, we should check nonce for base token and other tokens at the same time // right now, we are probably using a bit of luck here if daemon_topoheight >= int64(noncetopo)+3 { // if wallet has not been recently used, increase probability of user's tx being successfully mined + topoheight = daemon_topoheight - 3 } @@ -414,6 +420,333 @@ func (w *Wallet_Memory) TransferPayload0(transfers []rpc.Transfer, ringsize uint } max_bits += 6 // extra 6 bits + + //If this is a view only wallet, prepare and send the transaction to the offline wallet for signing. + //Import the signed transaction again and continue with the normal work flow to submit the transaction + //to the network + if (w.ViewOnly()==true) { + // Calculate an estimate of the full transaction cost, to see if the balance has enough funds: + var spend uint64 + var fees uint64 + var burn_value uint64 + spend=0 + fees=0 + burn_value=0 + fees_done:=false + + len_publickeylist:=len(rings[0]) + + for t := range transfers { + spend = spend + transfers[t].Amount + transfers[t].Burn + + //From transaction builder, use the estimate fees calculator: + var asset transaction.AssetPayload + + asset.SCID = transfers[t].SCID + asset.BurnValue = transfers[t].Burn + burn_value+=asset.BurnValue + + if fees == 0 && asset.SCID.IsZero() && !fees_done { + fees = fees + uint64(len(transfers)+2)*uint64((float64(config.FEE_PER_KB)*float64(float32(len_publickeylist/16)+w.GetFeeMultiplier()))) + if data, err := scdata.MarshalBinary(); err != nil { + panic(err) + } else { + fees = fees + (uint64(len(data))*15)/10 + } + fees_done = true + } + } + //Fee is applied for each transfer in the transaction: + fees = fees * uint64(len(transfers)) + +// fmt.Printf("spends: %d\n", spend); + if (current_balance < (spend+fees+burn_value)) { + err = fmt.Errorf("Spend amount (%d) + burn value (%d) + estimated fees (%d) is more than your current balance: %d\n",spend,burn_value,fees,current_balance) + return + } + + //Parameter [0]: Project - Dero='dero' + // [1]: Version - Layout of the command fields + // [2]: Command: sign_offline + // Version 1: [3] Array of transfers (outputs) + // [4] Array of ring balances + // [5] Array of rings + // [6] block_hash + // [7] height + // [8] Array of scdata + // [9] treehash + // [10] max_bits + // [11] gasstorage + // [12] account_balance + // ; Checksum of all the characters in the command. + var sReturn string + var sChecksumInput string + + sReturn="dero 1 sign_offline "; + sChecksumInput="dero 1 sign_offline "; + + var counter=0 + + sChecksumInput+="transfers:"; + sReturn=sReturn+"'["; + counter=0 + for t := range transfers { + baRPC, err := transfers[t].Payload_RPC.MarshalBinary() + if err!=nil { + panic("Could not convert payload_rpc to binary") + } + hexRPC := hex.EncodeToString(baRPC) + sTmp:=fmt.Sprintf("{\"SCID\":\"%s\",\"Destination\":\"%s\",\"Amount\":%d,\"Burn\":%d,\"RPC\":\"%s\"}", transfers[t].SCID, transfers[t].Destination, transfers[t].Amount, transfers[t].Burn, hexRPC ) + sReturn=sReturn+sTmp; + + sTmp=fmt.Sprintf("\"%s\" \"%s\" %d %d \"%s\"", transfers[t].SCID, transfers[t].Destination, transfers[t].Amount, transfers[t].Burn, hexRPC ) + sChecksumInput+=" "+sTmp + + //Last entry: Must close the array with ]' + //otherwide start the next entry with a , + if (counter == len(transfers)-1) { + sReturn=sReturn+"]' "; + } else { + sReturn=sReturn+","; + } + counter++ + } + + var counter1=0 + var counter2=0 + + sChecksumInput+=" rings_balances:" + sReturn=sReturn+"'[" + for t := range rings_balances { + //fmt.Printf("rings_balances{%d}:{%d} entries\n",counter1,len(rings_balances[t])) + sReturn=sReturn+"{"; + counter2=0 + for u := range rings_balances[t] { + sTmp := fmt.Sprintf("%x",rings_balances[t][u]) + sReturn = sReturn + sTmp + sChecksumInput+=" "+sTmp + + //Last entry: Must close the array with ]' + //otherwide start the next entry with a , + if (counter2 == len(rings_balances[t])-1) { + sReturn=sReturn+"}"; + } else { + sReturn=sReturn+","; + } + counter2++ + } + + //Last entry: Must close the array with ]' + //otherwide start the next entry with a , + if (counter1 == len(rings_balances)-1) { + sReturn=sReturn+"]' "; + } else { + sReturn=sReturn+","; + } + counter1++ + } + + //fmt.Printf("rings:{%d} entries\n", len(rings)) + sChecksumInput+=" rings:"; + sReturn=sReturn+"'["; + + counter1=0 + counter2=0 + for t := range rings { + //fmt.Printf("ring{%d}:{%d} entries\n",counter1,len(rings[t])) + sReturn=sReturn+"{"; + counter2=0 + for u := range rings[t] { + sTmp := fmt.Sprintf("%x",rings[t][u].Marshal() ) + sReturn = sReturn + sTmp + sChecksumInput+=" "+sTmp + + if (counter2 == len(rings_balances[t])-1) { + sReturn=sReturn+"}"; + } else { + sReturn=sReturn+","; + } + counter2++ + } + + if (counter1 == len(rings_balances)-1) { + sReturn=sReturn+"]' "; + } else { + sReturn=sReturn+","; + } + counter1++ + } + + + sTmp:=fmt.Sprintf("\"%s\" ",block_hash) + sReturn=sReturn + sTmp + sChecksumInput+=" "+sTmp + + sTmp=fmt.Sprintf("%d ",height) + sReturn=sReturn + sTmp + sChecksumInput+=sTmp + + sTmp=fmt.Sprintf("'%x' ",scdata) + sReturn=sReturn + sTmp + sChecksumInput+=sTmp + + sTmp=fmt.Sprintf("\"%x\" ",treehash_raw) + sReturn=sReturn + sTmp + sChecksumInput+=sTmp + + sTmp=fmt.Sprintf("%d ",max_bits) + sReturn=sReturn + sTmp + sChecksumInput+=sTmp + + sTmp=fmt.Sprintf("%x ",gasstorage) + sReturn=sReturn + sTmp + sChecksumInput+=sTmp + + sTmp=fmt.Sprintf("%x",current_balance) + sReturn=sReturn + sTmp + sChecksumInput+=sTmp + + //Parameter [13]: checksum + //A simple checksum of the full string, to detect copy/paste errors between the wallets + //The checksum equals the sum of the ASCII values of all the characters in the string: + iCalculatedChecksum:=0x01; + iCount:=0 + for t := range sReturn { + iVal := int(sReturn[t]) + iCalculatedChecksum = iCalculatedChecksum + iVal; + + iCount++; + } + sTmp = fmt.Sprintf(";%d", uint16(iCalculatedChecksum)) + sReturn=sReturn + sTmp + + //-------------------------------------------------------------------------------------- + // Compiling transaction data completed. Now need to exchange it with the offline wallet + // to get it signed + remote_request_prefix:="." + if globals.Arguments["--prefix"] != nil { + remote_request_prefix = globals.Arguments["--prefix"].(string) // override with user specified settings + } + + sFileOut:=remote_request_prefix+"/offline_request" + _ = os.Remove(sFileOut) // ./transaction + + if _, err = os.Stat(sFileOut); err == nil { + err = fmt.Errorf("Could not delete "+sFileOut) + return + } + + sFileIn:=remote_request_prefix+"/offline_response" + _ = os.Remove(sFileIn) + if _, err = os.Stat(sFileIn); err == nil { + err = fmt.Errorf("Found old response file at "+sFileIn+". Delete the file before starting a new transaction.\n") + return + } + + fmt.Printf("Saved transaction data to: "+sFileOut+". Transfer it to the offline (signing) wallet and have it signed there.\n" ) + + baData := []byte(sReturn) + err = os.WriteFile(sFileOut, baData, 0644) + if err!=nil { + err = fmt.Errorf("Error saving file. %s\n",err) + return + } + + bFound:=0 + counter=0 + //TODO: what is the longest we can wait to submit a transaction and still have it processed by the network? + fmt.Printf("Waiting 80 seconds for the signed transaction from the offline wallet at %s\n",sFileIn); + for counter <= 80 { + if _, err := os.Stat(sFileIn); err == nil { + bFound=1 + break; + } + counter++ + time.Sleep(1 * time.Second) + } + + if (bFound==0) { + err = fmt.Errorf("Could not find the signed transaction\n"); + return; + } + + baData, err = os.ReadFile(sFileIn) + if err!=nil { + err = fmt.Errorf("Could not read from %s. Check the file permissions.\n",sFileIn); + return; + } + fmt.Printf("Read %d bytes from %s\n",len(baData),sFileIn) + //Remove old files + _ = os.Remove(sFileIn) + + tx=nil + //Parameter [0]: Project - 'dero' + // [1]: Version - Layout of the command fields + // [2]: Command: signed + // Version 1: [3] Signed transaction + // ; Checksum of all the characters in the data stream + sInput := string(baData[:]) + saParts := strings.Split(sInput,";") + + if (len(saParts) != 2) { + fmt.Printf("Invalid number of parts in the transaction. Expected 2, found %d\n", len(saParts)) + return + } + + sProtocolChecksum := saParts[1] + iCalculatedChecksum=0x01; + for t := range saParts[0] { + iVal := int(saParts[0][t]) + iCalculatedChecksum = iCalculatedChecksum + iVal; + } + sCalculatedChecksum := fmt.Sprintf("%d", uint16(iCalculatedChecksum)) + + if (sProtocolChecksum!=sCalculatedChecksum) { + fmt.Printf("The checksum of the request data is invalid. Protocol: '%s', Calculates: '%s'\n", sProtocolChecksum, sCalculatedChecksum) + return + } + + saFields := strings.Split(saParts[0]," ") + if (len(saFields) != 4) { + err = fmt.Errorf("Invalid number of parts in the transaction. Expected 4, found %d\n", len(saFields)) + return; + } + + if (saFields[0] != "dero") { + err = fmt.Errorf("Expected Dero communication, Found %s\n",saFields[1]); + return; + } + + if (saFields[1] != "1") { + err = fmt.Errorf("Only transaction version 1 supported. Found %s\n",saFields[2]) + return + } + + if (saFields[2] != "signed") { + err = fmt.Errorf("Transaction doesn't start with 'signed'\n") + return + } + + tx = new(transaction.Transaction) + baData,err = hex.DecodeString( saFields[3] ) + if err!= nil { + err = fmt.Errorf("Could not decode the signed transaction.\n") + tx=nil + return + } + err = tx.Deserialize(baData) + if err!=nil { + err = fmt.Errorf("Could not deserialise the signed transaction.\n") + tx=nil + return + } + + fmt.Printf("Ready to broadcast the transaction\n\n") + + return; + } + + if !dry_run { tx = w.BuildTransaction(transfers, rings_balances, rings, block_hash, height, scdata, treehash_raw, max_bits, gasstorage) }