Skip to content

Commit

Permalink
Merge pull request #1557 from nspcc-dev/signature_collection/notary_c…
Browse files Browse the repository at this point in the history
…ontract

core: implement Notary contract
  • Loading branch information
roman-khimov authored Nov 25, 2020
2 parents 9b7cdcc + 9faa634 commit ef15139
Show file tree
Hide file tree
Showing 32 changed files with 1,269 additions and 284 deletions.
1 change: 1 addition & 0 deletions config/protocol.unit_testnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ ProtocolConfiguration:
- 127.0.0.1:20336
VerifyBlocks: true
VerifyTransactions: true
P2PSigExtensions: true

ApplicationConfiguration:
# LogPath could be set up in case you need stdout logs to some proper file.
Expand Down
40 changes: 27 additions & 13 deletions pkg/core/blockchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ func NewBlockchain(s storage.Store, cfg config.ProtocolConfiguration, log *zap.L
subCh: make(chan interface{}),
unsubCh: make(chan interface{}),

contracts: *native.NewContracts(),
contracts: *native.NewContracts(cfg.P2PSigExtensions),
}

if err := bc.init(); err != nil {
Expand Down Expand Up @@ -705,9 +705,11 @@ func (bc *Blockchain) storeBlock(block *block.Block, txpool *mempool.Pool) error
return err
}
if err := bc.contracts.Policy.OnPersistEnd(bc.dao); err != nil {
bc.lock.Unlock()
return fmt.Errorf("failed to call OnPersistEnd for Policy native contract: %w", err)
}
if err := bc.contracts.Designate.OnPersistEnd(bc.dao); err != nil {
bc.lock.Unlock()
return err
}
bc.dao.MPT.Flush()
Expand Down Expand Up @@ -1254,7 +1256,6 @@ func (bc *Blockchain) verifyHeader(currHeader, prevHeader *block.Header) error {

// Various errors that could be returned upon verification.
var (
ErrTxNotYetValid = errors.New("transaction is not yet valid")
ErrTxExpired = errors.New("transaction has expired")
ErrInsufficientFunds = errors.New("insufficient funds")
ErrTxSmallNetworkFee = errors.New("too small network fee")
Expand All @@ -1281,6 +1282,13 @@ func (bc *Blockchain) verifyAndPoolTx(t *transaction.Transaction, pool *mempool.
return fmt.Errorf("%w: (%d > MaxTransactionSize %d)", ErrTxTooBig, size, transaction.MaxTransactionSize)
}
needNetworkFee := int64(size) * bc.FeePerByte()
if bc.P2PSigExtensionsEnabled() {
attrs := t.GetAttributes(transaction.NotaryAssistedT)
if len(attrs) != 0 {
na := attrs[0].Value.(*transaction.NotaryAssisted)
needNetworkFee += (int64(na.NKeys) + 1) * transaction.NotaryServiceFeePerKey
}
}
netFee := t.NetworkFee - needNetworkFee
if netFee < 0 {
return fmt.Errorf("%w: net fee is %v, need %v", ErrTxSmallNetworkFee, t.NetworkFee, needNetworkFee)
Expand Down Expand Up @@ -1329,12 +1337,9 @@ func (bc *Blockchain) verifyTxAttributes(tx *transaction.Transaction) error {
switch attrType := tx.Attributes[i].Type; attrType {
case transaction.HighPriority:
h := bc.contracts.NEO.GetCommitteeAddress()
for i := range tx.Signers {
if tx.Signers[i].Account.Equals(h) {
return nil
}
if !tx.HasSigner(h) {
return fmt.Errorf("%w: high priority tx is not signed by committee", ErrInvalidAttribute)
}
return fmt.Errorf("%w: high priority tx is not signed by committee", ErrInvalidAttribute)
case transaction.OracleResponseT:
h, err := bc.contracts.Oracle.GetScriptHash(bc.dao)
if err != nil || h.Equals(util.Uint160{}) {
Expand Down Expand Up @@ -1365,23 +1370,30 @@ func (bc *Blockchain) verifyTxAttributes(tx *transaction.Transaction) error {
}
case transaction.NotValidBeforeT:
if !bc.config.P2PSigExtensions {
return errors.New("NotValidBefore attribute was found, but P2PSigExtensions are disabled")
return fmt.Errorf("%w: NotValidBefore attribute was found, but P2PSigExtensions are disabled", ErrInvalidAttribute)
}
nvb := tx.Attributes[i].Value.(*transaction.NotValidBefore)
if height := bc.BlockHeight(); height < nvb.Height {
return fmt.Errorf("%w: NotValidBefore = %d, current height = %d", ErrTxNotYetValid, nvb.Height, height)
return fmt.Errorf("%w: transaction is not yet valid: NotValidBefore = %d, current height = %d", ErrInvalidAttribute, nvb.Height, height)
}
case transaction.ConflictsT:
if !bc.config.P2PSigExtensions {
return errors.New("Conflicts attribute was found, but P2PSigExtensions are disabled")
return fmt.Errorf("%w: Conflicts attribute was found, but P2PSigExtensions are disabled", ErrInvalidAttribute)
}
conflicts := tx.Attributes[i].Value.(*transaction.Conflicts)
if err := bc.dao.HasTransaction(conflicts.Hash); errors.Is(err, dao.ErrAlreadyExists) {
return fmt.Errorf("conflicting transaction %s is already on chain", conflicts.Hash.StringLE())
return fmt.Errorf("%w: conflicting transaction %s is already on chain", ErrInvalidAttribute, conflicts.Hash.StringLE())
}
case transaction.NotaryAssistedT:
if !bc.config.P2PSigExtensions {
return fmt.Errorf("%w: NotaryAssisted attribute was found, but P2PSigExtensions are disabled", ErrInvalidAttribute)
}
if !tx.HasSigner(bc.contracts.Notary.Hash) {
return fmt.Errorf("%w: NotaryAssisted attribute was found, but transaction is not signed by the Notary native contract", ErrInvalidAttribute)
}
default:
if !bc.config.ReservedAttributes && attrType >= transaction.ReservedLowerBound && attrType <= transaction.ReservedUpperBound {
return errors.New("attribute of reserved type was found, but ReservedAttributes are disabled")
return fmt.Errorf("%w: attribute of reserved type was found, but ReservedAttributes are disabled", ErrInvalidAttribute)
}
}
}
Expand Down Expand Up @@ -1581,6 +1593,7 @@ func (bc *Blockchain) initVerificationVM(ic *interop.Context, hash util.Uint160,
var isNative bool
var initMD *manifest.Method
verification := witness.VerificationScript
flags := smartcontract.NoneFlag
if len(verification) != 0 {
if witness.ScriptHash() != hash {
return ErrWitnessHashMismatch
Expand All @@ -1601,10 +1614,11 @@ func (bc *Blockchain) initVerificationVM(ic *interop.Context, hash util.Uint160,
offset = md.Offset
initMD = cs.Manifest.ABI.GetMethod(manifest.MethodInit)
isNative = cs.ID < 0
flags = smartcontract.AllowStates
}

v := ic.VM
v.LoadScriptWithFlags(verification, smartcontract.NoneFlag)
v.LoadScriptWithFlags(verification, flags)
v.Jump(v.Context(), offset)
if isNative {
w := io.NewBufBinWriter()
Expand Down
209 changes: 205 additions & 4 deletions pkg/core/blockchain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ func TestVerifyTx(t *testing.T) {
require.Equal(t, 1, len(aer))
require.Equal(t, aer[0].VMState, vm.HaltState)

res, err := invokeNativePolicyMethod(bc, "blockAccount", accs[1].PrivateKey().GetScriptHash().BytesBE())
res, err := invokeContractMethod(bc, 100000000, bc.contracts.Policy.Hash, "blockAccount", accs[1].PrivateKey().GetScriptHash().BytesBE())
require.NoError(t, err)
checkResult(t, res, stackitem.NewBool(true))

Expand Down Expand Up @@ -621,7 +621,7 @@ func TestVerifyTx(t *testing.T) {
bc.config.P2PSigExtensions = true
t.Run("NotYetValid", func(t *testing.T) {
tx := getNVBTx(bc.blockHeight + 1)
require.True(t, errors.Is(bc.VerifyTx(tx), ErrTxNotYetValid))
require.True(t, errors.Is(bc.VerifyTx(tx), ErrInvalidAttribute))
})
t.Run("positive", func(t *testing.T) {
tx := getNVBTx(bc.blockHeight)
Expand Down Expand Up @@ -652,12 +652,12 @@ func TestVerifyTx(t *testing.T) {
return tx
}
t.Run("Disabled", func(t *testing.T) {
tx := getReservedTx(transaction.ReservedLowerBound + 2)
tx := getReservedTx(transaction.ReservedLowerBound + 3)
require.Error(t, bc.VerifyTx(tx))
})
t.Run("Enabled", func(t *testing.T) {
bc.config.ReservedAttributes = true
tx := getReservedTx(transaction.ReservedLowerBound + 2)
tx := getReservedTx(transaction.ReservedLowerBound + 3)
require.NoError(t, bc.VerifyTx(tx))
})
})
Expand Down Expand Up @@ -719,6 +719,207 @@ func TestVerifyTx(t *testing.T) {
})
})
})
t.Run("NotaryAssisted", func(t *testing.T) {
notary, err := wallet.NewAccount()
require.NoError(t, err)
txSetNotary := transaction.New(netmode.UnitTestNet, []byte{}, 0)
setSigner(txSetNotary, testchain.CommitteeScriptHash())
txSetNotary.Scripts = []transaction.Witness{{
InvocationScript: testchain.SignCommittee(txSetNotary.GetSignedPart()),
VerificationScript: testchain.CommitteeVerificationScript(),
}}
bl := block.New(netmode.UnitTestNet, false)
bl.Index = bc.BlockHeight() + 1
ic := bc.newInteropContext(trigger.All, bc.dao, bl, txSetNotary)
ic.SpawnVM()
ic.VM.LoadScript([]byte{byte(opcode.RET)})
require.NoError(t, bc.contracts.Designate.DesignateAsRole(ic, native.RoleP2PNotary, keys.PublicKeys{notary.PrivateKey().PublicKey()}))
require.NoError(t, bc.contracts.Designate.OnPersistEnd(ic.DAO))
_, err = ic.DAO.Persist()
require.NoError(t, err)
getNotaryAssistedTx := func(signaturesCount uint8, serviceFee int64) *transaction.Transaction {
tx := bc.newTestTx(h, testScript)
tx.Attributes = append(tx.Attributes, transaction.Attribute{Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{
NKeys: signaturesCount,
}})
tx.NetworkFee += serviceFee // additional fee for NotaryAssisted attribute
tx.NetworkFee += 4_000_000 // multisig check
tx.Signers = []transaction.Signer{{
Account: testchain.CommitteeScriptHash(),
Scopes: transaction.None,
},
{
Account: bc.contracts.Notary.Hash,
Scopes: transaction.None,
},
}
rawScript := testchain.CommitteeVerificationScript()
size := io.GetVarSize(tx)
netFee, sizeDelta := fee.Calculate(rawScript)
tx.NetworkFee += netFee
tx.NetworkFee += int64(size+sizeDelta) * bc.FeePerByte()
data := tx.GetSignedPart()
tx.Scripts = []transaction.Witness{
{
InvocationScript: testchain.SignCommittee(data),
VerificationScript: rawScript,
},
{
InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, notary.PrivateKey().Sign(data)...),
},
}
return tx
}
t.Run("Disabled", func(t *testing.T) {
bc.config.P2PSigExtensions = false
tx := getNotaryAssistedTx(0, 0)
require.True(t, errors.Is(bc.VerifyTx(tx), ErrInvalidAttribute))
})
t.Run("Enabled, insufficient network fee", func(t *testing.T) {
bc.config.P2PSigExtensions = true
tx := getNotaryAssistedTx(1, 0)
require.Error(t, bc.VerifyTx(tx))
})
t.Run("Test verify", func(t *testing.T) {
bc.config.P2PSigExtensions = true
t.Run("no NotaryAssisted attribute", func(t *testing.T) {
tx := getNotaryAssistedTx(1, (1+1)*transaction.NotaryServiceFeePerKey)
tx.Attributes = []transaction.Attribute{}
tx.Signers = []transaction.Signer{
{
Account: testchain.CommitteeScriptHash(),
Scopes: transaction.None,
},
{
Account: bc.contracts.Notary.Hash,
Scopes: transaction.None,
},
}
data := tx.GetSignedPart()
tx.Scripts = []transaction.Witness{
{
InvocationScript: testchain.SignCommittee(data),
VerificationScript: testchain.CommitteeVerificationScript(),
},
{
InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, notary.PrivateKey().Sign(data)...),
},
}
require.Error(t, bc.VerifyTx(tx))
})
t.Run("no deposit", func(t *testing.T) {
tx := getNotaryAssistedTx(1, (1+1)*transaction.NotaryServiceFeePerKey)
tx.Signers = []transaction.Signer{
{
Account: bc.contracts.Notary.Hash,
Scopes: transaction.None,
},
{
Account: testchain.CommitteeScriptHash(),
Scopes: transaction.None,
},
}
data := tx.GetSignedPart()
tx.Scripts = []transaction.Witness{
{
InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, notary.PrivateKey().Sign(data)...),
},
{
InvocationScript: testchain.SignCommittee(data),
VerificationScript: testchain.CommitteeVerificationScript(),
},
}
require.Error(t, bc.VerifyTx(tx))
})
t.Run("bad Notary signer scope", func(t *testing.T) {
tx := getNotaryAssistedTx(1, (1+1)*transaction.NotaryServiceFeePerKey)
tx.Signers = []transaction.Signer{
{
Account: testchain.CommitteeScriptHash(),
Scopes: transaction.None,
},
{
Account: bc.contracts.Notary.Hash,
Scopes: transaction.CalledByEntry,
},
}
data := tx.GetSignedPart()
tx.Scripts = []transaction.Witness{
{
InvocationScript: testchain.SignCommittee(data),
VerificationScript: testchain.CommitteeVerificationScript(),
},
{
InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, notary.PrivateKey().Sign(data)...),
},
}
require.Error(t, bc.VerifyTx(tx))
})
t.Run("not signed by Notary", func(t *testing.T) {
tx := getNotaryAssistedTx(1, (1+1)*transaction.NotaryServiceFeePerKey)
tx.Signers = []transaction.Signer{
{
Account: testchain.CommitteeScriptHash(),
Scopes: transaction.None,
},
}
data := tx.GetSignedPart()
tx.Scripts = []transaction.Witness{
{
InvocationScript: testchain.SignCommittee(data),
VerificationScript: testchain.CommitteeVerificationScript(),
},
}
require.Error(t, bc.VerifyTx(tx))
})
t.Run("bad Notary node witness", func(t *testing.T) {
tx := getNotaryAssistedTx(1, (1+1)*transaction.NotaryServiceFeePerKey)
tx.Signers = []transaction.Signer{
{
Account: testchain.CommitteeScriptHash(),
Scopes: transaction.None,
},
{
Account: bc.contracts.Notary.Hash,
Scopes: transaction.None,
},
}
data := tx.GetSignedPart()
acc, err := keys.NewPrivateKey()
require.NoError(t, err)
tx.Scripts = []transaction.Witness{
{
InvocationScript: testchain.SignCommittee(data),
VerificationScript: testchain.CommitteeVerificationScript(),
},
{
InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, acc.Sign(data)...),
},
}
require.Error(t, bc.VerifyTx(tx))
})
t.Run("missing payer", func(t *testing.T) {
tx := getNotaryAssistedTx(1, (1+1)*transaction.NotaryServiceFeePerKey)
tx.Signers = []transaction.Signer{
{
Account: bc.contracts.Notary.Hash,
Scopes: transaction.None,
},
}
data := tx.GetSignedPart()
tx.Scripts = []transaction.Witness{
{
InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, notary.PrivateKey().Sign(data)...),
},
}
require.Error(t, bc.VerifyTx(tx))
})
t.Run("positive", func(t *testing.T) {
tx := getNotaryAssistedTx(1, (1+1)*transaction.NotaryServiceFeePerKey)
require.NoError(t, bc.VerifyTx(tx))
})
})
})
})
}

Expand Down
Loading

0 comments on commit ef15139

Please sign in to comment.