Skip to content

Commit 11020de

Browse files
author
Tural Devrishev
committed
core: add Ethereum-compatible aliases for BLS12-381
Close #4041. Signed-off-by: Tural Devrishev <tural@nspcc.ru>
1 parent a2a11d1 commit 11020de

File tree

9 files changed

+298
-8
lines changed

9 files changed

+298
-8
lines changed

docs/node-configuration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -582,7 +582,7 @@ in development and can change in an incompatible way.
582582
| `Cockatrice` | Introduces the ability to update native contracts. Includes a couple of new native smart contract APIs: `keccak256` of native CryptoLib contract and `getCommitteeAddress` of native NeoToken contract. | https://github.com/nspcc-dev/neo-go/pull/3402 <br> https://github.com/neo-project/neo/pull/2942 <br> https://github.com/nspcc-dev/neo-go/pull/3301 <br> https://github.com/neo-project/neo/pull/2925 <br> https://github.com/nspcc-dev/neo-go/pull/3362 <br> https://github.com/neo-project/neo/pull/3154 |
583583
| `Domovoi` | Makes node use executing contract state for the contract call permissions check instead of the state stored in the native Management contract. In C# also makes System.Runtime.GetNotifications interop properly count stack references of notification parameters which prevents users from creating objects that exceed MaxStackSize constraint, but NeoGo has never had this bug, thus proper behaviour is preserved even before HFDomovoi. It results in the fact that some T5 testnet transactions have different ApplicationLogs compared to the C# node, but the node states match. | https://github.com/nspcc-dev/neo-go/pull/3476 <br> https://github.com/neo-project/neo/pull/3290 <br> https://github.com/nspcc-dev/neo-go/pull/3473 <br> https://github.com/neo-project/neo/pull/3290 <br> https://github.com/neo-project/neo/pull/3301 <br> https://github.com/nspcc-dev/neo-go/pull/3485 |
584584
| `Echidna` | Introduces `Designation` event extension with `Old` and `New` roles data to native RoleManagement contract. Adds support for `base64UrlEncode` and `base64UrlDecode` methods to native StdLib contract. Extends the list of required call flags for `registerCandidate`, `unregisterCandidate`and `vote` methods of native NeoToken contract with AllowNotify flag. Enables `onNEP17Payment` method of NEO contract for candidate registration. Introduces constraint for maximum number of execution notifications. Adds support for `recoverSecp256K1` method of native CryptoLib contract. Introduces `setMillisecondsPerBlock` and `getMillisecondsPerBlock` methods of native Policy contract. Introduces support for NotaryAssisted transaction attribute and native Notary contract. | https://github.com/nspcc-dev/neo-go/pull/3554 <br> https://github.com/nspcc-dev/neo-go/pull/3761 <br> https://github.com/nspcc-dev/neo-go/pull/3554 <br> https://github.com/neo-project/neo/pull/3597 <br> https://github.com/nspcc-dev/neo-go/pull/3700 <br> https://github.com/nspcc-dev/neo-go/pull/3640 <br> https://github.com/neo-project/neo/pull/3548 <br> https://github.com/nspcc-dev/neo-go/pull/3863 <br> https://github.com/neo-project/neo/pull/3696 <br> https://github.com/neo-project/neo/pull/3895 <br> https://github.com/nspcc-dev/neo-go/pull/3835 <br> https://github.com/nspcc-dev/neo-go/pull/3854 <br> https://github.com/neo-project/neo/pull/3175 <br> https://github.com/nspcc-dev/neo-go/pull/3478 <br> https://github.com/neo-project/neo/pull/3178 |
585-
| `Faun` | Adds `getBlockedAccounts` method to native Policy contract. Adds `hexEncode` and `hexDecode` methods to native StdLib contract. | https://github.com/nspcc-dev/neo-go/pull/3932 <br> https://github.com/nspcc-dev/neo-go/pull/4004 <br> https://github.com/neo-project/neo/pull/4147 <br> https://github.com/neo-project/neo/pull/4150 |
585+
| `Faun` | Adds `getBlockedAccounts` method to native Policy contract. Adds `hexEncode` and `hexDecode` methods to native StdLib contract. | https://github.com/nspcc-dev/neo-go/pull/3932 <br> https://github.com/nspcc-dev/neo-go/pull/4004 <br> https://github.com/neo-project/neo/pull/4147 <br> https://github.com/neo-project/neo/pull/4150 <br> https://github.com/nspcc-dev/neo-go/pull/4050 <br> https://github.com/neo-project/neo/pull/4186 |
586586

