From 71aa32406dfe3dacb6ad1a80dbd5b5a586b109ee Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Thu, 2 May 2024 19:17:02 +0300 Subject: [PATCH] native: add test for multisignature Koblitz witness verification Signed-off-by: Anna Shaleva --- .../cryptolib_verification_test.go | 325 +++++++++++++++++- pkg/vm/emit/emit.go | 5 + 2 files changed, 325 insertions(+), 5 deletions(-) diff --git a/pkg/core/native/native_test/cryptolib_verification_test.go b/pkg/core/native/native_test/cryptolib_verification_test.go index 580c2d80d..181a035bf 100644 --- a/pkg/core/native/native_test/cryptolib_verification_test.go +++ b/pkg/core/native/native_test/cryptolib_verification_test.go @@ -2,6 +2,7 @@ package native_test import ( "math/big" + "sort" "testing" "github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames" @@ -84,7 +85,7 @@ func TestCryptoLib_KoblitzVerificationScript(t *testing.T) { require.True(t, pk.PublicKey().Verify(signature, native.Keccak256(msg).BytesBE())) // Build invocation witness script for the user's account. - invBytes := buildKoblitzInvocationScript(t, signature) + invBytes := buildKoblitzInvocationScript(t, [][]byte{signature}) // Construct witness for signer #0 (the user itself). tx.Scripts = []transaction.Witness{ @@ -512,22 +513,34 @@ func buildKoblitzVerificationScriptCompat(t *testing.T, pub *keys.PublicKey) []b // 181 SYSCALL System.Contract.Call (627d5b52) } -// buildKoblitzInvocationScript builds witness invocation script for the transaction signature. The signature +// buildKoblitzInvocationScript builds witness invocation script for the transaction signatures. The signature // itself may be produced by public key over any curve (not required Koblitz, the algorithm is the same). -func buildKoblitzInvocationScript(t *testing.T, signature []byte) []byte { +// The signatures expected to be sorted by public key (if multiple signatures are provided). +func buildKoblitzInvocationScript(t *testing.T, signatures [][]byte) []byte { //Exactly like during standard // signature verification, the resulting script pushes Koblitz signature bytes onto stack. inv := io.NewBufBinWriter() - emit.Bytes(inv.BinWriter, signature) // message signatre bytes. + for _, sig := range signatures { + emit.Bytes(inv.BinWriter, sig) // message signature bytes. + } require.NoError(t, inv.Err) return inv.Bytes() - // Here's an example of the resulting witness invocation script (66 bytes length, always constant length): + // Here's an example of the resulting single witness invocation script (66 bytes length, always constant length): // NEO-GO-VM > loadbase64 DEBMGKU/MdSizlzaVNDUUbd1zMZQJ43eTaZ4vBCpmkJ/wVh1TYrAWEbFyHhkqq+aYxPCUS43NKJdJTXavcjB8sTP // READY: loaded 66 instructions // NEO-GO-VM 0 > ops // INDEX OPCODE PARAMETER // 0 PUSHDATA1 4c18a53f31d4a2ce5cda54d0d451b775ccc650278dde4da678bc10a99a427fc158754d8ac05846c5c87864aaaf9a6313c2512e3734a25d2535dabdc8c1f2c4cf << + // + // Here's an example of the 3 out of 4 multisignature invocation script (66 * m bytes length, always constant length): + // NEO-GO-VM > loadbase64 DEBsPMY3+7sWyZf0gCVcqPzwZ79p+KpeylgtbYIrXp4Tdi6E/8q3DIrEgK7DdVe3YdbfE+VPrpwym/ufBb8MRTB6DED5B9OZDGWdJApRfuy9LeUTa2mLsXP7mBRa181g0Jo7beylWzVgDqHHF2PilECMcLmRbFRknmQm4KgiGkDE+O6ZDEAYt61O2dMfasJHiQD95M5b4mR6NBnDsMTo2e59H3y4YguroVLiUxnQSc4qu9LWvEIKr4/ytjCCuANXOkJmSw8C + // READY: loaded 198 instructions + // NEO-GO-VM 0 > ops + // INDEX OPCODE PARAMETER + // 0 PUSHDATA1 6c3cc637fbbb16c997f480255ca8fcf067bf69f8aa5eca582d6d822b5e9e13762e84ffcab70c8ac480aec37557b761d6df13e54fae9c329bfb9f05bf0c45307a << + // 66 PUSHDATA1 f907d3990c659d240a517eecbd2de5136b698bb173fb98145ad7cd60d09a3b6deca55b35600ea1c71763e294408c70b9916c54649e6426e0a8221a40c4f8ee99 + // 132 PUSHDATA1 18b7ad4ed9d31f6ac2478900fde4ce5be2647a3419c3b0c4e8d9ee7d1f7cb8620baba152e25319d049ce2abbd2d6bc420aaf8ff2b63082b803573a42664b0f02 } // constructMessage constructs message for signing that consists of the @@ -562,3 +575,305 @@ func constructMessageSimple(t *testing.T, magic uint32, tx hash.Hashable) []byte func constructMessageCompat(t *testing.T, magic uint32, tx hash.Hashable) []byte { return hash.NetSha256(magic, tx).BytesBE() } + +// TestCryptoLib_KoblitzMultisigVerificationScript builds transaction with custom witness that contains +// the Koblitz tx multisignature bytes and Koblitz multisignature verification script. +// This test ensures that transaction signed by m out of n Koblitz keys passes verification and can +// be successfully accepted to the chain. +func TestCryptoLib_KoblitzMultisigVerificationScript(t *testing.T) { + check := func( + t *testing.T, + buildVerificationScript func(t *testing.T, m int, pub keys.PublicKeys) []byte, + constructMsg func(t *testing.T, magic uint32, tx hash.Hashable) []byte, + ) { + c := newGasClient(t) + gasInvoker := c.WithSigners(c.Committee) + e := c.Executor + + // Consider 4 users willing to sign 3/4 multisignature transaction Secp256k1 private keys. + const ( + n = 4 + m = 3 + ) + pks := make([]*keys.PrivateKey, n) + for i := range pks { + var err error + pks[i], err = keys.NewSecp256k1PrivateKey() + require.NoError(t, err) + } + // Sort private keys by their public keys. + sort.Slice(pks, func(i, j int) bool { + return pks[i].PublicKey().Cmp(pks[j].PublicKey()) < 0 + }) + + // Firstly, we need to build the N3 multisig account address based on the users' public keys. + // Pubs must be sorted, exactly like for the standard CheckMultisig. + pubs := make(keys.PublicKeys, n) + for i := range pks { + pubs[i] = pks[i].PublicKey() + } + vrfBytes := buildVerificationScript(t, m, pubs) + + // Construct the user's account script hash. It's effectively a verification script hash. + from := hash.Hash160(vrfBytes) + + // Supply this account with some initial balance so that the user is able to pay for his transactions. + gasInvoker.Invoke(t, true, "transfer", c.Committee.ScriptHash(), from, 10000_0000_0000, nil) + + // Construct transaction that transfers 5 GAS from the user's account to some other account. + to := util.Uint160{1, 2, 3} + amount := 5 + tx := gasInvoker.PrepareInvokeNoSign(t, "transfer", from, to, amount, nil) + tx.Signers = []transaction.Signer{ + { + Account: from, + Scopes: transaction.CalledByEntry, + }, + } + neotest.AddNetworkFee(t, e.Chain, tx) + neotest.AddSystemFee(e.Chain, tx, -1) + + // Add some more network fee to pay for the witness verification. This value may be calculated precisely, + // but let's keep some inaccurate value for the test. + tx.NetworkFee += 900_0000 + + // This transaction (along with the network magic) should be signed by the user's Koblitz private key. + msg := constructMsg(t, uint32(e.Chain.GetConfig().Magic), tx) + + // The users have to sign the hash of the message by their Koblitz key. Collect m signatures from first m keys. + // Signatures must be sorted by public key. + sigs := make([][]byte, m) + for i := range sigs { + j := i + if i > 0 { + j++ // Add some shift to ensure that verification script works correctly. + } + if i > 3 { + j++ // Add more shift for large number of public keys for the same purpose. + } + sigs[i] = pks[j].SignHash(native.Keccak256(msg)) + } + + // Build invocation witness script for the signatures. + invBytes := buildKoblitzInvocationScript(t, sigs) + + // Construct witness for signer #0 (the multisig account itself). + tx.Scripts = []transaction.Witness{ + { + InvocationScript: invBytes, + VerificationScript: vrfBytes, + }, + } + + // Add transaction to the chain. No error is expected on new block addition. Note, that this line performs + // all those checks that are executed during transaction acceptance in the real network. + e.AddNewBlock(t, tx) + + // Double-check: ensure funds have been transferred. + e.CheckGASBalance(t, to, big.NewInt(int64(amount))) + } + + // The proposed multisig verification script. + // (261 bytes, 8389470 GAS including Invocation script execution for 3/4 multisig). + // The user has to sign the keccak256([4-bytes-network-magic-LE, txHash-bytes-BE]). + check(t, buildKoblitzMultisigVerificationScript, constructMessage) +} + +// buildKoblitzMultisigVerificationScript builds witness verification script for m signatures out of n Koblitz public keys. +// Public keys must be sorted. Signatures (pushed by witness Invocation script) must be sorted by public keys. +// It checks m out of n multisignature of the following message: +// +// keccak256([4-bytes-network-magic-LE, txHash-bytes-BE]) +func buildKoblitzMultisigVerificationScript(t *testing.T, m int, pubs keys.PublicKeys) []byte { + if len(pubs) == 0 { + t.Fatalf("empty pubs list") + } + if m > len(pubs) { + t.Fatalf("m must be not greater than the number of public keys") + } + + n := len(pubs) // public keys must be sorted. + cryptoLibH := state.CreateNativeContractHash(nativenames.CryptoLib) + + // In fact, the following algorithm is implemented via NeoVM instructions: + // + // func Check(sigs []interop.Signature) bool { + // if m != len(sigs) { + // return false + // } + // var pubs []interop.PublicKey = []interop.PublicKey{...} + // msg := append(convert.ToBytes(runtime.GetNetwork()), runtime.GetScriptContainer().Hash...) + // var sigCnt = 0 + // var pubCnt = 0 + // for ; sigCnt < m && pubCnt < n; { // sigs must be sorted by pub + // sigCnt += crypto.VerifyWithECDsa(msg, pubs[pubCnt], sigs[sigCnt], crypto.Secp256k1Keccak256) + // pubCnt++ + // } + // return sigCnt == m + // } + vrf := io.NewBufBinWriter() + + // Initialize slots for local variables. Locals slot scheme: + // LOC0 -> sigs + // LOC1 -> pubs + // LOC2 -> msg (ByteString) + // LOC3 -> sigCnt (Integer) + // LOC4 -> pubCnt (Integer) + emit.InitSlot(vrf.BinWriter, 5, 0) + + // Check the number of signatures is m. Return false if not. + emit.Opcodes(vrf.BinWriter, opcode.DEPTH) // Push the number of signatures onto stack. + emit.Int(vrf.BinWriter, int64(m)) + emit.Instruction(vrf.BinWriter, opcode.JMPEQ, []byte{0}) // here and below short jumps are sufficient. + sigsLenCheckEndOffset := vrf.Len() // offset of the signatures count check. + emit.Opcodes(vrf.BinWriter, opcode.CLEAR, opcode.PUSHF, opcode.RET) // return if length of the signatures not equal to m. + + // Start the check. + checkStartOffset := vrf.Len() + + // Pack signatures and store at LOC0. + emit.Int(vrf.BinWriter, int64(m)) + emit.Opcodes(vrf.BinWriter, opcode.PACK, opcode.STLOC0) + + // Pack public keys and store at LOC1. + for _, pub := range pubs { + emit.Bytes(vrf.BinWriter, pub.Bytes()) + } + emit.Int(vrf.BinWriter, int64(n)) + emit.Opcodes(vrf.BinWriter, opcode.PACK, opcode.STLOC1) + + // Get message and store it at LOC2. + // msg = [4-network-magic-bytes-LE, tx-hash-BE] + emit.Syscall(vrf.BinWriter, interopnames.SystemRuntimeGetNetwork) // push network magic (Integer stackitem), can have 0-5 bytes length serialized. + // Convert network magic to 4-bytes-length LE byte array representation. + emit.Int(vrf.BinWriter, 0x100000000) + emit.Opcodes(vrf.BinWriter, opcode.ADD, // some new number that is 5 bytes at least when serialized, but first 4 bytes are intact network value (LE). + opcode.PUSH4, opcode.LEFT) // cut the first 4 bytes out of a number that is at least 5 bytes long, the result is 4-bytes-length LE network representation. + // Retrieve executing transaction hash. + emit.Syscall(vrf.BinWriter, interopnames.SystemRuntimeGetScriptContainer) // push the script container (executing transaction, actually). + emit.Opcodes(vrf.BinWriter, opcode.PUSH0, opcode.PICKITEM) // pick 0-th transaction item (the transaction hash). + // Concatenate network magic and transaction hash. + emit.Opcodes(vrf.BinWriter, opcode.CAT) // this instruction will convert network magic to bytes using BigInteger rules of conversion. + emit.Opcodes(vrf.BinWriter, opcode.STLOC2) // store msg as a local variable #2. + + // Initialize local variables: sigCnt, pubCnt. + emit.Opcodes(vrf.BinWriter, opcode.PUSH0, opcode.STLOC3, // initialize sigCnt. + opcode.PUSH0, opcode.STLOC4) // initialize pubCnt. + + // Loop condition check. + loopStartOffset := vrf.Len() + emit.Opcodes(vrf.BinWriter, opcode.LDLOC3) // load sigCnt. + emit.Int(vrf.BinWriter, int64(m)) // push m. + emit.Opcodes(vrf.BinWriter, opcode.GE, // sigCnt >= m + opcode.LDLOC4) // load pubCnt + emit.Int(vrf.BinWriter, int64(n)) // push n. + emit.Opcodes(vrf.BinWriter, opcode.GE, // pubCnt >= n + opcode.OR) // sigCnt >= m || pubCnt >= n + emit.Instruction(vrf.BinWriter, opcode.JMPIF, []byte{0}) // jump to the end of the script if (sigCnt >= m || pubCnt >= n). + loopConditionOffset := vrf.Len() + + // Loop start. Prepare arguments and call CryptoLib's verifyWithECDsa. + emit.Int(vrf.BinWriter, int64(native.Secp256k1Keccak256)) // push Koblitz curve identifier. + emit.Opcodes(vrf.BinWriter, opcode.LDLOC0, // load signatures. + opcode.LDLOC3, // load sigCnt. + opcode.PICKITEM, // pick signature at index sigCnt. + opcode.LDLOC1, // load pubs. + opcode.LDLOC4, // load pubCnt. + opcode.PICKITEM, // pick pub at index pubCnt. + opcode.LDLOC2, // load msg. + opcode.PUSH4, opcode.PACK) // pack 4 arguments for 'verifyWithECDsa' call. + emit.AppCallNoArgs(vrf.BinWriter, cryptoLibH, "verifyWithECDsa", callflag.All) // emit the call to 'verifyWithECDsa' itself. + + // Update loop variables. + emit.Opcodes(vrf.BinWriter, opcode.LDLOC3, opcode.ADD, opcode.STLOC3, // increment sigCnt if signature is valid. + opcode.LDLOC4, opcode.INC, opcode.STLOC4) // increment pubCnt. + + // End of the loop. + emit.Instruction(vrf.BinWriter, opcode.JMP, []byte{0}) // jump to the start of cycle. + loopEndOffset := vrf.Len() + + // Return condition: the number of valid signatures should be equal to m. + progRetOffset := vrf.Len() + emit.Opcodes(vrf.BinWriter, opcode.LDLOC3) // load sigCnt. + emit.Int(vrf.BinWriter, int64(m)) // push m. + emit.Opcodes(vrf.BinWriter, opcode.NUMEQUAL) // push m == sigCnt. + + require.NoError(t, vrf.Err) + script := vrf.Bytes() + + // Set JMP* instructions offsets. "-1" is for short JMP parameter offset. JMP parameters + // are relative offsets. + script[sigsLenCheckEndOffset-1] = byte(checkStartOffset - sigsLenCheckEndOffset + 2) + script[loopEndOffset-1] = byte(loopStartOffset - loopEndOffset + 2) + script[loopConditionOffset-1] = byte(progRetOffset - loopConditionOffset + 2) + + return script + // Here's an example of the resulting single witness invocation script (261 bytes length, the length may vary depending on m/n): + // NEO-GO-VM > loadbase64 VwUAQxMoBUkJQBPAcAwhAnDdr99Ja4K3I81KURO2xs8b+dYYVIaMhbDFTYO4FCnKDCECuBwcms5bdqbWeBZ1cnMAJ8z/uUMcxnIK0CxTyxNdYqAMIQLQHl4aPx8PZOgu4EQUh0qCPaCfaZZPLNNS9ZVPcmuXpwwhA+YKTuJo6wB/u/CQdzJczfQQaMk6LHfMlSZMdBD2qCV1FMBxQcX7oOADAAAAAAEAAACeFI1BLVEIMBDOi3IQcxB0axO4bBS4kiRCABhoa85pbM5qFMAfDA92ZXJpZnlXaXRoRUNEc2EMFBv1dasRiWiEE2EKNaEohs3gtmxyQWJ9W1JrnnNsnHQiuWsTsw== + // READY: loaded 262 instructions + // NEO-GO-VM 0 > ops + // INDEX OPCODE PARAMETER + // 0 INITSLOT 5 local, 0 arg << + // 3 DEPTH + // 4 PUSH3 + // 5 JMPEQ 10 (5/05) + // 7 CLEAR + // 8 PUSHF + // 9 RET + // 10 PUSH3 + // 11 PACK + // 12 STLOC0 + // 13 PUSHDATA1 0270ddafdf496b82b723cd4a5113b6c6cf1bf9d61854868c85b0c54d83b81429ca + // 48 PUSHDATA1 02b81c1c9ace5b76a6d678167572730027ccffb9431cc6720ad02c53cb135d62a0 + // 83 PUSHDATA1 02d01e5e1a3f1f0f64e82ee04414874a823da09f69964f2cd352f5954f726b97a7 + // 118 PUSHDATA1 03e60a4ee268eb007fbbf09077325ccdf41068c93a2c77cc95264c7410f6a82575 + // 153 PUSH4 + // 154 PACK + // 155 STLOC1 + // 156 SYSCALL System.Runtime.GetNetwork (c5fba0e0) + // 161 PUSHINT64 4294967296 (0000000001000000) + // 170 ADD + // 171 PUSH4 + // 172 LEFT + // 173 SYSCALL System.Runtime.GetScriptContainer (2d510830) + // 178 PUSH0 + // 179 PICKITEM + // 180 CAT + // 181 STLOC2 + // 182 PUSH0 + // 183 STLOC3 + // 184 PUSH0 + // 185 STLOC4 + // 186 LDLOC3 + // 187 PUSH3 + // 188 GE + // 189 LDLOC4 + // 190 PUSH4 + // 191 GE + // 192 OR + // 193 JMPIF 259 (66/42) + // 195 PUSHINT8 24 (18) + // 197 LDLOC0 + // 198 LDLOC3 + // 199 PICKITEM + // 200 LDLOC1 + // 201 LDLOC4 + // 202 PICKITEM + // 203 LDLOC2 + // 204 PUSH4 + // 205 PACK + // 206 PUSH15 + // 207 PUSHDATA1 766572696679576974684543447361 ("verifyWithECDsa") + // 224 PUSHDATA1 1bf575ab1189688413610a35a12886cde0b66c72 ("NNToUmdQBe5n8o53BTzjTFAnSEcpouyy3B", "0x726cb6e0cd8628a1350a611384688911ab75f51b") + // 246 SYSCALL System.Contract.Call (627d5b52) + // 251 LDLOC3 + // 252 ADD + // 253 STLOC3 + // 254 LDLOC4 + // 255 INC + // 256 STLOC4 + // 257 JMP 186 (-71/b9) + // 259 LDLOC3 + // 260 PUSH3 + // 261 NUMEQUAL +} diff --git a/pkg/vm/emit/emit.go b/pkg/vm/emit/emit.go index a51e3f781..7a2b1033c 100644 --- a/pkg/vm/emit/emit.go +++ b/pkg/vm/emit/emit.go @@ -29,6 +29,11 @@ func Opcodes(w *io.BinWriter, ops ...opcode.Opcode) { } } +// InitSlot emits INITSLOT instruction with the specified size of locals/args slots. +func InitSlot(w *io.BinWriter, locals, args uint8) { + Instruction(w, opcode.INITSLOT, []byte{locals, args}) +} + // Bool emits a bool type to the given buffer. func Bool(w *io.BinWriter, ok bool) { var opVal = opcode.PUSHT