core: add Notary module test
This commit is contained in:
parent
4ad9c7929b
commit
df26ca1c7f
7 changed files with 1168 additions and 1 deletions
|
@ -15,6 +15,7 @@ import (
|
||||||
"github.com/nspcc-dev/neo-go/internal/testserdes"
|
"github.com/nspcc-dev/neo-go/internal/testserdes"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/config"
|
"github.com/nspcc-dev/neo-go/pkg/config"
|
||||||
"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/blockchainer"
|
||||||
"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"
|
||||||
|
@ -391,6 +392,9 @@ func newNEP17Transfer(sc, from, to util.Uint160, amount int64, additionalArgs ..
|
||||||
w := io.NewBufBinWriter()
|
w := io.NewBufBinWriter()
|
||||||
emit.AppCall(w.BinWriter, sc, "transfer", callflag.All, from, to, amount, additionalArgs)
|
emit.AppCall(w.BinWriter, sc, "transfer", callflag.All, from, to, amount, additionalArgs)
|
||||||
emit.Opcodes(w.BinWriter, opcode.ASSERT)
|
emit.Opcodes(w.BinWriter, opcode.ASSERT)
|
||||||
|
if w.Err != nil {
|
||||||
|
panic(fmt.Errorf("failed to create nep17 transfer transaction: %w", w.Err))
|
||||||
|
}
|
||||||
|
|
||||||
script := w.Bytes()
|
script := w.Bytes()
|
||||||
return transaction.New(testchain.Network(), script, 11000000)
|
return transaction.New(testchain.Network(), script, 11000000)
|
||||||
|
@ -564,3 +568,19 @@ func checkBalanceOf(t *testing.T, chain *Blockchain, addr util.Uint160, expected
|
||||||
balance := chain.GetNEP17Balances(addr).Trackers[chain.contracts.GAS.ContractID]
|
balance := chain.GetNEP17Balances(addr).Trackers[chain.contracts.GAS.ContractID]
|
||||||
require.Equal(t, int64(expected), balance.Balance.Int64())
|
require.Equal(t, int64(expected), balance.Balance.Int64())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type NotaryFeerStub struct {
|
||||||
|
bc blockchainer.Blockchainer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f NotaryFeerStub) FeePerByte() int64 { return f.bc.FeePerByte() }
|
||||||
|
func (f NotaryFeerStub) GetUtilityTokenBalance(acc util.Uint160) *big.Int {
|
||||||
|
return f.bc.GetNotaryBalance(acc)
|
||||||
|
}
|
||||||
|
func (f NotaryFeerStub) BlockHeight() uint32 { return f.bc.BlockHeight() }
|
||||||
|
func (f NotaryFeerStub) P2PSigExtensionsEnabled() bool { return f.bc.P2PSigExtensionsEnabled() }
|
||||||
|
func NewNotaryFeerStub(bc blockchainer.Blockchainer) NotaryFeerStub {
|
||||||
|
return NotaryFeerStub{
|
||||||
|
bc: bc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
647
pkg/core/notary_test.go
Normal file
647
pkg/core/notary_test.go
Normal file
|
@ -0,0 +1,647 @@
|
||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"path"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/nspcc-dev/neo-go/internal/testchain"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/config"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/config/netmode"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/core/block"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/core/blockchainer"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/core/mempool"
|
||||||
|
"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/hash"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/network/payload"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/services/notary"
|
||||||
|
"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/vm/opcode"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.uber.org/zap/zaptest"
|
||||||
|
)
|
||||||
|
|
||||||
|
const notaryModulePath = "../services/notary/"
|
||||||
|
|
||||||
|
func getTestNotary(t *testing.T, bc *Blockchain, walletPath, pass string, onTx func(tx *transaction.Transaction) error) (*wallet.Account, *notary.Notary, *mempool.Pool) {
|
||||||
|
bc.config.P2PNotary = config.P2PNotary{
|
||||||
|
Enabled: true,
|
||||||
|
UnlockWallet: config.Wallet{
|
||||||
|
Path: path.Join(notaryModulePath, walletPath),
|
||||||
|
Password: pass,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mp := mempool.New(10, 1, true)
|
||||||
|
ntr, err := notary.NewNotary(bc, mp, zaptest.NewLogger(t), onTx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
w, err := wallet.NewWalletFromFile(path.Join(notaryModulePath, walletPath))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, w.Accounts[0].Decrypt(pass))
|
||||||
|
return w.Accounts[0], ntr, mp
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNotary(t *testing.T) {
|
||||||
|
bc := newTestChain(t)
|
||||||
|
defer bc.Close()
|
||||||
|
var (
|
||||||
|
nonce uint32
|
||||||
|
nvbDiffFallback uint32 = 20
|
||||||
|
)
|
||||||
|
|
||||||
|
mtx := sync.RWMutex{}
|
||||||
|
completedTxes := make(map[util.Uint256]*transaction.Transaction)
|
||||||
|
var unluckies []*payload.P2PNotaryRequest
|
||||||
|
var (
|
||||||
|
finalizeWithError bool
|
||||||
|
choosy bool
|
||||||
|
)
|
||||||
|
onTransaction := func(tx *transaction.Transaction) error {
|
||||||
|
mtx.Lock()
|
||||||
|
defer mtx.Unlock()
|
||||||
|
if !choosy {
|
||||||
|
if completedTxes[tx.Hash()] != nil {
|
||||||
|
panic("transaction was completed twice")
|
||||||
|
}
|
||||||
|
if finalizeWithError {
|
||||||
|
return errors.New("error while finalizing transaction")
|
||||||
|
}
|
||||||
|
completedTxes[tx.Hash()] = tx
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, unl := range unluckies {
|
||||||
|
if tx.Hash().Equals(unl.FallbackTransaction.Hash()) {
|
||||||
|
return errors.New("error while finalizing transaction")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
completedTxes[tx.Hash()] = tx
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
acc1, ntr1, mp1 := getTestNotary(t, bc, "./testdata/notary1.json", "one", onTransaction)
|
||||||
|
acc2, _, _ := getTestNotary(t, bc, "./testdata/notary2.json", "two", onTransaction)
|
||||||
|
randomAcc, err := keys.NewPrivateKey()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
bc.SetNotary(ntr1)
|
||||||
|
bc.RegisterPostBlock(func(bc blockchainer.Blockchainer, pool *mempool.Pool, b *block.Block) {
|
||||||
|
ntr1.PostPersist(bc, pool, b)
|
||||||
|
})
|
||||||
|
|
||||||
|
notaryNodes := keys.PublicKeys{acc1.PrivateKey().PublicKey(), acc2.PrivateKey().PublicKey()}
|
||||||
|
bc.setNodesByRole(t, true, native.RoleP2PNotary, notaryNodes)
|
||||||
|
|
||||||
|
createFallbackTx := func(requester *wallet.Account, mainTx *transaction.Transaction, nvbIncrement ...uint32) *transaction.Transaction {
|
||||||
|
fallback := transaction.New(testchain.Network(), []byte{byte(opcode.RET)}, 2000_0000)
|
||||||
|
fallback.Nonce = nonce
|
||||||
|
nonce++
|
||||||
|
fallback.SystemFee = 1_0000_0000
|
||||||
|
fallback.ValidUntilBlock = bc.BlockHeight() + 50
|
||||||
|
fallback.Signers = []transaction.Signer{
|
||||||
|
{
|
||||||
|
Account: bc.GetNotaryContractScriptHash(),
|
||||||
|
Scopes: transaction.None,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Account: requester.PrivateKey().PublicKey().GetScriptHash(),
|
||||||
|
Scopes: transaction.None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
nvb := bc.BlockHeight() + nvbDiffFallback
|
||||||
|
if len(nvbIncrement) != 0 {
|
||||||
|
nvb += nvbIncrement[0]
|
||||||
|
}
|
||||||
|
fallback.Attributes = []transaction.Attribute{
|
||||||
|
{
|
||||||
|
Type: transaction.NotaryAssistedT,
|
||||||
|
Value: &transaction.NotaryAssisted{NKeys: 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: transaction.NotValidBeforeT,
|
||||||
|
Value: &transaction.NotValidBefore{Height: nvb},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: transaction.ConflictsT,
|
||||||
|
Value: &transaction.Conflicts{Hash: mainTx.Hash()},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
fallback.Scripts = []transaction.Witness{
|
||||||
|
{
|
||||||
|
InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, make([]byte, 64)...),
|
||||||
|
VerificationScript: []byte{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
requester.SignTx(fallback)
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
createStandardRequest := func(requesters []*wallet.Account, NVBincrements ...uint32) []*payload.P2PNotaryRequest {
|
||||||
|
mainTx := *transaction.New(testchain.Network(), []byte{byte(opcode.RET)}, 11000000)
|
||||||
|
mainTx.Nonce = nonce
|
||||||
|
nonce++
|
||||||
|
mainTx.SystemFee = 100000000
|
||||||
|
mainTx.ValidUntilBlock = bc.BlockHeight() + 2*nvbDiffFallback
|
||||||
|
signers := make([]transaction.Signer, len(requesters)+1)
|
||||||
|
for i := range requesters {
|
||||||
|
signers[i] = transaction.Signer{
|
||||||
|
Account: requesters[i].PrivateKey().PublicKey().GetScriptHash(),
|
||||||
|
Scopes: transaction.None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
signers[len(signers)-1] = transaction.Signer{
|
||||||
|
Account: bc.GetNotaryContractScriptHash(),
|
||||||
|
Scopes: transaction.None,
|
||||||
|
}
|
||||||
|
mainTx.Signers = signers
|
||||||
|
mainTx.Attributes = []transaction.Attribute{
|
||||||
|
{
|
||||||
|
Type: transaction.NotaryAssistedT,
|
||||||
|
Value: &transaction.NotaryAssisted{NKeys: uint8(len(requesters))},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
payloads := make([]*payload.P2PNotaryRequest, len(requesters))
|
||||||
|
for i := range payloads {
|
||||||
|
cp := mainTx
|
||||||
|
main := &cp
|
||||||
|
scripts := make([]transaction.Witness, len(requesters)+1)
|
||||||
|
for j := range requesters {
|
||||||
|
scripts[j].VerificationScript = requesters[j].PrivateKey().PublicKey().GetVerificationScript()
|
||||||
|
}
|
||||||
|
scripts[i].InvocationScript = append([]byte{byte(opcode.PUSHDATA1), 64}, requesters[i].PrivateKey().Sign(main.GetSignedPart())...)
|
||||||
|
main.Scripts = scripts
|
||||||
|
var fallback *transaction.Transaction
|
||||||
|
if len(NVBincrements) == len(requesters) {
|
||||||
|
fallback = createFallbackTx(requesters[i], main, NVBincrements[i])
|
||||||
|
} else {
|
||||||
|
fallback = createFallbackTx(requesters[i], main)
|
||||||
|
}
|
||||||
|
payloads[i] = &payload.P2PNotaryRequest{
|
||||||
|
MainTransaction: main,
|
||||||
|
FallbackTransaction: fallback,
|
||||||
|
Network: netmode.UnitTestNet,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return payloads
|
||||||
|
}
|
||||||
|
createMultisigRequest := func(m int, requesters []*wallet.Account) []*payload.P2PNotaryRequest {
|
||||||
|
mainTx := *transaction.New(testchain.Network(), []byte{byte(opcode.RET)}, 11000000)
|
||||||
|
mainTx.Nonce = nonce
|
||||||
|
nonce++
|
||||||
|
mainTx.SystemFee = 100000000
|
||||||
|
mainTx.ValidUntilBlock = bc.BlockHeight() + 2*nvbDiffFallback
|
||||||
|
pubs := make(keys.PublicKeys, len(requesters))
|
||||||
|
for i, r := range requesters {
|
||||||
|
pubs[i] = r.PrivateKey().PublicKey()
|
||||||
|
}
|
||||||
|
script, err := smartcontract.CreateMultiSigRedeemScript(m, pubs)
|
||||||
|
require.NoError(t, err)
|
||||||
|
mainTx.Signers = []transaction.Signer{
|
||||||
|
{
|
||||||
|
Account: hash.Hash160(script),
|
||||||
|
Scopes: transaction.None,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Account: bc.GetNotaryContractScriptHash(),
|
||||||
|
Scopes: transaction.None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mainTx.Attributes = []transaction.Attribute{
|
||||||
|
{
|
||||||
|
Type: transaction.NotaryAssistedT,
|
||||||
|
Value: &transaction.NotaryAssisted{NKeys: uint8(len(requesters))},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
payloads := make([]*payload.P2PNotaryRequest, len(requesters))
|
||||||
|
// we'll collect only m signatures out of n (so only m payloads are needed), but let's create payloads for all requesters (for the next tests)
|
||||||
|
for i := range payloads {
|
||||||
|
cp := mainTx
|
||||||
|
main := &cp
|
||||||
|
main.Scripts = []transaction.Witness{
|
||||||
|
{
|
||||||
|
InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, requesters[i].PrivateKey().Sign(main.GetSignedPart())...),
|
||||||
|
VerificationScript: script,
|
||||||
|
},
|
||||||
|
{}, // empty Notary witness
|
||||||
|
}
|
||||||
|
fallback := createFallbackTx(requesters[i], main)
|
||||||
|
payloads[i] = &payload.P2PNotaryRequest{
|
||||||
|
MainTransaction: main,
|
||||||
|
FallbackTransaction: fallback,
|
||||||
|
Network: netmode.UnitTestNet,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return payloads
|
||||||
|
}
|
||||||
|
checkSigTx := func(t *testing.T, requests []*payload.P2PNotaryRequest, sentCount int, shouldComplete bool) {
|
||||||
|
nKeys := len(requests)
|
||||||
|
completedTx := completedTxes[requests[0].MainTransaction.Hash()]
|
||||||
|
if sentCount == nKeys && shouldComplete {
|
||||||
|
require.NotNil(t, completedTx, errors.New("main transaction expected to be completed"))
|
||||||
|
require.Equal(t, nKeys+1, len(completedTx.Signers))
|
||||||
|
require.Equal(t, nKeys+1, len(completedTx.Scripts))
|
||||||
|
interopCtx := bc.newInteropContext(trigger.Verification, bc.dao, nil, completedTx)
|
||||||
|
for i, req := range requests {
|
||||||
|
require.Equal(t, req.MainTransaction.Scripts[i], completedTx.Scripts[i])
|
||||||
|
_, err := bc.verifyHashAgainstScript(completedTx.Signers[i].Account, &completedTx.Scripts[i], interopCtx, -1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
require.Equal(t, transaction.Witness{
|
||||||
|
InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, acc1.PrivateKey().Sign(requests[0].MainTransaction.GetSignedPart())...),
|
||||||
|
VerificationScript: []byte{},
|
||||||
|
}, completedTx.Scripts[nKeys])
|
||||||
|
} else {
|
||||||
|
require.Nil(t, completedTx, fmt.Errorf("main transaction shouldn't be completed: sent %d out of %d requests", sentCount, nKeys))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
checkMultisigTx := func(t *testing.T, nSigs int, requests []*payload.P2PNotaryRequest, sentCount int, shouldComplete bool) {
|
||||||
|
completedTx := completedTxes[requests[0].MainTransaction.Hash()]
|
||||||
|
if sentCount >= nSigs && shouldComplete {
|
||||||
|
require.NotNil(t, completedTx, errors.New("main transaction expected to be completed"))
|
||||||
|
require.Equal(t, 2, len(completedTx.Signers))
|
||||||
|
require.Equal(t, 2, len(completedTx.Scripts))
|
||||||
|
interopCtx := bc.newInteropContext(trigger.Verification, bc.dao, nil, completedTx)
|
||||||
|
_, err := bc.verifyHashAgainstScript(completedTx.Signers[0].Account, &completedTx.Scripts[0], interopCtx, -1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, transaction.Witness{
|
||||||
|
InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, acc1.PrivateKey().Sign(requests[0].MainTransaction.GetSignedPart())...),
|
||||||
|
VerificationScript: []byte{},
|
||||||
|
}, completedTx.Scripts[1])
|
||||||
|
// check that only nSigs out of nKeys signatures are presented in the invocation script
|
||||||
|
for i, req := range requests[:nSigs] {
|
||||||
|
require.True(t, bytes.Contains(completedTx.Scripts[0].InvocationScript, req.MainTransaction.Scripts[0].InvocationScript), fmt.Errorf("signature from extra request #%d shouldn't be presented in the main tx", i))
|
||||||
|
}
|
||||||
|
// the rest (nKeys-nSigs) out of nKeys shouldn't be presented in the invocation script
|
||||||
|
for i, req := range requests[nSigs:] {
|
||||||
|
require.False(t, bytes.Contains(completedTx.Scripts[0].InvocationScript, req.MainTransaction.Scripts[0].InvocationScript), fmt.Errorf("signature from extra request #%d shouldn't be presented in the main tx", i))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
require.Nil(t, completedTx, fmt.Errorf("main transaction shouldn't be completed: sent %d out of %d requests", sentCount, nSigs))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
checkFallbackTxs := func(t *testing.T, requests []*payload.P2PNotaryRequest, shouldComplete bool) {
|
||||||
|
for i, req := range requests {
|
||||||
|
completedTx := completedTxes[req.FallbackTransaction.Hash()]
|
||||||
|
if shouldComplete {
|
||||||
|
require.NotNil(t, completedTx, fmt.Errorf("fallback transaction for request #%d expected to be completed", i))
|
||||||
|
require.Equal(t, 2, len(completedTx.Signers))
|
||||||
|
require.Equal(t, 2, len(completedTx.Scripts))
|
||||||
|
require.Equal(t, transaction.Witness{
|
||||||
|
InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, acc1.PrivateKey().Sign(req.FallbackTransaction.GetSignedPart())...),
|
||||||
|
VerificationScript: []byte{},
|
||||||
|
}, completedTx.Scripts[0])
|
||||||
|
interopCtx := bc.newInteropContext(trigger.Verification, bc.dao, nil, completedTx)
|
||||||
|
_, err := bc.verifyHashAgainstScript(completedTx.Signers[1].Account, &completedTx.Scripts[1], interopCtx, -1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
} else {
|
||||||
|
require.Nil(t, completedTx, fmt.Errorf("fallback transaction for request #%d shouldn't be completed", i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
checkCompleteStandardRequest := func(t *testing.T, nKeys int, shouldComplete bool, nvbIncrements ...uint32) []*payload.P2PNotaryRequest {
|
||||||
|
requesters := make([]*wallet.Account, nKeys)
|
||||||
|
for i := range requesters {
|
||||||
|
requesters[i], _ = wallet.NewAccount()
|
||||||
|
}
|
||||||
|
|
||||||
|
requests := createStandardRequest(requesters, nvbIncrements...)
|
||||||
|
sendOrder := make([]int, nKeys)
|
||||||
|
for i := range sendOrder {
|
||||||
|
sendOrder[i] = i
|
||||||
|
}
|
||||||
|
rand.Shuffle(nKeys, func(i, j int) {
|
||||||
|
sendOrder[j], sendOrder[i] = sendOrder[i], sendOrder[j]
|
||||||
|
})
|
||||||
|
for i := range requests {
|
||||||
|
ntr1.OnNewRequest(requests[sendOrder[i]])
|
||||||
|
checkSigTx(t, requests, i+1, shouldComplete)
|
||||||
|
completedCount := len(completedTxes)
|
||||||
|
|
||||||
|
// check that the same request won't be processed twice
|
||||||
|
ntr1.OnNewRequest(requests[sendOrder[i]])
|
||||||
|
checkSigTx(t, requests, i+1, shouldComplete)
|
||||||
|
require.Equal(t, completedCount, len(completedTxes))
|
||||||
|
}
|
||||||
|
return requests
|
||||||
|
}
|
||||||
|
checkCompleteMultisigRequest := func(t *testing.T, nSigs int, nKeys int, shouldComplete bool) []*payload.P2PNotaryRequest {
|
||||||
|
requesters := make([]*wallet.Account, nKeys)
|
||||||
|
for i := range requesters {
|
||||||
|
requesters[i], _ = wallet.NewAccount()
|
||||||
|
}
|
||||||
|
requests := createMultisigRequest(nSigs, requesters)
|
||||||
|
sendOrder := make([]int, nKeys)
|
||||||
|
for i := range sendOrder {
|
||||||
|
sendOrder[i] = i
|
||||||
|
}
|
||||||
|
rand.Shuffle(nKeys, func(i, j int) {
|
||||||
|
sendOrder[j], sendOrder[i] = sendOrder[i], sendOrder[j]
|
||||||
|
})
|
||||||
|
|
||||||
|
var submittedRequests []*payload.P2PNotaryRequest
|
||||||
|
// sent only nSigs (m out of n) requests - it should be enough to complete min tx
|
||||||
|
for i := 0; i < nSigs; i++ {
|
||||||
|
submittedRequests = append(submittedRequests, requests[sendOrder[i]])
|
||||||
|
|
||||||
|
ntr1.OnNewRequest(requests[sendOrder[i]])
|
||||||
|
checkMultisigTx(t, nSigs, submittedRequests, i+1, shouldComplete)
|
||||||
|
|
||||||
|
// check that the same request won't be processed twice
|
||||||
|
ntr1.OnNewRequest(requests[sendOrder[i]])
|
||||||
|
checkMultisigTx(t, nSigs, submittedRequests, i+1, shouldComplete)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sent the rest (n-m) out of n requests: main tx is already collected, so only fallbacks should be applied
|
||||||
|
completedCount := len(completedTxes)
|
||||||
|
for i := nSigs; i < nKeys; i++ {
|
||||||
|
submittedRequests = append(submittedRequests, requests[sendOrder[i]])
|
||||||
|
|
||||||
|
ntr1.OnNewRequest(requests[sendOrder[i]])
|
||||||
|
checkMultisigTx(t, nSigs, submittedRequests, i+1, shouldComplete)
|
||||||
|
require.Equal(t, completedCount, len(completedTxes))
|
||||||
|
}
|
||||||
|
|
||||||
|
return submittedRequests
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnNewRequest: missing account
|
||||||
|
ntr1.UpdateNotaryNodes(keys.PublicKeys{randomAcc.PublicKey()})
|
||||||
|
r := checkCompleteStandardRequest(t, 1, false)
|
||||||
|
checkFallbackTxs(t, r, false)
|
||||||
|
// set account back for the next tests
|
||||||
|
ntr1.UpdateNotaryNodes(keys.PublicKeys{acc1.PrivateKey().PublicKey()})
|
||||||
|
|
||||||
|
// OnNewRequest: signature request
|
||||||
|
for _, i := range []int{1, 2, 3, 10} {
|
||||||
|
r := checkCompleteStandardRequest(t, i, true)
|
||||||
|
checkFallbackTxs(t, r, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnNewRequest: multisignature request
|
||||||
|
r = checkCompleteMultisigRequest(t, 1, 1, true)
|
||||||
|
checkFallbackTxs(t, r, false)
|
||||||
|
r = checkCompleteMultisigRequest(t, 1, 2, true)
|
||||||
|
checkFallbackTxs(t, r, false)
|
||||||
|
r = checkCompleteMultisigRequest(t, 1, 3, true)
|
||||||
|
checkFallbackTxs(t, r, false)
|
||||||
|
r = checkCompleteMultisigRequest(t, 3, 3, true)
|
||||||
|
checkFallbackTxs(t, r, false)
|
||||||
|
r = checkCompleteMultisigRequest(t, 3, 4, true)
|
||||||
|
checkFallbackTxs(t, r, false)
|
||||||
|
r = checkCompleteMultisigRequest(t, 3, 10, true)
|
||||||
|
checkFallbackTxs(t, r, false)
|
||||||
|
|
||||||
|
// PostPersist: missing account
|
||||||
|
finalizeWithError = true
|
||||||
|
r = checkCompleteStandardRequest(t, 1, false)
|
||||||
|
checkFallbackTxs(t, r, false)
|
||||||
|
finalizeWithError = false
|
||||||
|
ntr1.UpdateNotaryNodes(keys.PublicKeys{randomAcc.PublicKey()})
|
||||||
|
require.NoError(t, bc.AddBlock(bc.newBlock()))
|
||||||
|
checkSigTx(t, r, 1, false)
|
||||||
|
checkFallbackTxs(t, r, false)
|
||||||
|
// set account back for the next tests
|
||||||
|
ntr1.UpdateNotaryNodes(keys.PublicKeys{acc1.PrivateKey().PublicKey()})
|
||||||
|
|
||||||
|
// PostPersist: complete main transaction, signature request
|
||||||
|
finalizeWithError = true
|
||||||
|
requests := checkCompleteStandardRequest(t, 3, false)
|
||||||
|
// check PostPersist with finalisation error
|
||||||
|
finalizeWithError = true
|
||||||
|
require.NoError(t, bc.AddBlock(bc.newBlock()))
|
||||||
|
checkSigTx(t, requests, len(requests), false)
|
||||||
|
// check PostPersist without finalisation error
|
||||||
|
finalizeWithError = false
|
||||||
|
require.NoError(t, bc.AddBlock(bc.newBlock()))
|
||||||
|
checkSigTx(t, requests, len(requests), true)
|
||||||
|
|
||||||
|
// PostPersist: complete main transaction, multisignature account
|
||||||
|
finalizeWithError = true
|
||||||
|
requests = checkCompleteMultisigRequest(t, 3, 4, false)
|
||||||
|
checkFallbackTxs(t, requests, false)
|
||||||
|
// check PostPersist with finalisation error
|
||||||
|
finalizeWithError = true
|
||||||
|
require.NoError(t, bc.AddBlock(bc.newBlock()))
|
||||||
|
checkMultisigTx(t, 3, requests, len(requests), false)
|
||||||
|
checkFallbackTxs(t, requests, false)
|
||||||
|
// check PostPersist without finalisation error
|
||||||
|
finalizeWithError = false
|
||||||
|
require.NoError(t, bc.AddBlock(bc.newBlock()))
|
||||||
|
checkMultisigTx(t, 3, requests, len(requests), true)
|
||||||
|
checkFallbackTxs(t, requests, false)
|
||||||
|
|
||||||
|
// PostPersist: complete fallback, signature request
|
||||||
|
finalizeWithError = true
|
||||||
|
requests = checkCompleteStandardRequest(t, 3, false)
|
||||||
|
checkFallbackTxs(t, requests, false)
|
||||||
|
// make fallbacks valid
|
||||||
|
_, err = bc.genBlocks(int(nvbDiffFallback))
|
||||||
|
require.NoError(t, err)
|
||||||
|
// check PostPersist for valid fallbacks with finalisation error
|
||||||
|
require.NoError(t, bc.AddBlock(bc.newBlock()))
|
||||||
|
checkSigTx(t, requests, len(requests), false)
|
||||||
|
checkFallbackTxs(t, requests, false)
|
||||||
|
// check PostPersist for valid fallbacks without finalisation error
|
||||||
|
finalizeWithError = false
|
||||||
|
require.NoError(t, bc.AddBlock(bc.newBlock()))
|
||||||
|
checkSigTx(t, requests, len(requests), false)
|
||||||
|
checkFallbackTxs(t, requests, true)
|
||||||
|
|
||||||
|
// PostPersist: complete fallback, multisignature request
|
||||||
|
nSigs, nKeys := 3, 5
|
||||||
|
// check OnNewRequest with finalization error
|
||||||
|
finalizeWithError = true
|
||||||
|
requests = checkCompleteMultisigRequest(t, nSigs, nKeys, false)
|
||||||
|
checkFallbackTxs(t, requests, false)
|
||||||
|
// make fallbacks valid
|
||||||
|
_, err = bc.genBlocks(int(nvbDiffFallback))
|
||||||
|
require.NoError(t, err)
|
||||||
|
// check PostPersist for valid fallbacks with finalisation error
|
||||||
|
require.NoError(t, bc.AddBlock(bc.newBlock()))
|
||||||
|
checkMultisigTx(t, nSigs, requests, len(requests), false)
|
||||||
|
checkFallbackTxs(t, requests, false)
|
||||||
|
// check PostPersist for valid fallbacks without finalisation error
|
||||||
|
finalizeWithError = false
|
||||||
|
require.NoError(t, bc.AddBlock(bc.newBlock()))
|
||||||
|
checkMultisigTx(t, nSigs, requests, len(requests), false)
|
||||||
|
checkFallbackTxs(t, requests[:nSigs], true)
|
||||||
|
// the rest of fallbacks should also be applied even if the main tx was already constructed by the moment they were sent
|
||||||
|
checkFallbackTxs(t, requests[nSigs:], true)
|
||||||
|
|
||||||
|
// PostPersist: partial fallbacks completion due to finalisation errors
|
||||||
|
finalizeWithError = true
|
||||||
|
requests = checkCompleteStandardRequest(t, 5, false)
|
||||||
|
checkFallbackTxs(t, requests, false)
|
||||||
|
// make fallbacks valid
|
||||||
|
_, err = bc.genBlocks(int(nvbDiffFallback))
|
||||||
|
require.NoError(t, err)
|
||||||
|
// some of fallbacks should fail finalisation
|
||||||
|
unluckies = []*payload.P2PNotaryRequest{requests[0], requests[4]}
|
||||||
|
lucky := requests[1:4]
|
||||||
|
choosy = true
|
||||||
|
// check PostPersist for lucky fallbacks
|
||||||
|
require.NoError(t, bc.AddBlock(bc.newBlock()))
|
||||||
|
checkSigTx(t, requests, len(requests), false)
|
||||||
|
checkFallbackTxs(t, lucky, true)
|
||||||
|
checkFallbackTxs(t, unluckies, false)
|
||||||
|
// reset finalisation function for unlucky fallbacks to finalise without an error
|
||||||
|
choosy = false
|
||||||
|
finalizeWithError = false
|
||||||
|
// check PostPersist for unlucky fallbacks
|
||||||
|
require.NoError(t, bc.AddBlock(bc.newBlock()))
|
||||||
|
checkSigTx(t, requests, len(requests), false)
|
||||||
|
checkFallbackTxs(t, lucky, true)
|
||||||
|
checkFallbackTxs(t, unluckies, true)
|
||||||
|
|
||||||
|
// PostPersist: different NVBs
|
||||||
|
// check OnNewRequest with finalization error and different NVBs
|
||||||
|
finalizeWithError = true
|
||||||
|
requests = checkCompleteStandardRequest(t, 5, false, 1, 2, 3, 4, 5)
|
||||||
|
checkFallbackTxs(t, requests, false)
|
||||||
|
// generate blocks to reach the most earlier fallback's NVB
|
||||||
|
_, err = bc.genBlocks(int(nvbDiffFallback))
|
||||||
|
require.NoError(t, err)
|
||||||
|
// check PostPersist for valid fallbacks without finalisation error
|
||||||
|
finalizeWithError = false
|
||||||
|
for i := range requests {
|
||||||
|
require.NoError(t, bc.AddBlock(bc.newBlock()))
|
||||||
|
checkSigTx(t, requests, len(requests), false)
|
||||||
|
checkFallbackTxs(t, requests[:i+1], true)
|
||||||
|
checkFallbackTxs(t, requests[i+1:], false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnRequestRemoval: missing account
|
||||||
|
// check OnNewRequest with finalization error
|
||||||
|
finalizeWithError = true
|
||||||
|
requests = checkCompleteStandardRequest(t, 4, false)
|
||||||
|
checkFallbackTxs(t, requests, false)
|
||||||
|
// make fallbacks valid and remove one fallback
|
||||||
|
_, err = bc.genBlocks(int(nvbDiffFallback))
|
||||||
|
require.NoError(t, err)
|
||||||
|
ntr1.UpdateNotaryNodes(keys.PublicKeys{randomAcc.PublicKey()})
|
||||||
|
ntr1.OnRequestRemoval(requests[3])
|
||||||
|
// non of the fallbacks should be completed
|
||||||
|
finalizeWithError = false
|
||||||
|
require.NoError(t, bc.AddBlock(bc.newBlock()))
|
||||||
|
checkSigTx(t, requests, len(requests), false)
|
||||||
|
checkFallbackTxs(t, requests, false)
|
||||||
|
// set account back for the next tests
|
||||||
|
ntr1.UpdateNotaryNodes(keys.PublicKeys{acc1.PrivateKey().PublicKey()})
|
||||||
|
|
||||||
|
// OnRequestRemoval: signature request, remove one fallback
|
||||||
|
// check OnNewRequest with finalization error
|
||||||
|
finalizeWithError = true
|
||||||
|
requests = checkCompleteStandardRequest(t, 4, false)
|
||||||
|
checkFallbackTxs(t, requests, false)
|
||||||
|
// make fallbacks valid and remove one fallback
|
||||||
|
_, err = bc.genBlocks(int(nvbDiffFallback))
|
||||||
|
require.NoError(t, err)
|
||||||
|
unlucky := requests[3]
|
||||||
|
ntr1.OnRequestRemoval(unlucky)
|
||||||
|
// rest of the fallbacks should be completed
|
||||||
|
finalizeWithError = false
|
||||||
|
require.NoError(t, bc.AddBlock(bc.newBlock()))
|
||||||
|
checkSigTx(t, requests, len(requests), false)
|
||||||
|
checkFallbackTxs(t, requests[:3], true)
|
||||||
|
require.Nil(t, completedTxes[unlucky.FallbackTransaction.Hash()])
|
||||||
|
|
||||||
|
// OnRequestRemoval: signature request, remove all fallbacks
|
||||||
|
finalizeWithError = true
|
||||||
|
requests = checkCompleteStandardRequest(t, 4, false)
|
||||||
|
// remove all fallbacks
|
||||||
|
_, err = bc.genBlocks(int(nvbDiffFallback))
|
||||||
|
require.NoError(t, err)
|
||||||
|
for i := range requests {
|
||||||
|
ntr1.OnRequestRemoval(requests[i])
|
||||||
|
}
|
||||||
|
// then the whole request should be removed, i.e. there are no completed transactions
|
||||||
|
finalizeWithError = false
|
||||||
|
require.NoError(t, bc.AddBlock(bc.newBlock()))
|
||||||
|
checkSigTx(t, requests, len(requests), false)
|
||||||
|
checkFallbackTxs(t, requests, false)
|
||||||
|
|
||||||
|
// OnRequestRemoval: signature request, remove unexisting fallback
|
||||||
|
ntr1.OnRequestRemoval(requests[0])
|
||||||
|
require.NoError(t, bc.AddBlock(bc.newBlock()))
|
||||||
|
checkSigTx(t, requests, len(requests), false)
|
||||||
|
checkFallbackTxs(t, requests, false)
|
||||||
|
|
||||||
|
// OnRequestRemoval: multisignature request, remove one fallback
|
||||||
|
nSigs, nKeys = 3, 5
|
||||||
|
// check OnNewRequest with finalization error
|
||||||
|
finalizeWithError = true
|
||||||
|
requests = checkCompleteMultisigRequest(t, nSigs, nKeys, false)
|
||||||
|
checkMultisigTx(t, nSigs, requests, len(requests), false)
|
||||||
|
checkFallbackTxs(t, requests, false)
|
||||||
|
// make fallbacks valid and remove the last fallback
|
||||||
|
_, err = bc.genBlocks(int(nvbDiffFallback))
|
||||||
|
require.NoError(t, err)
|
||||||
|
unlucky = requests[nSigs-1]
|
||||||
|
ntr1.OnRequestRemoval(unlucky)
|
||||||
|
// then (m-1) out of n fallbacks should be completed
|
||||||
|
finalizeWithError = false
|
||||||
|
require.NoError(t, bc.AddBlock(bc.newBlock()))
|
||||||
|
checkMultisigTx(t, nSigs, requests, len(requests), false)
|
||||||
|
checkFallbackTxs(t, requests[:nSigs-1], true)
|
||||||
|
require.Nil(t, completedTxes[unlucky.FallbackTransaction.Hash()])
|
||||||
|
// the rest (n-(m-1)) out of n fallbacks should also be completed even if main tx has been collected by the moment they were sent
|
||||||
|
checkFallbackTxs(t, requests[nSigs:], true)
|
||||||
|
|
||||||
|
// OnRequestRemoval: multisignature request, remove all fallbacks
|
||||||
|
finalizeWithError = true
|
||||||
|
requests = checkCompleteMultisigRequest(t, nSigs, nKeys, false)
|
||||||
|
// make fallbacks valid and then remove all of them
|
||||||
|
_, err = bc.genBlocks(int(nvbDiffFallback))
|
||||||
|
require.NoError(t, err)
|
||||||
|
for i := range requests {
|
||||||
|
ntr1.OnRequestRemoval(requests[i])
|
||||||
|
}
|
||||||
|
// then the whole request should be removed, i.e. there are no completed transactions
|
||||||
|
finalizeWithError = false
|
||||||
|
require.NoError(t, bc.AddBlock(bc.newBlock()))
|
||||||
|
checkMultisigTx(t, nSigs, requests, len(requests), false)
|
||||||
|
checkFallbackTxs(t, requests, false)
|
||||||
|
|
||||||
|
// // OnRequestRemoval: multisignature request, remove unexisting fallbac, i.e. there still shouldn't be any completed transactions after this
|
||||||
|
ntr1.OnRequestRemoval(requests[0])
|
||||||
|
require.NoError(t, bc.AddBlock(bc.newBlock()))
|
||||||
|
checkMultisigTx(t, nSigs, requests, len(requests), false)
|
||||||
|
checkFallbackTxs(t, requests, false)
|
||||||
|
|
||||||
|
// Subscriptions test
|
||||||
|
mp1.RunSubscriptions()
|
||||||
|
go ntr1.Run()
|
||||||
|
defer func() {
|
||||||
|
ntr1.Stop()
|
||||||
|
mp1.StopSubscriptions()
|
||||||
|
}()
|
||||||
|
finalizeWithError = false
|
||||||
|
requester1, _ := wallet.NewAccount()
|
||||||
|
requester2, _ := wallet.NewAccount()
|
||||||
|
amount := int64(100_0000_0000)
|
||||||
|
feer := NewNotaryFeerStub(bc)
|
||||||
|
transferTokenFromMultisigAccountCheckOK(t, bc, bc.GetNotaryContractScriptHash(), bc.contracts.GAS.Hash, amount, requester1.PrivateKey().PublicKey().GetScriptHash(), int64(bc.BlockHeight()+50))
|
||||||
|
checkBalanceOf(t, bc, bc.contracts.Notary.Hash, int(amount))
|
||||||
|
transferTokenFromMultisigAccountCheckOK(t, bc, bc.GetNotaryContractScriptHash(), bc.contracts.GAS.Hash, amount, requester2.PrivateKey().PublicKey().GetScriptHash(), int64(bc.BlockHeight()+50))
|
||||||
|
checkBalanceOf(t, bc, bc.contracts.Notary.Hash, int(2*amount))
|
||||||
|
// create request for 2 standard signatures => main tx should be completed after the second request is added to the pool
|
||||||
|
requests = createStandardRequest([]*wallet.Account{requester1, requester2})
|
||||||
|
require.NoError(t, mp1.Add(requests[0].FallbackTransaction, feer, requests[0]))
|
||||||
|
require.NoError(t, mp1.Add(requests[1].FallbackTransaction, feer, requests[1]))
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
mtx.RLock()
|
||||||
|
defer mtx.RUnlock()
|
||||||
|
return completedTxes[requests[0].MainTransaction.Hash()] != nil
|
||||||
|
}, time.Second, time.Millisecond)
|
||||||
|
checkFallbackTxs(t, requests, false)
|
||||||
|
}
|
71
pkg/services/notary/node_test.go
Normal file
71
pkg/services/notary/node_test.go
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
package notary
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/nspcc-dev/neo-go/internal/fakechain"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/config"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/core/blockchainer"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/core/mempool"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.uber.org/zap/zaptest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getTestNotary(t *testing.T, bc blockchainer.Blockchainer, walletPath, pass string) (*wallet.Account, *Notary, *mempool.Pool) {
|
||||||
|
bc.(*fakechain.FakeChain).ProtocolConfiguration.P2PNotary = config.P2PNotary{
|
||||||
|
Enabled: true,
|
||||||
|
UnlockWallet: config.Wallet{
|
||||||
|
Path: walletPath,
|
||||||
|
Password: pass,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mp := mempool.New(10, 1, true)
|
||||||
|
ntr, err := NewNotary(bc, mp, zaptest.NewLogger(t), nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
w, err := wallet.NewWalletFromFile(walletPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, w.Accounts[0].Decrypt(pass))
|
||||||
|
return w.Accounts[0], ntr, mp
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateNotaryNodes(t *testing.T) {
|
||||||
|
bc := fakechain.NewFakeChain()
|
||||||
|
acc, ntr, _ := getTestNotary(t, bc, "./testdata/notary1.json", "one")
|
||||||
|
randomKey, err := keys.NewPrivateKey()
|
||||||
|
require.NoError(t, err)
|
||||||
|
// currAcc is nil before UpdateNotaryNodes call
|
||||||
|
require.Nil(t, ntr.currAccount)
|
||||||
|
// set account for the first time
|
||||||
|
ntr.UpdateNotaryNodes(keys.PublicKeys{acc.PrivateKey().PublicKey()})
|
||||||
|
require.Equal(t, acc, ntr.currAccount)
|
||||||
|
|
||||||
|
t.Run("account is already set", func(t *testing.T) {
|
||||||
|
ntr.UpdateNotaryNodes(keys.PublicKeys{acc.PrivateKey().PublicKey(), randomKey.PublicKey()})
|
||||||
|
require.Equal(t, acc, ntr.currAccount)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("another account from the same wallet", func(t *testing.T) {
|
||||||
|
t.Run("good config password", func(t *testing.T) {
|
||||||
|
w, err := wallet.NewWalletFromFile("./testdata/notary1.json")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, w.Accounts[1].Decrypt("one"))
|
||||||
|
ntr.UpdateNotaryNodes(keys.PublicKeys{w.Accounts[1].PrivateKey().PublicKey()})
|
||||||
|
require.Equal(t, w.Accounts[1], ntr.currAccount)
|
||||||
|
})
|
||||||
|
t.Run("bad config password", func(t *testing.T) {
|
||||||
|
w, err := wallet.NewWalletFromFile("./testdata/notary1.json")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, w.Accounts[2].Decrypt("four"))
|
||||||
|
ntr.UpdateNotaryNodes(keys.PublicKeys{w.Accounts[2].PrivateKey().PublicKey()})
|
||||||
|
require.Nil(t, ntr.currAccount)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("unknown account", func(t *testing.T) {
|
||||||
|
ntr.UpdateNotaryNodes(keys.PublicKeys{randomKey.PublicKey()})
|
||||||
|
require.Nil(t, ntr.currAccount)
|
||||||
|
})
|
||||||
|
}
|
|
@ -300,7 +300,7 @@ func (n *Notary) PostPersist(bc blockchainer.Blockchainer, pool *mempool.Pool, b
|
||||||
func (n *Notary) finalize(tx *transaction.Transaction) error {
|
func (n *Notary) finalize(tx *transaction.Transaction) error {
|
||||||
acc := n.getAccount()
|
acc := n.getAccount()
|
||||||
if acc == nil {
|
if acc == nil {
|
||||||
panic(errors.New("no available Notary account"))
|
panic(errors.New("no available Notary account")) // unreachable code, because all callers of `finalize` check that acc != nil
|
||||||
}
|
}
|
||||||
notaryWitness := transaction.Witness{
|
notaryWitness := transaction.Witness{
|
||||||
InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, acc.PrivateKey().Sign(tx.GetSignedPart())...),
|
InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, acc.PrivateKey().Sign(tx.GetSignedPart())...),
|
||||||
|
|
427
pkg/services/notary/notary_test.go
Normal file
427
pkg/services/notary/notary_test.go
Normal file
|
@ -0,0 +1,427 @@
|
||||||
|
package notary
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/nspcc-dev/neo-go/internal/fakechain"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/config"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/core/mempool"
|
||||||
|
"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/util"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.uber.org/zap/zaptest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWallet(t *testing.T) {
|
||||||
|
bc := fakechain.NewFakeChain()
|
||||||
|
|
||||||
|
t.Run("unexisting wallet", func(t *testing.T) {
|
||||||
|
bc.ProtocolConfiguration.P2PNotary = config.P2PNotary{
|
||||||
|
Enabled: true,
|
||||||
|
UnlockWallet: config.Wallet{
|
||||||
|
Path: "./testdata/does_not_exists.json",
|
||||||
|
Password: "one",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, err := NewNotary(bc, mempool.New(1, 1, true), zaptest.NewLogger(t), nil)
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("bad password", func(t *testing.T) {
|
||||||
|
bc.ProtocolConfiguration.P2PNotary = config.P2PNotary{
|
||||||
|
Enabled: true,
|
||||||
|
UnlockWallet: config.Wallet{
|
||||||
|
Path: "./testdata/notary1.json",
|
||||||
|
Password: "invalid",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, err := NewNotary(bc, mempool.New(1, 1, true), zaptest.NewLogger(t), nil)
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("good", func(t *testing.T) {
|
||||||
|
bc.ProtocolConfiguration.P2PNotary = config.P2PNotary{
|
||||||
|
Enabled: true,
|
||||||
|
UnlockWallet: config.Wallet{
|
||||||
|
Path: "./testdata/notary1.json",
|
||||||
|
Password: "one",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, err := NewNotary(bc, mempool.New(1, 1, true), zaptest.NewLogger(t), nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVerifyIncompleteRequest(t *testing.T) {
|
||||||
|
bc := fakechain.NewFakeChain()
|
||||||
|
notaryContractHash := util.Uint160{1, 2, 3}
|
||||||
|
bc.NotaryContractScriptHash = notaryContractHash
|
||||||
|
_, ntr, _ := getTestNotary(t, bc, "./testdata/notary1.json", "one")
|
||||||
|
sig := append([]byte{byte(opcode.PUSHDATA1), 64}, make([]byte, 64)...) // we're not interested in signature correctness
|
||||||
|
acc1, _ := keys.NewPrivateKey()
|
||||||
|
acc2, _ := keys.NewPrivateKey()
|
||||||
|
acc3, _ := keys.NewPrivateKey()
|
||||||
|
sigScript1 := acc1.PublicKey().GetVerificationScript()
|
||||||
|
sigScript2 := acc2.PublicKey().GetVerificationScript()
|
||||||
|
sigScript3 := acc3.PublicKey().GetVerificationScript()
|
||||||
|
multisigScript1, err := smartcontract.CreateMultiSigRedeemScript(1, keys.PublicKeys{acc1.PublicKey(), acc2.PublicKey(), acc3.PublicKey()})
|
||||||
|
require.NoError(t, err)
|
||||||
|
multisigScriptHash1 := hash.Hash160(multisigScript1)
|
||||||
|
multisigScript2, err := smartcontract.CreateMultiSigRedeemScript(2, keys.PublicKeys{acc1.PublicKey(), acc2.PublicKey(), acc3.PublicKey()})
|
||||||
|
require.NoError(t, err)
|
||||||
|
multisigScriptHash2 := hash.Hash160(multisigScript2)
|
||||||
|
|
||||||
|
checkErr := func(t *testing.T, tx *transaction.Transaction, nKeys uint8) {
|
||||||
|
typ, nSigs, pubs, err := ntr.verifyIncompleteWitnesses(tx, nKeys)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Equal(t, Unknown, typ)
|
||||||
|
require.Equal(t, uint8(0), nSigs)
|
||||||
|
require.Nil(t, pubs)
|
||||||
|
}
|
||||||
|
|
||||||
|
errCases := map[string]struct {
|
||||||
|
tx *transaction.Transaction
|
||||||
|
nKeys uint8
|
||||||
|
}{
|
||||||
|
"not enough signers": {
|
||||||
|
tx: &transaction.Transaction{
|
||||||
|
Signers: []transaction.Signer{{Account: notaryContractHash}},
|
||||||
|
Scripts: []transaction.Witness{{}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"signers count and witnesses count mismatch": {
|
||||||
|
tx: &transaction.Transaction{
|
||||||
|
Signers: []transaction.Signer{{Account: notaryContractHash}, {}},
|
||||||
|
Scripts: []transaction.Witness{{}, {}, {}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"missing Notary witness": {
|
||||||
|
tx: &transaction.Transaction{
|
||||||
|
Signers: []transaction.Signer{{Account: acc1.GetScriptHash()}, {Account: acc2.GetScriptHash()}},
|
||||||
|
Scripts: []transaction.Witness{{}, {}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"unknown witness type": {
|
||||||
|
tx: &transaction.Transaction{
|
||||||
|
Signers: []transaction.Signer{{Account: acc1.PublicKey().GetScriptHash()}, {Account: notaryContractHash}},
|
||||||
|
Scripts: []transaction.Witness{
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"bad verification script": {
|
||||||
|
tx: &transaction.Transaction{
|
||||||
|
Signers: []transaction.Signer{{Account: acc1.PublicKey().GetScriptHash()}, {Account: notaryContractHash}},
|
||||||
|
Scripts: []transaction.Witness{
|
||||||
|
{
|
||||||
|
InvocationScript: []byte{},
|
||||||
|
VerificationScript: []byte{1, 2, 3},
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"several multisig witnesses": {
|
||||||
|
tx: &transaction.Transaction{
|
||||||
|
Signers: []transaction.Signer{{Account: multisigScriptHash1}, {Account: multisigScriptHash2}, {Account: notaryContractHash}},
|
||||||
|
Scripts: []transaction.Witness{
|
||||||
|
{
|
||||||
|
InvocationScript: sig,
|
||||||
|
VerificationScript: multisigScript1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
InvocationScript: sig,
|
||||||
|
VerificationScript: multisigScript2,
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nKeys: 2,
|
||||||
|
},
|
||||||
|
"multisig + sig": {
|
||||||
|
tx: &transaction.Transaction{
|
||||||
|
Signers: []transaction.Signer{{Account: multisigScriptHash1}, {Account: acc1.PublicKey().GetScriptHash()}, {Account: notaryContractHash}},
|
||||||
|
Scripts: []transaction.Witness{
|
||||||
|
{
|
||||||
|
InvocationScript: sig,
|
||||||
|
VerificationScript: multisigScript1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
InvocationScript: sig,
|
||||||
|
VerificationScript: sigScript1,
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nKeys: 2,
|
||||||
|
},
|
||||||
|
"sig + multisig": {
|
||||||
|
tx: &transaction.Transaction{
|
||||||
|
Signers: []transaction.Signer{{Account: acc1.PublicKey().GetScriptHash()}, {Account: multisigScriptHash1}, {Account: notaryContractHash}},
|
||||||
|
Scripts: []transaction.Witness{
|
||||||
|
{
|
||||||
|
InvocationScript: sig,
|
||||||
|
VerificationScript: sigScript1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
InvocationScript: sig,
|
||||||
|
VerificationScript: multisigScript1,
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nKeys: 2,
|
||||||
|
},
|
||||||
|
"empty multisig + sig": {
|
||||||
|
tx: &transaction.Transaction{
|
||||||
|
Signers: []transaction.Signer{{Account: multisigScriptHash1}, {Account: acc1.PublicKey().GetScriptHash()}, {Account: notaryContractHash}},
|
||||||
|
Scripts: []transaction.Witness{
|
||||||
|
{
|
||||||
|
InvocationScript: []byte{},
|
||||||
|
VerificationScript: multisigScript1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
InvocationScript: sig,
|
||||||
|
VerificationScript: sigScript1,
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nKeys: 2,
|
||||||
|
},
|
||||||
|
"sig + empty multisig": {
|
||||||
|
tx: &transaction.Transaction{
|
||||||
|
Signers: []transaction.Signer{{Account: acc1.PublicKey().GetScriptHash()}, {Account: multisigScriptHash1}, {Account: notaryContractHash}},
|
||||||
|
Scripts: []transaction.Witness{
|
||||||
|
{
|
||||||
|
InvocationScript: sig,
|
||||||
|
VerificationScript: sigScript1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
InvocationScript: []byte{},
|
||||||
|
VerificationScript: multisigScript1,
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nKeys: 2,
|
||||||
|
},
|
||||||
|
"multisig + empty sig": {
|
||||||
|
tx: &transaction.Transaction{
|
||||||
|
Signers: []transaction.Signer{{Account: multisigScriptHash1}, {Account: acc1.PublicKey().GetScriptHash()}, {Account: notaryContractHash}},
|
||||||
|
Scripts: []transaction.Witness{
|
||||||
|
{
|
||||||
|
InvocationScript: sig,
|
||||||
|
VerificationScript: multisigScript1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
InvocationScript: []byte{},
|
||||||
|
VerificationScript: sigScript1,
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nKeys: 2,
|
||||||
|
},
|
||||||
|
"empty sig + multisig": {
|
||||||
|
tx: &transaction.Transaction{
|
||||||
|
Signers: []transaction.Signer{{Account: acc1.PublicKey().GetScriptHash()}, {Account: multisigScriptHash1}, {Account: notaryContractHash}},
|
||||||
|
Scripts: []transaction.Witness{
|
||||||
|
{
|
||||||
|
InvocationScript: []byte{},
|
||||||
|
VerificationScript: sigScript1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
InvocationScript: sig,
|
||||||
|
VerificationScript: multisigScript1,
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nKeys: 2,
|
||||||
|
},
|
||||||
|
"sig: bad nKeys": {
|
||||||
|
tx: &transaction.Transaction{
|
||||||
|
Signers: []transaction.Signer{{Account: acc1.PublicKey().GetScriptHash()}, {Account: acc2.PublicKey().GetScriptHash()}, {Account: notaryContractHash}},
|
||||||
|
Scripts: []transaction.Witness{
|
||||||
|
{
|
||||||
|
InvocationScript: sig,
|
||||||
|
VerificationScript: sigScript1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
InvocationScript: sig,
|
||||||
|
VerificationScript: sigScript2,
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nKeys: 3,
|
||||||
|
},
|
||||||
|
"multisig: bad witnesses count": {
|
||||||
|
tx: &transaction.Transaction{
|
||||||
|
Signers: []transaction.Signer{{Account: multisigScriptHash1}, {Account: notaryContractHash}},
|
||||||
|
Scripts: []transaction.Witness{
|
||||||
|
{
|
||||||
|
InvocationScript: sig,
|
||||||
|
VerificationScript: multisigScript1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nKeys: 2,
|
||||||
|
},
|
||||||
|
"multisig: bad nKeys": {
|
||||||
|
tx: &transaction.Transaction{
|
||||||
|
Signers: []transaction.Signer{{Account: multisigScriptHash1}, {Account: notaryContractHash}},
|
||||||
|
Scripts: []transaction.Witness{
|
||||||
|
{
|
||||||
|
InvocationScript: sig,
|
||||||
|
VerificationScript: multisigScript1,
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nKeys: 2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, errCase := range errCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
checkErr(t, errCase.tx, errCase.nKeys)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := map[string]struct {
|
||||||
|
tx *transaction.Transaction
|
||||||
|
nKeys uint8
|
||||||
|
expectedType RequestType
|
||||||
|
expectedNSigs uint8
|
||||||
|
expectedPubs keys.PublicKeys
|
||||||
|
}{
|
||||||
|
"single sig": {
|
||||||
|
tx: &transaction.Transaction{
|
||||||
|
Signers: []transaction.Signer{{Account: acc1.GetScriptHash()}, {Account: notaryContractHash}},
|
||||||
|
Scripts: []transaction.Witness{
|
||||||
|
{
|
||||||
|
InvocationScript: sig,
|
||||||
|
VerificationScript: sigScript1,
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nKeys: 1,
|
||||||
|
expectedType: Signature,
|
||||||
|
expectedNSigs: 1,
|
||||||
|
},
|
||||||
|
"multiple sig": {
|
||||||
|
tx: &transaction.Transaction{
|
||||||
|
Signers: []transaction.Signer{{Account: acc1.GetScriptHash()}, {Account: acc2.GetScriptHash()}, {Account: acc3.GetScriptHash()}, {Account: notaryContractHash}},
|
||||||
|
Scripts: []transaction.Witness{
|
||||||
|
{
|
||||||
|
InvocationScript: sig,
|
||||||
|
VerificationScript: sigScript1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
InvocationScript: []byte{},
|
||||||
|
VerificationScript: []byte{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
InvocationScript: sig,
|
||||||
|
VerificationScript: sigScript3,
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nKeys: 3,
|
||||||
|
expectedType: Signature,
|
||||||
|
expectedNSigs: 3,
|
||||||
|
},
|
||||||
|
"multisig 1 out of 3": {
|
||||||
|
tx: &transaction.Transaction{
|
||||||
|
Signers: []transaction.Signer{{Account: multisigScriptHash1}, {Account: notaryContractHash}},
|
||||||
|
Scripts: []transaction.Witness{
|
||||||
|
{
|
||||||
|
InvocationScript: sig,
|
||||||
|
VerificationScript: multisigScript1,
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nKeys: 3,
|
||||||
|
expectedType: MultiSignature,
|
||||||
|
expectedNSigs: 1,
|
||||||
|
expectedPubs: keys.PublicKeys{acc1.PublicKey(), acc2.PublicKey(), acc3.PublicKey()},
|
||||||
|
},
|
||||||
|
"multisig 2 out of 3": {
|
||||||
|
tx: &transaction.Transaction{
|
||||||
|
Signers: []transaction.Signer{{Account: multisigScriptHash2}, {Account: notaryContractHash}},
|
||||||
|
Scripts: []transaction.Witness{
|
||||||
|
{
|
||||||
|
InvocationScript: sig,
|
||||||
|
VerificationScript: multisigScript2,
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nKeys: 3,
|
||||||
|
expectedType: MultiSignature,
|
||||||
|
expectedNSigs: 2,
|
||||||
|
expectedPubs: keys.PublicKeys{acc1.PublicKey(), acc2.PublicKey(), acc3.PublicKey()},
|
||||||
|
},
|
||||||
|
"empty + multisig": {
|
||||||
|
tx: &transaction.Transaction{
|
||||||
|
Signers: []transaction.Signer{{Account: acc1.PublicKey().GetScriptHash()}, {Account: multisigScriptHash1}, {Account: notaryContractHash}},
|
||||||
|
Scripts: []transaction.Witness{
|
||||||
|
{
|
||||||
|
InvocationScript: []byte{},
|
||||||
|
VerificationScript: []byte{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
InvocationScript: sig,
|
||||||
|
VerificationScript: multisigScript1,
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nKeys: 3,
|
||||||
|
expectedType: MultiSignature,
|
||||||
|
expectedNSigs: 1,
|
||||||
|
expectedPubs: keys.PublicKeys{acc1.PublicKey(), acc2.PublicKey(), acc3.PublicKey()},
|
||||||
|
},
|
||||||
|
"multisig + empty": {
|
||||||
|
tx: &transaction.Transaction{
|
||||||
|
Signers: []transaction.Signer{{Account: multisigScriptHash1}, {Account: acc1.PublicKey().GetScriptHash()}, {Account: notaryContractHash}},
|
||||||
|
Scripts: []transaction.Witness{
|
||||||
|
{
|
||||||
|
InvocationScript: sig,
|
||||||
|
VerificationScript: multisigScript1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
InvocationScript: []byte{},
|
||||||
|
VerificationScript: []byte{},
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nKeys: 3,
|
||||||
|
expectedType: MultiSignature,
|
||||||
|
expectedNSigs: 1,
|
||||||
|
expectedPubs: keys.PublicKeys{acc1.PublicKey(), acc2.PublicKey(), acc3.PublicKey()},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, testCase := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
typ, nSigs, pubs, err := ntr.verifyIncompleteWitnesses(testCase.tx, testCase.nKeys)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, testCase.expectedType, typ)
|
||||||
|
assert.Equal(t, testCase.expectedNSigs, nSigs)
|
||||||
|
assert.ElementsMatch(t, testCase.expectedPubs, pubs)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
1
pkg/services/notary/testdata/notary1.json
vendored
Normal file
1
pkg/services/notary/testdata/notary1.json
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{"version":"3.0","accounts":[{"address":"NSbjd7dSePTZ6QpADAuM5722QpBmL5124W","key":"6PYVWTfkNCYvyQhyFLHH5dyRyT6jSi8u8Z8kn122PACfsDWi4QgkGm8FyW","label":"NotaryNode1","contract":{"script":"DCEDm5PmbOfVPmYXTSVW903XnOhhNBTsF9oDlVYusIH/ui0LQZVEDXg=","parameters":[{"name":"parameter0","type":"Signature"}],"deployed":false},"lock":false,"isdefault":false},{"address":"NisSvmSd2Lp28tjr8EqCZB5ahHDvBExo2j","key":"6PYLvgnZNwhiiZPiSCw3B3bHSFwbSXgh3MkGt4gL69MD8Sw7LMnuUgM9KQ","label":"three","contract":{"script":"DCEDHRWEIGXHCwUU2Fc7B0qrYPezXR0sfdEduRExyzIKVC8LQZVEDXg=","parameters":[{"name":"parameter0","type":"Signature"}],"deployed":false},"lock":false,"isdefault":false},{"address":"NRCCdGifyUWKnFotZhcgKpmxhhVJSJb94r","key":"6PYKXkuJ7G6bTj62bjy8fsBLF5okYNdAEBhKPCv8nmcALCtk2yPtBo835p","label":"four","contract":{"script":"DCECmUfs/gqKHd3AdJm5+Ev6zkubV8pP8DZzgu8+t5WdphILQZVEDXg=","parameters":[{"name":"parameter0","type":"Signature"}],"deployed":false},"lock":false,"isdefault":false}],"scrypt":{"n":16384,"r":8,"p":8},"extra":{"Tokens":null}}
|
1
pkg/services/notary/testdata/notary2.json
vendored
Normal file
1
pkg/services/notary/testdata/notary2.json
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{"version":"3.0","accounts":[{"address":"NfFcJvWcHe8SSS92hNZhyQUJ6cg3pb36Tf","key":"6PYU2QoD52Xt9Z6QmNGUJWn89qUD1W6QqAL4Y8nfTWtTKvmVpQh8wsH6qY","label":"NotaryNode2","contract":{"script":"DCECIcKj0GFdv4b1NZrw9X6zLNLWzmNKAxtw6olIMZxpPRQLQZVEDXg=","parameters":[{"name":"parameter0","type":"Signature"}],"deployed":false},"lock":false,"isdefault":false}],"scrypt":{"n":16384,"r":8,"p":8},"extra":{"Tokens":null}}
|
Loading…
Reference in a new issue