forked from TrueCloudLab/neoneo-go
core: implement native Notary contract
This commit is contained in:
parent
c013522296
commit
17842dabd6
10 changed files with 981 additions and 83 deletions
|
@ -1388,6 +1388,9 @@ func (bc *Blockchain) verifyTxAttributes(tx *transaction.Transaction) error {
|
||||||
if !bc.config.P2PSigExtensions {
|
if !bc.config.P2PSigExtensions {
|
||||||
return fmt.Errorf("%w: NotaryAssisted attribute was found, but P2PSigExtensions are disabled", ErrInvalidAttribute)
|
return fmt.Errorf("%w: NotaryAssisted attribute was found, but P2PSigExtensions are disabled", ErrInvalidAttribute)
|
||||||
}
|
}
|
||||||
|
if !tx.HasSigner(bc.contracts.Notary.Hash) {
|
||||||
|
return fmt.Errorf("%w: NotaryAssisted attribute was found, but transaction is not signed by the Notary native contract", ErrInvalidAttribute)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
if !bc.config.ReservedAttributes && attrType >= transaction.ReservedLowerBound && attrType <= transaction.ReservedUpperBound {
|
if !bc.config.ReservedAttributes && attrType >= transaction.ReservedLowerBound && attrType <= transaction.ReservedUpperBound {
|
||||||
return fmt.Errorf("%w: attribute of reserved type was found, but ReservedAttributes are disabled", ErrInvalidAttribute)
|
return fmt.Errorf("%w: attribute of reserved type was found, but ReservedAttributes are disabled", ErrInvalidAttribute)
|
||||||
|
|
|
@ -286,7 +286,7 @@ func TestVerifyTx(t *testing.T) {
|
||||||
require.Equal(t, 1, len(aer))
|
require.Equal(t, 1, len(aer))
|
||||||
require.Equal(t, aer[0].VMState, vm.HaltState)
|
require.Equal(t, aer[0].VMState, vm.HaltState)
|
||||||
|
|
||||||
res, err := invokeNativePolicyMethod(bc, "blockAccount", accs[1].PrivateKey().GetScriptHash().BytesBE())
|
res, err := invokeContractMethod(bc, 100000000, bc.contracts.Policy.Hash, "blockAccount", accs[1].PrivateKey().GetScriptHash().BytesBE())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
checkResult(t, res, stackitem.NewBool(true))
|
checkResult(t, res, stackitem.NewBool(true))
|
||||||
|
|
||||||
|
@ -720,6 +720,23 @@ func TestVerifyTx(t *testing.T) {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
t.Run("NotaryAssisted", func(t *testing.T) {
|
t.Run("NotaryAssisted", func(t *testing.T) {
|
||||||
|
notary, err := wallet.NewAccount()
|
||||||
|
require.NoError(t, err)
|
||||||
|
txSetNotary := transaction.New(netmode.UnitTestNet, []byte{}, 0)
|
||||||
|
setSigner(txSetNotary, testchain.CommitteeScriptHash())
|
||||||
|
txSetNotary.Scripts = []transaction.Witness{{
|
||||||
|
InvocationScript: testchain.SignCommittee(txSetNotary.GetSignedPart()),
|
||||||
|
VerificationScript: testchain.CommitteeVerificationScript(),
|
||||||
|
}}
|
||||||
|
bl := block.New(netmode.UnitTestNet, false)
|
||||||
|
bl.Index = bc.BlockHeight() + 1
|
||||||
|
ic := bc.newInteropContext(trigger.All, bc.dao, bl, txSetNotary)
|
||||||
|
ic.SpawnVM()
|
||||||
|
ic.VM.LoadScript([]byte{byte(opcode.RET)})
|
||||||
|
require.NoError(t, bc.contracts.Designate.DesignateAsRole(ic, native.RoleP2PNotary, keys.PublicKeys{notary.PrivateKey().PublicKey()}))
|
||||||
|
require.NoError(t, bc.contracts.Designate.OnPersistEnd(ic.DAO))
|
||||||
|
_, err = ic.DAO.Persist()
|
||||||
|
require.NoError(t, err)
|
||||||
getNotaryAssistedTx := func(signaturesCount uint8, serviceFee int64) *transaction.Transaction {
|
getNotaryAssistedTx := func(signaturesCount uint8, serviceFee int64) *transaction.Transaction {
|
||||||
tx := bc.newTestTx(h, testScript)
|
tx := bc.newTestTx(h, testScript)
|
||||||
tx.Attributes = append(tx.Attributes, transaction.Attribute{Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{
|
tx.Attributes = append(tx.Attributes, transaction.Attribute{Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{
|
||||||
|
@ -730,34 +747,177 @@ func TestVerifyTx(t *testing.T) {
|
||||||
tx.Signers = []transaction.Signer{{
|
tx.Signers = []transaction.Signer{{
|
||||||
Account: testchain.CommitteeScriptHash(),
|
Account: testchain.CommitteeScriptHash(),
|
||||||
Scopes: transaction.None,
|
Scopes: transaction.None,
|
||||||
}}
|
},
|
||||||
|
{
|
||||||
|
Account: bc.contracts.Notary.Hash,
|
||||||
|
Scopes: transaction.None,
|
||||||
|
},
|
||||||
|
}
|
||||||
rawScript := testchain.CommitteeVerificationScript()
|
rawScript := testchain.CommitteeVerificationScript()
|
||||||
require.NoError(t, err)
|
|
||||||
size := io.GetVarSize(tx)
|
size := io.GetVarSize(tx)
|
||||||
netFee, sizeDelta := fee.Calculate(rawScript)
|
netFee, sizeDelta := fee.Calculate(rawScript)
|
||||||
tx.NetworkFee += netFee
|
tx.NetworkFee += netFee
|
||||||
tx.NetworkFee += int64(size+sizeDelta) * bc.FeePerByte()
|
tx.NetworkFee += int64(size+sizeDelta) * bc.FeePerByte()
|
||||||
data := tx.GetSignedPart()
|
data := tx.GetSignedPart()
|
||||||
tx.Scripts = []transaction.Witness{{
|
tx.Scripts = []transaction.Witness{
|
||||||
InvocationScript: testchain.SignCommittee(data),
|
{
|
||||||
VerificationScript: rawScript,
|
InvocationScript: testchain.SignCommittee(data),
|
||||||
}}
|
VerificationScript: rawScript,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, notary.PrivateKey().Sign(data)...),
|
||||||
|
},
|
||||||
|
}
|
||||||
return tx
|
return tx
|
||||||
}
|
}
|
||||||
t.Run("Disabled", func(t *testing.T) {
|
t.Run("Disabled", func(t *testing.T) {
|
||||||
bc.config.P2PSigExtensions = false
|
bc.config.P2PSigExtensions = false
|
||||||
tx := getNotaryAssistedTx(0, 0)
|
tx := getNotaryAssistedTx(0, 0)
|
||||||
require.Error(t, bc.VerifyTx(tx))
|
require.True(t, errors.Is(bc.VerifyTx(tx), ErrInvalidAttribute))
|
||||||
})
|
})
|
||||||
t.Run("Enabled, insufficient network fee", func(t *testing.T) {
|
t.Run("Enabled, insufficient network fee", func(t *testing.T) {
|
||||||
bc.config.P2PSigExtensions = true
|
bc.config.P2PSigExtensions = true
|
||||||
tx := getNotaryAssistedTx(1, 0)
|
tx := getNotaryAssistedTx(1, 0)
|
||||||
require.Error(t, bc.VerifyTx(tx))
|
require.Error(t, bc.VerifyTx(tx))
|
||||||
})
|
})
|
||||||
t.Run("Enabled, positive", func(t *testing.T) {
|
t.Run("Test verify", func(t *testing.T) {
|
||||||
bc.config.P2PSigExtensions = true
|
bc.config.P2PSigExtensions = true
|
||||||
tx := getNotaryAssistedTx(1, (1+1)*transaction.NotaryServiceFeePerKey)
|
t.Run("no NotaryAssisted attribute", func(t *testing.T) {
|
||||||
require.NoError(t, bc.VerifyTx(tx))
|
tx := getNotaryAssistedTx(1, (1+1)*transaction.NotaryServiceFeePerKey)
|
||||||
|
tx.Attributes = []transaction.Attribute{}
|
||||||
|
tx.Signers = []transaction.Signer{
|
||||||
|
{
|
||||||
|
Account: testchain.CommitteeScriptHash(),
|
||||||
|
Scopes: transaction.None,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Account: bc.contracts.Notary.Hash,
|
||||||
|
Scopes: transaction.None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
data := tx.GetSignedPart()
|
||||||
|
tx.Scripts = []transaction.Witness{
|
||||||
|
{
|
||||||
|
InvocationScript: testchain.SignCommittee(data),
|
||||||
|
VerificationScript: testchain.CommitteeVerificationScript(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, notary.PrivateKey().Sign(data)...),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
require.Error(t, bc.VerifyTx(tx))
|
||||||
|
})
|
||||||
|
t.Run("no deposit", func(t *testing.T) {
|
||||||
|
tx := getNotaryAssistedTx(1, (1+1)*transaction.NotaryServiceFeePerKey)
|
||||||
|
tx.Signers = []transaction.Signer{
|
||||||
|
{
|
||||||
|
Account: bc.contracts.Notary.Hash,
|
||||||
|
Scopes: transaction.None,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Account: testchain.CommitteeScriptHash(),
|
||||||
|
Scopes: transaction.None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
data := tx.GetSignedPart()
|
||||||
|
tx.Scripts = []transaction.Witness{
|
||||||
|
{
|
||||||
|
InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, notary.PrivateKey().Sign(data)...),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
InvocationScript: testchain.SignCommittee(data),
|
||||||
|
VerificationScript: testchain.CommitteeVerificationScript(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
require.Error(t, bc.VerifyTx(tx))
|
||||||
|
})
|
||||||
|
t.Run("bad Notary signer scope", func(t *testing.T) {
|
||||||
|
tx := getNotaryAssistedTx(1, (1+1)*transaction.NotaryServiceFeePerKey)
|
||||||
|
tx.Signers = []transaction.Signer{
|
||||||
|
{
|
||||||
|
Account: testchain.CommitteeScriptHash(),
|
||||||
|
Scopes: transaction.None,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Account: bc.contracts.Notary.Hash,
|
||||||
|
Scopes: transaction.CalledByEntry,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
data := tx.GetSignedPart()
|
||||||
|
tx.Scripts = []transaction.Witness{
|
||||||
|
{
|
||||||
|
InvocationScript: testchain.SignCommittee(data),
|
||||||
|
VerificationScript: testchain.CommitteeVerificationScript(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, notary.PrivateKey().Sign(data)...),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
require.Error(t, bc.VerifyTx(tx))
|
||||||
|
})
|
||||||
|
t.Run("not signed by Notary", func(t *testing.T) {
|
||||||
|
tx := getNotaryAssistedTx(1, (1+1)*transaction.NotaryServiceFeePerKey)
|
||||||
|
tx.Signers = []transaction.Signer{
|
||||||
|
{
|
||||||
|
Account: testchain.CommitteeScriptHash(),
|
||||||
|
Scopes: transaction.None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
data := tx.GetSignedPart()
|
||||||
|
tx.Scripts = []transaction.Witness{
|
||||||
|
{
|
||||||
|
InvocationScript: testchain.SignCommittee(data),
|
||||||
|
VerificationScript: testchain.CommitteeVerificationScript(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
require.Error(t, bc.VerifyTx(tx))
|
||||||
|
})
|
||||||
|
t.Run("bad Notary node witness", func(t *testing.T) {
|
||||||
|
tx := getNotaryAssistedTx(1, (1+1)*transaction.NotaryServiceFeePerKey)
|
||||||
|
tx.Signers = []transaction.Signer{
|
||||||
|
{
|
||||||
|
Account: testchain.CommitteeScriptHash(),
|
||||||
|
Scopes: transaction.None,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Account: bc.contracts.Notary.Hash,
|
||||||
|
Scopes: transaction.None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
data := tx.GetSignedPart()
|
||||||
|
acc, err := keys.NewPrivateKey()
|
||||||
|
require.NoError(t, err)
|
||||||
|
tx.Scripts = []transaction.Witness{
|
||||||
|
{
|
||||||
|
InvocationScript: testchain.SignCommittee(data),
|
||||||
|
VerificationScript: testchain.CommitteeVerificationScript(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, acc.Sign(data)...),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
require.Error(t, bc.VerifyTx(tx))
|
||||||
|
})
|
||||||
|
t.Run("missing payer", func(t *testing.T) {
|
||||||
|
tx := getNotaryAssistedTx(1, (1+1)*transaction.NotaryServiceFeePerKey)
|
||||||
|
tx.Signers = []transaction.Signer{
|
||||||
|
{
|
||||||
|
Account: bc.contracts.Notary.Hash,
|
||||||
|
Scopes: transaction.None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
data := tx.GetSignedPart()
|
||||||
|
tx.Scripts = []transaction.Witness{
|
||||||
|
{
|
||||||
|
InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, notary.PrivateKey().Sign(data)...),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
require.Error(t, bc.VerifyTx(tx))
|
||||||
|
})
|
||||||
|
t.Run("positive", func(t *testing.T) {
|
||||||
|
tx := getNotaryAssistedTx(1, (1+1)*transaction.NotaryServiceFeePerKey)
|
||||||
|
require.NoError(t, bc.VerifyTx(tx))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -18,14 +18,18 @@ import (
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/chaindump"
|
"github.com/nspcc-dev/neo-go/pkg/core/chaindump"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/fee"
|
"github.com/nspcc-dev/neo-go/pkg/core/fee"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/native"
|
"github.com/nspcc-dev/neo-go/pkg/core/native"
|
||||||
|
"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/storage"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
"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/hash"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
|
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"go.uber.org/zap/zaptest"
|
"go.uber.org/zap/zaptest"
|
||||||
|
@ -364,9 +368,9 @@ func initBasicChain(t *testing.T, bc *Blockchain) {
|
||||||
require.NoError(t, bc.AddBlock(b))
|
require.NoError(t, bc.AddBlock(b))
|
||||||
}
|
}
|
||||||
|
|
||||||
func newNEP17Transfer(sc, from, to util.Uint160, amount int64) *transaction.Transaction {
|
func newNEP17Transfer(sc, from, to util.Uint160, amount int64, additionalArgs ...interface{}) *transaction.Transaction {
|
||||||
w := io.NewBufBinWriter()
|
w := io.NewBufBinWriter()
|
||||||
emit.AppCallWithOperationAndArgs(w.BinWriter, sc, "transfer", from, to, amount, nil)
|
emit.AppCallWithOperationAndArgs(w.BinWriter, sc, "transfer", from, to, amount, additionalArgs)
|
||||||
emit.Opcodes(w.BinWriter, opcode.ASSERT)
|
emit.Opcodes(w.BinWriter, opcode.ASSERT)
|
||||||
|
|
||||||
script := w.Bytes()
|
script := w.Bytes()
|
||||||
|
@ -411,3 +415,56 @@ func addNetworkFee(bc *Blockchain, tx *transaction.Transaction, sender *wallet.A
|
||||||
tx.NetworkFee += int64(size) * bc.FeePerByte()
|
tx.NetworkFee += int64(size) * bc.FeePerByte()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func invokeContractMethod(chain *Blockchain, sysfee int64, hash util.Uint160, method string, args ...interface{}) (*state.AppExecResult, error) {
|
||||||
|
w := io.NewBufBinWriter()
|
||||||
|
emit.AppCallWithOperationAndArgs(w.BinWriter, hash, method, args...)
|
||||||
|
if w.Err != nil {
|
||||||
|
return nil, w.Err
|
||||||
|
}
|
||||||
|
script := w.Bytes()
|
||||||
|
tx := transaction.New(chain.GetConfig().Magic, script, sysfee)
|
||||||
|
tx.ValidUntilBlock = chain.blockHeight + 1
|
||||||
|
addSigners(tx)
|
||||||
|
err := testchain.SignTx(chain, tx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
b := chain.newBlock(tx)
|
||||||
|
err = chain.AddBlock(b)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := chain.GetAppExecResults(tx.Hash(), trigger.Application)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &res[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func transferTokenFromMultisigAccount(t *testing.T, chain *Blockchain, to, tokenHash util.Uint160, amount int64, additionalArgs ...interface{}) *transaction.Transaction {
|
||||||
|
transferTx := newNEP17Transfer(tokenHash, testchain.MultisigScriptHash(), to, amount, additionalArgs...)
|
||||||
|
transferTx.SystemFee = 100000000
|
||||||
|
transferTx.ValidUntilBlock = chain.BlockHeight() + 1
|
||||||
|
addSigners(transferTx)
|
||||||
|
require.NoError(t, testchain.SignTx(chain, transferTx))
|
||||||
|
b := chain.newBlock(transferTx)
|
||||||
|
require.NoError(t, chain.AddBlock(b))
|
||||||
|
return transferTx
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkResult(t *testing.T, result *state.AppExecResult, expected stackitem.Item) {
|
||||||
|
require.Equal(t, vm.HaltState, result.VMState)
|
||||||
|
require.Equal(t, 1, len(result.Stack))
|
||||||
|
require.Equal(t, expected, result.Stack[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkFAULTState(t *testing.T, result *state.AppExecResult) {
|
||||||
|
require.Equal(t, vm.FaultState, result.VMState)
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkBalanceOf(t *testing.T, chain *Blockchain, addr util.Uint160, expected int) {
|
||||||
|
balance := chain.GetNEP17Balances(addr).Trackers[chain.contracts.GAS.ContractID]
|
||||||
|
require.Equal(t, int64(expected), balance.Balance.Int64())
|
||||||
|
}
|
||||||
|
|
|
@ -12,6 +12,9 @@ import (
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
|
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// reservedContractID represents the upper bound of the reserved IDs for native contracts.
|
||||||
|
const reservedContractID = -100
|
||||||
|
|
||||||
// Contracts is a set of registered native contracts.
|
// Contracts is a set of registered native contracts.
|
||||||
type Contracts struct {
|
type Contracts struct {
|
||||||
NEO *NEO
|
NEO *NEO
|
||||||
|
@ -19,6 +22,7 @@ type Contracts struct {
|
||||||
Policy *Policy
|
Policy *Policy
|
||||||
Oracle *Oracle
|
Oracle *Oracle
|
||||||
Designate *Designate
|
Designate *Designate
|
||||||
|
Notary *Notary
|
||||||
Contracts []interop.Contract
|
Contracts []interop.Contract
|
||||||
// persistScript is vm script which executes "onPersist" method of every native contract.
|
// persistScript is vm script which executes "onPersist" method of every native contract.
|
||||||
persistScript []byte
|
persistScript []byte
|
||||||
|
@ -47,8 +51,8 @@ func (cs *Contracts) ByName(name string) interop.Contract {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewContracts returns new set of native contracts with new GAS, NEO and Policy
|
// NewContracts returns new set of native contracts with new GAS, NEO, Policy, Oracle,
|
||||||
// contracts.
|
// Designate and (optional) Notary contracts.
|
||||||
func NewContracts(p2pSigExtensionsEnabled bool) *Contracts {
|
func NewContracts(p2pSigExtensionsEnabled bool) *Contracts {
|
||||||
cs := new(Contracts)
|
cs := new(Contracts)
|
||||||
|
|
||||||
|
@ -78,6 +82,14 @@ func NewContracts(p2pSigExtensionsEnabled bool) *Contracts {
|
||||||
cs.Oracle.Desig = desig
|
cs.Oracle.Desig = desig
|
||||||
cs.Contracts = append(cs.Contracts, desig)
|
cs.Contracts = append(cs.Contracts, desig)
|
||||||
|
|
||||||
|
if p2pSigExtensionsEnabled {
|
||||||
|
notary := newNotary()
|
||||||
|
notary.GAS = gas
|
||||||
|
notary.Desig = desig
|
||||||
|
cs.Notary = notary
|
||||||
|
cs.Contracts = append(cs.Contracts, notary)
|
||||||
|
}
|
||||||
|
|
||||||
return cs
|
return cs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,7 +126,7 @@ func (cs *Contracts) GetPostPersistScript() []byte {
|
||||||
md := cs.Contracts[i].Metadata()
|
md := cs.Contracts[i].Metadata()
|
||||||
// Not every contract is persisted:
|
// Not every contract is persisted:
|
||||||
// https://github.com/neo-project/neo/blob/master/src/neo/Ledger/Blockchain.cs#L103
|
// https://github.com/neo-project/neo/blob/master/src/neo/Ledger/Blockchain.cs#L103
|
||||||
if md.ContractID == policyContractID || md.ContractID == gasContractID || md.ContractID == designateContractID {
|
if md.ContractID == policyContractID || md.ContractID == gasContractID || md.ContractID == designateContractID || md.ContractID == notaryContractID {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
emit.Int(w.BinWriter, 0)
|
emit.Int(w.BinWriter, 0)
|
||||||
|
|
|
@ -3,6 +3,7 @@ package native
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"math/big"
|
"math/big"
|
||||||
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/dao"
|
"github.com/nspcc-dev/neo-go/pkg/core/dao"
|
||||||
|
@ -320,6 +321,18 @@ func toUint160(s stackitem.Item) util.Uint160 {
|
||||||
return u
|
return u
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toUint32(s stackitem.Item) uint32 {
|
||||||
|
bigInt := toBigInt(s)
|
||||||
|
if !bigInt.IsInt64() {
|
||||||
|
panic("bigint is not an int64")
|
||||||
|
}
|
||||||
|
int64Value := bigInt.Int64()
|
||||||
|
if int64Value < 0 || int64Value > math.MaxUint32 {
|
||||||
|
panic("bigint does not fit into uint32")
|
||||||
|
}
|
||||||
|
return uint32(int64Value)
|
||||||
|
}
|
||||||
|
|
||||||
func getOnPersistWrapper(f func(ic *interop.Context) error) interop.Method {
|
func getOnPersistWrapper(f func(ic *interop.Context) error) interop.Method {
|
||||||
return func(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
|
return func(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
|
||||||
err := f(ic)
|
err := f(ic)
|
||||||
|
|
357
pkg/core/native/notary.go
Normal file
357
pkg/core/native/notary.go
Normal file
|
@ -0,0 +1,357 @@
|
||||||
|
package native
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"math/big"
|
||||||
|
|
||||||
|
"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/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/keys"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
||||||
|
"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"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Notary represents Notary native contract.
|
||||||
|
type Notary struct {
|
||||||
|
interop.ContractMD
|
||||||
|
GAS *GAS
|
||||||
|
Desig *Designate
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
notaryName = "Notary"
|
||||||
|
notaryContractID = reservedContractID - 1
|
||||||
|
|
||||||
|
// prefixDeposit is a prefix for storing Notary deposits.
|
||||||
|
prefixDeposit = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
// newNotary returns Notary native contract.
|
||||||
|
func newNotary() *Notary {
|
||||||
|
n := &Notary{ContractMD: *interop.NewContractMD(notaryName)}
|
||||||
|
n.ContractID = notaryContractID
|
||||||
|
|
||||||
|
desc := newDescriptor("onPayment", smartcontract.VoidType,
|
||||||
|
manifest.NewParameter("from", smartcontract.Hash160Type),
|
||||||
|
manifest.NewParameter("amount", smartcontract.IntegerType),
|
||||||
|
manifest.NewParameter("data", smartcontract.AnyType))
|
||||||
|
md := newMethodAndPrice(n.onPayment, 100_0000, smartcontract.AllowModifyStates)
|
||||||
|
n.AddMethod(md, desc, true)
|
||||||
|
|
||||||
|
desc = newDescriptor("lockDepositUntil", smartcontract.BoolType,
|
||||||
|
manifest.NewParameter("address", smartcontract.Hash160Type),
|
||||||
|
manifest.NewParameter("till", smartcontract.IntegerType))
|
||||||
|
md = newMethodAndPrice(n.lockDepositUntil, 100_0000, smartcontract.AllowModifyStates)
|
||||||
|
n.AddMethod(md, desc, true)
|
||||||
|
|
||||||
|
desc = newDescriptor("withdraw", smartcontract.BoolType,
|
||||||
|
manifest.NewParameter("from", smartcontract.Hash160Type),
|
||||||
|
manifest.NewParameter("to", smartcontract.Hash160Type))
|
||||||
|
md = newMethodAndPrice(n.withdraw, 100_0000, smartcontract.AllowModifyStates)
|
||||||
|
n.AddMethod(md, desc, true)
|
||||||
|
|
||||||
|
desc = newDescriptor("balanceOf", smartcontract.IntegerType,
|
||||||
|
manifest.NewParameter("addr", smartcontract.Hash160Type))
|
||||||
|
md = newMethodAndPrice(n.balanceOf, 100_0000, smartcontract.AllowStates)
|
||||||
|
n.AddMethod(md, desc, true)
|
||||||
|
|
||||||
|
desc = newDescriptor("expirationOf", smartcontract.IntegerType,
|
||||||
|
manifest.NewParameter("addr", smartcontract.Hash160Type))
|
||||||
|
md = newMethodAndPrice(n.expirationOf, 100_0000, smartcontract.AllowStates)
|
||||||
|
n.AddMethod(md, desc, true)
|
||||||
|
|
||||||
|
desc = newDescriptor("verify", smartcontract.BoolType,
|
||||||
|
manifest.NewParameter("signature", smartcontract.SignatureType))
|
||||||
|
md = newMethodAndPrice(n.verify, 100_0000, smartcontract.AllowStates)
|
||||||
|
n.AddMethod(md, desc, false)
|
||||||
|
|
||||||
|
desc = newDescriptor("onPersist", smartcontract.VoidType)
|
||||||
|
md = newMethodAndPrice(getOnPersistWrapper(n.OnPersist), 0, smartcontract.AllowModifyStates)
|
||||||
|
n.AddMethod(md, desc, false)
|
||||||
|
|
||||||
|
desc = newDescriptor("postPersist", smartcontract.VoidType)
|
||||||
|
md = newMethodAndPrice(getOnPersistWrapper(postPersistBase), 0, smartcontract.AllowModifyStates)
|
||||||
|
n.AddMethod(md, desc, false)
|
||||||
|
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metadata implements Contract interface.
|
||||||
|
func (n *Notary) Metadata() *interop.ContractMD {
|
||||||
|
return &n.ContractMD
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize initializes Notary native contract and implements Contract interface.
|
||||||
|
func (n *Notary) Initialize(ic *interop.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnPersist implements 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 {
|
||||||
|
err := n.removeDepositFor(ic.DAO, payer.Account)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to remove an empty deposit for %s from storage: %w", payer.Account.StringBE(), err)
|
||||||
|
}
|
||||||
|
} 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
|
||||||
|
}
|
||||||
|
singleReward := calculateNotaryReward(nFees, len(notaries))
|
||||||
|
for _, notary := range notaries {
|
||||||
|
n.GAS.mint(ic, notary.GetScriptHash(), singleReward)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// onPayment records 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])
|
||||||
|
}
|
||||||
|
till := toUint32(additionalParams[1])
|
||||||
|
currentHeight := ic.Chain.BlockHeight()
|
||||||
|
if till < currentHeight {
|
||||||
|
panic(fmt.Errorf("`till` shouldn't be less then the chain's height %d", currentHeight))
|
||||||
|
}
|
||||||
|
|
||||||
|
deposit := n.getDepositFor(ic.DAO, to)
|
||||||
|
if deposit == nil {
|
||||||
|
if amount.Cmp(big.NewInt(2*transaction.NotaryServiceFeePerKey)) < 0 {
|
||||||
|
panic(fmt.Errorf("first deposit can not be less then %d, got %d", 2*transaction.NotaryServiceFeePerKey, amount.Int64()))
|
||||||
|
}
|
||||||
|
deposit = &state.Deposit{
|
||||||
|
Amount: new(big.Int),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if till < deposit.Till {
|
||||||
|
panic(fmt.Errorf("`till` shouldn't be less then the previous value %d", 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 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.Chain.BlockHeight() {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
if ic.Chain.BlockHeight() < deposit.Till {
|
||||||
|
return stackitem.NewBool(false)
|
||||||
|
}
|
||||||
|
cs, err := ic.DAO.GetContractState(n.GAS.Hash)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("failed to get GAS contract state: %w", err))
|
||||||
|
}
|
||||||
|
transferArgs := []stackitem.Item{stackitem.NewByteArray(n.Hash.BytesBE()), stackitem.NewByteArray(to.BytesBE()), stackitem.NewBigInteger(deposit.Amount), stackitem.Null{}}
|
||||||
|
err = contract.CallExInternal(ic, cs, "transfer", transferArgs, smartcontract.All, vm.EnsureIsEmpty, func(ctx *vm.Context) { // we need EnsureIsEmpty because there's a callback popping result from the stack
|
||||||
|
isTransferOk := ic.VM.Estack().Pop().Bool()
|
||||||
|
if !isTransferOk {
|
||||||
|
panic("failed to transfer GAS from Notary account")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("failed to transfer GAS from Notary account: %w", err))
|
||||||
|
}
|
||||||
|
if err := n.removeDepositFor(ic.DAO, from); err != nil {
|
||||||
|
panic(fmt.Errorf("failed to remove withdrawn deposit for %s from the storage: %w", from.StringBE(), err))
|
||||||
|
}
|
||||||
|
return stackitem.NewBool(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// balanceOf returns deposited GAS amount for specified address.
|
||||||
|
func (n *Notary) balanceOf(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
||||||
|
acc := toUint160(args[0])
|
||||||
|
deposit := n.getDepositFor(ic.DAO, acc)
|
||||||
|
if deposit == nil {
|
||||||
|
return stackitem.NewBigInteger(big.NewInt(0))
|
||||||
|
}
|
||||||
|
return stackitem.NewBigInteger(deposit.Amount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// expirationOf Returns deposit lock height for specified address.
|
||||||
|
func (n *Notary) expirationOf(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
||||||
|
acc := toUint160(args[0])
|
||||||
|
deposit := n.getDepositFor(ic.DAO, acc)
|
||||||
|
if deposit == nil {
|
||||||
|
return stackitem.Make(0)
|
||||||
|
}
|
||||||
|
return stackitem.Make(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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
hash := tx.GetSignedHash().BytesBE()
|
||||||
|
var verified bool
|
||||||
|
for _, n := range notaries {
|
||||||
|
if n.Verify(sig, hash) {
|
||||||
|
verified = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return stackitem.NewBool(verified)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNotaryNodes returns public keys of notary nodes.
|
||||||
|
func (n *Notary) GetNotaryNodes(d dao.DAO) (keys.PublicKeys, error) {
|
||||||
|
nodes, _, err := n.Desig.GetDesignatedByRole(d, RoleP2PNotary, math.MaxUint32)
|
||||||
|
return nodes, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDepositFor returns state.Deposit for the account specified. It returns nil in case if
|
||||||
|
// deposit is not found in storage and panics in case of any other error.
|
||||||
|
func (n *Notary) getDepositFor(dao dao.DAO, acc util.Uint160) *state.Deposit {
|
||||||
|
key := append([]byte{prefixDeposit}, acc.BytesBE()...)
|
||||||
|
deposit := new(state.Deposit)
|
||||||
|
err := getSerializableFromDAO(n.ContractID, dao, key, deposit)
|
||||||
|
if err == nil {
|
||||||
|
return deposit
|
||||||
|
}
|
||||||
|
if err == storage.ErrKeyNotFound {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
panic(fmt.Errorf("failed to get deposit for %s from storage: %w", acc.StringBE(), err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// putDepositFor puts deposit on the balance of the specified account in the storage.
|
||||||
|
func (n *Notary) putDepositFor(dao dao.DAO, deposit *state.Deposit, acc util.Uint160) error {
|
||||||
|
key := append([]byte{prefixDeposit}, acc.BytesBE()...)
|
||||||
|
return putSerializableToDAO(n.ContractID, dao, key, deposit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeDepositFor removes deposit from the storage.
|
||||||
|
func (n *Notary) removeDepositFor(dao dao.DAO, acc util.Uint160) error {
|
||||||
|
key := append([]byte{prefixDeposit}, acc.BytesBE()...)
|
||||||
|
return dao.DeleteStorageItem(n.ContractID, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculateNotaryReward calculates the reward for a single notary node based on FEE's count and Notary nodes count.
|
||||||
|
func calculateNotaryReward(nFees int64, notariesCount int) *big.Int {
|
||||||
|
return big.NewInt(nFees * transaction.NotaryServiceFeePerKey / int64(notariesCount))
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ package native
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/dao"
|
"github.com/nspcc-dev/neo-go/pkg/core/dao"
|
||||||
|
"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/storage"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||||
)
|
)
|
||||||
|
@ -15,3 +16,14 @@ func getSerializableFromDAO(id int32, d dao.DAO, key []byte, item io.Serializabl
|
||||||
item.DecodeBinary(r)
|
item.DecodeBinary(r)
|
||||||
return r.Err
|
return r.Err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func putSerializableToDAO(id int32, d dao.DAO, key []byte, item io.Serializable) error {
|
||||||
|
w := io.NewBufBinWriter()
|
||||||
|
item.EncodeBinary(w.BinWriter)
|
||||||
|
if w.Err != nil {
|
||||||
|
return w.Err
|
||||||
|
}
|
||||||
|
return d.PutStorageItem(id, key, &state.StorageItem{
|
||||||
|
Value: w.Bytes(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
298
pkg/core/native_notary_test.go
Normal file
298
pkg/core/native_notary_test.go
Normal file
|
@ -0,0 +1,298 @@
|
||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/nspcc-dev/neo-go/internal/testchain"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/core/native"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNotaryContractPipeline(t *testing.T) {
|
||||||
|
chain := newTestChain(t)
|
||||||
|
defer chain.Close()
|
||||||
|
|
||||||
|
notaryHash := chain.contracts.Notary.Hash
|
||||||
|
gasHash := chain.contracts.GAS.Hash
|
||||||
|
depositLock := 30
|
||||||
|
|
||||||
|
// check Notary contract has no GAS on the account
|
||||||
|
checkBalanceOf(t, chain, notaryHash, 0)
|
||||||
|
|
||||||
|
// `balanceOf`: check multisig account has no GAS on deposit
|
||||||
|
balance, err := invokeContractMethod(chain, 100000000, notaryHash, "balanceOf", testchain.MultisigScriptHash())
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkResult(t, balance, stackitem.Make(0))
|
||||||
|
|
||||||
|
// `expirationOf`: should fail to get deposit which does not exist
|
||||||
|
till, err := invokeContractMethod(chain, 100000000, notaryHash, "expirationOf", testchain.MultisigScriptHash())
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkResult(t, till, stackitem.Make(0))
|
||||||
|
|
||||||
|
// `lockDepositUntil`: should fail because there's no deposit
|
||||||
|
lockDepositUntilRes, err := invokeContractMethod(chain, 100000000, notaryHash, "lockDepositUntil", testchain.MultisigScriptHash(), int64(depositLock+1))
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkResult(t, lockDepositUntilRes, stackitem.NewBool(false))
|
||||||
|
|
||||||
|
// `onPayment`: bad token
|
||||||
|
transferTx := transferTokenFromMultisigAccount(t, chain, notaryHash, chain.contracts.NEO.Hash, 1, nil, int64(depositLock))
|
||||||
|
res, err := chain.GetAppExecResults(transferTx.Hash(), trigger.Application)
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkFAULTState(t, &res[0])
|
||||||
|
|
||||||
|
// `onPayment`: insufficient first deposit
|
||||||
|
transferTx = transferTokenFromMultisigAccount(t, chain, notaryHash, gasHash, 2*transaction.NotaryServiceFeePerKey-1, nil, int64(depositLock))
|
||||||
|
res, err = chain.GetAppExecResults(transferTx.Hash(), trigger.Application)
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkFAULTState(t, &res[0])
|
||||||
|
|
||||||
|
// `onPayment`: invalid `data` (missing `till` parameter)
|
||||||
|
transferTx = transferTokenFromMultisigAccount(t, chain, notaryHash, gasHash, 2*transaction.NotaryServiceFeePerKey-1, nil)
|
||||||
|
res, err = chain.GetAppExecResults(transferTx.Hash(), trigger.Application)
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkFAULTState(t, &res[0])
|
||||||
|
|
||||||
|
// `onPayment`: good
|
||||||
|
transferTx = transferTokenFromMultisigAccount(t, chain, notaryHash, gasHash, 2*transaction.NotaryServiceFeePerKey, nil, int64(depositLock))
|
||||||
|
res, err = chain.GetAppExecResults(transferTx.Hash(), trigger.Application)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, vm.HaltState, res[0].VMState)
|
||||||
|
require.Equal(t, 0, len(res[0].Stack))
|
||||||
|
checkBalanceOf(t, chain, notaryHash, 2*transaction.NotaryServiceFeePerKey)
|
||||||
|
|
||||||
|
// `expirationOf`: check `till` was set
|
||||||
|
till, err = invokeContractMethod(chain, 100000000, notaryHash, "expirationOf", testchain.MultisigScriptHash())
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkResult(t, till, stackitem.Make(depositLock))
|
||||||
|
|
||||||
|
// `balanceOf`: check deposited amount for the multisig account
|
||||||
|
balance, err = invokeContractMethod(chain, 100000000, notaryHash, "balanceOf", testchain.MultisigScriptHash())
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkResult(t, balance, stackitem.Make(2*transaction.NotaryServiceFeePerKey))
|
||||||
|
|
||||||
|
// `onPayment`: good second deposit and explicit `to` paramenter
|
||||||
|
transferTx = transferTokenFromMultisigAccount(t, chain, notaryHash, gasHash, transaction.NotaryServiceFeePerKey, testchain.MultisigScriptHash(), int64(depositLock+1))
|
||||||
|
res, err = chain.GetAppExecResults(transferTx.Hash(), trigger.Application)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, vm.HaltState, res[0].VMState)
|
||||||
|
require.Equal(t, 0, len(res[0].Stack))
|
||||||
|
checkBalanceOf(t, chain, notaryHash, 3*transaction.NotaryServiceFeePerKey)
|
||||||
|
|
||||||
|
// `balanceOf`: check deposited amount for the multisig account
|
||||||
|
balance, err = invokeContractMethod(chain, 100000000, notaryHash, "balanceOf", testchain.MultisigScriptHash())
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkResult(t, balance, stackitem.Make(3*transaction.NotaryServiceFeePerKey))
|
||||||
|
|
||||||
|
// `expirationOf`: check `till` is updated.
|
||||||
|
till, err = invokeContractMethod(chain, 100000000, notaryHash, "expirationOf", testchain.MultisigScriptHash())
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkResult(t, till, stackitem.Make(depositLock+1))
|
||||||
|
|
||||||
|
// `onPayment`: empty payment, should fail because `till` less then the previous one
|
||||||
|
transferTx = transferTokenFromMultisigAccount(t, chain, notaryHash, gasHash, 0, testchain.MultisigScriptHash(), int64(depositLock))
|
||||||
|
res, err = chain.GetAppExecResults(transferTx.Hash(), trigger.Application)
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkFAULTState(t, &res[0])
|
||||||
|
checkBalanceOf(t, chain, notaryHash, 3*transaction.NotaryServiceFeePerKey)
|
||||||
|
till, err = invokeContractMethod(chain, 100000000, notaryHash, "expirationOf", testchain.MultisigScriptHash())
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkResult(t, till, stackitem.Make(depositLock+1))
|
||||||
|
|
||||||
|
// `onPayment`: empty payment, should fail because `till` less then the chain height
|
||||||
|
transferTx = transferTokenFromMultisigAccount(t, chain, notaryHash, gasHash, 0, testchain.MultisigScriptHash(), int64(1))
|
||||||
|
res, err = chain.GetAppExecResults(transferTx.Hash(), trigger.Application)
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkFAULTState(t, &res[0])
|
||||||
|
checkBalanceOf(t, chain, notaryHash, 3*transaction.NotaryServiceFeePerKey)
|
||||||
|
till, err = invokeContractMethod(chain, 100000000, notaryHash, "expirationOf", testchain.MultisigScriptHash())
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkResult(t, till, stackitem.Make(depositLock+1))
|
||||||
|
|
||||||
|
// `onPayment`: empty payment, should successfully update `till`
|
||||||
|
transferTx = transferTokenFromMultisigAccount(t, chain, notaryHash, gasHash, 0, testchain.MultisigScriptHash(), int64(depositLock+2))
|
||||||
|
res, err = chain.GetAppExecResults(transferTx.Hash(), trigger.Application)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, vm.HaltState, res[0].VMState)
|
||||||
|
require.Equal(t, 0, len(res[0].Stack))
|
||||||
|
checkBalanceOf(t, chain, notaryHash, 3*transaction.NotaryServiceFeePerKey)
|
||||||
|
till, err = invokeContractMethod(chain, 100000000, notaryHash, "expirationOf", testchain.MultisigScriptHash())
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkResult(t, till, stackitem.Make(depositLock+2))
|
||||||
|
|
||||||
|
// `lockDepositUntil`: bad witness
|
||||||
|
lockDepositUntilRes, err = invokeContractMethod(chain, 100000000, notaryHash, "lockDepositUntil", util.Uint160{1, 2, 3}, int64(depositLock+5))
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkResult(t, lockDepositUntilRes, stackitem.NewBool(false))
|
||||||
|
till, err = invokeContractMethod(chain, 100000000, notaryHash, "expirationOf", testchain.MultisigScriptHash())
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkResult(t, till, stackitem.Make(depositLock+2))
|
||||||
|
|
||||||
|
// `lockDepositUntil`: bad `till` (less then the previous one)
|
||||||
|
lockDepositUntilRes, err = invokeContractMethod(chain, 100000000, notaryHash, "lockDepositUntil", testchain.MultisigScriptHash(), int64(depositLock+1))
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkResult(t, lockDepositUntilRes, stackitem.NewBool(false))
|
||||||
|
till, err = invokeContractMethod(chain, 100000000, notaryHash, "expirationOf", testchain.MultisigScriptHash())
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkResult(t, till, stackitem.Make(depositLock+2))
|
||||||
|
|
||||||
|
// `lockDepositUntil`: bad `till` (less then the chain's height)
|
||||||
|
lockDepositUntilRes, err = invokeContractMethod(chain, 100000000, notaryHash, "lockDepositUntil", testchain.MultisigScriptHash(), int64(1))
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkResult(t, lockDepositUntilRes, stackitem.NewBool(false))
|
||||||
|
till, err = invokeContractMethod(chain, 100000000, notaryHash, "expirationOf", testchain.MultisigScriptHash())
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkResult(t, till, stackitem.Make(depositLock+2))
|
||||||
|
|
||||||
|
// `lockDepositUntil`: good `till`
|
||||||
|
lockDepositUntilRes, err = invokeContractMethod(chain, 100000000, notaryHash, "lockDepositUntil", testchain.MultisigScriptHash(), int64(depositLock+3))
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkResult(t, lockDepositUntilRes, stackitem.NewBool(true))
|
||||||
|
till, err = invokeContractMethod(chain, 100000000, notaryHash, "expirationOf", testchain.MultisigScriptHash())
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkResult(t, till, stackitem.Make(depositLock+3))
|
||||||
|
|
||||||
|
// transfer 1 GAS to the new account for the next test
|
||||||
|
acc, _ := wallet.NewAccount()
|
||||||
|
transferTx = transferTokenFromMultisigAccount(t, chain, acc.PrivateKey().PublicKey().GetScriptHash(), gasHash, 100000000)
|
||||||
|
res, err = chain.GetAppExecResults(transferTx.Hash(), trigger.Application)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, vm.HaltState, res[0].VMState)
|
||||||
|
require.Equal(t, 0, len(res[0].Stack))
|
||||||
|
|
||||||
|
// `withdraw`: bad witness
|
||||||
|
w := io.NewBufBinWriter()
|
||||||
|
emit.AppCallWithOperationAndArgs(w.BinWriter, notaryHash, "withdraw", testchain.MultisigScriptHash(), acc.PrivateKey().PublicKey().GetScriptHash())
|
||||||
|
require.NoError(t, w.Err)
|
||||||
|
script := w.Bytes()
|
||||||
|
withdrawTx := transaction.New(chain.GetConfig().Magic, script, 10000000)
|
||||||
|
withdrawTx.ValidUntilBlock = chain.blockHeight + 1
|
||||||
|
withdrawTx.NetworkFee = 10000000
|
||||||
|
withdrawTx.Signers = []transaction.Signer{
|
||||||
|
{
|
||||||
|
Account: acc.PrivateKey().PublicKey().GetScriptHash(),
|
||||||
|
Scopes: transaction.None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err = acc.SignTx(withdrawTx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
b := chain.newBlock(withdrawTx)
|
||||||
|
err = chain.AddBlock(b)
|
||||||
|
require.NoError(t, err)
|
||||||
|
appExecRes, err := chain.GetAppExecResults(withdrawTx.Hash(), trigger.Application)
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkResult(t, &appExecRes[0], stackitem.NewBool(false))
|
||||||
|
balance, err = invokeContractMethod(chain, 100000000, notaryHash, "balanceOf", testchain.MultisigScriptHash())
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkResult(t, balance, stackitem.Make(3*transaction.NotaryServiceFeePerKey))
|
||||||
|
|
||||||
|
// `withdraw`: locked deposit
|
||||||
|
withdrawRes, err := invokeContractMethod(chain, 100000000, notaryHash, "withdraw", testchain.MultisigScriptHash(), acc.PrivateKey().PublicKey().GetScriptHash())
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkResult(t, withdrawRes, stackitem.NewBool(false))
|
||||||
|
balance, err = invokeContractMethod(chain, 100000000, notaryHash, "balanceOf", testchain.MultisigScriptHash())
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkResult(t, balance, stackitem.Make(3*transaction.NotaryServiceFeePerKey))
|
||||||
|
|
||||||
|
// `withdraw`: unlock deposit and transfer GAS back to owner
|
||||||
|
chain.genBlocks(depositLock)
|
||||||
|
withdrawRes, err = invokeContractMethod(chain, 100000000, notaryHash, "withdraw", testchain.MultisigScriptHash(), testchain.MultisigScriptHash())
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkResult(t, withdrawRes, stackitem.NewBool(true))
|
||||||
|
balance, err = invokeContractMethod(chain, 100000000, notaryHash, "balanceOf", testchain.MultisigScriptHash())
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkResult(t, balance, stackitem.Make(0))
|
||||||
|
checkBalanceOf(t, chain, notaryHash, 0)
|
||||||
|
|
||||||
|
// `withdraw`: the second time it should fail, because there's no deposit left
|
||||||
|
withdrawRes, err = invokeContractMethod(chain, 100000000, notaryHash, "withdraw", testchain.MultisigScriptHash(), testchain.MultisigScriptHash())
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkResult(t, withdrawRes, stackitem.NewBool(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNotaryNodesReward(t *testing.T) {
|
||||||
|
checkReward := func(nKeys int, nNotaryNodes int, spendFullDeposit bool) {
|
||||||
|
chain := newTestChain(t)
|
||||||
|
defer chain.Close()
|
||||||
|
notaryHash := chain.contracts.Notary.Hash
|
||||||
|
gasHash := chain.contracts.GAS.Hash
|
||||||
|
signer := testchain.MultisigScriptHash()
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// set Notary nodes and check their balance
|
||||||
|
notaryNodes := make([]*keys.PrivateKey, nNotaryNodes)
|
||||||
|
notaryNodesPublicKeys := make(keys.PublicKeys, nNotaryNodes)
|
||||||
|
for i := range notaryNodes {
|
||||||
|
notaryNodes[i], err = keys.NewPrivateKey()
|
||||||
|
require.NoError(t, err)
|
||||||
|
notaryNodesPublicKeys[i] = notaryNodes[i].PublicKey()
|
||||||
|
}
|
||||||
|
chain.setNodesByRole(t, true, native.RoleP2PNotary, notaryNodesPublicKeys)
|
||||||
|
for _, notaryNode := range notaryNodesPublicKeys {
|
||||||
|
checkBalanceOf(t, chain, notaryNode.GetScriptHash(), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// deposit GAS for `signer` with lock until the next block
|
||||||
|
depositAmount := 100_0000 + (2+int64(nKeys))*transaction.NotaryServiceFeePerKey // sysfee + netfee of the next transaction
|
||||||
|
if !spendFullDeposit {
|
||||||
|
depositAmount += 1_0000
|
||||||
|
}
|
||||||
|
transferTx := transferTokenFromMultisigAccount(t, chain, notaryHash, gasHash, depositAmount, signer, int64(chain.BlockHeight()+1))
|
||||||
|
res, err := chain.GetAppExecResults(transferTx.Hash(), trigger.Application)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, vm.HaltState, res[0].VMState)
|
||||||
|
require.Equal(t, 0, len(res[0].Stack))
|
||||||
|
|
||||||
|
// send transaction with Notary contract as a sender
|
||||||
|
tx := chain.newTestTx(util.Uint160{}, []byte{byte(opcode.PUSH1)})
|
||||||
|
tx.Attributes = append(tx.Attributes, transaction.Attribute{Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{NKeys: uint8(nKeys)}})
|
||||||
|
tx.NetworkFee = (2 + int64(nKeys)) * transaction.NotaryServiceFeePerKey
|
||||||
|
tx.Signers = []transaction.Signer{
|
||||||
|
{
|
||||||
|
Account: notaryHash,
|
||||||
|
Scopes: transaction.None,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Account: signer,
|
||||||
|
Scopes: transaction.None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
data := tx.GetSignedPart()
|
||||||
|
tx.Scripts = []transaction.Witness{
|
||||||
|
{
|
||||||
|
InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, notaryNodes[0].Sign(data)...),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
InvocationScript: testchain.Sign(data),
|
||||||
|
VerificationScript: testchain.MultisigVerificationScript(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
b := chain.newBlock(tx)
|
||||||
|
require.NoError(t, chain.AddBlock(b))
|
||||||
|
checkBalanceOf(t, chain, notaryHash, int(depositAmount-tx.SystemFee-tx.NetworkFee))
|
||||||
|
for _, notaryNode := range notaryNodesPublicKeys {
|
||||||
|
checkBalanceOf(t, chain, notaryNode.GetScriptHash(), transaction.NotaryServiceFeePerKey*(nKeys+1)/nNotaryNodes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, spendDeposit := range []bool{true, false} {
|
||||||
|
checkReward(0, 1, spendDeposit)
|
||||||
|
checkReward(0, 2, spendDeposit)
|
||||||
|
checkReward(1, 1, spendDeposit)
|
||||||
|
checkReward(1, 2, spendDeposit)
|
||||||
|
checkReward(1, 3, spendDeposit)
|
||||||
|
checkReward(5, 1, spendDeposit)
|
||||||
|
checkReward(5, 2, spendDeposit)
|
||||||
|
checkReward(5, 6, spendDeposit)
|
||||||
|
checkReward(5, 7, spendDeposit)
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,18 +5,11 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/nspcc-dev/neo-go/internal/random"
|
"github.com/nspcc-dev/neo-go/internal/random"
|
||||||
"github.com/nspcc-dev/neo-go/internal/testchain"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/block"
|
"github.com/nspcc-dev/neo-go/pkg/core/block"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/native"
|
"github.com/nspcc-dev/neo-go/pkg/core/native"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/encoding/bigint"
|
"github.com/nspcc-dev/neo-go/pkg/encoding/bigint"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/network/payload"
|
"github.com/nspcc-dev/neo-go/pkg/network/payload"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
@ -24,6 +17,7 @@ import (
|
||||||
func TestMaxTransactionsPerBlock(t *testing.T) {
|
func TestMaxTransactionsPerBlock(t *testing.T) {
|
||||||
chain := newTestChain(t)
|
chain := newTestChain(t)
|
||||||
defer chain.Close()
|
defer chain.Close()
|
||||||
|
policyHash := chain.contracts.Policy.Metadata().Hash
|
||||||
|
|
||||||
t.Run("get, internal method", func(t *testing.T) {
|
t.Run("get, internal method", func(t *testing.T) {
|
||||||
n := chain.contracts.Policy.GetMaxTransactionsPerBlockInternal(chain.dao)
|
n := chain.contracts.Policy.GetMaxTransactionsPerBlockInternal(chain.dao)
|
||||||
|
@ -31,14 +25,14 @@ func TestMaxTransactionsPerBlock(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("get, contract method", func(t *testing.T) {
|
t.Run("get, contract method", func(t *testing.T) {
|
||||||
res, err := invokeNativePolicyMethod(chain, "getMaxTransactionsPerBlock")
|
res, err := invokeContractMethod(chain, 100000000, policyHash, "getMaxTransactionsPerBlock")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
checkResult(t, res, stackitem.NewBigInteger(big.NewInt(512)))
|
checkResult(t, res, stackitem.NewBigInteger(big.NewInt(512)))
|
||||||
require.NoError(t, chain.persist())
|
require.NoError(t, chain.persist())
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("set", func(t *testing.T) {
|
t.Run("set", func(t *testing.T) {
|
||||||
res, err := invokeNativePolicyMethod(chain, "setMaxTransactionsPerBlock", bigint.ToBytes(big.NewInt(1024)))
|
res, err := invokeContractMethod(chain, 100000000, policyHash, "setMaxTransactionsPerBlock", bigint.ToBytes(big.NewInt(1024)))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
checkResult(t, res, stackitem.NewBool(true))
|
checkResult(t, res, stackitem.NewBool(true))
|
||||||
require.NoError(t, chain.persist())
|
require.NoError(t, chain.persist())
|
||||||
|
@ -47,7 +41,7 @@ func TestMaxTransactionsPerBlock(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("set, too big value", func(t *testing.T) {
|
t.Run("set, too big value", func(t *testing.T) {
|
||||||
res, err := invokeNativePolicyMethod(chain, "setMaxTransactionsPerBlock", bigint.ToBytes(big.NewInt(block.MaxContentsPerBlock)))
|
res, err := invokeContractMethod(chain, 100000000, policyHash, "setMaxTransactionsPerBlock", bigint.ToBytes(big.NewInt(block.MaxContentsPerBlock)))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
checkFAULTState(t, res)
|
checkFAULTState(t, res)
|
||||||
})
|
})
|
||||||
|
@ -56,6 +50,7 @@ func TestMaxTransactionsPerBlock(t *testing.T) {
|
||||||
func TestMaxBlockSize(t *testing.T) {
|
func TestMaxBlockSize(t *testing.T) {
|
||||||
chain := newTestChain(t)
|
chain := newTestChain(t)
|
||||||
defer chain.Close()
|
defer chain.Close()
|
||||||
|
policyHash := chain.contracts.Policy.Metadata().Hash
|
||||||
|
|
||||||
t.Run("get, internal method", func(t *testing.T) {
|
t.Run("get, internal method", func(t *testing.T) {
|
||||||
n := chain.contracts.Policy.GetMaxBlockSizeInternal(chain.dao)
|
n := chain.contracts.Policy.GetMaxBlockSizeInternal(chain.dao)
|
||||||
|
@ -63,25 +58,25 @@ func TestMaxBlockSize(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("get, contract method", func(t *testing.T) {
|
t.Run("get, contract method", func(t *testing.T) {
|
||||||
res, err := invokeNativePolicyMethod(chain, "getMaxBlockSize")
|
res, err := invokeContractMethod(chain, 100000000, policyHash, "getMaxBlockSize")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
checkResult(t, res, stackitem.NewBigInteger(big.NewInt(1024*256)))
|
checkResult(t, res, stackitem.NewBigInteger(big.NewInt(1024*256)))
|
||||||
require.NoError(t, chain.persist())
|
require.NoError(t, chain.persist())
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("set", func(t *testing.T) {
|
t.Run("set", func(t *testing.T) {
|
||||||
res, err := invokeNativePolicyMethod(chain, "setMaxBlockSize", bigint.ToBytes(big.NewInt(102400)))
|
res, err := invokeContractMethod(chain, 100000000, policyHash, "setMaxBlockSize", bigint.ToBytes(big.NewInt(102400)))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
checkResult(t, res, stackitem.NewBool(true))
|
checkResult(t, res, stackitem.NewBool(true))
|
||||||
require.NoError(t, chain.persist())
|
require.NoError(t, chain.persist())
|
||||||
res, err = invokeNativePolicyMethod(chain, "getMaxBlockSize")
|
res, err = invokeContractMethod(chain, 100000000, policyHash, "getMaxBlockSize")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
checkResult(t, res, stackitem.NewBigInteger(big.NewInt(102400)))
|
checkResult(t, res, stackitem.NewBigInteger(big.NewInt(102400)))
|
||||||
require.NoError(t, chain.persist())
|
require.NoError(t, chain.persist())
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("set, too big value", func(t *testing.T) {
|
t.Run("set, too big value", func(t *testing.T) {
|
||||||
res, err := invokeNativePolicyMethod(chain, "setMaxBlockSize", bigint.ToBytes(big.NewInt(payload.MaxSize+1)))
|
res, err := invokeContractMethod(chain, 100000000, policyHash, "setMaxBlockSize", bigint.ToBytes(big.NewInt(payload.MaxSize+1)))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
checkFAULTState(t, res)
|
checkFAULTState(t, res)
|
||||||
})
|
})
|
||||||
|
@ -90,6 +85,7 @@ func TestMaxBlockSize(t *testing.T) {
|
||||||
func TestFeePerByte(t *testing.T) {
|
func TestFeePerByte(t *testing.T) {
|
||||||
chain := newTestChain(t)
|
chain := newTestChain(t)
|
||||||
defer chain.Close()
|
defer chain.Close()
|
||||||
|
policyHash := chain.contracts.Policy.Metadata().Hash
|
||||||
|
|
||||||
t.Run("get, internal method", func(t *testing.T) {
|
t.Run("get, internal method", func(t *testing.T) {
|
||||||
n := chain.contracts.Policy.GetFeePerByteInternal(chain.dao)
|
n := chain.contracts.Policy.GetFeePerByteInternal(chain.dao)
|
||||||
|
@ -97,14 +93,14 @@ func TestFeePerByte(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("get, contract method", func(t *testing.T) {
|
t.Run("get, contract method", func(t *testing.T) {
|
||||||
res, err := invokeNativePolicyMethod(chain, "getFeePerByte")
|
res, err := invokeContractMethod(chain, 100000000, policyHash, "getFeePerByte")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
checkResult(t, res, stackitem.NewBigInteger(big.NewInt(1000)))
|
checkResult(t, res, stackitem.NewBigInteger(big.NewInt(1000)))
|
||||||
require.NoError(t, chain.persist())
|
require.NoError(t, chain.persist())
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("set", func(t *testing.T) {
|
t.Run("set", func(t *testing.T) {
|
||||||
res, err := invokeNativePolicyMethod(chain, "setFeePerByte", bigint.ToBytes(big.NewInt(1024)))
|
res, err := invokeContractMethod(chain, 100000000, policyHash, "setFeePerByte", bigint.ToBytes(big.NewInt(1024)))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
checkResult(t, res, stackitem.NewBool(true))
|
checkResult(t, res, stackitem.NewBool(true))
|
||||||
require.NoError(t, chain.persist())
|
require.NoError(t, chain.persist())
|
||||||
|
@ -113,13 +109,13 @@ func TestFeePerByte(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("set, negative value", func(t *testing.T) {
|
t.Run("set, negative value", func(t *testing.T) {
|
||||||
res, err := invokeNativePolicyMethod(chain, "setFeePerByte", bigint.ToBytes(big.NewInt(-1)))
|
res, err := invokeContractMethod(chain, 100000000, policyHash, "setFeePerByte", bigint.ToBytes(big.NewInt(-1)))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
checkFAULTState(t, res)
|
checkFAULTState(t, res)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("set, too big value", func(t *testing.T) {
|
t.Run("set, too big value", func(t *testing.T) {
|
||||||
res, err := invokeNativePolicyMethod(chain, "setFeePerByte", bigint.ToBytes(big.NewInt(100_000_000+1)))
|
res, err := invokeContractMethod(chain, 100000000, policyHash, "setFeePerByte", bigint.ToBytes(big.NewInt(100_000_000+1)))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
checkFAULTState(t, res)
|
checkFAULTState(t, res)
|
||||||
})
|
})
|
||||||
|
@ -128,6 +124,7 @@ func TestFeePerByte(t *testing.T) {
|
||||||
func TestBlockSystemFee(t *testing.T) {
|
func TestBlockSystemFee(t *testing.T) {
|
||||||
chain := newTestChain(t)
|
chain := newTestChain(t)
|
||||||
defer chain.Close()
|
defer chain.Close()
|
||||||
|
policyHash := chain.contracts.Policy.Metadata().Hash
|
||||||
|
|
||||||
t.Run("get, internal method", func(t *testing.T) {
|
t.Run("get, internal method", func(t *testing.T) {
|
||||||
n := chain.contracts.Policy.GetMaxBlockSystemFeeInternal(chain.dao)
|
n := chain.contracts.Policy.GetMaxBlockSystemFeeInternal(chain.dao)
|
||||||
|
@ -135,24 +132,24 @@ func TestBlockSystemFee(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("get", func(t *testing.T) {
|
t.Run("get", func(t *testing.T) {
|
||||||
res, err := invokeNativePolicyMethod(chain, "getMaxBlockSystemFee")
|
res, err := invokeContractMethod(chain, 100000000, policyHash, "getMaxBlockSystemFee")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
checkResult(t, res, stackitem.NewBigInteger(big.NewInt(9000*native.GASFactor)))
|
checkResult(t, res, stackitem.NewBigInteger(big.NewInt(9000*native.GASFactor)))
|
||||||
require.NoError(t, chain.persist())
|
require.NoError(t, chain.persist())
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("set, too low fee", func(t *testing.T) {
|
t.Run("set, too low fee", func(t *testing.T) {
|
||||||
res, err := invokeNativePolicyMethod(chain, "setMaxBlockSystemFee", bigint.ToBytes(big.NewInt(4007600)))
|
res, err := invokeContractMethod(chain, 100000000, policyHash, "setMaxBlockSystemFee", bigint.ToBytes(big.NewInt(4007600)))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
checkFAULTState(t, res)
|
checkFAULTState(t, res)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("set, success", func(t *testing.T) {
|
t.Run("set, success", func(t *testing.T) {
|
||||||
res, err := invokeNativePolicyMethod(chain, "setMaxBlockSystemFee", bigint.ToBytes(big.NewInt(100000000)))
|
res, err := invokeContractMethod(chain, 100000000, policyHash, "setMaxBlockSystemFee", bigint.ToBytes(big.NewInt(100000000)))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
checkResult(t, res, stackitem.NewBool(true))
|
checkResult(t, res, stackitem.NewBool(true))
|
||||||
require.NoError(t, chain.persist())
|
require.NoError(t, chain.persist())
|
||||||
res, err = invokeNativePolicyMethod(chain, "getMaxBlockSystemFee")
|
res, err = invokeContractMethod(chain, 100000000, policyHash, "getMaxBlockSystemFee")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
checkResult(t, res, stackitem.NewBigInteger(big.NewInt(100000000)))
|
checkResult(t, res, stackitem.NewBigInteger(big.NewInt(100000000)))
|
||||||
require.NoError(t, chain.persist())
|
require.NoError(t, chain.persist())
|
||||||
|
@ -163,6 +160,7 @@ func TestBlockedAccounts(t *testing.T) {
|
||||||
chain := newTestChain(t)
|
chain := newTestChain(t)
|
||||||
defer chain.Close()
|
defer chain.Close()
|
||||||
account := util.Uint160{1, 2, 3}
|
account := util.Uint160{1, 2, 3}
|
||||||
|
policyHash := chain.contracts.Policy.Metadata().Hash
|
||||||
|
|
||||||
t.Run("isBlocked, internal method", func(t *testing.T) {
|
t.Run("isBlocked, internal method", func(t *testing.T) {
|
||||||
isBlocked := chain.contracts.Policy.IsBlockedInternal(chain.dao, random.Uint160())
|
isBlocked := chain.contracts.Policy.IsBlockedInternal(chain.dao, random.Uint160())
|
||||||
|
@ -170,14 +168,14 @@ func TestBlockedAccounts(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("isBlocked, contract method", func(t *testing.T) {
|
t.Run("isBlocked, contract method", func(t *testing.T) {
|
||||||
res, err := invokeNativePolicyMethod(chain, "isBlocked", random.Uint160())
|
res, err := invokeContractMethod(chain, 100000000, policyHash, "isBlocked", random.Uint160())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
checkResult(t, res, stackitem.NewBool(false))
|
checkResult(t, res, stackitem.NewBool(false))
|
||||||
require.NoError(t, chain.persist())
|
require.NoError(t, chain.persist())
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("block-unblock account", func(t *testing.T) {
|
t.Run("block-unblock account", func(t *testing.T) {
|
||||||
res, err := invokeNativePolicyMethod(chain, "blockAccount", account.BytesBE())
|
res, err := invokeContractMethod(chain, 100000000, policyHash, "blockAccount", account.BytesBE())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
checkResult(t, res, stackitem.NewBool(true))
|
checkResult(t, res, stackitem.NewBool(true))
|
||||||
|
|
||||||
|
@ -185,7 +183,7 @@ func TestBlockedAccounts(t *testing.T) {
|
||||||
require.Equal(t, isBlocked, true)
|
require.Equal(t, isBlocked, true)
|
||||||
require.NoError(t, chain.persist())
|
require.NoError(t, chain.persist())
|
||||||
|
|
||||||
res, err = invokeNativePolicyMethod(chain, "unblockAccount", account.BytesBE())
|
res, err = invokeContractMethod(chain, 100000000, policyHash, "unblockAccount", account.BytesBE())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
checkResult(t, res, stackitem.NewBool(true))
|
checkResult(t, res, stackitem.NewBool(true))
|
||||||
|
|
||||||
|
@ -196,65 +194,27 @@ func TestBlockedAccounts(t *testing.T) {
|
||||||
|
|
||||||
t.Run("double-block", func(t *testing.T) {
|
t.Run("double-block", func(t *testing.T) {
|
||||||
// block
|
// block
|
||||||
res, err := invokeNativePolicyMethod(chain, "blockAccount", account.BytesBE())
|
res, err := invokeContractMethod(chain, 100000000, policyHash, "blockAccount", account.BytesBE())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
checkResult(t, res, stackitem.NewBool(true))
|
checkResult(t, res, stackitem.NewBool(true))
|
||||||
require.NoError(t, chain.persist())
|
require.NoError(t, chain.persist())
|
||||||
|
|
||||||
// double-block should fail
|
// double-block should fail
|
||||||
res, err = invokeNativePolicyMethod(chain, "blockAccount", account.BytesBE())
|
res, err = invokeContractMethod(chain, 100000000, policyHash, "blockAccount", account.BytesBE())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
checkResult(t, res, stackitem.NewBool(false))
|
checkResult(t, res, stackitem.NewBool(false))
|
||||||
require.NoError(t, chain.persist())
|
require.NoError(t, chain.persist())
|
||||||
|
|
||||||
// unblock
|
// unblock
|
||||||
res, err = invokeNativePolicyMethod(chain, "unblockAccount", account.BytesBE())
|
res, err = invokeContractMethod(chain, 100000000, policyHash, "unblockAccount", account.BytesBE())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
checkResult(t, res, stackitem.NewBool(true))
|
checkResult(t, res, stackitem.NewBool(true))
|
||||||
require.NoError(t, chain.persist())
|
require.NoError(t, chain.persist())
|
||||||
|
|
||||||
// unblock the same account should fail as we don't have it blocked
|
// unblock the same account should fail as we don't have it blocked
|
||||||
res, err = invokeNativePolicyMethod(chain, "unblockAccount", account.BytesBE())
|
res, err = invokeContractMethod(chain, 100000000, policyHash, "unblockAccount", account.BytesBE())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
checkResult(t, res, stackitem.NewBool(false))
|
checkResult(t, res, stackitem.NewBool(false))
|
||||||
require.NoError(t, chain.persist())
|
require.NoError(t, chain.persist())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func invokeNativePolicyMethod(chain *Blockchain, method string, args ...interface{}) (*state.AppExecResult, error) {
|
|
||||||
w := io.NewBufBinWriter()
|
|
||||||
emit.AppCallWithOperationAndArgs(w.BinWriter, chain.contracts.Policy.Metadata().Hash, method, args...)
|
|
||||||
if w.Err != nil {
|
|
||||||
return nil, w.Err
|
|
||||||
}
|
|
||||||
script := w.Bytes()
|
|
||||||
tx := transaction.New(chain.GetConfig().Magic, script, 10000000)
|
|
||||||
validUntil := chain.blockHeight + 1
|
|
||||||
tx.ValidUntilBlock = validUntil
|
|
||||||
addSigners(tx)
|
|
||||||
err := testchain.SignTx(chain, tx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
b := chain.newBlock(tx)
|
|
||||||
err = chain.AddBlock(b)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := chain.GetAppExecResults(tx.Hash(), trigger.Application)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &res[0], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkResult(t *testing.T, result *state.AppExecResult, expected stackitem.Item) {
|
|
||||||
require.Equal(t, vm.HaltState, result.VMState)
|
|
||||||
require.Equal(t, 1, len(result.Stack))
|
|
||||||
require.Equal(t, expected, result.Stack[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkFAULTState(t *testing.T, result *state.AppExecResult) {
|
|
||||||
require.Equal(t, vm.FaultState, result.VMState)
|
|
||||||
}
|
|
||||||
|
|
26
pkg/core/state/deposit.go
Normal file
26
pkg/core/state/deposit.go
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
package state
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/big"
|
||||||
|
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/encoding/bigint"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Deposit represents GAS deposit from Notary contract.
|
||||||
|
type Deposit struct {
|
||||||
|
Amount *big.Int
|
||||||
|
Till uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodeBinary implements io.Serializable interface.
|
||||||
|
func (d *Deposit) EncodeBinary(w *io.BinWriter) {
|
||||||
|
w.WriteVarBytes(bigint.ToBytes(d.Amount))
|
||||||
|
w.WriteU32LE(d.Till)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeBinary implements io.Serializable interface.
|
||||||
|
func (d *Deposit) DecodeBinary(r *io.BinReader) {
|
||||||
|
d.Amount = bigint.FromBytes(r.ReadVarBytes())
|
||||||
|
d.Till = r.ReadU32LE()
|
||||||
|
}
|
Loading…
Reference in a new issue