mirror of https://github.com/codesoap/atto.git
5 changed files with 500 additions and 0 deletions
@ -0,0 +1,61 @@
@@ -0,0 +1,61 @@
|
||||
# Creating blocks from an offline computer |
||||
If you want to keep your seed extra safe you may choose to never take |
||||
it onto a computer that is connected to the internet. `atto-safesign` |
||||
enables you to do this by creating a file which contains initially |
||||
unsigned blocks. The blocks in this file can then be signed on the |
||||
offline computer and transferred back to the online computer to submit |
||||
the blocks to the Nano network. |
||||
|
||||
To transfer data between the off- and online computer, you can use a |
||||
USB thumb drive, but make sure that there is no maleware on the drive. |
||||
A safer alternative could be to use QR codes (e.g. with the tools |
||||
`qrencode` and `zbarimg`), to transfer the atto file with the blocks to |
||||
and from the offline computer after manually checking the contents of |
||||
the file. |
||||
|
||||
# Usage |
||||
``` |
||||
online$ # These steps take place on an online computer: |
||||
online$ echo $MY_PUBLIC_KEY | atto-safesign test.atto receive |
||||
online$ echo $MY_PUBLIC_KEY | atto-safesign test.atto representative nano_3up3y8cd3hhs7zdpmkpssgb1iyjpke3xwmgqy8rg58z1hwryqpjqnkuqayps |
||||
|
||||
offline$ # The sign subcommand can then be used on an offline computer: |
||||
offline$ pass nano | atto-safesign test.atto sign |
||||
|
||||
online$ # Back at the online computer, the now signed blocks can be submitted: |
||||
online$ atto-safesign test.atto submit |
||||
``` |
||||
|
||||
``` |
||||
$ atto-safesign -h |
||||
Usage: |
||||
atto-safesign -v |
||||
atto-safesign FILE receive |
||||
atto-safesign FILE representative REPRESENTATIVE |
||||
atto-safesign FILE send AMOUNT RECEIVER |
||||
atto-safesign [-a ACCOUNT_INDEX] [-y] FILE sign |
||||
atto-safesign FILE submit |
||||
|
||||
If the -v flag is provided, atto-safesign will print its version number. |
||||
|
||||
The receive, representative, send and submit subcommands expect a Nano |
||||
address as the first line of their standard input. |
||||
|
||||
The sign subcommand expects a seed as the first line of standard input. |
||||
It also expects manual confirmation before signing blocks, unless the -y |
||||
flag is given. |
||||
|
||||
The receive, representative and send subcommands will generate blocks |
||||
and append them to FILE. The blocks will still be lacking their |
||||
signature. |
||||
|
||||
The sign subcommand will add signatures to all blocks in FILE. It is the |
||||
only subcommand that requires no network connection. |
||||
|
||||
The submit subcommand will submit all blocks contained in FILE to the |
||||
Nano network. |
||||
|
||||
ACCOUNT_INDEX is an optional parameter, which allows you to use |
||||
different accounts derived from the given seed. By default the account |
||||
with index 0 is chosen. |
||||
``` |
@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
|
||||
package main |
||||
|
||||
var ( |
||||
// The node needs to support the work_generate action.
|
||||
// See e.g. https://publicnodes.somenano.com to find public nodes or
|
||||
// set up your own node to use. Here are some public alternatives:
|
||||
// - https://proxy.nanos.cc/proxy
|
||||
// - https://mynano.ninja/api/node
|
||||
// - https://rainstorm.city/api
|
||||
node = "https://proxy.powernode.cc/proxy" |
||||
|
||||
defaultRepresentative = "nano_18shbirtzhmkf7166h39nowj9c9zrpufeg75bkbyoobqwf1iu3srfm9eo3pz" |
||||
) |
@ -0,0 +1,286 @@
@@ -0,0 +1,286 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"bytes" |
||||
"encoding/json" |
||||
"flag" |
||||
"fmt" |
||||
"io" |
||||
"math/big" |
||||
"os" |
||||
|
||||
"github.com/codesoap/atto" |
||||
) |
||||
|
||||
var usage = `Usage: |
||||
atto-safesign -v |
||||
atto-safesign FILE receive |
||||
atto-safesign FILE representative REPRESENTATIVE |
||||
atto-safesign FILE send AMOUNT RECEIVER |
||||
atto-safesign [-a ACCOUNT_INDEX] [-y] FILE sign |
||||
atto-safesign FILE submit |
||||
|
||||
If the -v flag is provided, atto-safesign will print its version number. |
||||
|
||||
The receive, representative, send and submit subcommands expect a Nano |
||||
address as the first line of their standard input. |
||||
|
||||
The sign subcommand expects a seed as the first line of standard input. |
||||
It also expects manual confirmation before signing blocks, unless the -y |
||||
flag is given. |
||||
|
||||
The receive, representative and send subcommands will generate blocks |
||||
and append them to FILE. The blocks will still be lacking their |
||||
signature. |
||||
|
||||
The sign subcommand will add signatures to all blocks in FILE. It is the |
||||
only subcommand that requires no network connection. |
||||
|
||||
The submit subcommand will submit all blocks contained in FILE to the |
||||
Nano network. |
||||
|
||||
ACCOUNT_INDEX is an optional parameter, which allows you to use |
||||
different accounts derived from the given seed. By default the account |
||||
with index 0 is chosen. |
||||
` |
||||
|
||||
var accountIndexFlag uint |
||||
var yFlag bool |
||||
|
||||
func init() { |
||||
var vFlag bool |
||||
flag.Usage = func() { fmt.Fprint(os.Stderr, usage) } |
||||
flag.UintVar(&accountIndexFlag, "a", 0, "") |
||||
flag.BoolVar(&yFlag, "y", false, "") |
||||
flag.BoolVar(&vFlag, "v", false, "") |
||||
flag.Parse() |
||||
if vFlag { |
||||
fmt.Println("1.0.0") |
||||
os.Exit(0) |
||||
} |
||||
if flag.NArg() < 2 { |
||||
flag.Usage() |
||||
os.Exit(1) |
||||
} |
||||
var ok bool |
||||
switch flag.Arg(1) { |
||||
case "receive", "sign", "submit": |
||||
ok = flag.NArg() == 2 |
||||
case "representative": |
||||
ok = flag.NArg() == 3 |
||||
case "send": |
||||
ok = flag.NArg() == 4 |
||||
} |
||||
if !ok { |
||||
flag.Usage() |
||||
os.Exit(1) |
||||
} |
||||
} |
||||
|
||||
func main() { |
||||
var err error |
||||
switch flag.Arg(1) { |
||||
case "receive": |
||||
err = receive() |
||||
case "representative": |
||||
err = change() |
||||
case "send": |
||||
err = send() |
||||
case "sign": |
||||
err = sign() |
||||
case "submit": |
||||
err = submit() |
||||
} |
||||
if err != nil { |
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err) |
||||
os.Exit(2) |
||||
} |
||||
} |
||||
|
||||
func receive() error { |
||||
addr, err := getFirstStdinLine() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
account, err := atto.NewAccountFromAddress(addr) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
info, err := getLatestAccountInfo(account) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
pendings, err := account.FetchPending(node) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
for _, pending := range pendings { |
||||
block, err := info.Receive(pending) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
if err = block.FetchWork(node); err != nil { |
||||
return err |
||||
} |
||||
blockJSON, err := json.Marshal(block) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
err = appendLineToFile(blockJSON) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func change() error { |
||||
representative := flag.Arg(2) |
||||
addr, err := getFirstStdinLine() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
account, err := atto.NewAccountFromAddress(addr) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
info, err := getLatestAccountInfo(account) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
block, err := info.Change(representative) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
if err = block.FetchWork(node); err != nil { |
||||
return err |
||||
} |
||||
blockJSON, err := json.Marshal(block) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
return appendLineToFile(blockJSON) |
||||
} |
||||
|
||||
func send() error { |
||||
amount := flag.Arg(2) |
||||
receiver := flag.Arg(3) |
||||
addr, err := getFirstStdinLine() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
account, err := atto.NewAccountFromAddress(addr) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
info, err := getLatestAccountInfo(account) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
block, err := info.Send(amount, receiver) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
if err = block.FetchWork(node); err != nil { |
||||
return err |
||||
} |
||||
blockJSON, err := json.Marshal(block) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
return appendLineToFile(blockJSON) |
||||
} |
||||
|
||||
func sign() error { |
||||
seed, err := getFirstStdinLine() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
privateKey, err := atto.NewPrivateKey(seed, uint32(accountIndexFlag)) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
account, err := atto.NewAccount(privateKey) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
blocks, err := getBlocksFromFile() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
var outBuffer bytes.Buffer |
||||
for _, block := range blocks { |
||||
if account.Address != block.Account { |
||||
txt := "Used account with address '%s' cannot sign block with address '%s'" |
||||
return fmt.Errorf(txt, account.Address, block.Account) |
||||
} |
||||
if err = letUserVerifyBlock(block); err != nil { |
||||
return err |
||||
} |
||||
block.Sign(privateKey) |
||||
blockJSON, err := json.Marshal(block) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
// Buffer output so that file can be overwritten as late as possible
|
||||
// to avoid problems during the write as much as possible.
|
||||
outBuffer.Write(blockJSON) // err is always nil.
|
||||
outBuffer.Write([]byte{'\n'}) // err is always nil.
|
||||
} |
||||
|
||||
file, err := os.Create(flag.Arg(0)) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
defer file.Close() |
||||
_, err = io.Copy(file, &outBuffer) |
||||
return err |
||||
} |
||||
|
||||
func submit() error { |
||||
addr, err := getFirstStdinLine() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
account, err := atto.NewAccountFromAddress(addr) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
blocks, err := getBlocksFromFile() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
info, err := account.FetchAccountInfo(node) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
oldBalance, ok := big.NewInt(0).SetString(info.Balance, 10) |
||||
if !ok { |
||||
fmt.Errorf("cannot parse '%s' as an integer", info.Balance) |
||||
} |
||||
for _, block := range blocks { |
||||
newBalance, ok := big.NewInt(0).SetString(block.Balance, 10) |
||||
if !ok { |
||||
fmt.Errorf("cannot parse '%s' as an integer", block.Balance) |
||||
} |
||||
switch oldBalance.Cmp(newBalance) { |
||||
case -1: |
||||
block.SubType = atto.SubTypeReceive |
||||
case 0: |
||||
// If the balance does not change, this should be a "change" block.
|
||||
block.SubType = atto.SubTypeChange |
||||
case 1: |
||||
block.SubType = atto.SubTypeSend |
||||
} |
||||
fmt.Fprint(os.Stderr, "Submitting block... ") |
||||
err = block.Submit(node) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
fmt.Fprintln(os.Stderr, "done") |
||||
oldBalance = newBalance |
||||
} |
||||
return nil |
||||
} |
@ -0,0 +1,137 @@
@@ -0,0 +1,137 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"bufio" |
||||
"encoding/json" |
||||
"flag" |
||||
"fmt" |
||||
"io" |
||||
"math/big" |
||||
"os" |
||||
"runtime" |
||||
"strings" |
||||
|
||||
"github.com/codesoap/atto" |
||||
) |
||||
|
||||
func getFirstStdinLine() (string, error) { |
||||
in := bufio.NewReader(os.Stdin) |
||||
firstLine, err := in.ReadString('\n') |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
return strings.TrimSpace(firstLine), nil |
||||
} |
||||
|
||||
// getLatestAccountInfo returns an atto.AccountInfo with the latest
|
||||
// available block as it's Frontier. This is either the last block from
|
||||
// the file or the one fetched from the network, if the file contains no
|
||||
// blocks.
|
||||
func getLatestAccountInfo(acc atto.Account) (atto.AccountInfo, error) { |
||||
blocks, err := getBlocksFromFile() |
||||
if err != nil { |
||||
return atto.AccountInfo{}, err |
||||
} |
||||
if len(blocks) == 0 { |
||||
return acc.FetchAccountInfo(node) |
||||
} |
||||
latestBlock := blocks[len(blocks)-1] |
||||
hash, err := latestBlock.Hash() |
||||
if err != nil { |
||||
return atto.AccountInfo{}, err |
||||
} |
||||
info := atto.AccountInfo{ |
||||
Frontier: hash, |
||||
Representative: latestBlock.Representative, |
||||
Balance: latestBlock.Balance, |
||||
PublicKey: acc.PublicKey, |
||||
Address: acc.Address, |
||||
} |
||||
return info, nil |
||||
} |
||||
|
||||
func getBlocksFromFile() ([]atto.Block, error) { |
||||
file, err := os.Open(flag.Arg(0)) |
||||
if err != nil { |
||||
// The file has not been found, which is OK.
|
||||
return []atto.Block{}, nil |
||||
} |
||||
defer file.Close() |
||||
reader := bufio.NewReader(file) |
||||
blocks := make([]atto.Block, 0) |
||||
for { |
||||
line, err := reader.ReadBytes('\n') |
||||
if err == io.EOF { |
||||
break |
||||
} else if err != nil { |
||||
return nil, err |
||||
} |
||||
if len(line) < 2 || line[0] == '#' { |
||||
// Skip empty and comment lines
|
||||
continue |
||||
} |
||||
var block atto.Block |
||||
if err = json.Unmarshal(line, &block); err != nil { |
||||
return nil, err |
||||
} |
||||
blocks = append(blocks, block) |
||||
} |
||||
return blocks, nil |
||||
} |
||||
|
||||
func appendLineToFile(in []byte) error { |
||||
file, err := os.OpenFile(flag.Arg(0), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
defer file.Close() |
||||
if _, err = file.Write(in); err != nil { |
||||
return err |
||||
} |
||||
_, err = file.Write([]byte{'\n'}) |
||||
return err |
||||
} |
||||
|
||||
func rawToNanoString(raw *big.Int) string { |
||||
rawPerKnano, _ := big.NewInt(0).SetString("1000000000000000000000000000", 10) |
||||
balance := big.NewInt(0).Div(raw, rawPerKnano).Int64() |
||||
if balance < 0 { |
||||
balance = -balance |
||||
return fmt.Sprintf("-%d.%03d NANO", balance/1000, balance%1000) |
||||
} |
||||
return fmt.Sprintf("%d.%03d NANO", balance/1000, balance%1000) |
||||
} |
||||
|
||||
func letUserVerifyBlock(block atto.Block) (err error) { |
||||
if !yFlag { |
||||
balanceInt, ok := big.NewInt(0).SetString(block.Balance, 10) |
||||
if !ok { |
||||
return fmt.Errorf("cannot parse '%s' as an integer", block.Balance) |
||||
} |
||||
balanceNano := rawToNanoString(balanceInt) |
||||
txt := "Sign block that sets balance to %s and representative to %s? [y/N]: " |
||||
fmt.Fprintf(os.Stderr, txt, balanceNano, block.Representative) |
||||
|
||||
// Explicitly openning /dev/tty or CONIN$ ensures function, even if
|
||||
// the standard input is not a terminal.
|
||||
var tty *os.File |
||||
if runtime.GOOS == "windows" { |
||||
tty, err = os.Open("CONIN$") |
||||
} else { |
||||
tty, err = os.Open("/dev/tty") |
||||
} |
||||
if err != nil { |
||||
msg := "could not open terminal for confirmation input: %v" |
||||
return fmt.Errorf(msg, err) |
||||
} |
||||
defer tty.Close() |
||||
|
||||
var confirmation string |
||||
fmt.Fscanln(tty, &confirmation) |
||||
if confirmation != "y" && confirmation != "Y" { |
||||
fmt.Fprintln(os.Stderr, "Signing aborted.") |
||||
os.Exit(0) |
||||
} |
||||
} |
||||
return |
||||
} |
Loading…
Reference in new issue