mirror of https://github.com/codesoap/atto.git
mirror of https://github.com/codesoap/atto
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
196 lines
5.8 KiB
196 lines
5.8 KiB
package atto |
|
|
|
import ( |
|
"encoding/json" |
|
"fmt" |
|
"math/big" |
|
"strings" |
|
|
|
"filippo.io/edwards25519" |
|
"golang.org/x/crypto/blake2b" |
|
) |
|
|
|
// ErrAccountNotFound is used when an account could not be found by the |
|
// queried node. |
|
var ErrAccountNotFound = fmt.Errorf("account has not yet been opened") |
|
|
|
// ErrAccountManipulated is used when it seems like an account has been |
|
// manipulated. This probably means someone is trying to steal funds. |
|
var ErrAccountManipulated = fmt.Errorf("the received account info has been manipulated") |
|
|
|
// Account holds the public key and address of a Nano account. |
|
type Account struct { |
|
PublicKey *big.Int |
|
Address string |
|
} |
|
|
|
type blockInfo struct { |
|
Error string `json:"error"` |
|
Contents Block `json:"contents"` |
|
} |
|
|
|
// NewAccount creates a new Account and populates both its fields. |
|
func NewAccount(privateKey *big.Int) (a Account, err error) { |
|
a.PublicKey = derivePublicKey(privateKey) |
|
a.Address, err = getAddress(a.PublicKey) |
|
return |
|
} |
|
|
|
// NewAccountFromAddress creates a new Account and populates both its |
|
// fields. |
|
func NewAccountFromAddress(address string) (a Account, err error) { |
|
a.Address = address |
|
a.PublicKey, err = getPublicKeyFromAddress(address) |
|
return |
|
} |
|
|
|
func derivePublicKey(privateKey *big.Int) *big.Int { |
|
hashBytes := blake2b.Sum512(bigIntToBytes(privateKey, 32)) |
|
scalar, err := edwards25519.NewScalar().SetBytesWithClamping(hashBytes[:32]) |
|
if err != nil { |
|
panic(err) |
|
} |
|
publicKeyBytes := edwards25519.NewIdentityPoint().ScalarBaseMult(scalar).Bytes() |
|
return big.NewInt(0).SetBytes(publicKeyBytes) |
|
} |
|
|
|
func getAddress(publicKey *big.Int) (string, error) { |
|
base32PublicKey := base32Encode(publicKey) |
|
|
|
hasher, err := blake2b.New(5, nil) |
|
if err != nil { |
|
return "", err |
|
} |
|
publicKeyBytes := bigIntToBytes(publicKey, 32) |
|
if _, err := hasher.Write(publicKeyBytes); err != nil { |
|
return "", err |
|
} |
|
hashBytes := hasher.Sum(nil) |
|
base32Hash := base32Encode(big.NewInt(0).SetBytes(revertBytes(hashBytes))) |
|
|
|
address := "nano_" + |
|
strings.Repeat("1", 52-len(base32PublicKey)) + base32PublicKey + |
|
strings.Repeat("1", 8-len(base32Hash)) + base32Hash |
|
return address, nil |
|
} |
|
|
|
// FetchAccountInfo fetches the AccountInfo of Account from the given |
|
// node. |
|
// |
|
// It is also verified, that the retreived AccountInfo is valid by |
|
// doing a block_info RPC for the frontier, verifying the signature |
|
// and ensuring that no fields have been changed in the account_info |
|
// response. |
|
// |
|
// May return ErrAccountNotFound or ErrAccountManipulated. |
|
// |
|
// If ErrAccountNotFound is returned, FirstReceive can be used to |
|
// create a first Block and AccountInfo and create the account by then |
|
// submitting this Block. |
|
func (a Account) FetchAccountInfo(node string) (i AccountInfo, err error) { |
|
requestBody := fmt.Sprintf(`{`+ |
|
`"action": "account_info",`+ |
|
`"account": "%s",`+ |
|
`"representative": "true"`+ |
|
`}`, a.Address) |
|
responseBytes, err := doRPC(requestBody, node) |
|
if err != nil { |
|
return |
|
} |
|
if err = json.Unmarshal(responseBytes, &i); err != nil { |
|
return |
|
} |
|
// Need to check i.Error because of |
|
// https://github.com/nanocurrency/nano-node/issues/1782. |
|
if i.Error == "Account not found" { |
|
err = ErrAccountNotFound |
|
} else if i.Error != "" { |
|
err = fmt.Errorf("could not fetch account info: %s", i.Error) |
|
} else { |
|
i.PublicKey = a.PublicKey |
|
i.Address = a.Address |
|
err = a.verifyInfo(i, node) |
|
} |
|
return |
|
} |
|
|
|
// verifyInfo gets the frontier block of info, ensures that Hash, |
|
// Representative and Balance match and verifies it's signature. |
|
func (a Account) verifyInfo(info AccountInfo, node string) error { |
|
requestBody := fmt.Sprintf(`{`+ |
|
`"action": "block_info",`+ |
|
`"json_block": "true",`+ |
|
`"hash": "%s"`+ |
|
`}`, info.Frontier) |
|
responseBytes, err := doRPC(requestBody, node) |
|
if err != nil { |
|
return err |
|
} |
|
var block blockInfo |
|
if err = json.Unmarshal(responseBytes, &block); err != nil { |
|
return err |
|
} |
|
if info.Error != "" { |
|
return fmt.Errorf("could not get block info: %s", info.Error) |
|
} |
|
hash, err := block.Contents.Hash() |
|
if err != nil { |
|
return err |
|
} |
|
if err = block.Contents.verifySignature(a); err == errInvalidSignature || |
|
info.Frontier != hash || |
|
info.Representative != block.Contents.Representative || |
|
info.Balance != block.Contents.Balance { |
|
return ErrAccountManipulated |
|
} |
|
return err |
|
} |
|
|
|
// FetchPending fetches all unreceived blocks of Account from node. |
|
func (a Account) FetchPending(node string) ([]Pending, error) { |
|
requestBody := fmt.Sprintf(`{`+ |
|
`"action": "pending", `+ |
|
`"account": "%s", `+ |
|
`"include_only_confirmed": "true", `+ |
|
`"source": "true"`+ |
|
`}`, a.Address) |
|
responseBytes, err := doRPC(requestBody, node) |
|
if err != nil { |
|
return nil, err |
|
} |
|
var pending internalPending |
|
err = json.Unmarshal(responseBytes, &pending) |
|
// Need to check pending.Error because of |
|
// https://github.com/nanocurrency/nano-node/issues/1782. |
|
if err == nil && pending.Error != "" { |
|
err = fmt.Errorf("could not fetch unreceived sends: %s", pending.Error) |
|
} |
|
return internalPendingToPending(pending), err |
|
} |
|
|
|
// FirstReceive creates the first receive block of an account. The block |
|
// will still be missing its signature and work. FirstReceive will also |
|
// return AccountInfo, which can be used to create further blocks. |
|
func (a Account) FirstReceive(pending Pending, representative string) (AccountInfo, Block, error) { |
|
block := Block{ |
|
Type: "state", |
|
SubType: SubTypeReceive, |
|
Account: a.Address, |
|
Previous: "0000000000000000000000000000000000000000000000000000000000000000", |
|
Representative: representative, |
|
Balance: pending.Amount, |
|
Link: pending.Hash, |
|
} |
|
hash, err := block.Hash() |
|
if err != nil { |
|
return AccountInfo{}, Block{}, err |
|
} |
|
info := AccountInfo{ |
|
Frontier: hash, |
|
Representative: block.Representative, |
|
Balance: block.Balance, |
|
PublicKey: a.PublicKey, |
|
Address: a.Address, |
|
} |
|
return info, block, err |
|
}
|
|
|