├── LICENSE ├── README.md ├── account.go ├── account_info.go ├── block.go ├── cmd ├── atto-safesign │ ├── README.md │ ├── config.go │ ├── main.go │ └── util.go └── atto │ ├── config.go │ ├── main.go │ └── util.go ├── ed25519.go ├── go.mod ├── go.sum ├── process.go ├── receivable.go ├── request_interceptor.go ├── util.go ├── work.go └── work_test.go /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Richard Ulmer 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GoDoc](https://godoc.org/github.com/codesoap/atto?status.svg)](https://godoc.org/github.com/codesoap/atto) 2 | 3 | atto is a tiny Nano wallet, which focuses on ease of use through 4 | simplicity. Included is a rudimentary Go library to interact with Nano 5 | nodes. 6 | 7 | Disclaimer: I am no cryptographer and atto has not been audited. I 8 | cannot guarantee that atto is free of security compromising bugs. If 9 | you want to be extra cautious, I recommend offline signing, which is 10 | possible with the included [atto-safesign](cmd/atto-safesign/). 11 | 12 | # Installation 13 | You can download precompiled binaries from the [releases 14 | page](https://github.com/codesoap/atto/releases) or build atto yourself 15 | like this; go 1.15 or higher is required: 16 | 17 | ```shell 18 | git clone 'https://github.com/codesoap/atto.git' 19 | cd atto 20 | go build ./cmd/atto 21 | # The atto binary is now available at ./atto. You could also install 22 | # to ~/go/bin/ by executing "go install ./cmd/atto". 23 | ``` 24 | 25 | For Arch Linux @kseistrup also made [atto available in the 26 | AUR](https://aur.archlinux.org/packages/atto/). 27 | 28 | # Usage 29 | ```console 30 | $ # The new command generates a new seed. 31 | $ atto new 32 | D420296F5FEF486175FAA8F649DED00A5B0A096DB8D03972937542C51A7F296C 33 | $ # Store it in your password manager: 34 | $ pass insert nano 35 | Enter password for nano: D420296F5FEF486175FAA8F649DED00A5B0A096DB8D03972937542C51A7F296C 36 | Retype password for nano: D420296F5FEF486175FAA8F649DED00A5B0A096DB8D03972937542C51A7F296C 37 | 38 | $ # The address command shows the address for an account. 39 | $ pass nano | atto address 40 | nano_3cyb3rwp5ba47t5jdzm5o7apeduppsgzw8ockn1dqt4xcqgapta6gh5htnnh 41 | 42 | $ # With address and all following commands you can also provide an 43 | $ # alternative account index (default is 0): 44 | $ pass nano | atto -a 1 address 45 | nano_1o3igdpf8c4msdgwcop71x4o16zzkhe4kyku4axdi8iwh8wh13e4fwgherik 46 | 47 | $ # The balance command will receive receivable funds automatically. 48 | $ pass nano | atto balance 49 | Creating receive block for 1.025 from nano_34ymtnmhwseiex4eqf7nnf5wcyg44kknuuen5wwurm18ma91msf6e1pqo8hx... done 50 | Creating receive block for 0.1 from nano_39nd8eksw1ia6aokn96z4uthocke47hfsx9gr31othm1nrfwnzmmaeehiccq... done 51 | 1.337 NANO 52 | 53 | $ # Choosing a representative is important for keeping the network 54 | $ # decentralized. 55 | $ pass nano | atto representative nano_1jr699mk1fi6mxy1y76fmuyf3dgms8s5pzcsge5cyt1az93x4n18uxjenx93 56 | Creating change block... done 57 | 58 | $ # To avoid accidental loss of funds, the send command requires 59 | $ # confirmation, unless the -y flag is given: 60 | $ pass nano | atto send 0.1 nano_11zdqnjpisos53uighoaw95satm4ptdruck7xujbjcs44pbkkbw1h3zomns5 61 | Send 0.1 NANO to nano_11zdqnjpisos53uighoaw95satm4ptdruck7xujbjcs44pbkkbw1h3zomns5? [y/N]: y 62 | Creating send block... done 63 | 64 | $ atto -h 65 | Usage: 66 | atto -v 67 | atto n[ew] 68 | atto [-a ACCOUNT_INDEX] a[ddress] 69 | atto [-a ACCOUNT_INDEX] b[alance] 70 | atto [-a ACCOUNT_INDEX] r[epresentative] [NEW_REPRESENTATIVE] 71 | atto [-a ACCOUNT_INDEX] [-y] s[end] AMOUNT RECEIVER 72 | 73 | If the -v flag is provided, atto will print its version number. 74 | 75 | The new subcommand generates a new seed, which can later be used with 76 | the other subcommands. 77 | 78 | The address, balance, representative and send subcommands expect a seed 79 | as the first line of their standard input. Showing the first address of 80 | a newly generated key could work like this: 81 | atto new | tee seed.txt | atto address 82 | 83 | The send subcommand also expects manual confirmation of the transaction, 84 | unless the -y flag is given. 85 | 86 | The address subcommand displays addresses for a seed, the balance 87 | subcommand receives receivable blocks and shows the balance of an 88 | account, the representative subcommand shows the current representative 89 | if NEW_REPRESENTATIVE is not given and changes the account's 90 | representative if it is given and the send subcommand sends funds to an 91 | address. 92 | 93 | ACCOUNT_INDEX is an optional parameter, which must be a number between 0 94 | and 4,294,967,295. It allows you to use multiple accounts derived from 95 | the same seed. By default the account with index 0 is chosen. 96 | 97 | Environment: 98 | ATTO_BASIC_AUTH_USERNAME The username for HTTP Basic Authentication. 99 | If set, HTTP Basic Authentication will be 100 | used when making requests to the node. 101 | ATTO_BASIC_AUTH_PASSWORD The password to use for HTTP Basic 102 | Authentication. 103 | ``` 104 | 105 | # Technical details 106 | atto is written with ca. 1000 lines of code and uses minimal external 107 | dependencies. This makes it easy to audit the code yourself and ensure, 108 | that it does nothing you wouldn't want it to do. 109 | 110 | To change some defaults, like the node to use, take a look at 111 | `cmd/atto/config.go`. 112 | 113 | Signatures are created without the help of a node, to avoid your seed or 114 | private keys being stolen by a node operator. The received account info 115 | is always validated using block signatures to ensure the node operator 116 | cannot manipulate atto by, for example, reporting wrong balances. 117 | 118 | atto does not have any persistance and writes nothing to your 119 | file system. This makes atto very portable, but also means, that 120 | no history is stored locally. I recommend using a service like 121 | https://blocklattice.io/ to investigate transaction history. 122 | 123 | # Donations 124 | If you want to show your appreciation for atto, you can donate to me at 125 | `nano_1i7wsbehgwhxct91wpojr1j588ydikd64uc7p3kj54nofqioc6ydjopezf13`. 126 | -------------------------------------------------------------------------------- /account.go: -------------------------------------------------------------------------------- 1 | package atto 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "math/big" 7 | "strings" 8 | 9 | "filippo.io/edwards25519" 10 | "golang.org/x/crypto/blake2b" 11 | ) 12 | 13 | // ErrAccountNotFound is used when an account could not be found by the 14 | // queried node. 15 | var ErrAccountNotFound = fmt.Errorf("account has not yet been opened") 16 | 17 | // ErrAccountManipulated is used when it seems like an account has been 18 | // manipulated. This probably means someone is trying to steal funds. 19 | var ErrAccountManipulated = fmt.Errorf("the received account info has been manipulated") 20 | 21 | // Account holds the public key and address of a Nano account. 22 | type Account struct { 23 | PublicKey *big.Int 24 | Address string 25 | } 26 | 27 | type blockInfo struct { 28 | Error string `json:"error"` 29 | Contents Block `json:"contents"` 30 | } 31 | 32 | // NewAccount creates a new Account and populates both its fields. 33 | func NewAccount(privateKey *big.Int) (a Account, err error) { 34 | a.PublicKey = derivePublicKey(privateKey) 35 | a.Address, err = getAddress(a.PublicKey) 36 | return 37 | } 38 | 39 | // NewAccountFromAddress creates a new Account and populates both its 40 | // fields. 41 | func NewAccountFromAddress(address string) (a Account, err error) { 42 | a.Address = address 43 | a.PublicKey, err = getPublicKeyFromAddress(address) 44 | return 45 | } 46 | 47 | func derivePublicKey(privateKey *big.Int) *big.Int { 48 | hashBytes := blake2b.Sum512(bigIntToBytes(privateKey, 32)) 49 | scalar, err := edwards25519.NewScalar().SetBytesWithClamping(hashBytes[:32]) 50 | if err != nil { 51 | panic(err) 52 | } 53 | publicKeyBytes := edwards25519.NewIdentityPoint().ScalarBaseMult(scalar).Bytes() 54 | return big.NewInt(0).SetBytes(publicKeyBytes) 55 | } 56 | 57 | func getAddress(publicKey *big.Int) (string, error) { 58 | base32PublicKey := base32Encode(publicKey) 59 | 60 | hasher, err := blake2b.New(5, nil) 61 | if err != nil { 62 | return "", err 63 | } 64 | publicKeyBytes := bigIntToBytes(publicKey, 32) 65 | if _, err := hasher.Write(publicKeyBytes); err != nil { 66 | return "", err 67 | } 68 | hashBytes := hasher.Sum(nil) 69 | base32Hash := base32Encode(big.NewInt(0).SetBytes(revertBytes(hashBytes))) 70 | 71 | address := "nano_" + 72 | strings.Repeat("1", 52-len(base32PublicKey)) + base32PublicKey + 73 | strings.Repeat("1", 8-len(base32Hash)) + base32Hash 74 | return address, nil 75 | } 76 | 77 | // FetchAccountInfo fetches the AccountInfo of Account from the given 78 | // node. 79 | // 80 | // It is also verified, that the retreived AccountInfo is valid by 81 | // doing a block_info RPC for the frontier, verifying the signature 82 | // and ensuring that no fields have been changed in the account_info 83 | // response. 84 | // 85 | // May return ErrAccountNotFound or ErrAccountManipulated. 86 | // 87 | // If ErrAccountNotFound is returned, FirstReceive can be used to 88 | // create a first Block and AccountInfo and create the account by then 89 | // submitting this Block. 90 | func (a Account) FetchAccountInfo(node string) (i AccountInfo, err error) { 91 | requestBody := fmt.Sprintf(`{`+ 92 | `"action": "account_info",`+ 93 | `"account": "%s",`+ 94 | `"representative": "true"`+ 95 | `}`, a.Address) 96 | responseBytes, err := doRPC(requestBody, node) 97 | if err != nil { 98 | return 99 | } 100 | if err = json.Unmarshal(responseBytes, &i); err != nil { 101 | return 102 | } 103 | // Need to check i.Error because of 104 | // https://github.com/nanocurrency/nano-node/issues/1782. 105 | if i.Error == "Account not found" { 106 | err = ErrAccountNotFound 107 | } else if i.Error != "" { 108 | err = fmt.Errorf("could not fetch account info: %s", i.Error) 109 | } else { 110 | i.PublicKey = a.PublicKey 111 | i.Address = a.Address 112 | err = a.verifyInfo(i, node) 113 | } 114 | return 115 | } 116 | 117 | // verifyInfo gets the frontier block of info, ensures that Hash, 118 | // Representative and Balance match and verifies it's signature. 119 | func (a Account) verifyInfo(info AccountInfo, node string) error { 120 | requestBody := fmt.Sprintf(`{`+ 121 | `"action": "block_info",`+ 122 | `"json_block": "true",`+ 123 | `"hash": "%s"`+ 124 | `}`, info.Frontier) 125 | responseBytes, err := doRPC(requestBody, node) 126 | if err != nil { 127 | return err 128 | } 129 | var block blockInfo 130 | if err = json.Unmarshal(responseBytes, &block); err != nil { 131 | return err 132 | } 133 | if info.Error != "" { 134 | return fmt.Errorf("could not get block info: %s", info.Error) 135 | } 136 | hash, err := block.Contents.Hash() 137 | if err != nil { 138 | return err 139 | } 140 | if err = block.Contents.verifySignature(a); err == errInvalidSignature || 141 | info.Frontier != hash || 142 | info.Representative != block.Contents.Representative || 143 | info.Balance != block.Contents.Balance { 144 | return ErrAccountManipulated 145 | } 146 | return err 147 | } 148 | 149 | // FetchReceivable fetches all unreceived blocks of Account from node. 150 | func (a Account) FetchReceivable(node string) ([]Receivable, error) { 151 | requestBody := fmt.Sprintf(`{`+ 152 | `"action": "receivable", `+ 153 | `"account": "%s", `+ 154 | `"include_only_confirmed": "true", `+ 155 | `"source": "true"`+ 156 | `}`, a.Address) 157 | responseBytes, err := doRPC(requestBody, node) 158 | if err != nil { 159 | return nil, err 160 | } 161 | var receivable internalReceivable 162 | err = json.Unmarshal(responseBytes, &receivable) 163 | // Need to check receivable.Error because of 164 | // https://github.com/nanocurrency/nano-node/issues/1782. 165 | if err == nil && receivable.Error != "" { 166 | err = fmt.Errorf("could not fetch unreceived sends: %s", receivable.Error) 167 | } 168 | return internalReceivableToReceivable(receivable), err 169 | } 170 | 171 | // FirstReceive creates the first receive block of an account. The block 172 | // will still be missing its signature and work. FirstReceive will also 173 | // return AccountInfo, which can be used to create further blocks. 174 | func (a Account) FirstReceive(receivable Receivable, representative string) (AccountInfo, Block, error) { 175 | block := Block{ 176 | Type: "state", 177 | SubType: SubTypeReceive, 178 | Account: a.Address, 179 | Previous: "0000000000000000000000000000000000000000000000000000000000000000", 180 | Representative: representative, 181 | Balance: receivable.Amount, 182 | Link: receivable.Hash, 183 | } 184 | hash, err := block.Hash() 185 | if err != nil { 186 | return AccountInfo{}, Block{}, err 187 | } 188 | info := AccountInfo{ 189 | Frontier: hash, 190 | Representative: block.Representative, 191 | Balance: block.Balance, 192 | PublicKey: a.PublicKey, 193 | Address: a.Address, 194 | } 195 | return info, block, err 196 | } 197 | -------------------------------------------------------------------------------- /account_info.go: -------------------------------------------------------------------------------- 1 | package atto 2 | 3 | import ( 4 | "fmt" 5 | "math/big" 6 | "strings" 7 | ) 8 | 9 | // AccountInfo holds the basic data needed for Block creation. 10 | type AccountInfo struct { 11 | // Ignore this field. It only exists because of 12 | // https://github.com/nanocurrency/nano-node/issues/1782. 13 | Error string `json:"error"` 14 | 15 | Frontier string `json:"frontier"` 16 | Representative string `json:"representative"` 17 | Balance string `json:"balance"` 18 | 19 | PublicKey *big.Int `json:"-"` 20 | Address string `json:"-"` 21 | } 22 | 23 | // Send creates a send block, which will still be missing its signature 24 | // and work. The Frontier and Balance of the AccountInfo will be 25 | // updated. The amount is interpreted as Nano, not raw! 26 | func (i *AccountInfo) Send(amount, toAddr string) (Block, error) { 27 | balance, err := getBalanceAfterSend(i.Balance, amount) 28 | if err != nil { 29 | return Block{}, err 30 | } 31 | recipientNumber, err := getPublicKeyFromAddress(toAddr) 32 | if err != nil { 33 | return Block{}, err 34 | } 35 | recipientBytes := bigIntToBytes(recipientNumber, 32) 36 | block := Block{ 37 | Type: "state", 38 | SubType: SubTypeSend, 39 | Account: i.Address, 40 | Previous: i.Frontier, 41 | Representative: i.Representative, 42 | Balance: balance.String(), 43 | Link: fmt.Sprintf("%064X", recipientBytes), 44 | } 45 | hash, err := block.Hash() 46 | if err != nil { 47 | return Block{}, err 48 | } 49 | i.Frontier = hash 50 | i.Balance = block.Balance 51 | return block, err 52 | } 53 | 54 | func getBalanceAfterSend(oldBalance string, amount string) (*big.Int, error) { 55 | balance, ok := big.NewInt(0).SetString(oldBalance, 10) 56 | if !ok { 57 | err := fmt.Errorf("cannot parse '%s' as an integer", oldBalance) 58 | return nil, err 59 | } 60 | amountRaw, err := nanoToRaw(amount) 61 | if err != nil { 62 | return nil, err 63 | } 64 | return balance.Sub(balance, amountRaw), nil 65 | } 66 | 67 | func nanoToRaw(amountString string) (*big.Int, error) { 68 | i := strings.Index(amountString, ".") 69 | missingZerosUntilRaw := 30 70 | if i > -1 { 71 | missingZerosUntilRaw = 31 + i - len(amountString) 72 | amountString = amountString[:i] + amountString[i+1:] // Remove "." 73 | } 74 | amountString += strings.Repeat("0", missingZerosUntilRaw) 75 | amount, ok := big.NewInt(0).SetString(amountString, 10) 76 | if !ok { 77 | return nil, fmt.Errorf("cannot parse '%s' as an interger", amountString) 78 | } 79 | return amount, nil 80 | } 81 | 82 | // Change creates a change block, which will still be missing its 83 | // signature and work. The Frontier and Representative of the 84 | // AccountInfo will be updated. 85 | func (i *AccountInfo) Change(representative string) (Block, error) { 86 | block := Block{ 87 | Type: "state", 88 | SubType: SubTypeChange, 89 | Account: i.Address, 90 | Previous: i.Frontier, 91 | Representative: representative, 92 | Balance: i.Balance, 93 | Link: "0000000000000000000000000000000000000000000000000000000000000000", 94 | } 95 | hash, err := block.Hash() 96 | if err != nil { 97 | return Block{}, err 98 | } 99 | i.Frontier = hash 100 | return block, err 101 | } 102 | 103 | // Receive creates a receive block, which will still be missing its 104 | // signature and work. The Frontier and Balance of the AccountInfo will 105 | // be updated. 106 | func (i *AccountInfo) Receive(receivable Receivable) (Block, error) { 107 | updatedBalance, ok := big.NewInt(0).SetString(i.Balance, 10) 108 | if !ok { 109 | err := fmt.Errorf("cannot parse '%s' as an integer", i.Balance) 110 | return Block{}, err 111 | } 112 | amount, ok := big.NewInt(0).SetString(receivable.Amount, 10) 113 | if !ok { 114 | err := fmt.Errorf("cannot parse '%s' as an integer", receivable.Amount) 115 | return Block{}, err 116 | } 117 | if amount.Sign() < 1 { 118 | err := fmt.Errorf("amount '%s' is not positive", receivable.Amount) 119 | return Block{}, err 120 | } 121 | updatedBalance.Add(updatedBalance, amount) 122 | block := Block{ 123 | Type: "state", 124 | SubType: SubTypeReceive, 125 | Account: i.Address, 126 | Previous: i.Frontier, 127 | Representative: i.Representative, 128 | Balance: updatedBalance.String(), 129 | Link: receivable.Hash, 130 | } 131 | hash, err := block.Hash() 132 | if err != nil { 133 | return Block{}, err 134 | } 135 | i.Frontier = hash 136 | i.Balance = block.Balance 137 | return block, err 138 | } 139 | -------------------------------------------------------------------------------- /block.go: -------------------------------------------------------------------------------- 1 | package atto 2 | 3 | import ( 4 | "encoding/hex" 5 | "encoding/json" 6 | "fmt" 7 | "math/big" 8 | "strings" 9 | 10 | "golang.org/x/crypto/blake2b" 11 | ) 12 | 13 | var errInvalidSignature = fmt.Errorf("invalid block signature") 14 | 15 | // ErrSignatureMissing is used when the Signature of a Block is missing 16 | // but required for the attempted operation. 17 | var ErrSignatureMissing = fmt.Errorf("signature is missing") 18 | 19 | // ErrWorkMissing is used when the Work of a Block is missing but 20 | // required for the attempted operation. 21 | var ErrWorkMissing = fmt.Errorf("work is missing") 22 | 23 | var ( 24 | // See https://docs.nano.org/integration-guides/work-generation/#difficulty-thresholds 25 | defaultWorkThreshold uint64 = 0xfffffff800000000 26 | receiveWorkThreshold uint64 = 0xfffffe0000000000 27 | ) 28 | 29 | // BlockSubType represents the sub-type of a block. 30 | type BlockSubType int64 31 | 32 | const ( 33 | // SubTypeReceive denotes blocks which raise the balance. 34 | SubTypeReceive BlockSubType = iota 35 | 36 | // SubTypeChange denotes blocks which change the representative. 37 | SubTypeChange 38 | 39 | // SubTypeSend denotes blocks which lower the balance. 40 | SubTypeSend 41 | ) 42 | 43 | // Block represents a block in the block chain of an account. 44 | type Block struct { 45 | Type string `json:"type"` 46 | Account string `json:"account"` 47 | Previous string `json:"previous"` 48 | Representative string `json:"representative"` 49 | Balance string `json:"balance"` 50 | Link string `json:"link"` 51 | Signature string `json:"signature"` 52 | Work string `json:"work"` 53 | 54 | // This field is not part of the JSON but needed to improve the 55 | // performance of FetchWork and the security of Submit. 56 | SubType BlockSubType `json:"-"` 57 | } 58 | 59 | type workGenerateResponse struct { 60 | Error string `json:"error"` 61 | Work string `json:"work"` 62 | } 63 | 64 | // Sign computes and sets the Signature of b. 65 | func (b *Block) Sign(privateKey *big.Int) error { 66 | publicKey, err := getPublicKeyFromAddress(b.Account) 67 | if err != nil { 68 | return err 69 | } 70 | hash, err := b.hashBytes() 71 | if err != nil { 72 | return err 73 | } 74 | signature, err := sign(publicKey, privateKey, hash) 75 | if err != nil { 76 | return err 77 | } 78 | b.Signature = fmt.Sprintf("%0128X", signature) 79 | return nil 80 | } 81 | 82 | func (b *Block) verifySignature(a Account) (err error) { 83 | sig, ok := big.NewInt(0).SetString(b.Signature, 16) 84 | if !ok { 85 | return fmt.Errorf("cannot parse '%s' as an integer", b.Signature) 86 | } 87 | hash, err := b.hashBytes() 88 | if err != nil { 89 | return err 90 | } 91 | if !isValidSignature(a.PublicKey, hash, bigIntToBytes(sig, 64)) { 92 | err = errInvalidSignature 93 | } 94 | return 95 | } 96 | 97 | // FetchWork uses the generate_work RPC on node to fetch and then set 98 | // the Work of b. 99 | func (b *Block) FetchWork(node string) error { 100 | hash, err := b.workHash() 101 | if err != nil { 102 | return err 103 | } 104 | 105 | requestBody := fmt.Sprintf(`{"action":"work_generate", "hash":"%s"`, hash) 106 | if b.SubType == SubTypeReceive { 107 | // Receive blocks need less work, so lower the difficulty. 108 | requestBody += fmt.Sprintf(`, "difficulty":"%016x"`, receiveWorkThreshold) 109 | } 110 | requestBody += `}` 111 | 112 | responseBytes, err := doRPC(requestBody, node) 113 | if err != nil { 114 | return err 115 | } 116 | var response workGenerateResponse 117 | if err = json.Unmarshal(responseBytes, &response); err != nil { 118 | return err 119 | } 120 | // Need to check response.Error because of 121 | // https://github.com/nanocurrency/nano-node/issues/1782. 122 | if response.Error != "" { 123 | return fmt.Errorf("could not get work for block: %s", response.Error) 124 | } 125 | b.Work = response.Work 126 | return nil 127 | } 128 | 129 | // GenerateWork uses the CPU of the local computer to generate work and 130 | // then sets it as b.Work. 131 | func (b *Block) GenerateWork() error { 132 | hashString, err := b.workHash() 133 | if err != nil { 134 | return err 135 | } 136 | hash, err := hex.DecodeString(hashString) 137 | if err != nil { 138 | return err 139 | } 140 | workThreshold := defaultWorkThreshold 141 | if b.SubType == SubTypeReceive { 142 | // Receive blocks need less work, so lower the difficulty. 143 | workThreshold = receiveWorkThreshold 144 | } 145 | nonce, err := findNonce(workThreshold, hash) 146 | if err != nil { 147 | return err 148 | } 149 | b.Work = fmt.Sprintf("%016x", nonce) 150 | return nil 151 | } 152 | 153 | func (b Block) workHash() (string, error) { 154 | if b.Previous == strings.Repeat("0", 64) { 155 | publicKey, err := getPublicKeyFromAddress(b.Account) 156 | if err != nil { 157 | return "", err 158 | } 159 | return fmt.Sprintf("%064X", bigIntToBytes(publicKey, 32)), nil 160 | } 161 | return b.Previous, nil 162 | } 163 | 164 | // Hash calculates the block's hash and returns it's string 165 | // representation. 166 | func (b Block) Hash() (string, error) { 167 | hashBytes, err := b.hashBytes() 168 | if err != nil { 169 | return "", err 170 | } 171 | return fmt.Sprintf("%064X", hashBytes), nil 172 | } 173 | 174 | func (b Block) hashBytes() ([]byte, error) { 175 | // See https://nanoo.tools/block for a reference. 176 | 177 | msg := make([]byte, 176, 176) 178 | 179 | msg[31] = 0x6 // block preamble 180 | 181 | publicKey, err := getPublicKeyFromAddress(b.Account) 182 | if err != nil { 183 | return nil, err 184 | } 185 | copy(msg[32:64], bigIntToBytes(publicKey, 32)) 186 | 187 | previous, err := hex.DecodeString(b.Previous) 188 | if err != nil { 189 | return nil, err 190 | } 191 | copy(msg[64:96], previous) 192 | 193 | representative, err := getPublicKeyFromAddress(b.Representative) 194 | if err != nil { 195 | return nil, err 196 | } 197 | copy(msg[96:128], bigIntToBytes(representative, 32)) 198 | 199 | balance, ok := big.NewInt(0).SetString(b.Balance, 10) 200 | if !ok { 201 | return nil, fmt.Errorf("cannot parse '%s' as an integer", b.Balance) 202 | } 203 | copy(msg[128:144], bigIntToBytes(balance, 16)) 204 | 205 | link, err := hex.DecodeString(b.Link) 206 | if err != nil { 207 | return nil, err 208 | } 209 | copy(msg[144:176], link) 210 | 211 | hash := blake2b.Sum256(msg) 212 | return hash[:], nil 213 | } 214 | 215 | // Submit submits the Block to the given node. Work and Signature of b 216 | // must be populated beforehand. 217 | // 218 | // May return ErrWorkMissing or ErrSignatureMissing. 219 | func (b Block) Submit(node string) error { 220 | if b.Work == "" { 221 | return ErrWorkMissing 222 | } 223 | if b.Signature == "" { 224 | return ErrSignatureMissing 225 | } 226 | var subType string 227 | switch b.SubType { 228 | case SubTypeReceive: 229 | subType = "receive" 230 | case SubTypeChange: 231 | subType = "change" 232 | case SubTypeSend: 233 | subType = "send" 234 | } 235 | process := process{ 236 | Action: "process", 237 | JsonBlock: "true", 238 | SubType: subType, 239 | Block: b, 240 | } 241 | return doProcessRPC(process, node) 242 | } 243 | -------------------------------------------------------------------------------- /cmd/atto-safesign/README.md: -------------------------------------------------------------------------------- 1 | `atto-safesign` is intended to be used as an extension to `atto`, so I 2 | strongly recommend you familiarize yourself with `atto` before looking 3 | at `atto-safesign`. 4 | 5 | # Motivation 6 | If you want to keep your seed extra safe you may choose to never take 7 | it onto a computer that is connected to the internet. `atto-safesign` 8 | enables you to do this by creating a file which contains initially 9 | unsigned blocks. The blocks in this file can then be signed on the 10 | offline computer and transferred back to the online computer to submit 11 | the blocks to the Nano network. 12 | 13 | # Installation 14 | You can download precompiled binaries from the [releases 15 | page](https://github.com/codesoap/atto/releases) or build atto-safesign 16 | yourself like this; go 1.15 or higher is required: 17 | 18 | ```shell 19 | git clone 'https://github.com/codesoap/atto.git' 20 | cd atto 21 | go build ./cmd/atto-safesign/ 22 | # The atto-safesign binary is now available at ./atto-safesign. You could 23 | # also install to ~/go/bin/ by executing "go install ./cmd/atto-safesign/". 24 | ``` 25 | 26 | # Usage 27 | Here is an example use case where receivable blocks are received and the 28 | representative changed: 29 | 30 | ``` 31 | online$ # These steps take place on an online computer: 32 | online$ MY_ADDRESS=nano_1yqtxctufmrgfa5aq8gqa3eyr45hsghqau8ihe7hzaq1tdggjxsqbbkqofi7 33 | online$ echo $MY_ADDRESS | atto-safesign test.atto receive 34 | online$ echo $MY_ADDRESS | atto-safesign test.atto representative nano_3up3y8cd3hhs7zdpmkpssgb1iyjpke3xwmgqy8rg58z1hwryqpjqnkuqayps 35 | 36 | offline$ # The sign subcommand can then be used on an offline computer: 37 | offline$ pass nano | atto-safesign test.atto sign 38 | Sign block that sets balance to 0.1 NANO and representative to nano_18shbirtzhmkf7166h39nowj9c9zrpufeg75bkbyoobqwf1iu3srfm9eo3pz? [y/N]: y 39 | Sign block that sets balance to 0.232 NANO and representative to nano_18shbirtzhmkf7166h39nowj9c9zrpufeg75bkbyoobqwf1iu3srfm9eo3pz? [y/N]: y 40 | Sign block that sets balance to 0.232 NANO and representative to nano_3up3y8cd3hhs7zdpmkpssgb1iyjpke3xwmgqy8rg58z1hwryqpjqnkuqayps? [y/N]: y 41 | 42 | online$ # Back at the online computer, the now signed blocks can be submitted: 43 | online$ echo $MY_ADDRESS | atto-safesign test.atto submit 44 | Submitting block... done 45 | Submitting block... done 46 | Submitting block... done 47 | ``` 48 | 49 | This is `atto-safesign`'s help text: 50 | ```console 51 | $ atto-safesign -h 52 | Usage: 53 | atto-safesign -v 54 | atto-safesign FILE receive 55 | atto-safesign FILE representative REPRESENTATIVE 56 | atto-safesign FILE send AMOUNT RECEIVER 57 | atto-safesign [-a ACCOUNT_INDEX] [-y] FILE sign 58 | atto-safesign FILE submit 59 | 60 | If the -v flag is provided, atto-safesign will print its version number. 61 | 62 | The receive, representative, send and submit subcommands expect a Nano 63 | address as the first line of their standard input. This address will be 64 | the account of the generated and submitted blocks. 65 | 66 | The receive, representative and send subcommands will generate blocks 67 | and append them to FILE. The blocks will still be lacking their 68 | signature. The receive subcommand will create multiple blocks, if there 69 | are multiple receivable blocks. The representative subcommand will 70 | create a block for changing the representative and the send subcommand 71 | will create a block for sending funds to an address. 72 | 73 | The sign subcommand expects a seed as the first line of standard input. 74 | It also expects manual confirmation before signing blocks, unless the 75 | -y flag is given. The seed and ACCOUNT_INDEX must belong to the address 76 | used when creating blocks with receive, representative or send. 77 | 78 | The sign subcommand will add signatures to all blocks in FILE. It is the 79 | only subcommand that requires no network connection. 80 | 81 | The submit subcommand will submit all blocks contained in FILE to the 82 | Nano network. 83 | 84 | ACCOUNT_INDEX is an optional parameter, which allows you to use 85 | different accounts derived from the given seed. By default the account 86 | with index 0 is chosen. 87 | 88 | Environment: 89 | ATTO_BASIC_AUTH_USERNAME The username for HTTP Basic Authentication. 90 | If set, HTTP Basic Authentication will be 91 | used when making requests to the node. 92 | ATTO_BASIC_AUTH_PASSWORD The password to use for HTTP Basic 93 | Authentication. 94 | ``` 95 | -------------------------------------------------------------------------------- /cmd/atto-safesign/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | var ( 4 | // The node needs to support the work_generate action. See 5 | // e.g. https://publicnodes.somenano.com to find public nodes 6 | // or set up your own node. 7 | node = "https://rainstorm.city/api" 8 | 9 | // defaultRepresentative will be set as the representative when 10 | // opening an accout, but can be changed afterwards. See e.g. 11 | // https://blocklattice.io/representatives to find representatives. 12 | defaultRepresentative = "nano_1jtx5p8141zjtukz4msp1x93st7nh475f74odj8673qqm96xczmtcnanos1o" 13 | 14 | // workSource specifies where the work for block submission shall 15 | // come from. These options are available: 16 | // - workSourceLocal: The work is generated on the CPU of the 17 | // current computer. 18 | // - workSourceNode: The work is fetched from the node using the 19 | // work_generate action. Make sure that your node supports it. 20 | // - workSourceLocalFallback: It is attempted to fetch the work 21 | // from the node, but if this fails, it will be generated on 22 | // the CPU of the current computer. 23 | workSource = workSourceLocalFallback 24 | ) 25 | -------------------------------------------------------------------------------- /cmd/atto-safesign/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "flag" 7 | "fmt" 8 | "io" 9 | "math/big" 10 | "net/http" 11 | "os" 12 | 13 | "github.com/codesoap/atto" 14 | ) 15 | 16 | var usage = `Usage: 17 | atto-safesign -v 18 | atto-safesign FILE receive 19 | atto-safesign FILE representative REPRESENTATIVE 20 | atto-safesign FILE send AMOUNT RECEIVER 21 | atto-safesign [-a ACCOUNT_INDEX] [-y] FILE sign 22 | atto-safesign FILE submit 23 | 24 | If the -v flag is provided, atto-safesign will print its version number. 25 | 26 | The receive, representative, send and submit subcommands expect a Nano 27 | address as the first line of their standard input. This address will be 28 | the account of the generated and submitted blocks. 29 | 30 | The receive, representative and send subcommands will generate blocks 31 | and append them to FILE. The blocks will still be lacking their 32 | signature. The receive subcommand will create multiple blocks, if there 33 | are multiple receivable blocks. The representative subcommand will 34 | create a block for changing the representative and the send subcommand 35 | will create a block for sending funds to an address. 36 | 37 | The sign subcommand expects a seed as the first line of standard input. 38 | It also expects manual confirmation before signing blocks, unless the 39 | -y flag is given. The seed and ACCOUNT_INDEX must belong to the address 40 | used when creating blocks with receive, representative or send. 41 | 42 | The sign subcommand will add signatures to all blocks in FILE. It is the 43 | only subcommand that requires no network connection. 44 | 45 | The submit subcommand will submit all blocks contained in FILE to the 46 | Nano network. 47 | 48 | ACCOUNT_INDEX is an optional parameter, which allows you to use 49 | different accounts derived from the given seed. By default the account 50 | with index 0 is chosen. 51 | 52 | Environment: 53 | ATTO_BASIC_AUTH_USERNAME The username for HTTP Basic Authentication. 54 | If set, HTTP Basic Authentication will be 55 | used when making requests to the node. 56 | ATTO_BASIC_AUTH_PASSWORD The password to use for HTTP Basic 57 | Authentication. 58 | ` 59 | 60 | type workSourceType int 61 | 62 | const ( 63 | workSourceLocal workSourceType = iota 64 | workSourceNode 65 | workSourceLocalFallback 66 | ) 67 | 68 | var accountIndexFlag uint 69 | var yFlag bool 70 | 71 | func init() { 72 | var vFlag bool 73 | flag.Usage = func() { fmt.Fprint(os.Stderr, usage) } 74 | flag.UintVar(&accountIndexFlag, "a", 0, "") 75 | flag.BoolVar(&yFlag, "y", false, "") 76 | flag.BoolVar(&vFlag, "v", false, "") 77 | flag.Parse() 78 | if vFlag { 79 | fmt.Println("1.4.0") 80 | os.Exit(0) 81 | } 82 | if accountIndexFlag >= 1<<32 || flag.NArg() < 2 { 83 | flag.Usage() 84 | os.Exit(1) 85 | } 86 | var ok bool 87 | switch flag.Arg(1) { 88 | case "receive", "sign", "submit": 89 | ok = flag.NArg() == 2 90 | case "representative": 91 | ok = flag.NArg() == 3 92 | case "send": 93 | ok = flag.NArg() == 4 94 | } 95 | if !ok { 96 | flag.Usage() 97 | os.Exit(1) 98 | } 99 | setUpNodeAuthentication() 100 | } 101 | 102 | func setUpNodeAuthentication() { 103 | if os.Getenv("ATTO_BASIC_AUTH_USERNAME") != "" { 104 | username := os.Getenv("ATTO_BASIC_AUTH_USERNAME") 105 | password := os.Getenv("ATTO_BASIC_AUTH_PASSWORD") 106 | atto.RequestInterceptor = func(request *http.Request) error { 107 | request.SetBasicAuth(username, password) 108 | return nil 109 | } 110 | } 111 | } 112 | 113 | func main() { 114 | var err error 115 | switch flag.Arg(1) { 116 | case "receive": 117 | err = receive() 118 | case "representative": 119 | err = change() 120 | case "send": 121 | err = send() 122 | case "sign": 123 | err = sign() 124 | case "submit": 125 | err = submit() 126 | } 127 | if err != nil { 128 | fmt.Fprintf(os.Stderr, "Error: %v\n", err) 129 | os.Exit(2) 130 | } 131 | } 132 | 133 | func receive() error { 134 | addr, err := getFirstStdinLine() 135 | if err != nil { 136 | return err 137 | } 138 | account, err := atto.NewAccountFromAddress(addr) 139 | if err != nil { 140 | return err 141 | } 142 | firstReceive := false // Is this the very first block of the account? 143 | info, err := getLatestAccountInfo(account) 144 | if err == atto.ErrAccountNotFound { 145 | firstReceive = true 146 | } else if err != nil { 147 | return err 148 | } 149 | receivables, err := account.FetchReceivable(node) 150 | if err != nil { 151 | return err 152 | } 153 | for _, receivable := range receivables { 154 | var block atto.Block 155 | if firstReceive { 156 | info, block, err = account.FirstReceive(receivable, defaultRepresentative) 157 | firstReceive = false 158 | } else { 159 | block, err = info.Receive(receivable) 160 | } 161 | if err != nil { 162 | return err 163 | } 164 | if err = fillWork(&block, node); err != nil { 165 | return err 166 | } 167 | blockJSON, err := json.Marshal(block) 168 | if err != nil { 169 | return err 170 | } 171 | err = appendLineToFile(blockJSON) 172 | if err != nil { 173 | return err 174 | } 175 | } 176 | return nil 177 | } 178 | 179 | func change() error { 180 | representative := flag.Arg(2) 181 | addr, err := getFirstStdinLine() 182 | if err != nil { 183 | return err 184 | } 185 | account, err := atto.NewAccountFromAddress(addr) 186 | if err != nil { 187 | return err 188 | } 189 | info, err := getLatestAccountInfo(account) 190 | if err != nil { 191 | return err 192 | } 193 | block, err := info.Change(representative) 194 | if err != nil { 195 | return err 196 | } 197 | if err = fillWork(&block, node); err != nil { 198 | return err 199 | } 200 | blockJSON, err := json.Marshal(block) 201 | if err != nil { 202 | return err 203 | } 204 | return appendLineToFile(blockJSON) 205 | } 206 | 207 | func send() error { 208 | amount := flag.Arg(2) 209 | receiver := flag.Arg(3) 210 | addr, err := getFirstStdinLine() 211 | if err != nil { 212 | return err 213 | } 214 | account, err := atto.NewAccountFromAddress(addr) 215 | if err != nil { 216 | return err 217 | } 218 | info, err := getLatestAccountInfo(account) 219 | if err != nil { 220 | return err 221 | } 222 | block, err := info.Send(amount, receiver) 223 | if err != nil { 224 | return err 225 | } 226 | if err = fillWork(&block, node); err != nil { 227 | return err 228 | } 229 | blockJSON, err := json.Marshal(block) 230 | if err != nil { 231 | return err 232 | } 233 | return appendLineToFile(blockJSON) 234 | } 235 | 236 | func sign() error { 237 | seed, err := getFirstStdinLine() 238 | if err != nil { 239 | return err 240 | } 241 | privateKey, err := atto.NewPrivateKey(seed, uint32(accountIndexFlag)) 242 | if err != nil { 243 | return err 244 | } 245 | account, err := atto.NewAccount(privateKey) 246 | if err != nil { 247 | return err 248 | } 249 | blocks, err := getBlocksFromFile() 250 | if err != nil { 251 | return err 252 | } 253 | var outBuffer bytes.Buffer 254 | for _, block := range blocks { 255 | if account.Address != block.Account { 256 | txt := "Used account with address '%s' cannot sign block with address '%s'" 257 | return fmt.Errorf(txt, account.Address, block.Account) 258 | } 259 | if err = letUserVerifyBlock(block); err != nil { 260 | return err 261 | } 262 | block.Sign(privateKey) 263 | blockJSON, err := json.Marshal(block) 264 | if err != nil { 265 | return err 266 | } 267 | 268 | // Buffer output so that file can be overwritten as late as possible 269 | // to avoid problems during the write as much as possible. 270 | outBuffer.Write(blockJSON) // err is always nil. 271 | outBuffer.Write([]byte{'\n'}) // err is always nil. 272 | } 273 | 274 | file, err := os.Create(flag.Arg(0)) 275 | if err != nil { 276 | return err 277 | } 278 | defer file.Close() 279 | _, err = io.Copy(file, &outBuffer) 280 | return err 281 | } 282 | 283 | func submit() error { 284 | addr, err := getFirstStdinLine() 285 | if err != nil { 286 | return err 287 | } 288 | account, err := atto.NewAccountFromAddress(addr) 289 | if err != nil { 290 | return err 291 | } 292 | blocks, err := getBlocksFromFile() 293 | if err != nil { 294 | return err 295 | } 296 | 297 | var oldBalance *big.Int 298 | info, err := account.FetchAccountInfo(node) 299 | if err == atto.ErrAccountNotFound { 300 | oldBalance = big.NewInt(0) 301 | } else if err != nil { 302 | return err 303 | } else { 304 | var ok bool 305 | oldBalance, ok = big.NewInt(0).SetString(info.Balance, 10) 306 | if !ok { 307 | return fmt.Errorf("cannot parse '%s' as an integer", info.Balance) 308 | } 309 | } 310 | 311 | for _, block := range blocks { 312 | newBalance, ok := big.NewInt(0).SetString(block.Balance, 10) 313 | if !ok { 314 | return fmt.Errorf("cannot parse '%s' as an integer", block.Balance) 315 | } 316 | switch oldBalance.Cmp(newBalance) { 317 | case -1: 318 | block.SubType = atto.SubTypeReceive 319 | case 0: 320 | // If the balance does not change, this should be a "change" block. 321 | block.SubType = atto.SubTypeChange 322 | case 1: 323 | block.SubType = atto.SubTypeSend 324 | } 325 | fmt.Fprint(os.Stderr, "Submitting block... ") 326 | err = block.Submit(node) 327 | if err != nil { 328 | return err 329 | } 330 | fmt.Fprintln(os.Stderr, "done") 331 | oldBalance = newBalance 332 | } 333 | return nil 334 | } 335 | -------------------------------------------------------------------------------- /cmd/atto-safesign/util.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "flag" 7 | "fmt" 8 | "io" 9 | "math/big" 10 | "os" 11 | "runtime" 12 | "strings" 13 | 14 | "github.com/codesoap/atto" 15 | ) 16 | 17 | func getFirstStdinLine() (string, error) { 18 | in := bufio.NewReader(os.Stdin) 19 | firstLine, err := in.ReadString('\n') 20 | if err != nil { 21 | return "", err 22 | } 23 | return strings.TrimSpace(firstLine), nil 24 | } 25 | 26 | // getLatestAccountInfo returns an atto.AccountInfo with the latest 27 | // available block as it's Frontier. This is either the last block from 28 | // the file or the one fetched from the network, if the file contains no 29 | // blocks. 30 | func getLatestAccountInfo(acc atto.Account) (atto.AccountInfo, error) { 31 | blocks, err := getBlocksFromFile() 32 | if err != nil { 33 | return atto.AccountInfo{}, err 34 | } 35 | if len(blocks) == 0 { 36 | return acc.FetchAccountInfo(node) 37 | } 38 | latestBlock := blocks[len(blocks)-1] 39 | hash, err := latestBlock.Hash() 40 | if err != nil { 41 | return atto.AccountInfo{}, err 42 | } 43 | info := atto.AccountInfo{ 44 | Frontier: hash, 45 | Representative: latestBlock.Representative, 46 | Balance: latestBlock.Balance, 47 | PublicKey: acc.PublicKey, 48 | Address: acc.Address, 49 | } 50 | return info, nil 51 | } 52 | 53 | func getBlocksFromFile() ([]atto.Block, error) { 54 | file, err := os.Open(flag.Arg(0)) 55 | if err != nil { 56 | // The file has not been found, which is OK. 57 | return []atto.Block{}, nil 58 | } 59 | defer file.Close() 60 | reader := bufio.NewReader(file) 61 | blocks := make([]atto.Block, 0) 62 | for { 63 | line, err := reader.ReadBytes('\n') 64 | if err == io.EOF { 65 | break 66 | } else if err != nil { 67 | return nil, err 68 | } 69 | var block atto.Block 70 | if err = json.Unmarshal(line, &block); err != nil { 71 | return nil, err 72 | } 73 | blocks = append(blocks, block) 74 | } 75 | return blocks, nil 76 | } 77 | 78 | func appendLineToFile(in []byte) error { 79 | file, err := os.OpenFile(flag.Arg(0), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 80 | if err != nil { 81 | return err 82 | } 83 | defer file.Close() 84 | if _, err = file.Write(in); err != nil { 85 | return err 86 | } 87 | _, err = file.Write([]byte{'\n'}) 88 | return err 89 | } 90 | 91 | func rawToNanoString(raw *big.Int) string { 92 | rawPerNano, _ := big.NewInt(0).SetString("1000000000000000000000000000000", 10) 93 | absRaw := big.NewInt(0).Abs(raw) 94 | integerDigits, fractionalDigits := big.NewInt(0).QuoRem(absRaw, rawPerNano, big.NewInt(0)) 95 | res := integerDigits.String() 96 | if fractionalDigits.Sign() != 0 { 97 | fractionalDigitsString := fmt.Sprintf("%030s", fractionalDigits.String()) 98 | res += "." + strings.TrimRight(fractionalDigitsString, "0") 99 | } 100 | if raw.Sign() < 0 { 101 | return "-" + res + " NANO" 102 | } 103 | return res + " NANO" 104 | } 105 | 106 | func letUserVerifyBlock(block atto.Block) (err error) { 107 | if !yFlag { 108 | balanceInt, ok := big.NewInt(0).SetString(block.Balance, 10) 109 | if !ok { 110 | return fmt.Errorf("cannot parse '%s' as an integer", block.Balance) 111 | } 112 | balanceNano := rawToNanoString(balanceInt) 113 | txt := "Sign block that sets balance to %s and representative to %s? [y/N]: " 114 | fmt.Fprintf(os.Stderr, txt, balanceNano, block.Representative) 115 | 116 | // Explicitly openning /dev/tty or CONIN$ ensures function, even if 117 | // the standard input is not a terminal. 118 | var tty *os.File 119 | if runtime.GOOS == "windows" { 120 | tty, err = os.Open("CONIN$") 121 | } else { 122 | tty, err = os.Open("/dev/tty") 123 | } 124 | if err != nil { 125 | msg := "could not open terminal for confirmation input: %v" 126 | return fmt.Errorf(msg, err) 127 | } 128 | defer tty.Close() 129 | 130 | var confirmation string 131 | fmt.Fscanln(tty, &confirmation) 132 | if confirmation != "y" && confirmation != "Y" { 133 | fmt.Fprintln(os.Stderr, "Signing aborted.") 134 | os.Exit(0) 135 | } 136 | } 137 | return 138 | } 139 | 140 | func fillWork(block *atto.Block, node string) error { 141 | switch workSource { 142 | case workSourceLocal: 143 | return block.GenerateWork() 144 | case workSourceNode: 145 | return block.FetchWork(node) 146 | case workSourceLocalFallback: 147 | if err := block.FetchWork(node); err != nil { 148 | fmt.Fprintf(os.Stderr, "Could not fetch work from node (error: %v); generating it on CPU...\n", err) 149 | return block.GenerateWork() 150 | } 151 | return nil 152 | } 153 | return fmt.Errorf("unknown work source") 154 | } 155 | -------------------------------------------------------------------------------- /cmd/atto/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | var ( 4 | // The node that is used to interact with the nano network. See 5 | // e.g. https://publicnodes.somenano.com to find public nodes 6 | // or set up your own node. 7 | node = "https://rainstorm.city/api" 8 | 9 | // defaultRepresentative will be set as the representative when 10 | // opening an accout, but can be changed afterwards. See e.g. 11 | // https://blocklattice.io/representatives to find representatives. 12 | defaultRepresentative = "nano_1jtx5p8141zjtukz4msp1x93st7nh475f74odj8673qqm96xczmtcnanos1o" 13 | 14 | // workSource specifies where the work for block submission shall 15 | // come from. These options are available: 16 | // - workSourceLocal: The work is generated on the CPU of the 17 | // current computer. 18 | // - workSourceNode: The work is fetched from the node using the 19 | // work_generate action. Make sure that your node supports it. 20 | // - workSourceLocalFallback: It is attempted to fetch the work 21 | // from the node, but if this fails, it will be generated on 22 | // the CPU of the current computer. 23 | workSource = workSourceLocalFallback 24 | ) 25 | -------------------------------------------------------------------------------- /cmd/atto/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "math/big" 7 | "net/http" 8 | "os" 9 | 10 | "github.com/codesoap/atto" 11 | ) 12 | 13 | var usage = `Usage: 14 | atto -v 15 | atto n[ew] 16 | atto [-a ACCOUNT_INDEX] a[ddress] 17 | atto [-a ACCOUNT_INDEX] b[alance] 18 | atto [-a ACCOUNT_INDEX] r[epresentative] [NEW_REPRESENTATIVE] 19 | atto [-a ACCOUNT_INDEX] [-y] s[end] AMOUNT RECEIVER 20 | 21 | If the -v flag is provided, atto will print its version number. 22 | 23 | The new subcommand generates a new seed, which can later be used with 24 | the other subcommands. 25 | 26 | The address, balance, representative and send subcommands expect a seed 27 | as the first line of their standard input. Showing the first address of 28 | a newly generated key could work like this: 29 | atto new | tee seed.txt | atto address 30 | 31 | The send subcommand also expects manual confirmation of the transaction, 32 | unless the -y flag is given. 33 | 34 | The address subcommand displays addresses for a seed, the balance 35 | subcommand receives receivable blocks and shows the balance of an 36 | account, the representative subcommand shows the current representative 37 | if NEW_REPRESENTATIVE is not given and changes the account's 38 | representative if it is given and the send subcommand sends funds to an 39 | address. 40 | 41 | ACCOUNT_INDEX is an optional parameter, which must be a number between 0 42 | and 4,294,967,295. It allows you to use multiple accounts derived from 43 | the same seed. By default the account with index 0 is chosen. 44 | 45 | Environment: 46 | ATTO_BASIC_AUTH_USERNAME The username for HTTP Basic Authentication. 47 | If set, HTTP Basic Authentication will be 48 | used when making requests to the node. 49 | ATTO_BASIC_AUTH_PASSWORD The password to use for HTTP Basic 50 | Authentication. 51 | ` 52 | 53 | type workSourceType int 54 | 55 | const ( 56 | workSourceLocal workSourceType = iota 57 | workSourceNode 58 | workSourceLocalFallback 59 | ) 60 | 61 | var accountIndexFlag uint 62 | var yFlag bool 63 | 64 | func init() { 65 | var vFlag bool 66 | flag.Usage = func() { fmt.Fprint(os.Stderr, usage) } 67 | flag.UintVar(&accountIndexFlag, "a", 0, "") 68 | flag.BoolVar(&yFlag, "y", false, "") 69 | flag.BoolVar(&vFlag, "v", false, "") 70 | flag.Parse() 71 | if vFlag { 72 | fmt.Println("1.6.0") 73 | os.Exit(0) 74 | } 75 | if accountIndexFlag >= 1<<32 || flag.NArg() < 1 { 76 | flag.Usage() 77 | os.Exit(1) 78 | } 79 | var ok bool 80 | switch flag.Arg(0)[:1] { 81 | case "n", "a", "b": 82 | ok = flag.NArg() == 1 83 | case "r": 84 | ok = flag.NArg() == 1 || flag.NArg() == 2 85 | case "s": 86 | ok = flag.NArg() == 3 87 | } 88 | if !ok { 89 | flag.Usage() 90 | os.Exit(1) 91 | } 92 | setUpNodeAuthentication() 93 | } 94 | 95 | func setUpNodeAuthentication() { 96 | if os.Getenv("ATTO_BASIC_AUTH_USERNAME") != "" { 97 | username := os.Getenv("ATTO_BASIC_AUTH_USERNAME") 98 | password := os.Getenv("ATTO_BASIC_AUTH_PASSWORD") 99 | atto.RequestInterceptor = func(request *http.Request) error { 100 | request.SetBasicAuth(username, password) 101 | return nil 102 | } 103 | } 104 | } 105 | 106 | func main() { 107 | var err error 108 | switch flag.Arg(0)[:1] { 109 | case "n": 110 | err = printNewSeed() 111 | case "a": 112 | err = printAddress() 113 | case "b": 114 | err = printBalance() 115 | case "r": 116 | if flag.NArg() == 1 { 117 | err = printRepresentative() 118 | } else { 119 | err = changeRepresentative() 120 | } 121 | case "s": 122 | err = sendFunds() 123 | } 124 | if err != nil { 125 | fmt.Fprintf(os.Stderr, "Error: %v\n", err) 126 | os.Exit(2) 127 | } 128 | } 129 | 130 | func printNewSeed() error { 131 | seed, err := atto.GenerateSeed() 132 | if err == nil { 133 | fmt.Println(seed) 134 | } 135 | return err 136 | } 137 | 138 | func printAddress() error { 139 | seed, err := getSeed() 140 | if err != nil { 141 | return err 142 | } 143 | privateKey, err := atto.NewPrivateKey(seed, uint32(accountIndexFlag)) 144 | if err != nil { 145 | return err 146 | } 147 | account, err := atto.NewAccount(privateKey) 148 | if err == nil { 149 | fmt.Println(account.Address) 150 | } 151 | return err 152 | } 153 | 154 | func printBalance() error { 155 | seed, err := getSeed() 156 | if err != nil { 157 | return err 158 | } 159 | privateKey, err := atto.NewPrivateKey(seed, uint32(accountIndexFlag)) 160 | if err != nil { 161 | return err 162 | } 163 | account, err := atto.NewAccount(privateKey) 164 | if err != nil { 165 | return err 166 | } 167 | firstReceive := false // Is this the very first block of the account? 168 | info, err := account.FetchAccountInfo(node) 169 | if err == atto.ErrAccountNotFound { 170 | // Needed for printing balance, even if there are no receivable blocks: 171 | info.Balance = "0" 172 | 173 | firstReceive = true 174 | } else if err != nil { 175 | return err 176 | } 177 | receivables, err := account.FetchReceivable(node) 178 | if err != nil { 179 | return err 180 | } 181 | for _, receivable := range receivables { 182 | txt := "Creating receive block for %s from %s... " 183 | amount, ok := big.NewInt(0).SetString(receivable.Amount, 10) 184 | if !ok { 185 | return fmt.Errorf("cannot parse '%s' as an integer", receivable.Amount) 186 | } 187 | fmt.Fprintf(os.Stderr, txt, rawToNanoString(amount), receivable.Source) 188 | var block atto.Block 189 | if firstReceive { 190 | fmt.Fprintf(os.Stderr, "opening account... ") 191 | info, block, err = account.FirstReceive(receivable, defaultRepresentative) 192 | firstReceive = false 193 | } else { 194 | block, err = info.Receive(receivable) 195 | } 196 | if err != nil { 197 | return err 198 | } 199 | if err = block.Sign(privateKey); err != nil { 200 | return err 201 | } 202 | if err = fillWork(&block, node); err != nil { 203 | return err 204 | } 205 | if err = block.Submit(node); err != nil { 206 | return err 207 | } 208 | fmt.Fprintln(os.Stderr, "done") 209 | } 210 | newBalance, ok := big.NewInt(0).SetString(info.Balance, 10) 211 | if !ok { 212 | return fmt.Errorf("cannot parse '%s' as an integer", info.Balance) 213 | } 214 | fmt.Println(rawToNanoString(newBalance)) 215 | return nil 216 | } 217 | 218 | func printRepresentative() error { 219 | seed, err := getSeed() 220 | if err != nil { 221 | return err 222 | } 223 | privateKey, err := atto.NewPrivateKey(seed, uint32(accountIndexFlag)) 224 | if err != nil { 225 | return err 226 | } 227 | account, err := atto.NewAccount(privateKey) 228 | if err != nil { 229 | return err 230 | } 231 | info, err := account.FetchAccountInfo(node) 232 | if err != nil { 233 | return err 234 | } 235 | fmt.Fprintln(os.Stderr, info.Representative) 236 | return nil 237 | } 238 | 239 | func changeRepresentative() error { 240 | representative := flag.Arg(1) 241 | seed, err := getSeed() 242 | if err != nil { 243 | return err 244 | } 245 | privateKey, err := atto.NewPrivateKey(seed, uint32(accountIndexFlag)) 246 | if err != nil { 247 | return err 248 | } 249 | account, err := atto.NewAccount(privateKey) 250 | if err != nil { 251 | return err 252 | } 253 | info, err := account.FetchAccountInfo(node) 254 | if err != nil { 255 | return err 256 | } 257 | 258 | fmt.Fprintf(os.Stderr, "Creating change block... ") 259 | block, err := info.Change(representative) 260 | if err != nil { 261 | return err 262 | } 263 | if err = block.Sign(privateKey); err != nil { 264 | return err 265 | } 266 | if err = fillWork(&block, node); err != nil { 267 | return err 268 | } 269 | if err = block.Submit(node); err != nil { 270 | return err 271 | } 272 | fmt.Fprintln(os.Stderr, "done") 273 | return nil 274 | } 275 | 276 | func sendFunds() error { 277 | amount := flag.Arg(1) 278 | recipient := flag.Arg(2) 279 | seed, err := getSeed() 280 | if err != nil { 281 | return err 282 | } 283 | privateKey, err := atto.NewPrivateKey(seed, uint32(accountIndexFlag)) 284 | if err != nil { 285 | return err 286 | } 287 | account, err := atto.NewAccount(privateKey) 288 | if err != nil { 289 | return err 290 | } 291 | if err = letUserVerifySend(amount, recipient); err != nil { 292 | return err 293 | } 294 | info, err := account.FetchAccountInfo(node) 295 | if err != nil { 296 | return err 297 | } 298 | 299 | fmt.Fprintf(os.Stderr, "Creating send block... ") 300 | block, err := info.Send(amount, recipient) 301 | if err != nil { 302 | return err 303 | } 304 | if err = block.Sign(privateKey); err != nil { 305 | return err 306 | } 307 | if err = fillWork(&block, node); err != nil { 308 | return err 309 | } 310 | if err = block.Submit(node); err != nil { 311 | return err 312 | } 313 | fmt.Fprintln(os.Stderr, "done") 314 | return nil 315 | } 316 | -------------------------------------------------------------------------------- /cmd/atto/util.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "math/big" 7 | "os" 8 | "runtime" 9 | "strings" 10 | 11 | "github.com/codesoap/atto" 12 | ) 13 | 14 | // getSeed returns the first line of the standard input. 15 | func getSeed() (string, error) { 16 | in := bufio.NewReader(os.Stdin) 17 | firstLine, err := in.ReadString('\n') 18 | if err != nil { 19 | return "", err 20 | } 21 | return strings.TrimSpace(firstLine), nil 22 | } 23 | 24 | func rawToNanoString(raw *big.Int) string { 25 | rawPerNano, _ := big.NewInt(0).SetString("1000000000000000000000000000000", 10) 26 | absRaw := big.NewInt(0).Abs(raw) 27 | integerDigits, fractionalDigits := big.NewInt(0).QuoRem(absRaw, rawPerNano, big.NewInt(0)) 28 | res := integerDigits.String() 29 | if fractionalDigits.Sign() != 0 { 30 | fractionalDigitsString := fmt.Sprintf("%030s", fractionalDigits.String()) 31 | res += "." + strings.TrimRight(fractionalDigitsString, "0") 32 | } 33 | if raw.Sign() < 0 { 34 | return "-" + res + " NANO" 35 | } 36 | return res + " NANO" 37 | } 38 | 39 | func letUserVerifySend(amount, recipient string) (err error) { 40 | if !yFlag { 41 | fmt.Printf("Send %s NANO to %s? [y/N]: ", amount, recipient) 42 | 43 | // Explicitly openning /dev/tty or CONIN$ ensures function, even if 44 | // the standard input is not a terminal. 45 | var tty *os.File 46 | if runtime.GOOS == "windows" { 47 | tty, err = os.Open("CONIN$") 48 | } else { 49 | tty, err = os.Open("/dev/tty") 50 | } 51 | if err != nil { 52 | msg := "could not open terminal for confirmation input: %v" 53 | return fmt.Errorf(msg, err) 54 | } 55 | defer tty.Close() 56 | 57 | var confirmation string 58 | fmt.Fscanln(tty, &confirmation) 59 | if confirmation != "y" && confirmation != "Y" { 60 | fmt.Fprintln(os.Stderr, "Send aborted.") 61 | os.Exit(0) 62 | } 63 | } 64 | return 65 | } 66 | 67 | func fillWork(block *atto.Block, node string) error { 68 | switch workSource { 69 | case workSourceLocal: 70 | return block.GenerateWork() 71 | case workSourceNode: 72 | return block.FetchWork(node) 73 | case workSourceLocalFallback: 74 | if err := block.FetchWork(node); err != nil { 75 | fmt.Fprintf(os.Stderr, "Could not fetch work from node (error: %v); generating it on CPU... ", err) 76 | return block.GenerateWork() 77 | } 78 | return nil 79 | } 80 | return fmt.Errorf("unknown work source") 81 | } 82 | -------------------------------------------------------------------------------- /ed25519.go: -------------------------------------------------------------------------------- 1 | package atto 2 | 3 | import ( 4 | "math/big" 5 | 6 | "filippo.io/edwards25519" 7 | "golang.org/x/crypto/blake2b" 8 | ) 9 | 10 | func sign(publicKey, privateKey *big.Int, msg []byte) ([]byte, error) { 11 | // This implementation based on the one from github.com/iotaledger/iota.go. 12 | 13 | signature := make([]byte, 64, 64) 14 | 15 | h, err := blake2b.New512(nil) 16 | if err != nil { 17 | return signature, err 18 | } 19 | h.Write(bigIntToBytes(privateKey, 32)) 20 | 21 | var digest1, messageDigest, hramDigest [64]byte 22 | h.Sum(digest1[:0]) 23 | 24 | s, err := new(edwards25519.Scalar).SetBytesWithClamping(digest1[:32]) 25 | if err != nil { 26 | return signature, err 27 | } 28 | 29 | h.Reset() 30 | h.Write(digest1[32:]) 31 | h.Write(msg) 32 | h.Sum(messageDigest[:0]) 33 | 34 | rReduced, err := new(edwards25519.Scalar).SetUniformBytes(messageDigest[:]) 35 | if err != nil { 36 | return signature, err 37 | } 38 | R := new(edwards25519.Point).ScalarBaseMult(rReduced) 39 | 40 | encodedR := R.Bytes() 41 | 42 | h.Reset() 43 | h.Write(encodedR[:]) 44 | h.Write(bigIntToBytes(publicKey, 32)) 45 | h.Write(msg) 46 | h.Sum(hramDigest[:0]) 47 | 48 | kReduced, err := new(edwards25519.Scalar).SetUniformBytes(hramDigest[:]) 49 | if err != nil { 50 | return signature, err 51 | } 52 | S := new(edwards25519.Scalar).MultiplyAdd(kReduced, s, rReduced) 53 | 54 | copy(signature[:], encodedR[:]) 55 | copy(signature[32:], S.Bytes()) 56 | 57 | return signature, nil 58 | } 59 | 60 | func isValidSignature(publicKey *big.Int, msg, sig []byte) bool { 61 | // This implementation based on the one from github.com/iotaledger/iota.go. 62 | 63 | publicKeyBytes := bigIntToBytes(publicKey, 32) 64 | 65 | // ZIP215: this works because SetBytes does not check that encodings are canonical 66 | A, err := new(edwards25519.Point).SetBytes(publicKeyBytes) 67 | if err != nil { 68 | return false 69 | } 70 | A.Negate(A) 71 | 72 | h, err := blake2b.New512(nil) 73 | if err != nil { 74 | return false 75 | } 76 | h.Write(sig[:32]) 77 | h.Write(publicKeyBytes) 78 | h.Write(msg) 79 | var digest [64]byte 80 | h.Sum(digest[:0]) 81 | hReduced, err := new(edwards25519.Scalar).SetUniformBytes(digest[:]) 82 | if err != nil { 83 | return false 84 | } 85 | 86 | // ZIP215: this works because SetBytes does not check that encodings are canonical 87 | checkR, err := new(edwards25519.Point).SetBytes(sig[:32]) 88 | if err != nil { 89 | return false 90 | } 91 | 92 | // https://tools.ietf.org/html/rfc8032#section-5.1.7 requires that s be in 93 | // the range [0, order) in order to prevent signature malleability 94 | s, err := new(edwards25519.Scalar).SetCanonicalBytes(sig[32:]) 95 | if err != nil { 96 | return false 97 | } 98 | 99 | R := new(edwards25519.Point).VarTimeDoubleScalarBaseMult(hReduced, A, s) 100 | 101 | // ZIP215: We want to check [8](R - checkR) == 0 102 | p := new(edwards25519.Point).Subtract(R, checkR) // p = R - checkR 103 | p.Add(p, p) // p = [2]p 104 | p.Add(p, p) // p = [4]p 105 | p.Add(p, p) // p = [8]p 106 | return p.Equal(edwards25519.NewIdentityPoint()) == 1 // p == 0 107 | } 108 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/codesoap/atto 2 | 3 | go 1.15 4 | 5 | require ( 6 | filippo.io/edwards25519 v1.1.0 7 | github.com/klauspost/cpuid/v2 v2.2.9 8 | golang.org/x/crypto v0.32.0 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= 2 | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 3 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 4 | github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= 5 | github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= 6 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 7 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 8 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 9 | golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= 10 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 11 | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= 12 | golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= 13 | golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= 14 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 15 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 16 | golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 17 | golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 18 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 19 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 20 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 21 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 22 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 23 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 24 | golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= 25 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 26 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 27 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 28 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 29 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 30 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 31 | golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 32 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 33 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 34 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 35 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 36 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 37 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 38 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 39 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 40 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 41 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 42 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 43 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 44 | golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 45 | golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= 46 | golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 47 | golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= 48 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 49 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 50 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 51 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 52 | golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= 53 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 54 | golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= 55 | golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= 56 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 57 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 58 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 59 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 60 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 61 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 62 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 63 | golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 64 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 65 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 66 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 67 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 68 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 69 | golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 70 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 71 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 72 | -------------------------------------------------------------------------------- /process.go: -------------------------------------------------------------------------------- 1 | package atto 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | type process struct { 9 | Action string `json:"action"` 10 | JsonBlock string `json:"json_block"` 11 | SubType string `json:"subtype"` 12 | Block Block `json:"block"` 13 | } 14 | 15 | type processResponse struct { 16 | Error string `json:"error"` 17 | } 18 | 19 | func doProcessRPC(process process, node string) error { 20 | var requestBody, responseBytes []byte 21 | requestBody, err := json.Marshal(process) 22 | if err != nil { 23 | return err 24 | } 25 | responseBytes, err = doRPC(string(requestBody), node) 26 | if err != nil { 27 | return err 28 | } 29 | var processResponse processResponse 30 | if err = json.Unmarshal(responseBytes, &processResponse); err != nil { 31 | return err 32 | } 33 | // Need to check processResponse.Error because of 34 | // https://github.com/nanocurrency/nano-node/issues/1782. 35 | if processResponse.Error != "" { 36 | err = fmt.Errorf("could not publish block: %s", processResponse.Error) 37 | } 38 | return err 39 | } 40 | -------------------------------------------------------------------------------- /receivable.go: -------------------------------------------------------------------------------- 1 | package atto 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | // Receivable represents a block that is waiting to be received. 8 | type Receivable struct { 9 | Hash string 10 | Amount string 11 | Source string 12 | } 13 | 14 | type internalReceivable struct { 15 | Error string `json:"error"` 16 | Blocks receivableBlocks `json:"blocks"` 17 | } 18 | 19 | type receivableBlocks map[string]receivableBlock 20 | 21 | // UnmarshalJSON just unmarshals a list of strings, but 22 | // interprets an empty string as an empty list. This is 23 | // necessary due to a bug in the Nano node implementation. See 24 | // https://github.com/nanocurrency/nano-node/issues/3161. 25 | func (b *receivableBlocks) UnmarshalJSON(in []byte) error { 26 | if string(in) == `""` { 27 | return nil 28 | } 29 | var raw map[string]receivableBlock 30 | err := json.Unmarshal(in, &raw) 31 | *b = receivableBlocks(raw) 32 | return err 33 | } 34 | 35 | type receivableBlock struct { 36 | Amount string `json:"amount"` 37 | Source string `json:"source"` 38 | } 39 | 40 | func internalReceivableToReceivable(internalReceivable internalReceivable) []Receivable { 41 | receivables := make([]Receivable, 0) 42 | for hash, source := range internalReceivable.Blocks { 43 | receivable := Receivable{hash, source.Amount, source.Source} 44 | receivables = append(receivables, receivable) 45 | } 46 | return receivables 47 | } 48 | -------------------------------------------------------------------------------- /request_interceptor.go: -------------------------------------------------------------------------------- 1 | package atto 2 | 3 | import "net/http" 4 | 5 | // The RequestInterceptor is a function that is used to modify all HTTP 6 | // requests that are sent to a node. If it is nil, requests are not 7 | // modified. 8 | // 9 | // May be used, for example, to authenticate requests. 10 | var RequestInterceptor func(request *http.Request) error 11 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package atto 2 | 3 | import ( 4 | "crypto/rand" 5 | "fmt" 6 | "io/ioutil" 7 | "math/big" 8 | "net/http" 9 | "strings" 10 | 11 | "golang.org/x/crypto/blake2b" 12 | ) 13 | 14 | // GenerateSeed generates a new random seed. 15 | func GenerateSeed() (string, error) { 16 | b := make([]byte, 32) 17 | _, err := rand.Read(b) 18 | return fmt.Sprintf("%X", b), err 19 | } 20 | 21 | // NewPrivateKey creates a private key from the given seed and index. 22 | func NewPrivateKey(seed string, index uint32) (*big.Int, error) { 23 | seedInt, ok := big.NewInt(0).SetString(seed, 16) 24 | if !ok { 25 | return nil, fmt.Errorf("could not parse seed") 26 | } 27 | seedBytes := bigIntToBytes(seedInt, 32) 28 | indexBytes := bigIntToBytes(big.NewInt(int64(index)), 4) 29 | in := append(seedBytes, indexBytes...) 30 | privateKeyBytes := blake2b.Sum256(in) 31 | return big.NewInt(0).SetBytes(privateKeyBytes[:]), nil 32 | } 33 | 34 | func base32Encode(in *big.Int) string { 35 | alphabet := []byte("13456789abcdefghijkmnopqrstuwxyz") 36 | bigZero := big.NewInt(0) 37 | bigRadix := big.NewInt(32) 38 | num := big.NewInt(0).SetBytes(in.Bytes()) 39 | out := make([]byte, 0) 40 | mod := new(big.Int) 41 | for num.Cmp(bigZero) > 0 { 42 | num.DivMod(num, bigRadix, mod) 43 | out = append(out, alphabet[mod.Int64()]) 44 | } 45 | for i := 0; i < len(out)/2; i++ { 46 | out[i], out[len(out)-1-i] = out[len(out)-1-i], out[i] 47 | } 48 | return string(out) 49 | } 50 | 51 | func base32Decode(in string) (*big.Int, error) { 52 | reverseAlphabet := map[rune]*big.Int{} 53 | reverseAlphabet['1'] = big.NewInt(0) 54 | reverseAlphabet['3'] = big.NewInt(1) 55 | reverseAlphabet['4'] = big.NewInt(2) 56 | reverseAlphabet['5'] = big.NewInt(3) 57 | reverseAlphabet['6'] = big.NewInt(4) 58 | reverseAlphabet['7'] = big.NewInt(5) 59 | reverseAlphabet['8'] = big.NewInt(6) 60 | reverseAlphabet['9'] = big.NewInt(7) 61 | reverseAlphabet['a'] = big.NewInt(8) 62 | reverseAlphabet['b'] = big.NewInt(9) 63 | reverseAlphabet['c'] = big.NewInt(10) 64 | reverseAlphabet['d'] = big.NewInt(11) 65 | reverseAlphabet['e'] = big.NewInt(12) 66 | reverseAlphabet['f'] = big.NewInt(13) 67 | reverseAlphabet['g'] = big.NewInt(14) 68 | reverseAlphabet['h'] = big.NewInt(15) 69 | reverseAlphabet['i'] = big.NewInt(16) 70 | reverseAlphabet['j'] = big.NewInt(17) 71 | reverseAlphabet['k'] = big.NewInt(18) 72 | reverseAlphabet['m'] = big.NewInt(19) 73 | reverseAlphabet['n'] = big.NewInt(20) 74 | reverseAlphabet['o'] = big.NewInt(21) 75 | reverseAlphabet['p'] = big.NewInt(22) 76 | reverseAlphabet['q'] = big.NewInt(23) 77 | reverseAlphabet['r'] = big.NewInt(24) 78 | reverseAlphabet['s'] = big.NewInt(25) 79 | reverseAlphabet['t'] = big.NewInt(26) 80 | reverseAlphabet['u'] = big.NewInt(27) 81 | reverseAlphabet['w'] = big.NewInt(28) 82 | reverseAlphabet['x'] = big.NewInt(29) 83 | reverseAlphabet['y'] = big.NewInt(30) 84 | reverseAlphabet['z'] = big.NewInt(31) 85 | out := big.NewInt(0) 86 | radix := big.NewInt(32) 87 | for _, r := range in { 88 | out.Mul(out, radix) 89 | val, ok := reverseAlphabet[r] 90 | if !ok { 91 | return out, fmt.Errorf("'%c' is no legal base32 character", r) 92 | } 93 | out.Add(out, val) 94 | } 95 | return out, nil 96 | } 97 | 98 | func bigIntToBytes(x *big.Int, n int) []byte { 99 | return x.FillBytes(make([]byte, n, n)) 100 | } 101 | 102 | func revertBytes(in []byte) []byte { 103 | for i := 0; i < len(in)/2; i++ { 104 | in[i], in[len(in)-1-i] = in[len(in)-1-i], in[i] 105 | } 106 | return in 107 | } 108 | 109 | func doRPC(requestBody, node string) (responseBytes []byte, err error) { 110 | req, err := http.NewRequest("POST", node, strings.NewReader(requestBody)) 111 | if err != nil { 112 | return 113 | } 114 | req.Header.Add("Content-Type", "application/json") 115 | if RequestInterceptor != nil { 116 | if err = RequestInterceptor(req); err != nil { 117 | err = fmt.Errorf("request interceptor failed: %v", err) 118 | return 119 | } 120 | } 121 | resp, err := http.DefaultClient.Do(req) 122 | if err != nil { 123 | return 124 | } 125 | defer resp.Body.Close() 126 | if resp.StatusCode != 200 { 127 | err = fmt.Errorf("received unexpected HTTP return code %d", resp.StatusCode) 128 | return 129 | } 130 | return ioutil.ReadAll(resp.Body) 131 | } 132 | 133 | func getPublicKeyFromAddress(address string) (*big.Int, error) { 134 | if len(address) == 64 { 135 | return base32Decode(address[4:56]) 136 | } else if len(address) == 65 { 137 | return base32Decode(address[5:57]) 138 | } 139 | return nil, fmt.Errorf("could not parse address %s", address) 140 | } 141 | -------------------------------------------------------------------------------- /work.go: -------------------------------------------------------------------------------- 1 | package atto 2 | 3 | import ( 4 | "context" 5 | "encoding/binary" 6 | 7 | "github.com/klauspost/cpuid/v2" 8 | "golang.org/x/crypto/blake2b" 9 | ) 10 | 11 | // Using cpuid.CPU.LogicalCores seems to yield the best performance. 12 | var workerRoutines = cpuid.CPU.LogicalCores 13 | 14 | type workerResult struct { 15 | nonce uint64 16 | err error 17 | } 18 | 19 | func findNonce(workThreshold uint64, suffix []byte) (uint64, error) { 20 | // See https://docs.nano.org/integration-guides/work-generation/#work-equation 21 | // See https://docs.nano.org/protocol-design/spam-work-and-prioritization/#work-algorithm-details 22 | results := make(chan workerResult) 23 | ctx, cancel := context.WithCancel(context.Background()) 24 | for i := 0; i < workerRoutines; i++ { 25 | go calculateHashes(workThreshold, suffix, uint64(i), results, ctx) 26 | } 27 | result := <-results 28 | stopWorkers(results, cancel) 29 | return result.nonce, result.err 30 | } 31 | 32 | func stopWorkers(results chan workerResult, cancel context.CancelFunc) { 33 | cancel() 34 | 35 | // Empty channel, so that workers get ready to quit: 36 | for { 37 | select { 38 | case <-results: 39 | default: 40 | return 41 | } 42 | } 43 | } 44 | 45 | func calculateHashes(workThreshold uint64, suffix []byte, nonce uint64, results chan workerResult, ctx context.Context) { 46 | nonceBytes := make([]byte, 8) 47 | hasher, err := blake2b.New(8, nil) 48 | if err != nil { 49 | results <- workerResult{err: err} 50 | return 51 | } 52 | for { 53 | select { 54 | case <-ctx.Done(): 55 | return 56 | default: 57 | binary.LittleEndian.PutUint64(nonceBytes, nonce) 58 | _, err := hasher.Write(append(nonceBytes, suffix...)) 59 | if err != nil { 60 | results <- workerResult{err: err} 61 | return 62 | } 63 | hashBytes := hasher.Sum(nil) 64 | hashNumber := binary.LittleEndian.Uint64(hashBytes) 65 | if hashNumber >= workThreshold { 66 | results <- workerResult{nonce: nonce} 67 | } 68 | hasher.Reset() 69 | nonce += uint64(workerRoutines) 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /work_test.go: -------------------------------------------------------------------------------- 1 | package atto 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func BenchmarkNonceSearch(b *testing.B) { 8 | for i := 0; i < b.N; i++ { 9 | findNonce(0xffffff0000000000, nil) 10 | } 11 | } 12 | --------------------------------------------------------------------------------