Skip to content

Commit 27f824d

Browse files
committed
native: add test for custom Koblitz witness verification script
Every single thing is already implemented in the protocol for Koblitz verification scripts. Signed-off-by: Anna Shaleva <[email protected]>
1 parent 198af7f commit 27f824d

File tree

1 file changed

+354
-0
lines changed

1 file changed

+354
-0
lines changed
Lines changed: 354 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,354 @@
1+
package native_test
2+
3+
import (
4+
"math/big"
5+
"testing"
6+
7+
"github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames"
8+
"github.com/nspcc-dev/neo-go/pkg/core/native"
9+
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
10+
"github.com/nspcc-dev/neo-go/pkg/core/state"
11+
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
12+
"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
13+
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
14+
"github.com/nspcc-dev/neo-go/pkg/io"
15+
"github.com/nspcc-dev/neo-go/pkg/neotest"
16+
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
17+
"github.com/nspcc-dev/neo-go/pkg/util"
18+
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
19+
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
20+
"github.com/stretchr/testify/require"
21+
)
22+
23+
// TestCryptoLib_KoblitzVerificationScript builds transaction with custom witness that contains
24+
// the Koblitz tx signature bytes and Koblitz signature verification script.
25+
// This test ensures that transaction signed by Koblitz key passes verification and can
26+
// be successfully accepted to the chain.
27+
func TestCryptoLib_KoblitzVerificationScript(t *testing.T) {
28+
check := func(
29+
t *testing.T,
30+
buildVerificationScript func(t *testing.T, pub *keys.PublicKey) []byte,
31+
constructMsg func(t *testing.T, magic uint32, tx hash.Hashable) []byte,
32+
) {
33+
c := newGasClient(t)
34+
gasInvoker := c.WithSigners(c.Committee)
35+
e := c.Executor
36+
37+
// Consider the user that is able to sign txs only with Secp256k1 private key.
38+
// Let this user build, sign and push a GAS transfer transaction from its account
39+
// to some other account.
40+
pk, err := keys.NewSecp256k1PrivateKey()
41+
require.NoError(t, err)
42+
43+
// Firstly, we need to build the N3 user's account address based on the user's public key.
44+
// The address itself is Hash160 from the verification script corresponding to the user's public key.
45+
// Since user's private key belongs to Koblitz curve, we can't use System.Crypto.CheckSig interop
46+
// in the verification script. Likely, we have a 'verifyWithECDsa' method in native CriptoLib contract
47+
// that is able to check Koblitz signature. So let's build custom verification script based on this call.
48+
// The script should call 'verifyWithECDsa' method of native CriptoLib contract with Koblitz curve identifier
49+
// and check the provided message signature against the user's Koblitz public key.
50+
vrfBytes := buildVerificationScript(t, pk.PublicKey())
51+
52+
// Construct the user's account script hash. It's effectively a verification script hash.
53+
from := hash.Hash160(vrfBytes)
54+
55+
// Supply this account with some initial balance so that the user is able to pay for his transactions.
56+
gasInvoker.Invoke(t, true, "transfer", c.Committee.ScriptHash(), from, 10000_0000_0000, nil)
57+
58+
// Construct transaction that transfers 5 GAS from the user's account to some other account.
59+
to := util.Uint160{1, 2, 3}
60+
amount := 5
61+
tx := gasInvoker.PrepareInvokeNoSign(t, "transfer", from, to, amount, nil)
62+
tx.Signers = []transaction.Signer{
63+
{
64+
Account: from,
65+
Scopes: transaction.CalledByEntry,
66+
},
67+
}
68+
neotest.AddNetworkFee(t, e.Chain, tx)
69+
neotest.AddSystemFee(e.Chain, tx, -1)
70+
71+
// Add some more network fee to pay for the witness verification. This value may be calculated precisely,
72+
// but let's keep some inaccurate value for the test.
73+
tx.NetworkFee += 540_0000
74+
75+
// This transaction (along with the network magic) should be signed by the user's Koblitz private key.
76+
msg := constructMsg(t, uint32(e.Chain.GetConfig().Magic), tx)
77+
// Note: unlike N3 standard signing scheme, the user has to sign the **hash of the hash** of the [magic+txHash]
78+
// since during witness verification only transaction hash is available in the execution
79+
// context (not the whole serialized transaction bytes). And CryptoLib's 'verifyWithECDsa' method
80+
// takes as an argument unhashed message and then takes sha256 hash of this message.
81+
signature := pk.SignHash(hash.Sha256(msg))
82+
83+
// Ensure that signature verification passes. This line here is just for testing purposes,
84+
// it won't be present in the real code.
85+
require.True(t, pk.PublicKey().Verify(signature, hash.Sha256(msg).BytesBE()))
86+
87+
// Build invocation witness script for the user's account.
88+
invBytes := buildKoblitzInvocationScript(t, signature)
89+
90+
// Construct witness for signer #0 (the user itself).
91+
tx.Scripts = []transaction.Witness{
92+
{
93+
InvocationScript: invBytes,
94+
VerificationScript: vrfBytes,
95+
},
96+
}
97+
98+
// Add transaction to the chain. No error is expected on new block addition. Note, that this line performs
99+
// all those checks that are executed during transaction acceptance in the real network.
100+
e.AddNewBlock(t, tx)
101+
102+
// Double-check: ensure funds have been transferred.
103+
e.CheckGASBalance(t, to, big.NewInt(int64(amount)))
104+
}
105+
106+
// N3 compatible witness verification script has larger constant length and higher execution cost
107+
// (186 bytes, 5116020 GAS including Invocation script execution).
108+
check(t, buildKoblitzVerificationScriptCompat, constructMessageCompat)
109+
// N3 incompatible witness verification script has lower variable length and lower execution cost
110+
// (136 bytes, 4120620 GAS including Invocation script execution).
111+
check(t, buildKoblitzVerificationScriptSimple, constructMessageSimple)
112+
}
113+
114+
// buildKoblitzVerificationScript builds custom verification script for the provided Koblitz public key.
115+
// It checks that the following message is signed by the provided public key:
116+
//
117+
// sha256([4-bytes-network-magic-LE, txHash-bytes-BE])
118+
//
119+
// It produces constant-length verification script (186 bytes) independently of the network parameters.
120+
func buildKoblitzVerificationScriptCompat(t *testing.T, pub *keys.PublicKey) []byte {
121+
criptoLibH := state.CreateNativeContractHash(nativenames.CryptoLib)
122+
123+
// vrf is witness verification script corresponding to the pub.
124+
vrf := io.NewBufBinWriter()
125+
emit.Int(vrf.BinWriter, int64(native.Secp256k1)) // push Koblitz curve identifier.
126+
emit.Opcodes(vrf.BinWriter, opcode.SWAP) // swap curve identifier with the signature.
127+
emit.Bytes(vrf.BinWriter, pub.Bytes()) // emit the caller's public key.
128+
// Construct and push the signed message. The signed message is effectively the network-dependent transaction hash,
129+
// i.e. msg = Sha256([4-bytes-network-magic-LE, tx.Hash()])
130+
// Firstly, convert network magic (uint32) to LE buffer.
131+
emit.Syscall(vrf.BinWriter, interopnames.SystemRuntimeGetNetwork) // push network magic.
132+
// First byte: n & 0xFF
133+
emit.Opcodes(vrf.BinWriter, opcode.DUP)
134+
emit.Int(vrf.BinWriter, 0xFF) // TODO: this can be optimize in order not to allocate 0xFF every time, but need to compare execution price.
135+
emit.Opcodes(vrf.BinWriter, opcode.AND,
136+
opcode.SWAP, // Swap with the original network n.
137+
opcode.PUSH8,
138+
opcode.SHR)
139+
// Second byte: n >> 8 & 0xFF
140+
emit.Opcodes(vrf.BinWriter, opcode.DUP)
141+
emit.Int(vrf.BinWriter, 0xFF)
142+
emit.Opcodes(vrf.BinWriter, opcode.AND,
143+
opcode.SWAP, // Swap with the n >> 8.
144+
opcode.PUSH8,
145+
opcode.SHR)
146+
// Third byte: n >> 16 & 0xFF
147+
emit.Opcodes(vrf.BinWriter, opcode.DUP)
148+
emit.Int(vrf.BinWriter, 0xFF)
149+
emit.Opcodes(vrf.BinWriter, opcode.AND,
150+
opcode.SWAP, // Swap with the n >> 16.
151+
opcode.PUSH8,
152+
opcode.SHR)
153+
// Fourth byte: n >> 24 & 0xFF
154+
emit.Int(vrf.BinWriter, 0xFF) // no DUP is needed since it's the last shift.
155+
emit.Opcodes(vrf.BinWriter, opcode.AND)
156+
// Put these 4 bytes into buffer.
157+
emit.Opcodes(vrf.BinWriter, opcode.PUSH4, opcode.NEWBUFFER) // allocate new 4-bytes-length buffer.
158+
emit.Opcodes(vrf.BinWriter,
159+
// Set fourth byte.
160+
opcode.DUP, opcode.PUSH3,
161+
opcode.PUSH3, opcode.ROLL,
162+
opcode.SETITEM,
163+
// Set third byte.
164+
opcode.DUP, opcode.PUSH2,
165+
opcode.PUSH3, opcode.ROLL,
166+
opcode.SETITEM,
167+
// Set second byte.
168+
opcode.DUP, opcode.PUSH1,
169+
opcode.PUSH3, opcode.ROLL,
170+
opcode.SETITEM,
171+
// Set first byte.
172+
opcode.DUP, opcode.PUSH0,
173+
opcode.PUSH3, opcode.ROLL,
174+
opcode.SETITEM)
175+
// Retrieve executing transaction hash.
176+
emit.Syscall(vrf.BinWriter, interopnames.SystemRuntimeGetScriptContainer) // push the script container (executing transaction, actually).
177+
emit.Opcodes(vrf.BinWriter, opcode.PUSH0, opcode.PICKITEM, // pick 0-th transaction item (the transaction hash).
178+
opcode.CAT, // concatenate network magic and transaction hash.
179+
opcode.PUSH1, // push 1 (the number of arguments of 'sha256' method of native CryptoLib).
180+
opcode.PACK) // pack arguments for 'sha256' call.
181+
emit.AppCallNoArgs(vrf.BinWriter, criptoLibH, "sha256", callflag.All) // emit the call to 'sha256' itself.
182+
// Continue construction of 'verifyWithECDsa' call.
183+
emit.Opcodes(vrf.BinWriter, opcode.PUSH4, opcode.PACK) // pack arguments for 'verifyWithECDsa' call.
184+
emit.AppCallNoArgs(vrf.BinWriter, criptoLibH, "verifyWithECDsa", callflag.All) // emit the call to 'verifyWithECDsa' itself.
185+
require.NoError(t, vrf.Err)
186+
187+
return vrf.Bytes()
188+
// Here's an example of the resulting witness verification script (186 bytes length, always constant length):
189+
// NEO-GO-VM 0 > loadbase64 ABZQDCECYn75w2MePMuPvExbbEnjjM7eWnmvseGwcI+7lYp4AtdBxfug4EoB/wCRUBipSgH/AJFQGKlKAf8AkVAYqQH/AJEUiEoTE1LQShITUtBKERNS0EoQE1LQQS1RCDAQzosRwB8MBnNoYTI1NgwUG/V1qxGJaIQTYQo1oSiGzeC2bHJBYn1bUhTAHwwPdmVyaWZ5V2l0aEVDRHNhDBQb9XWrEYlohBNhCjWhKIbN4LZsckFifVtS
190+
// READY: loaded 186 instructions
191+
// NEO-GO-VM 0 > ops
192+
// INDEX OPCODE PARAMETER
193+
// 0 PUSHINT8 22 (16) <<
194+
// 2 SWAP
195+
// 3 PUSHDATA1 02627ef9c3631e3ccb8fbc4c5b6c49e38ccede5a79afb1e1b0708fbb958a7802d7
196+
// 38 SYSCALL System.Runtime.GetNetwork (c5fba0e0)
197+
// 43 DUP
198+
// 44 PUSHINT16 255 (ff00)
199+
// 47 AND
200+
// 48 SWAP
201+
// 49 PUSH8
202+
// 50 SHR
203+
// 51 DUP
204+
// 52 PUSHINT16 255 (ff00)
205+
// 55 AND
206+
// 56 SWAP
207+
// 57 PUSH8
208+
// 58 SHR
209+
// 59 DUP
210+
// 60 PUSHINT16 255 (ff00)
211+
// 63 AND
212+
// 64 SWAP
213+
// 65 PUSH8
214+
// 66 SHR
215+
// 67 PUSHINT16 255 (ff00)
216+
// 70 AND
217+
// 71 PUSH4
218+
// 72 NEWBUFFER
219+
// 73 DUP
220+
// 74 PUSH3
221+
// 75 PUSH3
222+
// 76 ROLL
223+
// 77 SETITEM
224+
// 78 DUP
225+
// 79 PUSH2
226+
// 80 PUSH3
227+
// 81 ROLL
228+
// 82 SETITEM
229+
// 83 DUP
230+
// 84 PUSH1
231+
// 85 PUSH3
232+
// 86 ROLL
233+
// 87 SETITEM
234+
// 88 DUP
235+
// 89 PUSH0
236+
// 90 PUSH3
237+
// 91 ROLL
238+
// 92 SETITEM
239+
// 93 SYSCALL System.Runtime.GetScriptContainer (2d510830)
240+
// 98 PUSH0
241+
// 99 PICKITEM
242+
// 100 CAT
243+
// 101 PUSH1
244+
// 102 PACK
245+
// 103 PUSH15
246+
// 104 PUSHDATA1 736861323536 ("sha256")
247+
// 112 PUSHDATA1 1bf575ab1189688413610a35a12886cde0b66c72 ("NNToUmdQBe5n8o53BTzjTFAnSEcpouyy3B", "0x726cb6e0cd8628a1350a611384688911ab75f51b")
248+
// 134 SYSCALL System.Contract.Call (627d5b52)
249+
// 139 PUSH4
250+
// 140 PACK
251+
// 141 PUSH15
252+
// 142 PUSHDATA1 766572696679576974684543447361 ("verifyWithECDsa")
253+
// 159 PUSHDATA1 1bf575ab1189688413610a35a12886cde0b66c72 ("NNToUmdQBe5n8o53BTzjTFAnSEcpouyy3B", "0x726cb6e0cd8628a1350a611384688911ab75f51b")
254+
// 181 SYSCALL System.Contract.Call (627d5b52)
255+
}
256+
257+
// buildKoblitzVerificationScriptSimple builds witness verification script for Koblitz public key.
258+
// This method differs from buildKoblitzVerificationScriptCompat in that it checks
259+
//
260+
// sha256([var-bytes-network-magic, txHash-bytes-BE])
261+
//
262+
// instead of
263+
//
264+
// sha256([4-bytes-network-magic-LE, txHash-bytes-BE]).
265+
//
266+
// Note, that in this way of signing the length of var-network-magic-bytes is non-constant and depends
267+
// on the network value (but around 136 bytes). This way of signing differs from the standard and thus,
268+
// requires minor compatibility changes from the wallet SDK.
269+
func buildKoblitzVerificationScriptSimple(t *testing.T, pub *keys.PublicKey) []byte {
270+
criptoLibH := state.CreateNativeContractHash(nativenames.CryptoLib)
271+
272+
// vrf is witness verification script corresponding to the pub.
273+
// vrf is witness verification script corresponding to the pk.
274+
vrf := io.NewBufBinWriter()
275+
emit.Int(vrf.BinWriter, int64(native.Secp256k1)) // push Koblitz curve identifier.
276+
emit.Opcodes(vrf.BinWriter, opcode.SWAP) // swap curve identifier with the signature.
277+
emit.Bytes(vrf.BinWriter, pub.Bytes()) // emit the caller's public key.
278+
// Construct and push the signed message. The signed message is effectively the network-dependent transaction hash,
279+
// i.e. msg = Sha256([network-magic-bytes, tx.Hash()])
280+
// Firstly, retrieve network magic (it's uint32 wrapped into BigInteger and represented as Integer stackitem on stack).
281+
emit.Syscall(vrf.BinWriter, interopnames.SystemRuntimeGetNetwork) // push network magic.
282+
// Retrieve executing transaction hash.
283+
emit.Syscall(vrf.BinWriter, interopnames.SystemRuntimeGetScriptContainer) // push the script container (executing transaction, actually).
284+
emit.Opcodes(vrf.BinWriter, opcode.PUSH0, opcode.PICKITEM, // pick 0-th transaction item (the transaction hash).
285+
opcode.CAT, // concatenate network magic and transaction hash; this instruction will convert network magic to bytes using BigInteger rules of conversion.
286+
opcode.PUSH1, // push 1 (the number of arguments of 'sha256' method of native CryptoLib).
287+
opcode.PACK) // pack arguments for 'sha256' call.
288+
emit.AppCallNoArgs(vrf.BinWriter, criptoLibH, "sha256", callflag.All) // emit the call to 'sha256' itself.
289+
// Continue construction of 'verifyWithECDsa' call.
290+
emit.Opcodes(vrf.BinWriter, opcode.PUSH4, opcode.PACK) // pack arguments for 'verifyWithECDsa' call.
291+
emit.AppCallNoArgs(vrf.BinWriter, criptoLibH, "verifyWithECDsa", callflag.All) // emit the call to 'verifyWithECDsa' itself.
292+
require.NoError(t, vrf.Err)
293+
294+
return vrf.Bytes()
295+
// Here's an example of the resulting witness verification script (136 bytes length, but the length depends on the network magic value):
296+
// NEO-GO-VM 0 > loadbase64 ABZQDCEDp38Tevu0to16RQqloo/jNfgExYmoCElLS2JuuYcH831Bxfug4EEtUQgwEM6LEcAfDAZzaGEyNTYMFBv1dasRiWiEE2EKNaEohs3gtmxyQWJ9W1IUwB8MD3ZlcmlmeVdpdGhFQ0RzYQwUG/V1qxGJaIQTYQo1oSiGzeC2bHJBYn1bUg==
297+
// READY: loaded 136 instructions
298+
// NEO-GO-VM 0 > ops
299+
// INDEX OPCODE PARAMETER
300+
// 0 PUSHINT8 22 (16) <<
301+
// 2 SWAP
302+
// 3 PUSHDATA1 03a77f137afbb4b68d7a450aa5a28fe335f804c589a808494b4b626eb98707f37d
303+
// 38 SYSCALL System.Runtime.GetNetwork (c5fba0e0)
304+
// 43 SYSCALL System.Runtime.GetScriptContainer (2d510830)
305+
// 48 PUSH0
306+
// 49 PICKITEM
307+
// 50 CAT
308+
// 51 PUSH1
309+
// 52 PACK
310+
// 53 PUSH15
311+
// 54 PUSHDATA1 736861323536 ("sha256")
312+
// 62 PUSHDATA1 1bf575ab1189688413610a35a12886cde0b66c72 ("NNToUmdQBe5n8o53BTzjTFAnSEcpouyy3B", "0x726cb6e0cd8628a1350a611384688911ab75f51b")
313+
// 84 SYSCALL System.Contract.Call (627d5b52)
314+
// 89 PUSH4
315+
// 90 PACK
316+
// 91 PUSH15
317+
// 92 PUSHDATA1 766572696679576974684543447361 ("verifyWithECDsa")
318+
// 109 PUSHDATA1 1bf575ab1189688413610a35a12886cde0b66c72 ("NNToUmdQBe5n8o53BTzjTFAnSEcpouyy3B", "0x726cb6e0cd8628a1350a611384688911ab75f51b")
319+
// 131 SYSCALL System.Contract.Call (627d5b52)
320+
}
321+
322+
// buildKoblitzInvocationScript builds witness invocation script for the transaction signature. The signature
323+
// itself may be produced by public key over any curve (not required Koblitz, the algorithm is the same).
324+
func buildKoblitzInvocationScript(t *testing.T, signature []byte) []byte {
325+
//Exactly like during standard
326+
// signature verification, the resulting script pushes Koblitz signature bytes onto stack.
327+
inv := io.NewBufBinWriter()
328+
emit.Bytes(inv.BinWriter, signature) // message signatre bytes.
329+
require.NoError(t, inv.Err)
330+
331+
return inv.Bytes()
332+
// Here's an example of the resulting witness invocation script (66 bytes length, always constant length):
333+
// NEO-GO-VM > loadbase64 DEBMGKU/MdSizlzaVNDUUbd1zMZQJ43eTaZ4vBCpmkJ/wVh1TYrAWEbFyHhkqq+aYxPCUS43NKJdJTXavcjB8sTP
334+
// READY: loaded 66 instructions
335+
// NEO-GO-VM 0 > ops
336+
// INDEX OPCODE PARAMETER
337+
// 0 PUSHDATA1 4c18a53f31d4a2ce5cda54d0d451b775ccc650278dde4da678bc10a99a427fc158754d8ac05846c5c87864aaaf9a6313c2512e3734a25d2535dabdc8c1f2c4cf <<
338+
}
339+
340+
// constructMessageCompat constructs message for signing following the N3 rules:
341+
//
342+
// sha256([4-bytes-network-magic-LE, txHash-bytes-BE])
343+
func constructMessageCompat(t *testing.T, magic uint32, tx hash.Hashable) []byte {
344+
return hash.NetSha256(magic, tx).BytesBE()
345+
}
346+
347+
// constructMessageCompat constructs message for signing that does not follow N3 rules,
348+
// but entails smaller verification script size and smaller verification price:
349+
//
350+
// sha256([var-bytes-network-magic, txHash-bytes-BE])
351+
func constructMessageSimple(t *testing.T, magic uint32, tx hash.Hashable) []byte {
352+
m := big.NewInt(int64(magic))
353+
return hash.Sha256(append(m.Bytes(), tx.Hash().BytesBE()...)).BytesBE()
354+
}

0 commit comments

Comments
 (0)