neoneo-go/pkg/core/native/notary.go
Anna Shaleva a6f52a7180 core: prohibit reentry to Notary withdraw
If we're withdrawing funds to contract that has onNEP17Payment method,
then it may call Notary's withdraw one more time, but the account's
state is not yet updated by this moment.

The problem is similar to https://github.com/neo-project/neo/pull/2734.

Signed-off-by: Anna Shaleva <shaleva.ann@nspcc.ru>
2024-03-18 17:08:32 +03:00

461 lines
16 KiB
Go

package native
import (
"errors"
"fmt"
"math"
"math/big"
"github.com/nspcc-dev/neo-go/pkg/config"
"github.com/nspcc-dev/neo-go/pkg/core/dao"
"github.com/nspcc-dev/neo-go/pkg/core/interop"
"github.com/nspcc-dev/neo-go/pkg/core/interop/contract"
"github.com/nspcc-dev/neo-go/pkg/core/interop/runtime"
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
"github.com/nspcc-dev/neo-go/pkg/core/native/nativeprices"
"github.com/nspcc-dev/neo-go/pkg/core/native/noderoles"
"github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/core/storage"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
)
// Notary represents Notary native contract.
type Notary struct {
interop.ContractMD
GAS *GAS
NEO *NEO
Desig *Designate
Policy *Policy
}
type NotaryCache struct {
maxNotValidBeforeDelta uint32
}
// NotaryService is a Notary module interface.
type NotaryService interface {
UpdateNotaryNodes(pubs keys.PublicKeys)
}
const (
notaryContractID = -10
// prefixDeposit is a prefix for storing Notary deposits.
prefixDeposit = 1
defaultDepositDeltaTill = 5760
defaultMaxNotValidBeforeDelta = 140 // 20 rounds for 7 validators, a little more than half an hour
)
var maxNotValidBeforeDeltaKey = []byte{10}
var (
_ interop.Contract = (*Notary)(nil)
_ dao.NativeContractCache = (*NotaryCache)(nil)
)
// Copy implements NativeContractCache interface.
func (c *NotaryCache) Copy() dao.NativeContractCache {
cp := &NotaryCache{}
copyNotaryCache(c, cp)
return cp
}
func copyNotaryCache(src, dst *NotaryCache) {
*dst = *src
}
// newNotary returns Notary native contract.
func newNotary() *Notary {
n := &Notary{ContractMD: *interop.NewContractMD(nativenames.Notary, notaryContractID)}
defer n.UpdateHash()
desc := newDescriptor("onNEP17Payment", smartcontract.VoidType,
manifest.NewParameter("from", smartcontract.Hash160Type),
manifest.NewParameter("amount", smartcontract.IntegerType),
manifest.NewParameter("data", smartcontract.AnyType))
md := newMethodAndPrice(n.onPayment, 1<<15, callflag.States)
n.AddMethod(md, desc)
desc = newDescriptor("lockDepositUntil", smartcontract.BoolType,
manifest.NewParameter("address", smartcontract.Hash160Type),
manifest.NewParameter("till", smartcontract.IntegerType))
md = newMethodAndPrice(n.lockDepositUntil, 1<<15, callflag.States)
n.AddMethod(md, desc)
desc = newDescriptor("withdraw", smartcontract.BoolType,
manifest.NewParameter("from", smartcontract.Hash160Type),
manifest.NewParameter("to", smartcontract.Hash160Type))
md = newMethodAndPrice(n.withdraw, 1<<15, callflag.All)
n.AddMethod(md, desc)
desc = newDescriptor("balanceOf", smartcontract.IntegerType,
manifest.NewParameter("addr", smartcontract.Hash160Type))
md = newMethodAndPrice(n.balanceOf, 1<<15, callflag.ReadStates)
n.AddMethod(md, desc)
desc = newDescriptor("expirationOf", smartcontract.IntegerType,
manifest.NewParameter("addr", smartcontract.Hash160Type))
md = newMethodAndPrice(n.expirationOf, 1<<15, callflag.ReadStates)
n.AddMethod(md, desc)
desc = newDescriptor("verify", smartcontract.BoolType,
manifest.NewParameter("signature", smartcontract.SignatureType))
md = newMethodAndPrice(n.verify, nativeprices.NotaryVerificationPrice, callflag.ReadStates)
n.AddMethod(md, desc)
desc = newDescriptor("getMaxNotValidBeforeDelta", smartcontract.IntegerType)
md = newMethodAndPrice(n.getMaxNotValidBeforeDelta, 1<<15, callflag.ReadStates)
n.AddMethod(md, desc)
desc = newDescriptor("setMaxNotValidBeforeDelta", smartcontract.VoidType,
manifest.NewParameter("value", smartcontract.IntegerType))
md = newMethodAndPrice(n.setMaxNotValidBeforeDelta, 1<<15, callflag.States)
n.AddMethod(md, desc)
return n
}
// Metadata implements the Contract interface.
func (n *Notary) Metadata() *interop.ContractMD {
return &n.ContractMD
}
// Initialize initializes Notary native contract and implements the Contract interface.
func (n *Notary) Initialize(ic *interop.Context) error {
setIntWithKey(n.ID, ic.DAO, maxNotValidBeforeDeltaKey, defaultMaxNotValidBeforeDelta)
cache := &NotaryCache{
maxNotValidBeforeDelta: defaultMaxNotValidBeforeDelta,
}
ic.DAO.SetCache(n.ID, cache)
return nil
}
func (n *Notary) InitializeCache(blockHeight uint32, d *dao.Simple) error {
cache := &NotaryCache{
maxNotValidBeforeDelta: uint32(getIntWithKey(n.ID, d, maxNotValidBeforeDeltaKey)),
}
d.SetCache(n.ID, cache)
return nil
}
// OnPersist implements the Contract interface.
func (n *Notary) OnPersist(ic *interop.Context) error {
var (
nFees int64
notaries keys.PublicKeys
err error
)
for _, tx := range ic.Block.Transactions {
if tx.HasAttribute(transaction.NotaryAssistedT) {
if notaries == nil {
notaries, err = n.GetNotaryNodes(ic.DAO)
if err != nil {
return fmt.Errorf("failed to get notary nodes: %w", err)
}
}
nKeys := tx.GetAttributes(transaction.NotaryAssistedT)[0].Value.(*transaction.NotaryAssisted).NKeys
nFees += int64(nKeys) + 1
if tx.Sender() == n.Hash {
payer := tx.Signers[1]
balance := n.GetDepositFor(ic.DAO, payer.Account)
balance.Amount.Sub(balance.Amount, big.NewInt(tx.SystemFee+tx.NetworkFee))
if balance.Amount.Sign() == 0 {
n.removeDepositFor(ic.DAO, payer.Account)
} else {
err := n.putDepositFor(ic.DAO, balance, payer.Account)
if err != nil {
return fmt.Errorf("failed to update deposit for %s: %w", payer.Account.StringBE(), err)
}
}
}
}
}
if nFees == 0 {
return nil
}
feePerKey := n.Policy.GetAttributeFeeInternal(ic.DAO, transaction.NotaryAssistedT)
singleReward := calculateNotaryReward(nFees, feePerKey, len(notaries))
for _, notary := range notaries {
n.GAS.mint(ic, notary.GetScriptHash(), singleReward, false)
}
return nil
}
// PostPersist implements the Contract interface.
func (n *Notary) PostPersist(ic *interop.Context) error {
return nil
}
// ActiveIn implements the Contract interface.
func (n *Notary) ActiveIn() *config.Hardfork {
return nil
}
// onPayment records the deposited amount as belonging to "from" address with a lock
// till the specified chain's height.
func (n *Notary) onPayment(ic *interop.Context, args []stackitem.Item) stackitem.Item {
if h := ic.VM.GetCallingScriptHash(); h != n.GAS.Hash {
panic(fmt.Errorf("only GAS can be accepted for deposit, got %s", h.StringBE()))
}
from := toUint160(args[0])
to := from
amount := toBigInt(args[1])
data, ok := args[2].(*stackitem.Array)
if !ok || len(data.Value().([]stackitem.Item)) != 2 {
panic(errors.New("`data` parameter should be an array of 2 elements"))
}
additionalParams := data.Value().([]stackitem.Item)
if !additionalParams[0].Equals(stackitem.Null{}) {
to = toUint160(additionalParams[0])
}
allowedChangeTill := ic.Tx.Sender() == to
currentHeight := ic.BlockHeight()
deposit := n.GetDepositFor(ic.DAO, to)
till := toUint32(additionalParams[1])
if till < currentHeight+2 {
panic(fmt.Errorf("`till` shouldn't be less than the chain's height + 1 (%d at min)", currentHeight+2))
}
if deposit != nil && till < deposit.Till {
panic(fmt.Errorf("`till` shouldn't be less than the previous value %d", deposit.Till))
}
feePerKey := n.Policy.GetAttributeFeeInternal(ic.DAO, transaction.NotaryAssistedT)
if deposit == nil {
if amount.Cmp(big.NewInt(2*feePerKey)) < 0 {
panic(fmt.Errorf("first deposit can not be less than %d, got %d", 2*feePerKey, amount.Int64()))
}
deposit = &state.Deposit{
Amount: new(big.Int),
}
if !allowedChangeTill {
till = currentHeight + defaultDepositDeltaTill
}
} else if !allowedChangeTill { // only deposit's owner is allowed to set or update `till`
till = deposit.Till
}
deposit.Amount.Add(deposit.Amount, amount)
deposit.Till = till
if err := n.putDepositFor(ic.DAO, deposit, to); err != nil {
panic(fmt.Errorf("failed to put deposit for %s into the storage: %w", from.StringBE(), err))
}
return stackitem.Null{}
}
// lockDepositUntil updates the chain's height until which the deposit is locked.
func (n *Notary) lockDepositUntil(ic *interop.Context, args []stackitem.Item) stackitem.Item {
addr := toUint160(args[0])
ok, err := runtime.CheckHashedWitness(ic, addr)
if err != nil {
panic(fmt.Errorf("failed to check witness for %s: %w", addr.StringBE(), err))
}
if !ok {
return stackitem.NewBool(false)
}
till := toUint32(args[1])
if till < (ic.BlockHeight() + 1 + 1) { // deposit can't expire at the current persisting block.
return stackitem.NewBool(false)
}
deposit := n.GetDepositFor(ic.DAO, addr)
if deposit == nil {
return stackitem.NewBool(false)
}
if till < deposit.Till {
return stackitem.NewBool(false)
}
deposit.Till = till
err = n.putDepositFor(ic.DAO, deposit, addr)
if err != nil {
panic(fmt.Errorf("failed to put deposit for %s into the storage: %w", addr.StringBE(), err))
}
return stackitem.NewBool(true)
}
// withdraw sends all deposited GAS for "from" address to "to" address.
func (n *Notary) withdraw(ic *interop.Context, args []stackitem.Item) stackitem.Item {
from := toUint160(args[0])
ok, err := runtime.CheckHashedWitness(ic, from)
if err != nil {
panic(fmt.Errorf("failed to check witness for %s: %w", from.StringBE(), err))
}
if !ok {
return stackitem.NewBool(false)
}
to := from
if !args[1].Equals(stackitem.Null{}) {
to = toUint160(args[1])
}
deposit := n.GetDepositFor(ic.DAO, from)
if deposit == nil {
return stackitem.NewBool(false)
}
// Allow withdrawal only after `till` block was persisted, thus, use ic.BlockHeight().
if ic.BlockHeight() < deposit.Till {
return stackitem.NewBool(false)
}
cs, err := ic.GetContract(n.GAS.Hash)
if err != nil {
panic(fmt.Errorf("failed to get GAS contract state: %w", err))
}
// Remove deposit before GAS transfer processing to avoid double-withdrawal.
// In case if Gas contract call fails, state will be rolled back.
n.removeDepositFor(ic.DAO, from)
transferArgs := []stackitem.Item{stackitem.NewByteArray(n.Hash.BytesBE()), stackitem.NewByteArray(to.BytesBE()), stackitem.NewBigInteger(deposit.Amount), stackitem.Null{}}
err = contract.CallFromNative(ic, n.Hash, cs, "transfer", transferArgs, true)
if err != nil {
panic(fmt.Errorf("failed to transfer GAS from Notary account: %w", err))
}
if !ic.VM.Estack().Pop().Bool() {
panic("failed to transfer GAS from Notary account: `transfer` returned false")
}
return stackitem.NewBool(true)
}
// balanceOf returns the deposited GAS amount for the specified address.
func (n *Notary) balanceOf(ic *interop.Context, args []stackitem.Item) stackitem.Item {
acc := toUint160(args[0])
return stackitem.NewBigInteger(n.BalanceOf(ic.DAO, acc))
}
// BalanceOf is an internal representation of `balanceOf` Notary method.
func (n *Notary) BalanceOf(dao *dao.Simple, acc util.Uint160) *big.Int {
deposit := n.GetDepositFor(dao, acc)
if deposit == nil {
return big.NewInt(0)
}
return deposit.Amount
}
// expirationOf returns the deposit lock height for the specified address.
func (n *Notary) expirationOf(ic *interop.Context, args []stackitem.Item) stackitem.Item {
acc := toUint160(args[0])
return stackitem.Make(n.ExpirationOf(ic.DAO, acc))
}
// ExpirationOf is an internal representation of `expirationOf` Notary method.
func (n *Notary) ExpirationOf(dao *dao.Simple, acc util.Uint160) uint32 {
deposit := n.GetDepositFor(dao, acc)
if deposit == nil {
return 0
}
return deposit.Till
}
// verify checks whether the transaction was signed by one of the notaries.
func (n *Notary) verify(ic *interop.Context, args []stackitem.Item) stackitem.Item {
sig, err := args[0].TryBytes()
if err != nil {
panic(fmt.Errorf("failed to get signature bytes: %w", err))
}
tx := ic.Tx
if len(tx.GetAttributes(transaction.NotaryAssistedT)) == 0 {
return stackitem.NewBool(false)
}
for _, signer := range tx.Signers {
if signer.Account == n.Hash {
if signer.Scopes != transaction.None {
return stackitem.NewBool(false)
}
break
}
}
if tx.Sender() == n.Hash {
if len(tx.Signers) != 2 {
return stackitem.NewBool(false)
}
payer := tx.Signers[1].Account
balance := n.GetDepositFor(ic.DAO, payer)
if balance == nil || balance.Amount.Cmp(big.NewInt(tx.NetworkFee+tx.SystemFee)) < 0 {
return stackitem.NewBool(false)
}
}
notaries, err := n.GetNotaryNodes(ic.DAO)
if err != nil {
panic(fmt.Errorf("failed to get notary nodes: %w", err))
}
shash := hash.NetSha256(uint32(ic.Network), tx)
var verified bool
for _, n := range notaries {
if n.Verify(sig, shash[:]) {
verified = true
break
}
}
return stackitem.NewBool(verified)
}
// GetNotaryNodes returns public keys of notary nodes.
func (n *Notary) GetNotaryNodes(d *dao.Simple) (keys.PublicKeys, error) {
nodes, _, err := n.Desig.GetDesignatedByRole(d, noderoles.P2PNotary, math.MaxUint32)
return nodes, err
}
// getMaxNotValidBeforeDelta is a Notary contract method and returns the maximum NotValidBefore delta.
func (n *Notary) getMaxNotValidBeforeDelta(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
return stackitem.NewBigInteger(big.NewInt(int64(n.GetMaxNotValidBeforeDelta(ic.DAO))))
}
// GetMaxNotValidBeforeDelta is an internal representation of Notary getMaxNotValidBeforeDelta method.
func (n *Notary) GetMaxNotValidBeforeDelta(dao *dao.Simple) uint32 {
cache := dao.GetROCache(n.ID).(*NotaryCache)
return cache.maxNotValidBeforeDelta
}
// setMaxNotValidBeforeDelta is a Notary contract method and sets the maximum NotValidBefore delta.
func (n *Notary) setMaxNotValidBeforeDelta(ic *interop.Context, args []stackitem.Item) stackitem.Item {
value := toUint32(args[0])
cfg := ic.Chain.GetConfig()
maxInc := cfg.MaxValidUntilBlockIncrement
if value > maxInc/2 || value < uint32(cfg.GetNumOfCNs(ic.BlockHeight())) {
panic(fmt.Errorf("MaxNotValidBeforeDelta cannot be more than %d or less than %d", maxInc/2, cfg.GetNumOfCNs(ic.BlockHeight())))
}
if !n.NEO.checkCommittee(ic) {
panic("invalid committee signature")
}
setIntWithKey(n.ID, ic.DAO, maxNotValidBeforeDeltaKey, int64(value))
cache := ic.DAO.GetRWCache(n.ID).(*NotaryCache)
cache.maxNotValidBeforeDelta = value
return stackitem.Null{}
}
// GetDepositFor returns state.Deposit for the account specified. It returns nil in case
// the deposit is not found in the storage and panics in case of any other error.
func (n *Notary) GetDepositFor(dao *dao.Simple, acc util.Uint160) *state.Deposit {
key := append([]byte{prefixDeposit}, acc.BytesBE()...)
deposit := new(state.Deposit)
err := getConvertibleFromDAO(n.ID, dao, key, deposit)
if err == nil {
return deposit
}
if errors.Is(err, storage.ErrKeyNotFound) {
return nil
}
panic(fmt.Errorf("failed to get deposit for %s from storage: %w", acc.StringBE(), err))
}
// putDepositFor puts the deposit on the balance of the specified account in the storage.
func (n *Notary) putDepositFor(dao *dao.Simple, deposit *state.Deposit, acc util.Uint160) error {
key := append([]byte{prefixDeposit}, acc.BytesBE()...)
return putConvertibleToDAO(n.ID, dao, key, deposit)
}
// removeDepositFor removes the deposit from the storage.
func (n *Notary) removeDepositFor(dao *dao.Simple, acc util.Uint160) {
key := append([]byte{prefixDeposit}, acc.BytesBE()...)
dao.DeleteStorageItem(n.ID, key)
}
// calculateNotaryReward calculates the reward for a single notary node based on FEE's count and Notary nodes count.
func calculateNotaryReward(nFees int64, feePerKey int64, notariesCount int) *big.Int {
return big.NewInt(nFees * feePerKey / int64(notariesCount))
}