354 lines
9.2 KiB
Go
354 lines
9.2 KiB
Go
|
package accounting
|
||
|
|
||
|
import (
|
||
|
"crypto/ecdsa"
|
||
|
"crypto/rand"
|
||
|
"encoding/binary"
|
||
|
"reflect"
|
||
|
|
||
|
"github.com/mr-tron/base58"
|
||
|
crypto "github.com/nspcc-dev/neofs-crypto"
|
||
|
"github.com/nspcc-dev/neofs-proto/chain"
|
||
|
"github.com/nspcc-dev/neofs-proto/decimal"
|
||
|
"github.com/nspcc-dev/neofs-proto/internal"
|
||
|
"github.com/nspcc-dev/neofs-proto/refs"
|
||
|
"github.com/pkg/errors"
|
||
|
)
|
||
|
|
||
|
type (
|
||
|
// Cheque structure that describes a user request for withdrawal of funds.
|
||
|
Cheque struct {
|
||
|
ID ChequeID
|
||
|
Owner refs.OwnerID
|
||
|
Amount *decimal.Decimal
|
||
|
Height uint64
|
||
|
Signatures []ChequeSignature
|
||
|
}
|
||
|
|
||
|
// BalanceReceiver interface that is used to retrieve user balance by address.
|
||
|
BalanceReceiver interface {
|
||
|
Balance(accountAddress string) (*Account, error)
|
||
|
}
|
||
|
|
||
|
// ChequeID is identifier of user request for withdrawal of funds.
|
||
|
ChequeID string
|
||
|
|
||
|
// CID type alias.
|
||
|
CID = refs.CID
|
||
|
|
||
|
// SGID type alias.
|
||
|
SGID = refs.SGID
|
||
|
|
||
|
// ChequeSignature contains public key and hash, and is used to verify signatures.
|
||
|
ChequeSignature struct {
|
||
|
Key *ecdsa.PublicKey
|
||
|
Hash []byte
|
||
|
}
|
||
|
)
|
||
|
|
||
|
const (
|
||
|
// ErrWrongSignature is raised when wrong signature is passed.
|
||
|
ErrWrongSignature = internal.Error("wrong signature")
|
||
|
|
||
|
// ErrWrongPublicKey is raised when wrong public key is passed.
|
||
|
ErrWrongPublicKey = internal.Error("wrong public key")
|
||
|
|
||
|
// ErrWrongChequeData is raised when passed bytes cannot not be parsed as valid Cheque.
|
||
|
ErrWrongChequeData = internal.Error("wrong cheque data")
|
||
|
|
||
|
// ErrInvalidLength is raised when passed bytes cannot not be parsed as valid ChequeID.
|
||
|
ErrInvalidLength = internal.Error("invalid length")
|
||
|
|
||
|
u16size = 2
|
||
|
u64size = 8
|
||
|
|
||
|
signaturesOffset = chain.AddressLength + refs.OwnerIDSize + u64size + u64size
|
||
|
)
|
||
|
|
||
|
// NewChequeID generates valid random ChequeID using crypto/rand.Reader.
|
||
|
func NewChequeID() (ChequeID, error) {
|
||
|
d := make([]byte, chain.AddressLength)
|
||
|
if _, err := rand.Read(d); err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
|
||
|
id := base58.Encode(d)
|
||
|
|
||
|
return ChequeID(id), nil
|
||
|
}
|
||
|
|
||
|
// String returns string representation of ChequeID.
|
||
|
func (b ChequeID) String() string { return string(b) }
|
||
|
|
||
|
// Empty returns true, if ChequeID is empty.
|
||
|
func (b ChequeID) Empty() bool { return len(b) == 0 }
|
||
|
|
||
|
// Valid validates ChequeID.
|
||
|
func (b ChequeID) Valid() bool {
|
||
|
d, err := base58.Decode(string(b))
|
||
|
return err == nil && len(d) == chain.AddressLength
|
||
|
}
|
||
|
|
||
|
// Bytes returns bytes representation of ChequeID.
|
||
|
func (b ChequeID) Bytes() []byte {
|
||
|
d, err := base58.Decode(string(b))
|
||
|
if err != nil {
|
||
|
return make([]byte, chain.AddressLength)
|
||
|
}
|
||
|
return d
|
||
|
}
|
||
|
|
||
|
// Equal checks that current ChequeID is equal to passed ChequeID.
|
||
|
func (b ChequeID) Equal(b2 ChequeID) bool {
|
||
|
return b.Valid() && b2.Valid() && string(b) == string(b2)
|
||
|
}
|
||
|
|
||
|
// Unmarshal tries to parse []byte into valid ChequeID.
|
||
|
func (b *ChequeID) Unmarshal(data []byte) error {
|
||
|
*b = ChequeID(base58.Encode(data))
|
||
|
if !b.Valid() {
|
||
|
return ErrInvalidLength
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// Size returns size (chain.AddressLength).
|
||
|
func (b ChequeID) Size() int {
|
||
|
return chain.AddressLength
|
||
|
}
|
||
|
|
||
|
// MarshalTo tries to marshal ChequeID into passed bytes and returns
|
||
|
// count of copied bytes or error, if bytes len is not enough to contain ChequeID.
|
||
|
func (b ChequeID) MarshalTo(data []byte) (int, error) {
|
||
|
if len(data) < chain.AddressLength {
|
||
|
return 0, ErrInvalidLength
|
||
|
}
|
||
|
return copy(data, b.Bytes()), nil
|
||
|
}
|
||
|
|
||
|
// Equals checks that m and tx are valid and equal Tx values.
|
||
|
func (m Tx) Equals(tx Tx) bool {
|
||
|
return m.From == tx.From &&
|
||
|
m.To == tx.To &&
|
||
|
m.Type == tx.Type &&
|
||
|
m.Amount == tx.Amount
|
||
|
}
|
||
|
|
||
|
// Verify validates current Cheque and Signatures that are generated for current Cheque.
|
||
|
func (b Cheque) Verify() error {
|
||
|
data := b.marshalBody()
|
||
|
for i, sign := range b.Signatures {
|
||
|
if err := crypto.VerifyRFC6979(sign.Key, data, sign.Hash); err != nil {
|
||
|
return errors.Wrapf(ErrWrongSignature, "item #%d: %s", i, err.Error())
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// Sign is used to sign current Cheque and stores result inside b.Signatures.
|
||
|
func (b *Cheque) Sign(key *ecdsa.PrivateKey) error {
|
||
|
hash, err := crypto.SignRFC6979(key, b.marshalBody())
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
b.Signatures = append(b.Signatures, ChequeSignature{
|
||
|
Key: &key.PublicKey,
|
||
|
Hash: hash,
|
||
|
})
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (b *Cheque) marshalBody() []byte {
|
||
|
buf := make([]byte, signaturesOffset)
|
||
|
|
||
|
var offset int
|
||
|
|
||
|
offset += copy(buf, b.ID.Bytes())
|
||
|
offset += copy(buf[offset:], b.Owner.Bytes())
|
||
|
|
||
|
binary.BigEndian.PutUint64(buf[offset:], uint64(b.Amount.Value))
|
||
|
offset += u64size
|
||
|
|
||
|
binary.BigEndian.PutUint64(buf[offset:], b.Height)
|
||
|
|
||
|
return buf
|
||
|
}
|
||
|
|
||
|
func (b *Cheque) unmarshalBody(buf []byte) error {
|
||
|
var offset int
|
||
|
|
||
|
if len(buf) < signaturesOffset {
|
||
|
return ErrWrongChequeData
|
||
|
}
|
||
|
|
||
|
{ // unmarshal UUID
|
||
|
if err := b.ID.Unmarshal(buf[offset : offset+chain.AddressLength]); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
offset += chain.AddressLength
|
||
|
}
|
||
|
|
||
|
{ // unmarshal OwnerID
|
||
|
if err := b.Owner.Unmarshal(buf[offset : offset+refs.OwnerIDSize]); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
offset += refs.OwnerIDSize
|
||
|
}
|
||
|
|
||
|
{ // unmarshal amount
|
||
|
amount := int64(binary.BigEndian.Uint64(buf[offset:]))
|
||
|
b.Amount = decimal.New(amount)
|
||
|
offset += u64size
|
||
|
}
|
||
|
|
||
|
{ // unmarshal height
|
||
|
b.Height = binary.BigEndian.Uint64(buf[offset:])
|
||
|
offset += u64size
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// MarshalBinary is used to marshal Cheque into bytes.
|
||
|
func (b Cheque) MarshalBinary() ([]byte, error) {
|
||
|
var (
|
||
|
count = len(b.Signatures)
|
||
|
buf = make([]byte, b.Size())
|
||
|
offset = copy(buf, b.marshalBody())
|
||
|
)
|
||
|
|
||
|
binary.BigEndian.PutUint16(buf[offset:], uint16(count))
|
||
|
offset += u16size
|
||
|
|
||
|
for _, sign := range b.Signatures {
|
||
|
key := crypto.MarshalPublicKey(sign.Key)
|
||
|
offset += copy(buf[offset:], key)
|
||
|
offset += copy(buf[offset:], sign.Hash)
|
||
|
}
|
||
|
|
||
|
return buf, nil
|
||
|
}
|
||
|
|
||
|
// Size returns size of Cheque (count of bytes needs to store it).
|
||
|
func (b Cheque) Size() int {
|
||
|
return signaturesOffset + u16size +
|
||
|
len(b.Signatures)*(crypto.PublicKeyCompressedSize+crypto.RFC6979SignatureSize)
|
||
|
}
|
||
|
|
||
|
// UnmarshalBinary tries to parse []byte into valid Cheque.
|
||
|
func (b *Cheque) UnmarshalBinary(buf []byte) error {
|
||
|
if err := b.unmarshalBody(buf); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
body := buf[:signaturesOffset]
|
||
|
|
||
|
count := int64(binary.BigEndian.Uint16(buf[signaturesOffset:]))
|
||
|
offset := signaturesOffset + u16size
|
||
|
|
||
|
if ln := count * int64(crypto.PublicKeyCompressedSize+crypto.RFC6979SignatureSize); ln > int64(len(buf[offset:])) {
|
||
|
return ErrWrongChequeData
|
||
|
}
|
||
|
|
||
|
for i := int64(0); i < count; i++ {
|
||
|
sign := ChequeSignature{
|
||
|
Key: crypto.UnmarshalPublicKey(buf[offset : offset+crypto.PublicKeyCompressedSize]),
|
||
|
Hash: make([]byte, crypto.RFC6979SignatureSize),
|
||
|
}
|
||
|
|
||
|
offset += crypto.PublicKeyCompressedSize
|
||
|
if sign.Key == nil {
|
||
|
return errors.Wrapf(ErrWrongPublicKey, "item #%d", i)
|
||
|
}
|
||
|
|
||
|
offset += copy(sign.Hash, buf[offset:offset+crypto.RFC6979SignatureSize])
|
||
|
if err := crypto.VerifyRFC6979(sign.Key, body, sign.Hash); err != nil {
|
||
|
return errors.Wrapf(ErrWrongSignature, "item #%d: %s (offset=%d, len=%d)", i, err.Error(), offset, len(sign.Hash))
|
||
|
}
|
||
|
|
||
|
b.Signatures = append(b.Signatures, sign)
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// ErrNotEnoughFunds generates error using address and amounts.
|
||
|
func ErrNotEnoughFunds(addr string, needed, residue *decimal.Decimal) error {
|
||
|
return errors.Errorf("not enough funds (requested=%s, residue=%s, addr=%s", needed, residue, addr)
|
||
|
}
|
||
|
|
||
|
func (m *Account) hasLockAcc(addr string) bool {
|
||
|
for i := range m.LockAccounts {
|
||
|
if m.LockAccounts[i].Address == addr {
|
||
|
return true
|
||
|
}
|
||
|
}
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
// ValidateLock checks that account can be locked.
|
||
|
func (m *Account) ValidateLock() error {
|
||
|
switch {
|
||
|
case m.Address == "":
|
||
|
return ErrEmptyAddress
|
||
|
case m.ParentAddress == "":
|
||
|
return ErrEmptyParentAddress
|
||
|
case m.LockTarget == nil:
|
||
|
return ErrEmptyLockTarget
|
||
|
}
|
||
|
|
||
|
switch v := m.LockTarget.Target.(type) {
|
||
|
case *LockTarget_WithdrawTarget:
|
||
|
if v.WithdrawTarget.Cheque != m.Address {
|
||
|
return errors.Errorf("wrong cheque ID: expected %s, has %s", m.Address, v.WithdrawTarget.Cheque)
|
||
|
}
|
||
|
case *LockTarget_ContainerCreateTarget:
|
||
|
switch {
|
||
|
case v.ContainerCreateTarget.CID.Empty():
|
||
|
return ErrEmptyContainerID
|
||
|
}
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// CanLock checks possibility to lock funds.
|
||
|
func (m *Account) CanLock(lockAcc *Account) error {
|
||
|
switch {
|
||
|
case m.ActiveFunds.LT(lockAcc.ActiveFunds):
|
||
|
return ErrNotEnoughFunds(lockAcc.ParentAddress, lockAcc.ActiveFunds, m.ActiveFunds)
|
||
|
case m.hasLockAcc(lockAcc.Address):
|
||
|
return errors.Errorf("could not lock account(%s) funds: duplicating lock(%s)", m.Address, lockAcc.Address)
|
||
|
default:
|
||
|
return nil
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// LockForWithdraw checks that account contains locked funds by passed ChequeID.
|
||
|
func (m *Account) LockForWithdraw(chequeID string) bool {
|
||
|
switch v := m.LockTarget.Target.(type) {
|
||
|
case *LockTarget_WithdrawTarget:
|
||
|
return v.WithdrawTarget.Cheque == chequeID
|
||
|
}
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
// LockForContainerCreate checks that account contains locked funds for container creation.
|
||
|
func (m *Account) LockForContainerCreate(cid refs.CID) bool {
|
||
|
switch v := m.LockTarget.Target.(type) {
|
||
|
case *LockTarget_ContainerCreateTarget:
|
||
|
return v.ContainerCreateTarget.CID.Equal(cid)
|
||
|
}
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
// Equal checks that current Settlement is equal to passed Settlement.
|
||
|
func (m *Settlement) Equal(s *Settlement) bool {
|
||
|
if s == nil || m.Epoch != s.Epoch || len(m.Transactions) != len(s.Transactions) {
|
||
|
return false
|
||
|
}
|
||
|
return len(m.Transactions) == 0 || reflect.DeepEqual(m.Transactions, s.Transactions)
|
||
|
}
|