services: add ability to combine notary signers

Notes for witnesses:
* [N sig + M multisig + K contract] combination is possible where N, M, K >=0.
* Each verification script should be properly filled in.
* Each invocation script should either be empty or contain exactly one
signature.
This commit is contained in:
Anna Shaleva 2021-10-20 18:43:32 +03:00 committed by AnnaShaleva
parent fcc7f7349d
commit 75d7891ca1
5 changed files with 592 additions and 475 deletions

View file

@ -35,7 +35,9 @@ for this service. `FEE` is set to be 0.1 GAS.
We'll also use `NKeys` definition as the number of keys that participate in the
process of signature collection. This is the number of keys that could potentially
sign the transaction, for transactions lacking appropriate witnesses that would be
the number of witnesses, for "M out of N" multisignature scripts that's N.
the number of witnesses, for "M out of N" multisignature scripts that's N, for
combination of K standard signature witnesses and L multisignature "M out of N"
witnesses that's K+N*L.
### Transaction attributes
@ -107,11 +109,13 @@ This payload has two incomplete transactions inside:
than the current chain height and it must have `Conflicts` attribute with the
hash of the main transaction. It at the same time must have `Notary assisted`
attribute with a count of zero.
- *Main tx*. This is the one that actually needs to be completed, it either
doesn't have all witnesses attached (in this case none of them can be
multisignature), or it only has a partial multisignature, currenlty only one of
the two is allowed. This transaction must have `Notary assisted` attribute with
a count of `NKeys` (and Notary contract as one of the signers).
- *Main tx*. This is the one that actually needs to be completed, it:
1. *either* doesn't have all witnesses attached
2. *or* it only has a partial multisignature
3. *or* have not all witnesses attached and some of the rest are partial multisignature
This transaction must have `Notary assisted` attribute with a count of `NKeys`
(and Notary contract as one of the signers).
See the [Notary request submission guide](#2-request-submission) to learn how to
construct and send the payload.
@ -297,10 +301,8 @@ the steps to create a signature request:
* First signer is the one who pays transaction fees.
* Each signer is either multisignature or standard signature or a contract
signer.
* Multisignature and signature signers can't be combined.
* Multisignature and signature signers can be combined.
* Contract signer can be combined with any other signer.
* Maximum number of multisignature signers is 1.
* Maximum number of signature or contract signers is unlimited.
Include Notary native contract in the list of signers with the following
constraints:
@ -374,7 +376,8 @@ the steps to create a signature request:
9. Construct the list of main transactions witnesses (that will be `Scripts`
transaction field). Use the following rules:
- Contract-based witness should have `Invocation` script that pushes arguments
on stack (it may be empty) and empty `Verification` script.
on stack (it may be empty) and empty `Verification` script. Currently, **only
empty** `Invocation` scripts are supported for contract-based witnesses.
- **Notary contract witness** (which is also a contract-based witness) should
have empty `Verification` script. `Invocation` script should be of the form
[opcode.PUSHDATA1, 64, make([]byte, 64)...], i.e. to be a placeholder for
@ -388,8 +391,7 @@ the steps to create a signature request:
if `Invocation` script is to be collected from other notary requests.
`Invocation` script either should push on stack signature bytes (one
signature at max per one resuest) **or** (in case if there's no ability to
provide proper signature) **should be of the form [opcode.PUSHDATA1, 64,
make([]byte, 64)...]**, i.e. to be a placeholder for signature.
provide proper signature) **should be empty**.
10. Define lifetime for the fallback transaction. Let the `fallbackValidFor` be
the lifetime. Let `N` be the current chain's height and `VUB` be
`ValidUntilBlock` value estimated at the step 3. Then notary node is trying to

View file