587587
## DB compatibility
588588

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,5 @@ require (
7272
google.golang.org/grpc v1.75.1 // indirect
7373
google.golang.org/protobuf v1.36.9 // indirect
7474
)
75+
76+
replace github.com/nspcc-dev/neo-go/pkg/interop => ./pkg/interop

pkg/compiler/native_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ func TestNativeHelpersCompile(t *testing.T) {
250250
{"bls12381Mul", []string{"crypto.Bls12381Point{}", "[]byte{1, 2, 3}", "true"}},
251251
{"bls12381Pairing", []string{"crypto.Bls12381Point{}", "crypto.Bls12381Point{}"}},
252252
{"keccak256", []string{"[]byte{1, 2, 3}"}},
253+
{"bls12381MultiExp", []string{"[]crypto.Bls12381Pair{}"}},
253254
})
254255
runNativeTestCases(t, *cs.ByName(nativenames.StdLib).Metadata(), "std", []nativeTestCase{
255256
{"serialize", []string{"[]byte{1, 2, 3}"}},

pkg/config/hardfork.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ const (
5252
HFEchidna // Echidna
5353
// HFFaun represents hard-fork introduced in #3931, #4004 (ported from
5454
// https://github.com/neo-project/neo/pull/4147,
55-
// https://github.com/neo-project/neo/pull/4150).
55+
// https://github.com/neo-project/neo/pull/4150), #4050 (ported from
56+
// https://github.com/neo-project/neo/pull/4186).
5657
HFFaun // Faun
5758
// hfLast denotes the end of hardforks enum. Consider adding new hardforks
5859
// before hfLast.

pkg/core/native/crypto.go

Lines changed: 101 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"fmt"
99
"math/big"
1010

11+
bls12381 "github.com/consensys/gnark-crypto/ecc/bls12-381"
1112
"github.com/consensys/gnark-crypto/ecc/bls12-381/fr"
1213
"github.com/decred/dcrd/dcrec/secp256k1/v4"
1314
"github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa"
@@ -40,10 +41,11 @@ type NamedCurveHash byte
4041

4142
// Various pairs of named elliptic curves and hash functions.
4243
const (
43-
Secp256k1Sha256 NamedCurveHash = 22
44-
Secp256r1Sha256 NamedCurveHash = 23
45-
Secp256k1Keccak256 NamedCurveHash = 122
46-
Secp256r1Keccak256 NamedCurveHash = 123
44+
Secp256k1Sha256 NamedCurveHash = 22
45+
Secp256r1Sha256 NamedCurveHash = 23
46+
Secp256k1Keccak256 NamedCurveHash = 122
47+
Secp256r1Keccak256 NamedCurveHash = 123
48+
Bls12381MultiExpMaxPairs = 128
4749
)
4850

4951
func newCrypto() *Crypto {
@@ -134,6 +136,11 @@ func newCrypto() *Crypto {
134136
manifest.NewParameter("signature", smartcontract.ByteArrayType))
135137
md = NewMethodAndPrice(c.recoverSecp256K1, 1<<15, callflag.NoneFlag, config.HFEchidna)
136138
c.AddMethod(md, desc)
139+
140+
desc = NewDescriptor("bls12381MultiExp", smartcontract.InteropInterfaceType,
141+
manifest.NewParameter("pairs", smartcontract.ArrayType))
142+
md = NewMethodAndPrice(c.bls12381MultiExp, 1<<23, callflag.NoneFlag, config.HFFaun)
143+
c.AddMethod(md, desc)
137144
return c
138145
}
139146

@@ -362,6 +369,96 @@ func (c *Crypto) bls12381Add(_ *interop.Context, args []stackitem.Item) stackite
362369
return stackitem.NewInterop(p)
363370
}
364371

372+
func (c *Crypto) bls12381MultiExp(_ *interop.Context, args []stackitem.Item) stackitem.Item {
373+
pairs := args[0].Value().([]stackitem.Item)
374+
if len(pairs) == 0 {
375+
panic("BLS12-381 multi exponent requires at least one pair")
376+
}
377+
if len(pairs) > Bls12381MultiExpMaxPairs {
378+
panic(fmt.Sprintf("BLS12-381 multi exponent supports at most %d pairs", Bls12381MultiExpMaxPairs))
379+
}
380+
var (
381+
useG2 int // 1 => use G2
382+
accumulator blsPoint
383+
)
384+
for _, si := range pairs {
385+
if si.Type() != stackitem.ArrayT && si.Type() != stackitem.StructT {
386+
panic("BLS12-381 multi exponent pair must be Array or Struct")
387+
}
388+
pair := si.Value().([]stackitem.Item)
389+
if len(pair) != 2 {
390+
panic("BLS12-381 multi exponent pair must contain point and scalar")
391+
}
392+
if pair[0].Type() != stackitem.InteropT {
393+
panic("BLS12-381 multi exponent requires interop points")
394+
}
395+
point, ok := pair[0].Value().(blsPoint)
396+
if !ok {
397+
panic("BLS12-381 multi exponent interop must contain blsPoint")
398+
}
399+
switch point.point.(type) {
400+
case *bls12381.G1Jac, *bls12381.G1Affine:
401+
useG2 = ensureGroupType(useG2, -1)
402+
case *bls12381.G2Jac, *bls12381.G2Affine:
403+
useG2 = ensureGroupType(useG2, 1)
404+
default:
405+
panic("BLS12-381 type mismatch")
406+
}
407+
scalar, err := parseScalar(pair[1])
408+
if err != nil {
409+
panic(err)
410+
}
411+
if scalar.BigInt(new(big.Int)).Sign() == 0 {
412+
continue
413+
}
414+
res, err := blsPointMul(point, scalar.BigInt(new(big.Int)))
415+
if err != nil {
416+
panic(err)
417+
}
418+
if accumulator.point == nil {
419+
accumulator.point = res.point
420+
} else if accumulator, err = blsPointAdd(accumulator, res); err != nil {
421+
panic(err)
422+
}
423+
}
424+
if useG2 == 0 {
425+
panic("BLS12-381 multi exponent requires at least one valid pair")
426+
}
427+
return stackitem.NewInterop(accumulator)
428+
}
429+
430+
func ensureGroupType(useG2, isG2 int) int {
431+
if useG2 == 0 {
432+
return isG2
433+
}
434+
if useG2 != isG2 {
435+
panic("BLS12-381 multi exponent cannot mix groups")
436+
}
437+
return isG2
438+
}
439+
440+
func parseScalar(item stackitem.Item) (*fr.Element, error) {
441+
bytes, err := item.TryBytes()
442+
if err != nil {
443+
return nil, fmt.Errorf("invalid multiplier: %w", err)
444+
}
445+
l := len(bytes)
446+
if l != fr.Bytes && l != 2*fr.Bytes {
447+
return nil, fmt.Errorf("invalid multiplier: 32- or 64-bytes scalar is expected, got %d", l)
448+
}
449+
if l == fr.Bytes {
450+
v, err := fr.LittleEndian.Element((*[fr.Bytes]byte)(bytes))
451+
if err == nil {
452+
return &v, nil
453+
}
454+
}
455+
beBytes := make([]byte, l)
456+
for i := range l {
457+
beBytes[l-i-1] = bytes[i]
458+
}
459+
return new(fr.Element).SetBigInt(new(big.Int).SetBytes(beBytes)), nil
460+
}
461+
365462
func scalarFromBytes(bytes []byte, neg bool) (*fr.Element, error) {
366463
alpha := new(fr.Element)
367464
if len(bytes) != fr.Bytes {

pkg/core/native/crypto_test.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,18 @@ import (
44
"crypto/ed25519"
55
"encoding/binary"
66
"encoding/hex"
7+
"fmt"
78
"math"
89
"math/big"
910
"slices"
1011
"testing"
1112

13+
bls12381 "github.com/consensys/gnark-crypto/ecc/bls12-381"
1214
"github.com/consensys/gnark-crypto/ecc/bls12-381/fr"
1315
"github.com/nspcc-dev/neo-go/pkg/core/interop"
1416
"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
1517
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
18+
"github.com/nspcc-dev/neo-go/pkg/encoding/bigint"
1619
"github.com/nspcc-dev/neo-go/pkg/vm"
1720
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
1821
"github.com/stretchr/testify/require"
@@ -393,3 +396,109 @@ func TestKeccak256(t *testing.T) {
393396

394397
require.Equal(t, expected, actual)
395398
}
399+
400+
func TestBlsMultiExp(t *testing.T) {
401+
const (
402+
g1Hex = "97f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb"
403+
g2Hex = "93e02b6052719f607dacd3a088274f65596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e" +
404+
"024aa2b2f08f0a91260805272dc51051c6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb8"
405+
)
406+
g1, err := hex.DecodeString(g1Hex)
407+
require.NoError(t, err)
408+
409+
g2, err := hex.DecodeString(g2Hex)
410+
require.NoError(t, err)
411+
412+
crypto := newCrypto()
413+
414+
t.Run("multiExpG1", func(t *testing.T) {
415+
var g1Point bls12381.G1Affine
416+
_, err = g1Point.SetBytes(g1)
417+
require.NoError(t, err)
418+
one := make([]byte, fr.Bytes)
419+
bigint.ToPreallocatedBytes(new(big.Int).SetUint64(1), one)
420+
two := make([]byte, fr.Bytes)
421+
bigint.ToPreallocatedBytes(new(big.Int).SetUint64(2), two)
422+
423+
pairs := stackitem.NewArray([]stackitem.Item{
424+
stackitem.NewArray([]stackitem.Item{
425+
stackitem.NewInterop(blsPoint{point: &g1Point}),
426+
stackitem.NewByteArray(one),
427+
}),
428+
stackitem.NewArray([]stackitem.Item{
429+
stackitem.NewInterop(blsPoint{point: &g1Point}),
430+
stackitem.NewByteArray(two),
431+
}),
432+
})
433+
actual, ok := crypto.bls12381MultiExp(nil, []stackitem.Item{pairs}).(*stackitem.Interop).Value().(blsPoint)
434+
require.True(t, ok)
435+
expected, err := blsPointMul(blsPoint{point: &g1Point}, new(big.Int).SetUint64(3))
436+
require.NoError(t, err)
437+
require.Equal(t, expected, actual)
438+
})
439+
440+
t.Run("multiExpG2", func(t *testing.T) {
441+
var g2Point bls12381.G2Affine
442+
_, err = g2Point.SetBytes(g2)
443+
require.NoError(t, err)
444+
five := make([]byte, fr.Bytes)
445+
bigint.ToPreallocatedBytes(new(big.Int).SetUint64(5), five)
446+
447+
pairs := stackitem.NewArray([]stackitem.Item{
448+
stackitem.NewArray([]stackitem.Item{
449+
stackitem.NewInterop(blsPoint{point: &g2Point}),
450+
stackitem.NewByteArray(five),
451+
}),
452+
})
453+
actual, ok := crypto.bls12381MultiExp(nil, []stackitem.Item{pairs}).(*stackitem.Interop).Value().(blsPoint)
454+
require.True(t, ok)
455+
expected, err := blsPointMul(blsPoint{point: &g2Point}, new(big.Int).SetUint64(5))
456+
require.NoError(t, err)
457+
require.Equal(t, expected, actual)
458+
})
459+
460+
t.Run("reduces scalar", func(t *testing.T) {
461+
var g1Point bls12381.G1Affine
462+
_, err = g1Point.SetBytes(g1)
463+
require.NoError(t, err)
464+
twoPlusR := make([]byte, fr.Bytes)
465+
bigint.ToPreallocatedBytes(new(big.Int).Add(fr.Modulus(), big.NewInt(2)), twoPlusR)
466+
467+
pairs := stackitem.NewArray([]stackitem.Item{
468+
stackitem.NewArray([]stackitem.Item{
469+
stackitem.NewInterop(blsPoint{point: &g1Point}),
470+
stackitem.NewByteArray(twoPlusR),
471+
}),
472+
})
473+
actual, ok := crypto.bls12381MultiExp(nil, []stackitem.Item{pairs}).(*stackitem.Interop).Value().(blsPoint)
474+
require.True(t, ok)
475+
expected, err := blsPointMul(blsPoint{point: &g1Point}, new(big.Int).SetUint64(2))
476+
require.NoError(t, err)
477+
require.Equal(t, expected, actual)
478+
})
479+
480+
t.Run("empty pairs", func(t *testing.T) {
481+
require.PanicsWithValue(t, "BLS12-381 multi exponent requires at least one pair", func() {
482+
crypto.bls12381MultiExp(nil, []stackitem.Item{stackitem.NewArray([]stackitem.Item{})})
483+
})
484+
})
485+
t.Run("too many pairs", func(t *testing.T) {
486+
require.PanicsWithValue(t, fmt.Sprintf("BLS12-381 multi exponent supports at most %d pairs", Bls12381MultiExpMaxPairs), func() {
487+
crypto.bls12381MultiExp(nil, []stackitem.Item{stackitem.NewArray(make([]stackitem.Item, Bls12381MultiExpMaxPairs+1))})
488+
})
489+
})
490+
t.Run("invalid pair type", func(t *testing.T) {
491+
require.PanicsWithValue(t, "BLS12-381 multi exponent pair must be Array or Struct", func() {
492+
crypto.bls12381MultiExp(nil, []stackitem.Item{stackitem.NewArray([]stackitem.Item{
493+
stackitem.NewMap(),
494+
})})
495+
})
496+
})
497+
t.Run("invalid pair length", func(t *testing.T) {
498+
require.PanicsWithValue(t, "BLS12-381 multi exponent pair must contain point and scalar", func() {
499+
crypto.bls12381MultiExp(nil, []stackitem.Item{stackitem.NewArray([]stackitem.Item{
500+
stackitem.NewArray(nil),
501+
})})
502+
})
503+
})
504+
}

pkg/core/native/native_test/cryptolib_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ import (
1010
bls12381 "github.com/consensys/gnark-crypto/ecc/bls12-381"
1111
"github.com/consensys/gnark-crypto/ecc/bls12-381/fr"
1212
"github.com/decred/dcrd/dcrec/secp256k1/v4"
13+
"github.com/nspcc-dev/neo-go/pkg/compiler"
1314
"github.com/nspcc-dev/neo-go/pkg/config"
1415
"github.com/nspcc-dev/neo-go/pkg/core/native"
16+
"github.com/nspcc-dev/neo-go/pkg/core/native/nativehashes"
1517
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
1618
"github.com/nspcc-dev/neo-go/pkg/io"
1719
"github.com/nspcc-dev/neo-go/pkg/neotest"
@@ -595,3 +597,69 @@ func TestCryptoLib_RecoverSecp256K1_EIP2098Compat(t *testing.T) {
595597
committeeInvoker.Invoke(t, pubBytes, "recoverSecp256K1", msgH, sigCompact)
596598
}
597599
}
600+
601+
func TestCryptoLib_Bls12381MultiExp(t *testing.T) {
602+
bc, acc := chain.NewSingleWithCustomConfig(t, func(c *config.Blockchain) {
603+
c.Hardforks = map[string]uint32{
604+
config.HFFaun.String(): 2,
605+
}
606+
})
607+
608+
e := neotest.NewExecutor(t, bc, acc, acc)
609+
c := e.CommitteeInvoker(nativehashes.CryptoLib)
610+
611+
var g1Point bls12381.G1Affine
612+
_, err := g1Point.SetBytes(g1)
613+
require.NoError(t, err)
614+
615+
c.InvokeFail(t, "method not found: bls12381MultiExp/1", "bls12381MultiExp", stackitem.NewArray(nil))
616+
617+
script := io.NewBufBinWriter()
618+
multiplier := make([]byte, fr.Bytes)
619+
multiplier[0] = 1
620+
emit.Bytes(script.BinWriter, multiplier)
621+
emit.AppCall(script.BinWriter, c.Hash, "bls12381Deserialize", callflag.All, g1)
622+
emit.Opcodes(script.BinWriter, opcode.PUSH2, opcode.PACK, opcode.PUSH1, opcode.PACK, opcode.PUSH1, opcode.PACK)
623+
emit.AppCallNoArgs(script.BinWriter, c.Hash, "bls12381MultiExp", callflag.All)
624+
625+
e.AddNewBlock(t)
626+
stack, err := c.TestInvokeScript(t, script.Bytes(), c.Signers)
627+
require.NoError(t, err)
628+
require.Equal(t, 1, stack.Len())
629+
itm := stack.Pop().Item()
630+
require.Equal(t, stackitem.InteropT, itm.Type())
631+
actual, ok := itm.(*stackitem.Interop).Value().(serializable)
632+
require.True(t, ok)
633+
require.Equal(t, hex.EncodeToString(g1), hex.EncodeToString(actual.Bytes()))
634+
}
635+
636+
func TestCryptoLib_Bls12381MultiExpInteropAPI(t *testing.T) {
637+
bc, acc := chain.NewSingleWithCustomConfig(t, func(c *config.Blockchain) {
638+
c.Hardforks = map[string]uint32{
639+
config.HFFaun.String(): 0,
640+
}
641+
})
642+
e := neotest.NewExecutor(t, bc, acc, acc)
643+
644+
src := `package testcryptolib
645+
import "github.com/nspcc-dev/neo-go/pkg/interop/native/crypto"
646+
func Bls12381MultiExp(point, scalar []byte) []byte {
647+
pairs := []crypto.Bls12381Pair{
648+
{
649+
Point: crypto.Bls12381Deserialize(point),
650+
Scalar: scalar,
651+
},
652+
}
653+
return crypto.Bls12381Serialize(crypto.Bls12381MultiExp(pairs))
654+
}`
655+
656+
ctr := neotest.CompileSource(t, e.Validator.ScriptHash(), strings.NewReader(src), &compiler.Options{
657+
Name: "testcryptolib_contract",
658+
})
659+
e.DeployContract(t, ctr, nil)
660+
661+
ctrInvoker := e.NewInvoker(ctr.Hash, e.Committee)
662+
multiplier := make([]byte, fr.Bytes)
663+
multiplier[0] = 1
664+
ctrInvoker.Invoke(t, g1, "bls12381MultiExp", g1, multiplier)
665+
}

0 commit comments

Comments
 (0)