forked from TrueCloudLab/neoneo-go
Merge pull request #2658 from nspcc-dev/calculate-network-fee-fixes
calculatenetworkfee improvements
This commit is contained in:
commit
bf06b32278
9 changed files with 261 additions and 64 deletions
|
@ -355,26 +355,10 @@ the steps to create a signature request:
|
||||||
`Contract` field. That's needed to skip notary verification during regular
|
`Contract` field. That's needed to skip notary verification during regular
|
||||||
network fee calculation at the next step.
|
network fee calculation at the next step.
|
||||||
|
|
||||||
7. Calculate network fee for the transaction (that will be `NetworkFee`
|
6. Fill in the main transaction `Nonce` field.
|
||||||
transaction field). Network fee consists of several parts:
|
7. Construct a list of main transactions witnesses (that will be `Scripts`
|
||||||
- *Notary network fee.* That's the amount of GAS needed to be paid for
|
transaction field). Uses standard rules for witnesses of not yet signed
|
||||||
`NotaryAssisted` attribute usage and for notary contract witness
|
transaction (it can't be signed at this stage because network fee is missing):
|
||||||
verification (that is to be added by the notary node in the end of
|
|
||||||
signature collection process). Use
|
|
||||||
[func (*Client) CalculateNotaryFee](https://pkg.go.dev/github.com/nspcc-dev/neo-go@v0.97.2/pkg/rpcclient#Client.CalculateNotaryFee)
|
|
||||||
to calculate notary network fee. Use `NKeys` estimated at step 4 as an
|
|
||||||
argument.
|
|
||||||
- *Regular network fee.* That's the amount of GAS to be paid for other witnesses
|
|
||||||
verification. Use
|
|
||||||
[func (*Client) AddNetworkFee](https://pkg.go.dev/github.com/nspcc-dev/neo-go@v0.97.2/pkg/rpcclient#Client.AddNetworkFee)
|
|
||||||
to calculate regular network fee and add it to the transaction. Use
|
|
||||||
partially-filled main transaction from the previous steps as `tx` argument.
|
|
||||||
Use notary network fee calculated at the previous substep as `extraFee`
|
|
||||||
argument. Use the list of accounts constructed at step 5 as `accs`
|
|
||||||
argument.
|
|
||||||
8. Fill in the main transaction `Nonce` field.
|
|
||||||
9. Construct a list of main transactions witnesses (that will be `Scripts`
|
|
||||||
transaction field). Use the following rules:
|
|
||||||
- A contract-based witness should have `Invocation` script that pushes arguments
|
- A contract-based witness should have `Invocation` script that pushes arguments
|
||||||
on stack (it may be empty) and empty `Verification` script. If multiple notary
|
on stack (it may be empty) and empty `Verification` script. If multiple notary
|
||||||
requests provide different `Invocation` scripts, the first one will be used
|
requests provide different `Invocation` scripts, the first one will be used
|
||||||
|
@ -386,13 +370,17 @@ the steps to create a signature request:
|
||||||
- A standard signature witness must have regular `Verification` script filled
|
- A standard signature witness must have regular `Verification` script filled
|
||||||
even if the `Invocation` script is to be collected from other notary
|
even if the `Invocation` script is to be collected from other notary
|
||||||
requests.
|
requests.
|
||||||
`Invocation` script either should push signature bytes on stack **or** (in
|
`Invocation` script **should be empty**.
|
||||||
case the signature is to be collected) **should be empty**.
|
|
||||||
- A multisignature witness must have regular `Verification` script filled even
|
- A multisignature witness must have regular `Verification` script filled even
|
||||||
if `Invocation` script is to be collected from other notary requests.
|
if `Invocation` script is to be collected from other notary requests.
|
||||||
`Invocation` script either should push on stack signature bytes (one
|
`Invocation` script either **should be empty**.
|
||||||
signature at max per one request) **or** (in case there's no ability to
|
8. Calculate network fee for the transaction (that will be `NetworkFee`
|
||||||
provide proper signature) **should be empty**.
|
transaction field). Use [func (*Client) CalculateNetworkFee](https://pkg.go.dev/github.com/nspcc-dev/neo-go@v0.99.2/pkg/rpcclient#Client.CalculateNetworkFee)
|
||||||
|
method with the main transaction given to it.
|
||||||
|
9. Fill in all signatures that can be provded by the client creating request,
|
||||||
|
that includes simple-signature accounts and multisignature accounts where
|
||||||
|
the client has one of the keys (in which case an invocation script is
|
||||||
|
created that pushes just one signature onto the stack).
|
||||||
10. Define lifetime for the fallback transaction. Let the `fallbackValidFor` be
|
10. Define lifetime for the fallback transaction. Let the `fallbackValidFor` be
|
||||||
the lifetime. Let `N` be the current chain's height and `VUB` be
|
the lifetime. Let `N` be the current chain's height and `VUB` be
|
||||||
`ValidUntilBlock` value estimated at step 3. Then, the notary node is trying to
|
`ValidUntilBlock` value estimated at step 3. Then, the notary node is trying to
|
||||||
|
@ -409,7 +397,7 @@ the steps to create a signature request:
|
||||||
special on fallback invocation, you can use simple `opcode.RET` script.
|
special on fallback invocation, you can use simple `opcode.RET` script.
|
||||||
12. Sign and submit P2P notary request. Use
|
12. Sign and submit P2P notary request. Use
|
||||||
[func (*Client) SignAndPushP2PNotaryRequest](https://pkg.go.dev/github.com/nspcc-dev/neo-go@v0.97.2/pkg/rpcclient#Client.SignAndPushP2PNotaryRequest) for it.
|
[func (*Client) SignAndPushP2PNotaryRequest](https://pkg.go.dev/github.com/nspcc-dev/neo-go@v0.97.2/pkg/rpcclient#Client.SignAndPushP2PNotaryRequest) for it.
|
||||||
- Use the signed main transaction from step 8 as `mainTx` argument.
|
- Use the signed main transaction from step 9 as `mainTx` argument.
|
||||||
- Use the fallback script from step 10 as `fallbackScript` argument.
|
- Use the fallback script from step 10 as `fallbackScript` argument.
|
||||||
- Use `-1` as `fallbackSysFee` argument to define system fee by test
|
- Use `-1` as `fallbackSysFee` argument to define system fee by test
|
||||||
invocation or provide any custom value.
|
invocation or provide any custom value.
|
||||||
|
|
15
docs/rpc.md
15
docs/rpc.md
|
@ -98,6 +98,21 @@ following data types:
|
||||||
|
|
||||||
Any call that takes any of these types for input in JSON format is affected.
|
Any call that takes any of these types for input in JSON format is affected.
|
||||||
|
|
||||||
|
##### `calculatenetworkfee`
|
||||||
|
|
||||||
|
NeoGo tries to cover more cases with its calculatenetworkfee implementation,
|
||||||
|
whereas C# node support only standard signature contracts and deployed
|
||||||
|
contracts that can execute `verify` successfully on incomplete (not yet signed
|
||||||
|
properly) transaction, NeoGo also works with deployed contracts that fail at
|
||||||
|
this stage and executes non-standard contracts (that can fail
|
||||||
|
too). It's ignoring the result of any verification script (since the method
|
||||||
|
calculates fee and doesn't care about transaction validity). Invocation script
|
||||||
|
is used as is when provided, but absent it the system will try to infer one
|
||||||
|
based on the `verify` method signature (pushing dummy signatures or
|
||||||
|
hashes). If signature has some types which contents can't be adequately
|
||||||
|
guessed (arrays, maps, interop, void) they're ignored. See
|
||||||
|
neo-project/neo#2805 as well.
|
||||||
|
|
||||||
##### `invokefunction`, `invokescript`
|
##### `invokefunction`, `invokescript`
|
||||||
|
|
||||||
neo-go implementation of `invokefunction` does not return `tx`
|
neo-go implementation of `invokefunction` does not return `tx`
|
||||||
|
|
|
@ -2337,6 +2337,9 @@ func (bc *Blockchain) InitVerificationContext(ic *interop.Context, hash util.Uin
|
||||||
func (bc *Blockchain) VerifyWitness(h util.Uint160, c hash.Hashable, w *transaction.Witness, gas int64) (int64, error) {
|
func (bc *Blockchain) VerifyWitness(h util.Uint160, c hash.Hashable, w *transaction.Witness, gas int64) (int64, error) {
|
||||||
ic := bc.newInteropContext(trigger.Verification, bc.dao, nil, nil)
|
ic := bc.newInteropContext(trigger.Verification, bc.dao, nil, nil)
|
||||||
ic.Container = c
|
ic.Container = c
|
||||||
|
if tx, ok := c.(*transaction.Transaction); ok {
|
||||||
|
ic.Tx = tx
|
||||||
|
}
|
||||||
return bc.verifyHashAgainstScript(h, w, ic, gas)
|
return bc.verifyHashAgainstScript(h, w, ic, gas)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -912,23 +912,22 @@ func (c *Client) SignAndPushP2PNotaryRequest(mainTx *transaction.Transaction, fa
|
||||||
Value: &transaction.Conflicts{Hash: mainTx.Hash()},
|
Value: &transaction.Conflicts{Hash: mainTx.Hash()},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
extraNetFee, err := c.CalculateNotaryFee(0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
fallbackNetFee += extraNetFee
|
|
||||||
|
|
||||||
dummyAccount := &wallet.Account{Contract: &wallet.Contract{Deployed: false}} // don't call `verify` for Notary contract witness, because it will fail
|
|
||||||
err = c.AddNetworkFee(fallbackTx, fallbackNetFee, dummyAccount, acc)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to add network fee: %w", err)
|
|
||||||
}
|
|
||||||
fallbackTx.Scripts = []transaction.Witness{
|
fallbackTx.Scripts = []transaction.Witness{
|
||||||
{
|
{
|
||||||
InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, make([]byte, 64)...),
|
InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, make([]byte, 64)...),
|
||||||
VerificationScript: []byte{},
|
VerificationScript: []byte{},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
InvocationScript: []byte{},
|
||||||
|
VerificationScript: acc.GetVerificationScript(),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
fallbackTx.NetworkFee, err = c.CalculateNetworkFee(fallbackTx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to add network fee: %w", err)
|
||||||
|
}
|
||||||
|
fallbackTx.NetworkFee += fallbackNetFee
|
||||||
m, err := c.GetNetwork()
|
m, err := c.GetNetwork()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to sign fallback tx: %w", err)
|
return nil, fmt.Errorf("failed to sign fallback tx: %w", err)
|
||||||
|
@ -957,6 +956,9 @@ func (c *Client) SignAndPushP2PNotaryRequest(mainTx *transaction.Transaction, fa
|
||||||
|
|
||||||
// CalculateNotaryFee calculates network fee for one dummy Notary witness and NotaryAssisted attribute with NKeys specified.
|
// CalculateNotaryFee calculates network fee for one dummy Notary witness and NotaryAssisted attribute with NKeys specified.
|
||||||
// The result should be added to the transaction's net fee for successful verification.
|
// The result should be added to the transaction's net fee for successful verification.
|
||||||
|
//
|
||||||
|
// Deprecated: NeoGo calculatenetworkfee method handles notary fees as well since 0.99.3, so
|
||||||
|
// this method is just no longer needed and will be removed in future versions.
|
||||||
func (c *Client) CalculateNotaryFee(nKeys uint8) (int64, error) {
|
func (c *Client) CalculateNotaryFee(nKeys uint8) (int64, error) {
|
||||||
baseExecFee, err := c.GetExecFeeFactor()
|
baseExecFee, err := c.GetExecFeeFactor()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -998,8 +998,8 @@ func TestCalculateNotaryFee(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
t.Run("client not initialized", func(t *testing.T) {
|
t.Run("client not initialized", func(t *testing.T) {
|
||||||
_, err := c.CalculateNotaryFee(0)
|
_, err := c.CalculateNotaryFee(0) //nolint:staticcheck // SA1019: c.CalculateNotaryFee is deprecated
|
||||||
require.NoError(t, err) // Do not require client initialisation for this.
|
require.NoError(t, err) // Do not require client initialisation for this.
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,6 @@ import (
|
||||||
"github.com/nspcc-dev/neo-go/pkg/config/netmode"
|
"github.com/nspcc-dev/neo-go/pkg/config/netmode"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core"
|
"github.com/nspcc-dev/neo-go/pkg/core"
|
||||||
"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/fee"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/interop"
|
"github.com/nspcc-dev/neo-go/pkg/core/interop"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/interop/iterator"
|
"github.com/nspcc-dev/neo-go/pkg/core/interop/iterator"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/mempool"
|
"github.com/nspcc-dev/neo-go/pkg/core/mempool"
|
||||||
|
@ -45,9 +44,12 @@ import (
|
||||||
"github.com/nspcc-dev/neo-go/pkg/network/payload"
|
"github.com/nspcc-dev/neo-go/pkg/network/payload"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/services/oracle/broadcaster"
|
"github.com/nspcc-dev/neo-go/pkg/services/oracle/broadcaster"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/services/rpcsrv/params"
|
"github.com/nspcc-dev/neo-go/pkg/services/rpcsrv/params"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
|
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
||||||
|
@ -758,36 +760,50 @@ func (s *Server) calculateNetworkFee(reqParams params.Params) (interface{}, *neo
|
||||||
return 0, neorpc.WrapErrorWithData(neorpc.ErrInvalidParams, fmt.Sprintf("failed to compute tx size: %s", err))
|
return 0, neorpc.WrapErrorWithData(neorpc.ErrInvalidParams, fmt.Sprintf("failed to compute tx size: %s", err))
|
||||||
}
|
}
|
||||||
size := len(hashablePart) + io.GetVarSize(len(tx.Signers))
|
size := len(hashablePart) + io.GetVarSize(len(tx.Signers))
|
||||||
var (
|
var netFee int64
|
||||||
ef int64
|
|
||||||
netFee int64
|
|
||||||
)
|
|
||||||
for i, signer := range tx.Signers {
|
for i, signer := range tx.Signers {
|
||||||
var verificationScript []byte
|
w := tx.Scripts[i]
|
||||||
for _, w := range tx.Scripts {
|
if len(w.InvocationScript) == 0 { // No invocation provided, try to infer one.
|
||||||
if w.VerificationScript != nil && hash.Hash160(w.VerificationScript).Equals(signer.Account) {
|
var paramz []manifest.Parameter
|
||||||
// then it's a standard sig/multisig witness
|
if len(w.VerificationScript) == 0 { // Contract-based verification
|
||||||
verificationScript = w.VerificationScript
|
cs := s.chain.GetContractState(signer.Account)
|
||||||
break
|
if cs == nil {
|
||||||
|
return 0, neorpc.WrapErrorWithData(neorpc.ErrInvalidParams, fmt.Sprintf("signer %d has no verification script and no deployed contract", i))
|
||||||
|
}
|
||||||
|
md := cs.Manifest.ABI.GetMethod(manifest.MethodVerify, -1)
|
||||||
|
if md == nil || md.ReturnType != smartcontract.BoolType {
|
||||||
|
return 0, neorpc.WrapErrorWithData(neorpc.ErrInvalidParams, fmt.Sprintf("signer %d has no verify method in deployed contract", i))
|
||||||
|
}
|
||||||
|
paramz = md.Parameters // Might as well have none params and it's OK.
|
||||||
|
} else { // Regular signature verification.
|
||||||
|
if vm.IsSignatureContract(w.VerificationScript) {
|
||||||
|
paramz = []manifest.Parameter{{Type: smartcontract.SignatureType}}
|
||||||
|
} else if nSigs, _, ok := vm.ParseMultiSigContract(w.VerificationScript); ok {
|
||||||
|
paramz = make([]manifest.Parameter, nSigs)
|
||||||
|
for j := 0; j < nSigs; j++ {
|
||||||
|
paramz[j] = manifest.Parameter{Type: smartcontract.SignatureType}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
inv := io.NewBufBinWriter()
|
||||||
if verificationScript == nil { // then it still might be a contract-based verification
|
for _, p := range paramz {
|
||||||
gasConsumed, err := s.chain.VerifyWitness(signer.Account, tx, &tx.Scripts[i], int64(s.config.MaxGasInvoke))
|
p.Type.EncodeDefaultValue(inv.BinWriter)
|
||||||
if err != nil {
|
|
||||||
return 0, neorpc.NewRPCError("Invalid signature", fmt.Sprintf("contract verification for signer #%d failed: %s", i, err))
|
|
||||||
}
|
}
|
||||||
netFee += gasConsumed
|
if inv.Err != nil {
|
||||||
size += io.GetVarSize([]byte{}) + // verification script is empty (contract-based witness)
|
return nil, neorpc.NewInternalServerError(fmt.Sprintf("failed to create dummy invocation script (signer %d): %s", i, inv.Err.Error()))
|
||||||
io.GetVarSize(tx.Scripts[i].InvocationScript) // invocation script might not be empty (args for `verify`)
|
}
|
||||||
continue
|
w.InvocationScript = inv.Bytes()
|
||||||
}
|
}
|
||||||
|
gasConsumed, _ := s.chain.VerifyWitness(signer.Account, tx, &w, int64(s.config.MaxGasInvoke))
|
||||||
if ef == 0 {
|
netFee += gasConsumed
|
||||||
ef = s.chain.GetBaseExecFee()
|
size += io.GetVarSize(w.VerificationScript) + io.GetVarSize(w.InvocationScript)
|
||||||
|
}
|
||||||
|
if s.chain.P2PSigExtensionsEnabled() {
|
||||||
|
attrs := tx.GetAttributes(transaction.NotaryAssistedT)
|
||||||
|
if len(attrs) != 0 {
|
||||||
|
na := attrs[0].Value.(*transaction.NotaryAssisted)
|
||||||
|
netFee += (int64(na.NKeys) + 1) * s.chain.GetNotaryServiceFeePerKey()
|
||||||
}
|
}
|
||||||
fee, sizeDelta := fee.Calculate(ef, verificationScript)
|
|
||||||
netFee += fee
|
|
||||||
size += sizeDelta
|
|
||||||
}
|
}
|
||||||
fee := s.chain.FeePerByte()
|
fee := s.chain.FeePerByte()
|
||||||
netFee += int64(size) * fee
|
netFee += int64(size) * fee
|
||||||
|
|
|
@ -39,6 +39,7 @@ import (
|
||||||
"github.com/nspcc-dev/neo-go/pkg/network/payload"
|
"github.com/nspcc-dev/neo-go/pkg/network/payload"
|
||||||
rpc2 "github.com/nspcc-dev/neo-go/pkg/services/oracle/broadcaster"
|
rpc2 "github.com/nspcc-dev/neo-go/pkg/services/oracle/broadcaster"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/services/rpcsrv/params"
|
"github.com/nspcc-dev/neo-go/pkg/services/rpcsrv/params"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
||||||
|
@ -2453,6 +2454,122 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) []
|
||||||
}, 2*time.Duration(rpcSrv.config.SessionExpirationTime)*time.Second, 10*time.Millisecond)
|
}, 2*time.Duration(rpcSrv.config.SessionExpirationTime)*time.Second, 10*time.Millisecond)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
t.Run("calculatenetworkfee", func(t *testing.T) {
|
||||||
|
t.Run("no parameters", func(t *testing.T) {
|
||||||
|
body := doRPCCall(`{"jsonrpc": "2.0", "id": 1, "method": "calculatenetworkfee", "params": []}"`, httpSrv.URL, t)
|
||||||
|
_ = checkErrGetResult(t, body, true, "Invalid Params")
|
||||||
|
})
|
||||||
|
t.Run("non-base64 parameter", func(t *testing.T) {
|
||||||
|
body := doRPCCall(`{"jsonrpc": "2.0", "id": 1, "method": "calculatenetworkfee", "params": ["noatbase64"]}"`, httpSrv.URL, t)
|
||||||
|
_ = checkErrGetResult(t, body, true, "Invalid Params")
|
||||||
|
})
|
||||||
|
t.Run("non-transaction parameter", func(t *testing.T) {
|
||||||
|
body := doRPCCall(`{"jsonrpc": "2.0", "id": 1, "method": "calculatenetworkfee", "params": ["bm90IGEgdHJhbnNhY3Rpb24K"]}"`, httpSrv.URL, t)
|
||||||
|
_ = checkErrGetResult(t, body, true, "Invalid Params")
|
||||||
|
})
|
||||||
|
calcReq := func(t *testing.T, tx *transaction.Transaction) []byte {
|
||||||
|
rpc := fmt.Sprintf(`{"jsonrpc": "2.0", "id": 1, "method": "calculatenetworkfee", "params": ["%s"]}"`, base64.StdEncoding.EncodeToString(tx.Bytes()))
|
||||||
|
return doRPCCall(rpc, httpSrv.URL, t)
|
||||||
|
}
|
||||||
|
t.Run("non-contract with zero verification", func(t *testing.T) {
|
||||||
|
tx := &transaction.Transaction{
|
||||||
|
Script: []byte{byte(opcode.RET)},
|
||||||
|
Signers: []transaction.Signer{{Account: util.Uint160{1, 2, 3}, Scopes: transaction.CalledByEntry}},
|
||||||
|
Scripts: []transaction.Witness{{
|
||||||
|
InvocationScript: []byte{},
|
||||||
|
VerificationScript: []byte{},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
body := calcReq(t, tx)
|
||||||
|
_ = checkErrGetResult(t, body, true, "signer 0 has no verification script and no deployed contract")
|
||||||
|
})
|
||||||
|
t.Run("contract with no verify", func(t *testing.T) {
|
||||||
|
tx := &transaction.Transaction{
|
||||||
|
Script: []byte{byte(opcode.RET)},
|
||||||
|
Signers: []transaction.Signer{{Account: nnsHash, Scopes: transaction.CalledByEntry}},
|
||||||
|
Scripts: []transaction.Witness{{
|
||||||
|
InvocationScript: []byte{},
|
||||||
|
VerificationScript: []byte{},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
body := calcReq(t, tx)
|
||||||
|
_ = checkErrGetResult(t, body, true, "signer 0 has no verify method in deployed contract")
|
||||||
|
})
|
||||||
|
checkCalc := func(t *testing.T, tx *transaction.Transaction, fee int64) {
|
||||||
|
resp := checkErrGetResult(t, calcReq(t, tx), false)
|
||||||
|
res := new(result.NetworkFee)
|
||||||
|
require.NoError(t, json.Unmarshal(resp, res))
|
||||||
|
require.Equal(t, fee, res.Value)
|
||||||
|
}
|
||||||
|
t.Run("simple GAS transfer", func(t *testing.T) {
|
||||||
|
priv0 := testchain.PrivateKeyByID(0)
|
||||||
|
script, err := smartcontract.CreateCallWithAssertScript(chain.UtilityTokenHash(), "transfer",
|
||||||
|
priv0.GetScriptHash(), priv0.GetScriptHash(), 1, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
tx := &transaction.Transaction{
|
||||||
|
Script: script,
|
||||||
|
Signers: []transaction.Signer{{Account: priv0.GetScriptHash(), Scopes: transaction.CalledByEntry}},
|
||||||
|
Scripts: []transaction.Witness{{
|
||||||
|
InvocationScript: []byte{},
|
||||||
|
VerificationScript: priv0.PublicKey().GetVerificationScript(),
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
checkCalc(t, tx, 1228520) // Perfectly matches FeeIsSignatureContractDetailed() C# test.
|
||||||
|
})
|
||||||
|
t.Run("multisignature tx", func(t *testing.T) {
|
||||||
|
priv0 := testchain.PrivateKeyByID(0)
|
||||||
|
priv1 := testchain.PrivateKeyByID(1)
|
||||||
|
accScript, err := smartcontract.CreateDefaultMultiSigRedeemScript(keys.PublicKeys{priv0.PublicKey(), priv1.PublicKey()})
|
||||||
|
require.NoError(t, err)
|
||||||
|
multiAcc := hash.Hash160(accScript)
|
||||||
|
txScript, err := smartcontract.CreateCallWithAssertScript(chain.UtilityTokenHash(), "transfer",
|
||||||
|
multiAcc, priv0.GetScriptHash(), 1, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
tx := &transaction.Transaction{
|
||||||
|
Script: txScript,
|
||||||
|
Signers: []transaction.Signer{{Account: multiAcc, Scopes: transaction.CalledByEntry}},
|
||||||
|
Scripts: []transaction.Witness{{
|
||||||
|
InvocationScript: []byte{},
|
||||||
|
VerificationScript: accScript,
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
checkCalc(t, tx, 2315100) // Perfectly matches FeeIsMultiSigContract() C# test.
|
||||||
|
})
|
||||||
|
checkContract := func(t *testing.T, verAcc util.Uint160, invoc []byte, fee int64) {
|
||||||
|
txScript, err := smartcontract.CreateCallWithAssertScript(chain.UtilityTokenHash(), "transfer",
|
||||||
|
verAcc, verAcc, 1, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
tx := &transaction.Transaction{
|
||||||
|
Script: txScript,
|
||||||
|
Signers: []transaction.Signer{{Account: verAcc, Scopes: transaction.CalledByEntry}},
|
||||||
|
Scripts: []transaction.Witness{{
|
||||||
|
InvocationScript: invoc,
|
||||||
|
VerificationScript: []byte{},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
checkCalc(t, tx, fee)
|
||||||
|
}
|
||||||
|
t.Run("contract-based verification", func(t *testing.T) {
|
||||||
|
verAcc, err := util.Uint160DecodeStringLE(verifyContractHash)
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkContract(t, verAcc, []byte{}, 636610) // No C# match, but we believe it's OK.
|
||||||
|
})
|
||||||
|
t.Run("contract-based verification with parameters", func(t *testing.T) {
|
||||||
|
verAcc, err := util.Uint160DecodeStringLE(verifyWithArgsContractHash)
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkContract(t, verAcc, []byte{}, 737530) // No C# match, but we believe it's OK and it differs from the one above.
|
||||||
|
})
|
||||||
|
t.Run("contract-based verification with invocation script", func(t *testing.T) {
|
||||||
|
verAcc, err := util.Uint160DecodeStringLE(verifyWithArgsContractHash)
|
||||||
|
require.NoError(t, err)
|
||||||
|
invocWriter := io.NewBufBinWriter()
|
||||||
|
emit.Bool(invocWriter.BinWriter, false)
|
||||||
|
emit.Int(invocWriter.BinWriter, 5)
|
||||||
|
emit.String(invocWriter.BinWriter, "")
|
||||||
|
invocScript := invocWriter.Bytes()
|
||||||
|
checkContract(t, verAcc, invocScript, 640360) // No C# match, but we believe it's OK and it has a specific invocation script overriding anything server-side.
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *executor) getHeader(s string) *block.Header {
|
func (e *executor) getHeader(s string) *block.Header {
|
||||||
|
|
|
@ -12,6 +12,8 @@ import (
|
||||||
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
|
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -139,6 +141,35 @@ func (pt *ParamType) DecodeBinary(r *io.BinReader) {
|
||||||
*pt = ParamType(r.ReadB())
|
*pt = ParamType(r.ReadB())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EncodeDefaultValue writes a script to push the default parameter value onto
|
||||||
|
// the evaluation stack into the given writer. It's mostly useful for constructing
|
||||||
|
// dummy invocation scripts when parameter types are known, but they can't be
|
||||||
|
// filled in. A best effort approach is used, it can't be perfect since for many
|
||||||
|
// types the exact values can be arbitrarily long, but it tries to do something
|
||||||
|
// reasonable in each case. For signatures, strings, arrays and "any" type a 64-byte
|
||||||
|
// zero-filled value is used, hash160 and hash256 use appropriately sized values,
|
||||||
|
// public key is represented by 33-byte value while 32 bytes are used for integer
|
||||||
|
// and a simple push+convert is used for boolean. Other types produce no code at all.
|
||||||
|
func (pt ParamType) EncodeDefaultValue(w *io.BinWriter) {
|
||||||
|
var b [64]byte
|
||||||
|
|
||||||
|
switch pt {
|
||||||
|
case AnyType, SignatureType, StringType, ByteArrayType:
|
||||||
|
emit.Bytes(w, b[:])
|
||||||
|
case BoolType:
|
||||||
|
emit.Bool(w, true)
|
||||||
|
case IntegerType:
|
||||||
|
emit.Instruction(w, opcode.PUSHINT256, b[:32])
|
||||||
|
case Hash160Type:
|
||||||
|
emit.Bytes(w, b[:20])
|
||||||
|
case Hash256Type:
|
||||||
|
emit.Bytes(w, b[:32])
|
||||||
|
case PublicKeyType:
|
||||||
|
emit.Bytes(w, b[:33])
|
||||||
|
case ArrayType, MapType, InteropInterfaceType, VoidType:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ParseParamType is a user-friendly string to ParamType converter, it's
|
// ParseParamType is a user-friendly string to ParamType converter, it's
|
||||||
// case-insensitive and makes the following conversions:
|
// case-insensitive and makes the following conversions:
|
||||||
//
|
//
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"math/big"
|
"math/big"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
@ -335,6 +336,30 @@ func TestAdjustValToType(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEncodeDefaultValue(t *testing.T) {
|
||||||
|
for p, l := range map[ParamType]int{
|
||||||
|
UnknownType: 0,
|
||||||
|
AnyType: 66,
|
||||||
|
BoolType: 3,
|
||||||
|
IntegerType: 33,
|
||||||
|
ByteArrayType: 66,
|
||||||
|
StringType: 66,
|
||||||
|
Hash160Type: 22,
|
||||||
|
Hash256Type: 34,
|
||||||
|
PublicKeyType: 35,
|
||||||
|
SignatureType: 66,
|
||||||
|
ArrayType: 0,
|
||||||
|
MapType: 0,
|
||||||
|
InteropInterfaceType: 0,
|
||||||
|
VoidType: 0,
|
||||||
|
} {
|
||||||
|
b := io.NewBufBinWriter()
|
||||||
|
p.EncodeDefaultValue(b.BinWriter)
|
||||||
|
require.NoError(t, b.Err)
|
||||||
|
require.Equalf(t, l, len(b.Bytes()), p.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func mustHex(s string) []byte {
|
func mustHex(s string) []byte {
|
||||||
b, err := hex.DecodeString(s)
|
b, err := hex.DecodeString(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
Loading…
Reference in a new issue