@ -1,7 +1,6 @@
package core
import (
"bytes"
"errors"
"fmt"
"math/rand"
@ -107,7 +106,6 @@ func TestNotary(t *testing.T) {
defer mtx.RUnlock()
return completedTxes[h]
}
var completedTx *transaction.Transaction
require.Eventually(t, func() bool {
mtx.RLock()
@ -138,6 +136,11 @@ func TestNotary(t *testing.T) {
notaryNodes := keys.PublicKeys{acc1.PrivateKey().PublicKey(), acc2.PrivateKey().PublicKey()}
bc.setNodesByRole(t, true, noderoles.P2PNotary, notaryNodes)
type requester struct {
accounts []*wallet.Account
m int
typ notary.RequestType
}
createFallbackTx := func(requester *wallet.Account, mainTx *transaction.Transaction, nvbIncrement ...uint32) *transaction.Transaction {
fallback := transaction.New([]byte{byte(opcode.RET)}, 2000_0000)
fallback.Nonce = nonce
@ -182,19 +185,37 @@ func TestNotary(t *testing.T) {
require.NoError(t, err)
return fallback
}
createStandardRequest := func(requesters []*wallet.Account, NVBincrements ...uint32) []*payload.P2PNotaryRequest {
createMixedRequest := func(requesters []requester, NVBincrements ...uint32) []*payload.P2PNotaryRequest {
mainTx := *transaction.New([]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)
var (
nKeys uint8
verificationScripts [][]byte
)
for i := range requesters {
var script []byte
switch requesters[i].typ {
case notary.Signature:
script = requesters[i].accounts[0].PrivateKey().PublicKey().GetVerificationScript()
nKeys++
case notary.MultiSignature:
pubs := make(keys.PublicKeys, len(requesters[i].accounts))
for j, r := range requesters[i].accounts {
pubs[j] = r.PrivateKey().PublicKey()
}
script, err = smartcontract.CreateMultiSigRedeemScript(requesters[i].m, pubs)
require.NoError(t, err)
nKeys += uint8(len(requesters[i].accounts))
}
signers[i] = transaction.Signer{
Account: requesters[i].PrivateKey().PublicKey().GetScriptHash(),
Account: hash.Hash160(script),
Scopes: transaction.None,
}
verificationScripts = append(verificationScripts, script)
}
signers[len(signers)-1] = transaction.Signer{
Account: bc.GetNotaryContractScriptHash(),
@ -204,132 +225,73 @@ func TestNotary(t *testing.T) {
mainTx.Attributes = []transaction.Attribute{
{
Type: transaction.NotaryAssistedT,
Value: &transaction.NotaryAssisted{NKeys: uint8(len(requesters))},
Value: &transaction.NotaryAssisted{NKeys: nKeys},
},
}
payloads := make([]*payload.P2PNotaryRequest, len(requesters))
for i := range payloads {
payloads := make([]*payload.P2PNotaryRequest, nKeys)
plIndex := 0
// 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, r := range requesters {
for _, acc := range r.accounts {
cp := mainTx
main := &cp
scripts := make([]transaction.Witness, len(requesters)+1)
for j := range requesters {
scripts[j].VerificationScript = requesters[j].PrivateKey().PublicKey().GetVerificationScript()
main.Scripts = make([]transaction.Witness, len(requesters))
for j := range main.Scripts {
main.Scripts[j].VerificationScript = verificationScripts[j]
if i == j {
main.Scripts[j].InvocationScript = append([]byte{byte(opcode.PUSHDATA1), 64}, acc.PrivateKey().SignHashable(uint32(testchain.Network()), main)...)
}
scripts[i].InvocationScript = append([]byte{byte(opcode.PUSHDATA1), 64}, requesters[i].PrivateKey().SignHashable(uint32(testchain.Network()), main)...)
main.Scripts = scripts
}
main.Scripts = append(main.Scripts, transaction.Witness{}) // empty Notary witness
_ = main.Size() // for size update test
var fallback *transaction.Transaction
if len(NVBincrements) == len(requesters) {
fallback = createFallbackTx(requesters[i], main, NVBincrements[i])
if len(NVBincrements) == int(nKeys) {
fallback = createFallbackTx(acc, main, NVBincrements[plIndex])
} else {
fallback = createFallbackTx(requesters[i], main)
fallback = createFallbackTx(acc, main)
}
_ = fallback.Size() // for size update test
payloads[i] = &payload.P2PNotaryRequest{
payloads[plIndex] = &payload.P2PNotaryRequest{
MainTransaction: main,
FallbackTransaction: fallback,
}
plIndex++
}
}
return payloads
}
createMultisigRequest := func(m int, requesters []*wallet.Account) []*payload.P2PNotaryRequest {
mainTx := *transaction.New([]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().SignHashable(uint32(testchain.Network()), main)...),
VerificationScript: script,
},
{}, // empty Notary witness
}
fallback := createFallbackTx(requesters[i], main)
payloads[i] = &payload.P2PNotaryRequest{
MainTransaction: main,
FallbackTransaction: fallback,
checkMainTx := func(t *testing.T, requesters []requester, requests []*payload.P2PNotaryRequest, sentCount int, shouldComplete bool) {
nSigs := 0
for _, r := range requesters {
switch r.typ {
case notary.Signature:
nSigs++
case notary.MultiSignature:
nSigs += r.m
}
}
return payloads
}
checkSigTx := func(t *testing.T, requests []*payload.P2PNotaryRequest, sentCount int, shouldComplete bool) {
nKeys := len(requests)
if sentCount == nKeys && shouldComplete {
nSigners := len(requesters) + 1
if sentCount >= nSigs && shouldComplete {
completedTx := getCompletedTx(t, true, requests[0].MainTransaction.Hash())
require.Equal(t, nKeys+1, len(completedTx.Signers))
require.Equal(t, nKeys+1, len(completedTx.Scripts))
require.Equal(t, nSigners, len(completedTx.Signers))
require.Equal(t, nSigners, len(completedTx.Scripts))
// check that tx size was updated
require.Equal(t, io.GetVarSize(completedTx), completedTx.Size())
for i := 0; i < len(completedTx.Scripts)-1; i++ {
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().SignHashable(uint32(testchain.Network()), requests[0].MainTransaction)...),
VerificationScript: []byte{},
}, completedTx.Scripts[nKeys])
} else {
completedTx := getCompletedTx(t, false, requests[0].MainTransaction.Hash())
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) {
if sentCount >= nSigs && shouldComplete {
completedTx := getCompletedTx(t, true, requests[0].MainTransaction.Hash())
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().SignHashable(uint32(testchain.Network()), requests[0].MainTransaction)...),
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))
}
}, completedTx.Scripts[len(completedTx.Scripts)-1])
} else {
completedTx := getCompletedTx(t, false, requests[0].MainTransaction.Hash())
require.Nil(t, completedTx, fmt.Errorf("main transaction shouldn't be completed: sent %d out of %d requests", sentCount, nSigs))
@ -358,13 +320,17 @@ func TestNotary(t *testing.T) {
}
}
}
checkCompleteStandardRequest := func(t *testing.T, nKeys int, shouldComplete bool, nvbIncrements ...uint32) []*payload.P2PNotaryRequest {
requesters := make([]*wallet.Account, nKeys)
checkCompleteStandardRequest := func(t *testing.T, nKeys int, shouldComplete bool, nvbIncrements ...uint32) ([]*payload.P2PNotaryRequest, []requester) {
requesters := make([]requester, nKeys)
for i := range requesters {
requesters[i], _ = wallet.NewAccount()
acc, _ := wallet.NewAccount()
requesters[i] = requester{
accounts: []*wallet.Account{acc},
typ: notary.Signature,
}
}
requests := createStandardRequest(requesters, nvbIncrements...)
requests := createMixedRequest(requesters, nvbIncrements...)
sendOrder := make([]int, nKeys)
for i := range sendOrder {
sendOrder[i] = i
@ -374,22 +340,29 @@ func TestNotary(t *testing.T) {
})
for i := range requests {
ntr1.OnNewRequest(requests[sendOrder[i]])
checkSigTx(t, requests, i+1, shouldComplete)
checkMainTx(t, requesters, 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)
checkMainTx(t, requesters, requests, i+1, shouldComplete)
require.Equal(t, completedCount, len(completedTxes))
}
return requests
return requests, requesters
}
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()
checkCompleteMultisigRequest := func(t *testing.T, nSigs int, nKeys int, shouldComplete bool) ([]*payload.P2PNotaryRequest, []requester) {
accounts := make([]*wallet.Account, nKeys)
for i := range accounts {
accounts[i], _ = wallet.NewAccount()
}
requests := createMultisigRequest(nSigs, requesters)
requesters := []requester{
{
accounts: accounts,
m: nSigs,
typ: notary.MultiSignature,
},
}
requests := createMixedRequest(requesters)
sendOrder := make([]int, nKeys)
for i := range sendOrder {
sendOrder[i] = i
@ -404,11 +377,11 @@ func TestNotary(t *testing.T) {
submittedRequests = append(submittedRequests, requests[sendOrder[i]])
ntr1.OnNewRequest(requests[sendOrder[i]])
checkMultisigTx(t, nSigs, submittedRequests, i+1, shouldComplete)
checkMainTx(t, requesters, 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)
checkMainTx(t, requesters, submittedRequests, i+1, shouldComplete)
}
// sent the rest (n-m) out of n requests: main tx is already collected, so only fallbacks should be applied
@ -417,120 +390,161 @@ func TestNotary(t *testing.T) {
submittedRequests = append(submittedRequests, requests[sendOrder[i]])
ntr1.OnNewRequest(requests[sendOrder[i]])
checkMultisigTx(t, nSigs, submittedRequests, i+1, shouldComplete)
checkMainTx(t, requesters, submittedRequests, i+1, shouldComplete)
require.Equal(t, completedCount, len(completedTxes))
}
return submittedRequests
return submittedRequests, requesters
}
checkCompleteMixedRequest := func(t *testing.T, nSigSigners int, shouldComplete bool) ([]*payload.P2PNotaryRequest, []requester) {
requesters := make([]requester, nSigSigners)
for i := range requesters {
acc, _ := wallet.NewAccount()
requesters[i] = requester{
accounts: []*wallet.Account{acc},
typ: notary.Signature,
}
}
multisigAccounts := make([]*wallet.Account, 3)
for i := range multisigAccounts {
multisigAccounts[i], _ = wallet.NewAccount()
}
requesters = append(requesters, requester{
accounts: multisigAccounts,
m: 2,
typ: notary.MultiSignature,
})
requests := createMixedRequest(requesters)
for i := range requests {
ntr1.OnNewRequest(requests[i])
checkMainTx(t, requesters, requests, i+1, shouldComplete)
completedCount := len(completedTxes)
// check that the same request won't be processed twice
ntr1.OnNewRequest(requests[i])
checkMainTx(t, requesters, requests, i+1, shouldComplete)
require.Equal(t, completedCount, len(completedTxes))
}
return requests, requesters
}
// OnNewRequest: missing account
ntr1.UpdateNotaryNodes(keys.PublicKeys{randomAcc.PublicKey()})
r := checkCompleteStandardRequest(t, 1, false)
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)
r, _ := checkCompleteStandardRequest(t, i, true)
checkFallbackTxs(t, r, false)
}
// OnNewRequest: multisignature request
r = checkCompleteMultisigRequest(t, 1, 1, true)
r, _ = checkCompleteMultisigRequest(t, 1, 1, true)
checkFallbackTxs(t, r, false)
r = checkCompleteMultisigRequest(t, 1, 2, true)
r, _ = checkCompleteMultisigRequest(t, 1, 2, true)
checkFallbackTxs(t, r, false)
r = checkCompleteMultisigRequest(t, 1, 3, true)
r, _ = checkCompleteMultisigRequest(t, 1, 3, true)
checkFallbackTxs(t, r, false)
r = checkCompleteMultisigRequest(t, 3, 3, true)
r, _ = checkCompleteMultisigRequest(t, 3, 3, true)
checkFallbackTxs(t, r, false)
r = checkCompleteMultisigRequest(t, 3, 4, true)
r, _ = checkCompleteMultisigRequest(t, 3, 4, true)
checkFallbackTxs(t, r, false)
r = checkCompleteMultisigRequest(t, 3, 10, true)
r, _ = checkCompleteMultisigRequest(t, 3, 10, true)
checkFallbackTxs(t, r, false)
// OnNewRequest: mixed request
r, _ = checkCompleteMixedRequest(t, 1, true)
checkFallbackTxs(t, r, false)
r, _ = checkCompleteMixedRequest(t, 2, true)
checkFallbackTxs(t, r, false)
r, _ = checkCompleteMixedRequest(t, 3, true)
checkFallbackTxs(t, r, false)
// PostPersist: missing account
setFinalizeWithError(true)
r = checkCompleteStandardRequest(t, 1, false)
r, requesters := checkCompleteStandardRequest(t, 1, false)
checkFallbackTxs(t, r, false)
ntr1.UpdateNotaryNodes(keys.PublicKeys{randomAcc.PublicKey()})
setFinalizeWithError(false)
require.NoError(t, bc.AddBlock(bc.newBlock()))
checkSigTx(t, r, 1, false)
checkMainTx(t, requesters, 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
setFinalizeWithError(true)
requests := checkCompleteStandardRequest(t, 3, false)
requests, requesters := checkCompleteStandardRequest(t, 3, false)
// check PostPersist with finalisation error
setFinalizeWithError(true)
require.NoError(t, bc.AddBlock(bc.newBlock()))
checkSigTx(t, requests, len(requests), false)
checkMainTx(t, requesters, requests, len(requests), false)
// check PostPersist without finalisation error
setFinalizeWithError(false)
require.NoError(t, bc.AddBlock(bc.newBlock()))
checkSigTx(t, requests, len(requests), true)
checkMainTx(t, requesters, requests, len(requests), true)
// PostPersist: complete main transaction, multisignature account
setFinalizeWithError(true)
requests = checkCompleteMultisigRequest(t, 3, 4, false)
requests, requesters = checkCompleteMultisigRequest(t, 3, 4, false)
checkFallbackTxs(t, requests, false)
// check PostPersist with finalisation error
setFinalizeWithError(true)
require.NoError(t, bc.AddBlock(bc.newBlock()))
checkMultisigTx(t, 3, requests, len(requests), false)
checkMainTx(t, requesters, requests, len(requests), false)
checkFallbackTxs(t, requests, false)
// check PostPersist without finalisation error
setFinalizeWithError(false)
require.NoError(t, bc.AddBlock(bc.newBlock()))
checkMultisigTx(t, 3, requests, len(requests), true)
checkMainTx(t, requesters, requests, len(requests), true)
checkFallbackTxs(t, requests, false)
// PostPersist: complete fallback, signature request
setFinalizeWithError(true)
requests = checkCompleteStandardRequest(t, 3, false)
requests, requesters = 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)
checkMainTx(t, requesters, requests, len(requests), false)
checkFallbackTxs(t, requests, false)
// check PostPersist for valid fallbacks without finalisation error
setFinalizeWithError(false)
require.NoError(t, bc.AddBlock(bc.newBlock()))
checkSigTx(t, requests, len(requests), false)
checkMainTx(t, requesters, requests, len(requests), false)
checkFallbackTxs(t, requests, true)
// PostPersist: complete fallback, multisignature request
nSigs, nKeys := 3, 5
// check OnNewRequest with finalization error
setFinalizeWithError(true)
requests = checkCompleteMultisigRequest(t, nSigs, nKeys, false)
requests, requesters = 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)
checkMainTx(t, requesters, requests, len(requests), false)
checkFallbackTxs(t, requests, false)
// check PostPersist for valid fallbacks without finalisation error
setFinalizeWithError(false)
require.NoError(t, bc.AddBlock(bc.newBlock()))
checkMultisigTx(t, nSigs, requests, len(requests), false)
checkMainTx(t, requesters, 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
setFinalizeWithError(true)
requests = checkCompleteStandardRequest(t, 5, false)
requests, requesters = checkCompleteStandardRequest(t, 5, false)
checkFallbackTxs(t, requests, false)
// make fallbacks valid
_, err = bc.genBlocks(int(nvbDiffFallback))
@ -541,7 +555,7 @@ func TestNotary(t *testing.T) {
setChoosy(true)
// check PostPersist for lucky fallbacks
require.NoError(t, bc.AddBlock(bc.newBlock()))
checkSigTx(t, requests, len(requests), false)
checkMainTx(t, requesters, requests, len(requests), false)
checkFallbackTxs(t, lucky, true)
checkFallbackTxs(t, unluckies, false)
// reset finalisation function for unlucky fallbacks to finalise without an error
@ -549,14 +563,14 @@ func TestNotary(t *testing.T) {
setFinalizeWithError(false)
// check PostPersist for unlucky fallbacks
require.NoError(t, bc.AddBlock(bc.newBlock()))
checkSigTx(t, requests, len(requests), false)
checkMainTx(t, requesters, requests, len(requests), false)
checkFallbackTxs(t, lucky, true)
checkFallbackTxs(t, unluckies, true)
// PostPersist: different NVBs
// check OnNewRequest with finalization error and different NVBs
setFinalizeWithError(true)
requests = checkCompleteStandardRequest(t, 5, false, 1, 2, 3, 4, 5)
requests, requesters = 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))
@ -578,7 +592,7 @@ func TestNotary(t *testing.T) {
defer mtx.RUnlock()
return len(completedTxes)-start >= i+1
}, time.Second*3, time.Millisecond)
checkSigTx(t, requests, len(requests), false)
checkMainTx(t, requesters, requests, len(requests), false)
checkFallbackTxs(t, requests[:i+1], true)
checkFallbackTxs(t, requests[i+1:], false)
}
@ -586,7 +600,7 @@ func TestNotary(t *testing.T) {
// OnRequestRemoval: missing account
// check OnNewRequest with finalization error
setFinalizeWithError(true)
requests = checkCompleteStandardRequest(t, 4, false)
requests, requesters = checkCompleteStandardRequest(t, 4, false)
checkFallbackTxs(t, requests, false)
// make fallbacks valid and remove one fallback
_, err = bc.genBlocks(int(nvbDiffFallback))
@ -596,7 +610,7 @@ func TestNotary(t *testing.T) {
// non of the fallbacks should be completed
setFinalizeWithError(false)
require.NoError(t, bc.AddBlock(bc.newBlock()))
checkSigTx(t, requests, len(requests), false)
checkMainTx(t, requesters, requests, len(requests), false)
checkFallbackTxs(t, requests, false)
// set account back for the next tests
ntr1.UpdateNotaryNodes(keys.PublicKeys{acc1.PrivateKey().PublicKey()})
@ -604,7 +618,7 @@ func TestNotary(t *testing.T) {
// OnRequestRemoval: signature request, remove one fallback
// check OnNewRequest with finalization error
setFinalizeWithError(true)
requests = checkCompleteStandardRequest(t, 4, false)
requests, requesters = checkCompleteStandardRequest(t, 4, false)
checkFallbackTxs(t, requests, false)
// make fallbacks valid and remove one fallback
_, err = bc.genBlocks(int(nvbDiffFallback))
@ -614,13 +628,13 @@ func TestNotary(t *testing.T) {
// rest of the fallbacks should be completed
setFinalizeWithError(false)
require.NoError(t, bc.AddBlock(bc.newBlock()))
checkSigTx(t, requests, len(requests), false)
checkMainTx(t, requesters, requests, len(requests), false)
checkFallbackTxs(t, requests[:3], true)
require.Nil(t, completedTxes[unlucky.FallbackTransaction.Hash()])
// OnRequestRemoval: signature request, remove all fallbacks
setFinalizeWithError(true)
requests = checkCompleteStandardRequest(t, 4, false)
requests, requesters = checkCompleteStandardRequest(t, 4, false)
// remove all fallbacks
_, err = bc.genBlocks(int(nvbDiffFallback))
require.NoError(t, err)
@ -630,21 +644,21 @@ func TestNotary(t *testing.T) {
// then the whole request should be removed, i.e. there are no completed transactions
setFinalizeWithError(false)
require.NoError(t, bc.AddBlock(bc.newBlock()))
checkSigTx(t, requests, len(requests), false)
checkMainTx(t, requesters, 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)
checkMainTx(t, requesters, requests, len(requests), false)
checkFallbackTxs(t, requests, false)
// OnRequestRemoval: multisignature request, remove one fallback
nSigs, nKeys = 3, 5
// check OnNewRequest with finalization error
setFinalizeWithError(true)
requests = checkCompleteMultisigRequest(t, nSigs, nKeys, false)
checkMultisigTx(t, nSigs, requests, len(requests), false)
requests, requesters = checkCompleteMultisigRequest(t, nSigs, nKeys, false)
checkMainTx(t, requesters, requests, len(requests), false)
checkFallbackTxs(t, requests, false)
// make fallbacks valid and remove the last fallback
_, err = bc.genBlocks(int(nvbDiffFallback))
@ -654,7 +668,7 @@ func TestNotary(t *testing.T) {
// then (m-1) out of n fallbacks should be completed
setFinalizeWithError(false)
require.NoError(t, bc.AddBlock(bc.newBlock()))
checkMultisigTx(t, nSigs, requests, len(requests), false)
checkMainTx(t, requesters, 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
@ -662,7 +676,7 @@ func TestNotary(t *testing.T) {
// OnRequestRemoval: multisignature request, remove all fallbacks
setFinalizeWithError(true)
requests = checkCompleteMultisigRequest(t, nSigs, nKeys, false)
requests, requesters = checkCompleteMultisigRequest(t, nSigs, nKeys, false)
// make fallbacks valid and then remove all of them
_, err = bc.genBlocks(int(nvbDiffFallback))
require.NoError(t, err)
@ -672,13 +686,13 @@ func TestNotary(t *testing.T) {
// then the whole request should be removed, i.e. there are no completed transactions
setFinalizeWithError(false)
require.NoError(t, bc.AddBlock(bc.newBlock()))
checkMultisigTx(t, nSigs, requests, len(requests), false)
checkMainTx(t, requesters, 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)
checkMainTx(t, requesters, requests, len(requests), false)
checkFallbackTxs(t, requests, false)
// Subscriptions test
@ -692,7 +706,16 @@ func TestNotary(t *testing.T) {
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})
requests = createMixedRequest([]requester{
{
accounts: []*wallet.Account{requester1},
typ: notary.Signature,
},
{
accounts: []*wallet.Account{requester2},
typ: notary.Signature,
},
})
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 {

View file

@ -67,9 +67,9 @@ type (
const defaultTxChannelCapacity = 100
// request represents Notary service request.
type request struct {
typ RequestType
type (
// request represents Notary service request.
request struct {
// isSent indicates whether main transaction was successfully sent to the network.
isSent bool
main *transaction.Transaction
@ -77,15 +77,37 @@ type request struct {
// We stop trying to send mainTx to the network if the chain reaches minNotValidBefore height.
minNotValidBefore uint32
fallbacks []*transaction.Transaction
witnessInfo []witnessInfo
}
// witnessInfo represents information about signer and its witness.
witnessInfo struct {
typ RequestType
// nSigsLeft is the number of signatures left to collect to complete main transaction.
// Initial nSigsLeft value is defined as following:
// nSigsLeft == nKeys for standard signature request;
// nSigsLeft <= nKeys for multisignature request;
// nSigsLeft == 0 when all received requests were invalid, so check request.typ before access to nSigs.
nSigsLeft uint8
// sigs is a map of partial multisig invocation scripts [opcode.PUSHDATA1+64+signatureBytes] grouped by public keys
// sigs is a map of partial multisig invocation scripts [opcode.PUSHDATA1+64+signatureBytes] grouped by public keys.
sigs map[*keys.PublicKey][]byte
// pubs is a set of public keys participating in the multisignature witness collection.
pubs keys.PublicKeys
}
)
// isMainCompleted denotes whether all signatures for the main transaction was collected.
func (r request) isMainCompleted() bool {
if r.witnessInfo == nil {
return false
}
for _, wi := range r.witnessInfo {
if wi.nSigsLeft != 0 {
return false
}
}
return true
}
// NewNotary returns new Notary module.
@ -163,7 +185,13 @@ func (n *Notary) OnNewRequest(payload *payload.P2PNotaryRequest) {
nvbFallback := payload.FallbackTransaction.GetAttributes(transaction.NotValidBeforeT)[0].Value.(*transaction.NotValidBefore).Height
nKeys := payload.MainTransaction.GetAttributes(transaction.NotaryAssistedT)[0].Value.(*transaction.NotaryAssisted).NKeys
typ, nSigs, pubs, validationErr := n.verifyIncompleteWitnesses(payload.MainTransaction, nKeys)
newInfo, validationErr := n.verifyIncompleteWitnesses(payload.MainTransaction, nKeys)
if validationErr != nil {
n.Config.Log.Info("verification of main notary transaction failed; fallback transaction will be completed",
zap.String("main hash", payload.MainTransaction.Hash().StringLE()),
zap.String("fallback hash", payload.FallbackTransaction.Hash().StringLE()),
zap.String("verification error", validationErr.Error()))
}
n.reqMtx.Lock()
defer n.reqMtx.Unlock()
r, exists := n.requests[payload.MainTransaction.Hash()]
@ -176,75 +204,66 @@ func (n *Notary) OnNewRequest(payload *payload.P2PNotaryRequest) {
if nvbFallback < r.minNotValidBefore {
r.minNotValidBefore = nvbFallback
}
if r.typ == Unknown && validationErr == nil {
r.typ = typ
r.nSigsLeft = nSigs
}
} else {
r = &request{
nSigsLeft: nSigs,
main: payload.MainTransaction,
typ: typ,
minNotValidBefore: nvbFallback,
}
n.requests[payload.MainTransaction.Hash()] = r
}
if r.witnessInfo == nil && validationErr == nil {
r.witnessInfo = newInfo
}
r.fallbacks = append(r.fallbacks, payload.FallbackTransaction)
if exists && r.typ != Unknown && r.nSigsLeft == 0 { // already collected sufficient number of signatures to complete main transaction
if exists && r.isMainCompleted() || validationErr != nil {
return
}
if validationErr == nil {
loop:
mainHash := hash.NetSha256(uint32(n.Network), r.main).BytesBE()
for i, w := range payload.MainTransaction.Scripts {
if payload.MainTransaction.Signers[i].Account.Equals(n.Config.Chain.GetNotaryContractScriptHash()) {
if r.witnessInfo[i].typ == Contract || // check that we need to fill that witness
len(w.InvocationScript) == 0 || // check that signature for this witness was provided
r.witnessInfo[i].nSigsLeft == 0 { // check that signature wasn't yet added (consider receiving the same payload multiple times)
continue
}
if len(w.InvocationScript) != 0 && len(w.VerificationScript) != 0 {
switch r.typ {
switch r.witnessInfo[i].typ {
case Signature:
if !exists {
r.nSigsLeft--
} else if len(r.main.Scripts[i].InvocationScript) == 0 { // need this check because signature can already be added (consider receiving the same payload multiple times)
if r.witnessInfo[i].pubs[0].Verify(w.InvocationScript[2:], mainHash) {
r.main.Scripts[i] = w
r.nSigsLeft--
}
if r.nSigsLeft == 0 {
break loop
r.witnessInfo[i].nSigsLeft--
}
case MultiSignature:
if r.sigs == nil {
r.sigs = make(map[*keys.PublicKey][]byte)
if r.witnessInfo[i].sigs == nil {
r.witnessInfo[i].sigs = make(map[*keys.PublicKey][]byte)
}
hash := hash.NetSha256(uint32(n.Network), r.main).BytesBE()
for _, pub := range pubs {
if r.sigs[pub] != nil {
for _, pub := range r.witnessInfo[i].pubs {
if r.witnessInfo[i].sigs[pub] != nil {
continue // signature for this pub has already been added
}
if pub.Verify(w.InvocationScript[2:], hash) { // then pub is the owner of the signature
r.sigs[pub] = w.InvocationScript
r.nSigsLeft--
if r.nSigsLeft == 0 {
if pub.Verify(w.InvocationScript[2:], mainHash) { // then pub is the owner of the signature
r.witnessInfo[i].sigs[pub] = w.InvocationScript
r.witnessInfo[i].nSigsLeft--
if r.witnessInfo[i].nSigsLeft == 0 {
var invScript []byte
for j := range pubs {
if sig, ok := r.sigs[pubs[j]]; ok {
for j := range r.witnessInfo[i].pubs {
if sig, ok := r.witnessInfo[i].sigs[r.witnessInfo[i].pubs[j]]; ok {
invScript = append(invScript, sig...)
}
}
r.main.Scripts[i].InvocationScript = invScript
}
break loop
break
}
}
// pubKey was not found for the signature i.e. signature is bad - we're OK with that, let the fallback TX to be added
break loop // only one multisignature is allowed
// pubKey was not found for the signature (i.e. signature is bad) or the signature has already
// been added - we're OK with that, let the fallback TX to be added
}
}
}
}
if r.typ != Unknown && r.nSigsLeft == 0 && r.minNotValidBefore > n.Config.Chain.BlockHeight() {
if r.isMainCompleted() && r.minNotValidBefore > n.Config.Chain.BlockHeight() {
if err := n.finalize(acc, r.main, payload.MainTransaction.Hash()); err != nil {
n.Config.Log.Error("failed to finalize main transaction", zap.Error(err))
n.Config.Log.Error("failed to finalize main transaction",
zap.String("hash", r.main.Hash().StringLE()),
zap.Error(err))
}
}
}
@ -285,7 +304,7 @@ func (n *Notary) PostPersist() {
defer n.reqMtx.Unlock()
currHeight := n.Config.Chain.BlockHeight()
for h, r := range n.requests {
if !r.isSent && r.typ != Unknown && r.nSigsLeft == 0 && r.minNotValidBefore > currHeight {
if !r.isSent && r.isMainCompleted() && r.minNotValidBefore > currHeight {
if err := n.finalize(acc, r.main, h); err != nil {
n.Config.Log.Error("failed to finalize main transaction", zap.Error(err))
}
@ -404,74 +423,67 @@ func updateTxSize(tx *transaction.Transaction) (*transaction.Transaction, error)
// verifyIncompleteWitnesses checks that tx either doesn't have all witnesses attached (in this case none of them
// can be multisignature), or it only has a partial multisignature. It returns the request type (sig/multisig), the
// number of signatures to be collected, sorted public keys (for multisig request only) and an error.
func (n *Notary) verifyIncompleteWitnesses(tx *transaction.Transaction, nKeys uint8) (RequestType, uint8, keys.PublicKeys, error) {
var (
typ RequestType
nSigs int
nKeysActual uint8
pubsBytes [][]byte
pubs keys.PublicKeys
ok bool
)
func (n *Notary) verifyIncompleteWitnesses(tx *transaction.Transaction, nKeysExpected uint8) ([]witnessInfo, error) {
var nKeysActual uint8
if len(tx.Signers) < 2 {
return Unknown, 0, nil, errors.New("transaction should have at least 2 signers")
return nil, errors.New("transaction should have at least 2 signers")
}
if !tx.HasSigner(n.Config.Chain.GetNotaryContractScriptHash()) {
return Unknown, 0, nil, fmt.Errorf("P2PNotary contract should be a signer of the transaction")
return nil, fmt.Errorf("P2PNotary contract should be a signer of the transaction")
}
result := make([]witnessInfo, len(tx.Signers))
for i, w := range tx.Scripts {
// do not check witness for Notary contract -- it will be replaced by proper witness in any case.
if tx.Signers[i].Account == n.Config.Chain.GetNotaryContractScriptHash() {
continue
}
// Do not check witness for Notary contract -- it will be replaced by proper witness in any case.
// Also do not check other contract-based witnesses (they can be combined with anything)
if len(w.VerificationScript) == 0 {
// then it's a contract verification (can be combined with anything)
result[i] = witnessInfo{
typ: Contract,
nSigsLeft: 0,
}
continue
}
if !tx.Signers[i].Account.Equals(hash.Hash160(w.VerificationScript)) { // https://github.com/nspcc-dev/neo-go/pull/1658#discussion_r564265987
return Unknown, 0, nil, fmt.Errorf("transaction should have valid verification script for signer #%d", i)
return nil, fmt.Errorf("transaction should have valid verification script for signer #%d", i)
}
if nSigs, pubsBytes, ok = vm.ParseMultiSigContract(w.VerificationScript); ok {
if typ == Signature || typ == MultiSignature {
return Unknown, 0, nil, fmt.Errorf("bad type of witness #%d: only one multisignature witness is allowed", i)
}
typ = MultiSignature
nKeysActual = uint8(len(pubsBytes))
// Each verification script is allowed to have either one signature or zero signatures. If signature is provided, then need to verify it.
if len(w.InvocationScript) != 0 {
if len(w.InvocationScript) != 66 || !bytes.HasPrefix(w.InvocationScript, []byte{byte(opcode.PUSHDATA1), 64}) {
return Unknown, 0, nil, fmt.Errorf("multisignature invocation script should have length = 66 and be of the form [PUSHDATA1, 64, signatureBytes...]")
return nil, fmt.Errorf("witness #%d: invocation script should have length = 66 and be of the form [PUSHDATA1, 64, signatureBytes...]", i)
}
continue
}
if vm.IsSignatureContract(w.VerificationScript) {
if typ == MultiSignature {
return Unknown, 0, nil, fmt.Errorf("bad type of witness #%d: multisignature witness can not be combined with other witnesses", i)
if nSigs, pubsBytes, ok := vm.ParseMultiSigContract(w.VerificationScript); ok {
result[i] = witnessInfo{
typ: MultiSignature,
nSigsLeft: uint8(nSigs),
pubs: make(keys.PublicKeys, len(pubsBytes)),
}
typ = Signature
nSigs = int(nKeys)
continue
}
return Unknown, 0, nil, fmt.Errorf("unable to define the type of witness #%d", i)
}
switch typ {
case Signature:
if len(tx.Scripts) < int(nKeys+1) {
return Unknown, 0, nil, fmt.Errorf("transaction should comtain at least %d witnesses (1 for notary + nKeys)", nKeys+1)
}
case MultiSignature:
if nKeysActual != nKeys {
return Unknown, 0, nil, fmt.Errorf("bad m out of n partial multisignature witness: expected n = %d, got n = %d", nKeys, nKeysActual)
}
pubs = make(keys.PublicKeys, len(pubsBytes))
for i, pBytes := range pubsBytes {
for j, pBytes := range pubsBytes {
pub, err := keys.NewPublicKeyFromBytes(pBytes, elliptic.P256())
if err != nil {
return Unknown, 0, nil, fmt.Errorf("invalid bytes of #%d public key: %s", i, hex.EncodeToString(pBytes))
return nil, fmt.Errorf("witness #%d: invalid bytes of #%d public key: %s", i, j, hex.EncodeToString(pBytes))
}
pubs[i] = pub
result[i].pubs[j] = pub
}
default:
return Unknown, 0, nil, errors.New("unexpected Notary request type")
nKeysActual += uint8(len(pubsBytes))
continue
}
return typ, uint8(nSigs), pubs, nil
if pBytes, ok := vm.ParseSignatureContract(w.VerificationScript); ok {
pub, err := keys.NewPublicKeyFromBytes(pBytes, elliptic.P256())
if err != nil {
return nil, fmt.Errorf("witness #%d: invalid bytes of public key: %s", i, hex.EncodeToString(pBytes))
}
result[i] = witnessInfo{
typ: Signature,
nSigsLeft: 1,
pubs: keys.PublicKeys{pub},
}
nKeysActual++
continue
}
return nil, fmt.Errorf("witness #%d: unable to detect witness type, only sig/multisig/contract are supported", i)
}
if nKeysActual != nKeysExpected {
return nil, fmt.Errorf("expected and actual NKeys mismatch: %d vs %d", nKeysExpected, nKeysActual)
}
return result, nil
}

View file

@ -13,7 +13,6 @@ import (
"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"
)
@ -67,11 +66,9 @@ func TestVerifyIncompleteRequest(t *testing.T) {
multisigScriptHash2 := hash.Hash160(multisigScript2)
checkErr := func(t *testing.T, tx *transaction.Transaction, nKeys uint8) {
typ, nSigs, pubs, err := ntr.verifyIncompleteWitnesses(tx, nKeys)
witnessInfo, err := ntr.verifyIncompleteWitnesses(tx, nKeys)
require.Error(t, err)
require.Equal(t, Unknown, typ)
require.Equal(t, uint8(0), nSigs)
require.Nil(t, pubs)
require.Nil(t, witnessInfo)
}
errCases := map[string]struct {
@ -90,15 +87,6 @@ func TestVerifyIncompleteRequest(t *testing.T) {
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}},
@ -111,125 +99,6 @@ func TestVerifyIncompleteRequest(t *testing.T) {
},
},
},
"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}},
@ -283,9 +152,7 @@ func TestVerifyIncompleteRequest(t *testing.T) {
testCases := map[string]struct {
tx *transaction.Transaction
nKeys uint8
expectedType RequestType
expectedNSigs uint8
expectedPubs keys.PublicKeys
expectedInfo []witnessInfo
}{
"single sig": {
tx: &transaction.Transaction{
@ -299,8 +166,10 @@ func TestVerifyIncompleteRequest(t *testing.T) {
},
},
nKeys: 1,
expectedType: Signature,
expectedNSigs: 1,
expectedInfo: []witnessInfo{
{typ: Signature, nSigsLeft: 1, pubs: keys.PublicKeys{acc1.PublicKey()}},
{typ: Contract},
},
},
"multiple sig": {
tx: &transaction.Transaction{
@ -312,7 +181,7 @@ func TestVerifyIncompleteRequest(t *testing.T) {
},
{
InvocationScript: []byte{},
VerificationScript: []byte{},
VerificationScript: sigScript2,
},
{
InvocationScript: sig,
@ -322,10 +191,14 @@ func TestVerifyIncompleteRequest(t *testing.T) {
},
},
nKeys: 3,
expectedType: Signature,
expectedNSigs: 3,
expectedInfo: []witnessInfo{
{typ: Signature, nSigsLeft: 1, pubs: keys.PublicKeys{acc1.PublicKey()}},
{typ: Signature, nSigsLeft: 1, pubs: keys.PublicKeys{acc2.PublicKey()}},
{typ: Signature, nSigsLeft: 1, pubs: keys.PublicKeys{acc3.PublicKey()}},
{typ: Contract},
},
"multisig 1 out of 3": {
},
"single multisig 1 out of 3": {
tx: &transaction.Transaction{
Signers: []transaction.Signer{{Account: multisigScriptHash1}, {Account: notaryContractHash}},
Scripts: []transaction.Witness{
@ -337,11 +210,12 @@ func TestVerifyIncompleteRequest(t *testing.T) {
},
},
nKeys: 3,
expectedType: MultiSignature,
expectedNSigs: 1,
expectedPubs: keys.PublicKeys{acc1.PublicKey(), acc2.PublicKey(), acc3.PublicKey()},
expectedInfo: []witnessInfo{
{typ: MultiSignature, nSigsLeft: 1, pubs: keys.PublicKeys{acc1.PublicKey(), acc2.PublicKey(), acc3.PublicKey()}},
{typ: Contract},
},
"multisig 2 out of 3": {
},
"single multisig 2 out of 3": {
tx: &transaction.Transaction{
Signers: []transaction.Signer{{Account: multisigScriptHash2}, {Account: notaryContractHash}},
Scripts: []transaction.Witness{
@ -353,17 +227,18 @@ func TestVerifyIncompleteRequest(t *testing.T) {
},
},
nKeys: 3,
expectedType: MultiSignature,
expectedNSigs: 2,
expectedPubs: keys.PublicKeys{acc1.PublicKey(), acc2.PublicKey(), acc3.PublicKey()},
expectedInfo: []witnessInfo{
{typ: MultiSignature, nSigsLeft: 2, pubs: keys.PublicKeys{acc1.PublicKey(), acc2.PublicKey(), acc3.PublicKey()}},
{typ: Contract},
},
"empty + multisig": {
},
"empty sig + single multisig 1 out of 3": {
tx: &transaction.Transaction{
Signers: []transaction.Signer{{Account: acc1.PublicKey().GetScriptHash()}, {Account: multisigScriptHash1}, {Account: notaryContractHash}},
Scripts: []transaction.Witness{
{
InvocationScript: []byte{},
VerificationScript: []byte{},
VerificationScript: sigScript1,
},
{
InvocationScript: sig,
@ -372,12 +247,14 @@ func TestVerifyIncompleteRequest(t *testing.T) {
{},
},
},
nKeys: 3,
expectedType: MultiSignature,
expectedNSigs: 1,
expectedPubs: keys.PublicKeys{acc1.PublicKey(), acc2.PublicKey(), acc3.PublicKey()},
nKeys: 1 + 3,
expectedInfo: []witnessInfo{
{typ: Signature, nSigsLeft: 1, pubs: keys.PublicKeys{acc1.PublicKey()}},
{typ: MultiSignature, nSigsLeft: 1, pubs: keys.PublicKeys{acc1.PublicKey(), acc2.PublicKey(), acc3.PublicKey()}},
{typ: Contract},
},
"multisig + empty": {
},
"single multisig 1 out of 3 + empty single sig": {
tx: &transaction.Transaction{
Signers: []transaction.Signer{{Account: multisigScriptHash1}, {Account: acc1.PublicKey().GetScriptHash()}, {Account: notaryContractHash}},
Scripts: []transaction.Witness{
@ -387,25 +264,228 @@ func TestVerifyIncompleteRequest(t *testing.T) {
},
{
InvocationScript: []byte{},
VerificationScript: []byte{},
VerificationScript: sigScript1,
},
{},
},
},
nKeys: 3,
expectedType: MultiSignature,
expectedNSigs: 1,
expectedPubs: keys.PublicKeys{acc1.PublicKey(), acc2.PublicKey(), acc3.PublicKey()},
nKeys: 3 + 1,
expectedInfo: []witnessInfo{
{typ: MultiSignature, nSigsLeft: 1, pubs: keys.PublicKeys{acc1.PublicKey(), acc2.PublicKey(), acc3.PublicKey()}},
{typ: Signature, nSigsLeft: 1, pubs: keys.PublicKeys{acc1.PublicKey()}},
{typ: Contract},
},
},
"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: 3 + 3,
expectedInfo: []witnessInfo{
{typ: MultiSignature, nSigsLeft: 1, pubs: keys.PublicKeys{acc1.PublicKey(), acc2.PublicKey(), acc3.PublicKey()}},
{typ: MultiSignature, nSigsLeft: 2, pubs: keys.PublicKeys{acc1.PublicKey(), acc2.PublicKey(), acc3.PublicKey()}},
{typ: Contract},
},
},
"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: 3 + 1,
expectedInfo: []witnessInfo{
{typ: MultiSignature, nSigsLeft: 1, pubs: keys.PublicKeys{acc1.PublicKey(), acc2.PublicKey(), acc3.PublicKey()}},
{typ: Signature, nSigsLeft: 1, pubs: keys.PublicKeys{acc1.PublicKey()}},
{typ: Contract},
},
},
"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: 1 + 3,
expectedInfo: []witnessInfo{
{typ: Signature, nSigsLeft: 1, pubs: keys.PublicKeys{acc1.PublicKey()}},
{typ: MultiSignature, nSigsLeft: 1, pubs: keys.PublicKeys{acc1.PublicKey(), acc2.PublicKey(), acc3.PublicKey()}},
{typ: Contract},
},
},
"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: 3 + 1,
expectedInfo: []witnessInfo{
{typ: MultiSignature, nSigsLeft: 1, pubs: keys.PublicKeys{acc1.PublicKey(), acc2.PublicKey(), acc3.PublicKey()}},
{typ: Signature, nSigsLeft: 1, pubs: keys.PublicKeys{acc1.PublicKey()}},
{typ: Contract},
},
},
"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: 1 + 3,
expectedInfo: []witnessInfo{
{typ: Signature, nSigsLeft: 1, pubs: keys.PublicKeys{acc1.PublicKey()}},
{typ: MultiSignature, nSigsLeft: 1, pubs: keys.PublicKeys{acc1.PublicKey(), acc2.PublicKey(), acc3.PublicKey()}},
{typ: Contract},
},
},
"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: 3 + 1,
expectedInfo: []witnessInfo{
{typ: MultiSignature, nSigsLeft: 1, pubs: keys.PublicKeys{acc1.PublicKey(), acc2.PublicKey(), acc3.PublicKey()}},
{typ: Signature, nSigsLeft: 1, pubs: keys.PublicKeys{acc1.PublicKey()}},
{typ: Contract},
},
},
"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: 1 + 3,
expectedInfo: []witnessInfo{
{typ: Signature, nSigsLeft: 1, pubs: keys.PublicKeys{acc1.PublicKey()}},
{typ: MultiSignature, nSigsLeft: 1, pubs: keys.PublicKeys{acc1.PublicKey(), acc2.PublicKey(), acc3.PublicKey()}},
{typ: Contract},
},
},
"multiple sigs + multiple multisigs": {
tx: &transaction.Transaction{
Signers: []transaction.Signer{{Account: multisigScriptHash1},
{Account: acc1.PublicKey().GetScriptHash()},
{Account: acc2.PublicKey().GetScriptHash()},
{Account: acc3.PublicKey().GetScriptHash()},
{Account: multisigScriptHash2},
{Account: notaryContractHash}},
Scripts: []transaction.Witness{
{
InvocationScript: sig,
VerificationScript: multisigScript1,
},
{
InvocationScript: sig,
VerificationScript: sigScript1,
},
{
InvocationScript: []byte{},
VerificationScript: sigScript2,
},
{
InvocationScript: sig,
VerificationScript: sigScript3,
},
{
InvocationScript: []byte{},
VerificationScript: multisigScript2,
},
{},
},
},
nKeys: 3 + 1 + 1 + 1 + 3,
expectedInfo: []witnessInfo{
{typ: MultiSignature, nSigsLeft: 1, pubs: keys.PublicKeys{acc1.PublicKey(), acc2.PublicKey(), acc3.PublicKey()}},
{typ: Signature, nSigsLeft: 1, pubs: keys.PublicKeys{acc1.PublicKey()}},
{typ: Signature, nSigsLeft: 1, pubs: keys.PublicKeys{acc2.PublicKey()}},
{typ: Signature, nSigsLeft: 1, pubs: keys.PublicKeys{acc3.PublicKey()}},
{typ: MultiSignature, nSigsLeft: 2, pubs: keys.PublicKeys{acc1.PublicKey(), acc2.PublicKey(), acc3.PublicKey()}},
{typ: Contract},
},
},
}
for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
typ, nSigs, pubs, err := ntr.verifyIncompleteWitnesses(testCase.tx, testCase.nKeys)
actualInfo, 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)
require.Equal(t, len(testCase.expectedInfo), len(actualInfo))
for i, expected := range testCase.expectedInfo {
actual := actualInfo[i]
require.Equal(t, expected.typ, actual.typ)
require.Equal(t, expected.nSigsLeft, actual.nSigsLeft)
require.ElementsMatch(t, expected.pubs, actual.pubs)
require.Nil(t, actual.sigs)
}
})
}
}

View file

@ -4,10 +4,10 @@ package notary
type RequestType byte
const (
// Unknown represents unknown request type which means that main tx witnesses are invalid.
Unknown RequestType = 0x00
// Signature represents standard single signature request type.
Signature RequestType = 0x01
// MultiSignature represents m out of n multisignature request type.
MultiSignature RequestType = 0x02
// Contract represents contract witness type.
Contract RequestType = 0x03
)