diff --git a/go.mod b/go.mod index 5908417..4ab3934 100644 --- a/go.mod +++ b/go.mod @@ -3,16 +3,20 @@ module github.com/waku-org/go-zerokit-rln go 1.19 require ( - github.com/stretchr/testify v1.7.2 - github.com/waku-org/go-zerokit-rln-apple v0.0.0-20240116135015-f6f595c7b8ef - github.com/waku-org/go-zerokit-rln-arm v0.0.0-20240116134931-a8b8c6ab4b80 - github.com/waku-org/go-zerokit-rln-x86_64 v0.0.0-20240116135046-2875fec12afc + github.com/consensys/gnark-crypto v0.12.1 + github.com/stretchr/testify v1.8.4 + github.com/waku-org/go-zerokit-rln-apple v0.0.0-20240124080743-37fbb869c330 + github.com/waku-org/go-zerokit-rln-arm v0.0.0-20240124081101-5e4387508113 + github.com/waku-org/go-zerokit-rln-x86_64 v0.0.0-20240124081123-f90cfc88a1dc + golang.org/x/crypto v0.18.0 ) require ( + github.com/bits-and-blooms/bitset v1.10.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + github.com/rogpeppe/go-internal v1.9.0 // indirect + golang.org/x/sys v0.16.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 65546fe..43ca8e9 100644 --- a/go.sum +++ b/go.sum @@ -1,36 +1,31 @@ +github.com/bits-and-blooms/bitset v1.10.0 h1:ePXTeiPEazB5+opbv5fr8umg2R/1NlzgDsyepwsSr88= +github.com/bits-and-blooms/bitset v1.10.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/consensys/gnark-crypto v0.12.1 h1:lHH39WuuFgVHONRl3J0LRBtuYdQTumFSDtJF7HpyG8M= +github.com/consensys/gnark-crypto v0.12.1/go.mod h1:v2Gy7L/4ZRosZ7Ivs+9SfUDr0f5UlG+EM5t7MPHiLuY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leanovate/gopter v0.2.9 h1:fQjYxZaynp97ozCzfOyOuAGOU4aU/z37zf/tOujFk7c= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= -github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= -github.com/waku-org/go-zerokit-rln-apple v0.0.0-20230916172309-ee0ee61dde2b h1:KgZVhsLkxsj5gb/FfndSCQu6VYwALrCOgYI3poR95yE= -github.com/waku-org/go-zerokit-rln-apple v0.0.0-20230916172309-ee0ee61dde2b/go.mod h1:KYykqtdApHVYZ3G0spwMnoxc5jH5eI3jyO9SwsSfi48= -github.com/waku-org/go-zerokit-rln-apple v0.0.0-20240116092850-ad40ef83a1b5 h1:yqco0Gy1zdBDkHMAPqJ95jSlPVUxAsxK5xVeiUdxQN8= -github.com/waku-org/go-zerokit-rln-apple v0.0.0-20240116092850-ad40ef83a1b5/go.mod h1:KYykqtdApHVYZ3G0spwMnoxc5jH5eI3jyO9SwsSfi48= -github.com/waku-org/go-zerokit-rln-apple v0.0.0-20240116121347-ee5a1d931442 h1:xDs9CAyhgOq9PXo06zTnd8VsPjngAyTGujeDTfsTwGg= -github.com/waku-org/go-zerokit-rln-apple v0.0.0-20240116121347-ee5a1d931442/go.mod h1:KYykqtdApHVYZ3G0spwMnoxc5jH5eI3jyO9SwsSfi48= -github.com/waku-org/go-zerokit-rln-apple v0.0.0-20240116135015-f6f595c7b8ef h1:MAkZryAeRhiH3TKHRK2h+WztZI1VqfQ/oeXMIxKZNy0= -github.com/waku-org/go-zerokit-rln-apple v0.0.0-20240116135015-f6f595c7b8ef/go.mod h1:KYykqtdApHVYZ3G0spwMnoxc5jH5eI3jyO9SwsSfi48= -github.com/waku-org/go-zerokit-rln-arm v0.0.0-20230916171929-1dd9494ff065 h1:Sd7QD/1Yo2o2M1MY49F8Zr4KNBPUEK5cz5HoXQVJbrs= -github.com/waku-org/go-zerokit-rln-arm v0.0.0-20230916171929-1dd9494ff065/go.mod h1:7cSGUoGVIla1IpnChrLbkVjkYgdOcr7rcifEfh4ReR4= -github.com/waku-org/go-zerokit-rln-arm v0.0.0-20240116134931-a8b8c6ab4b80 h1:3KObRaYJnTI41U0reNBk7DYr5PVCTq8T9gJLXGfPfaY= -github.com/waku-org/go-zerokit-rln-arm v0.0.0-20240116134931-a8b8c6ab4b80/go.mod h1:7cSGUoGVIla1IpnChrLbkVjkYgdOcr7rcifEfh4ReR4= -github.com/waku-org/go-zerokit-rln-x86_64 v0.0.0-20230916171518-2a77c3734dd1 h1:4HSdWMFMufpRo3ECTX6BrvA+VzKhXZf7mS0rTa5cCWU= -github.com/waku-org/go-zerokit-rln-x86_64 v0.0.0-20230916171518-2a77c3734dd1/go.mod h1:+LeEYoW5/uBUTVjtBGLEVCUe9mOYAlu5ZPkIxLOSr5Y= -github.com/waku-org/go-zerokit-rln-x86_64 v0.0.0-20240116135046-2875fec12afc h1:YjIgIrqlNVC+hXNy2ykYy3JvBKima64J9jXbKM/ULpw= -github.com/waku-org/go-zerokit-rln-x86_64 v0.0.0-20240116135046-2875fec12afc/go.mod h1:+LeEYoW5/uBUTVjtBGLEVCUe9mOYAlu5ZPkIxLOSr5Y= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/waku-org/go-zerokit-rln-apple v0.0.0-20240124080743-37fbb869c330 h1:TJmn6GQ5HpxdZraZn6DjUqWy8UV+8pB4yWcsWFAngqE= +github.com/waku-org/go-zerokit-rln-apple v0.0.0-20240124080743-37fbb869c330/go.mod h1:KYykqtdApHVYZ3G0spwMnoxc5jH5eI3jyO9SwsSfi48= +github.com/waku-org/go-zerokit-rln-arm v0.0.0-20240124081101-5e4387508113 h1:dPwc4LAWLXb4Pssej/NtGA9A0UMQwi+JafQPdnhjRWM= +github.com/waku-org/go-zerokit-rln-arm v0.0.0-20240124081101-5e4387508113/go.mod h1:7cSGUoGVIla1IpnChrLbkVjkYgdOcr7rcifEfh4ReR4= +github.com/waku-org/go-zerokit-rln-x86_64 v0.0.0-20240124081123-f90cfc88a1dc h1:GUZlr25hXLu/PeASqm8P5dPOyD4CdfvkzyEtXEBLbr8= +github.com/waku-org/go-zerokit-rln-x86_64 v0.0.0-20240124081123-f90cfc88a1dc/go.mod h1:+LeEYoW5/uBUTVjtBGLEVCUe9mOYAlu5ZPkIxLOSr5Y= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/rln/link/apple.go b/rln/link/apple.go index 6f3d80e..c09ae08 100644 --- a/rln/link/apple.go +++ b/rln/link/apple.go @@ -91,6 +91,10 @@ func (i RLNWrapper) GenerateRLNProof(input []byte) ([]byte, error) { return i.ffi.GenerateRLNProof(input) } +func (i RLNWrapper) GenerateRLNProofWithWitness(input []byte) ([]byte, error) { + return i.ffi.GenerateRLNProofWithWitness(input) +} + func (i RLNWrapper) VerifyWithRoots(input []byte, roots []byte) (bool, error) { return i.ffi.VerifyWithRoots(input, roots) } diff --git a/rln/link/arm.go b/rln/link/arm.go index b0313ef..f7db9a7 100644 --- a/rln/link/arm.go +++ b/rln/link/arm.go @@ -90,6 +90,10 @@ func (i RLNWrapper) GenerateRLNProof(input []byte) ([]byte, error) { return i.ffi.GenerateRLNProof(input) } +func (i RLNWrapper) GenerateRLNProofWithWitness(input []byte) ([]byte, error) { + return i.ffi.GenerateRLNProofWithWitness(input) +} + func (i RLNWrapper) VerifyWithRoots(input []byte, roots []byte) (bool, error) { return i.ffi.VerifyWithRoots(input, roots) } diff --git a/rln/link/x86_64.go b/rln/link/x86_64.go index 320d482..d151518 100644 --- a/rln/link/x86_64.go +++ b/rln/link/x86_64.go @@ -91,6 +91,10 @@ func (i RLNWrapper) GenerateRLNProof(input []byte) ([]byte, error) { return i.ffi.GenerateRLNProof(input) } +func (i RLNWrapper) GenerateRLNProofWithWitness(input []byte) ([]byte, error) { + return i.ffi.GenerateRLNProofWithWitness(input) +} + func (i RLNWrapper) VerifyWithRoots(input []byte, roots []byte) (bool, error) { return i.ffi.VerifyWithRoots(input, roots) } diff --git a/rln/rln.go b/rln/rln.go index 282227a..80b5353 100644 --- a/rln/rln.go +++ b/rln/rln.go @@ -6,11 +6,14 @@ import ( "encoding/json" "errors" "fmt" - "math/big" "github.com/waku-org/go-zerokit-rln/rln/link" ) +// Same as: https://github.com/vacp2p/zerokit/blob/v0.3.5/rln/src/public.rs#L35 +// Prevents a RLN ZK proof generated for one application to be re-used in another one. +var RLN_IDENTIFIER = [32]byte{166, 140, 43, 8, 8, 22, 206, 113, 151, 128, 118, 40, 119, 197, 218, 174, 11, 117, 84, 228, 96, 211, 212, 140, 145, 104, 146, 99, 24, 192, 217, 4} + // RLN represents the context used for rln. type RLN struct { w *link.RLNWrapper @@ -131,13 +134,20 @@ func (r *RLN) SeededMembershipKeyGen(seed []byte) (*IdentityCredential, error) { // appendLength returns length prefixed version of the input with the following format // [len<8>|input], the len is a 8 byte value serialized in little endian - func appendLength(input []byte) []byte { inputLen := make([]byte, 8) binary.LittleEndian.PutUint64(inputLen, uint64(len(input))) return append(inputLen, input...) } +// Similar to appendLength but for 32 byte values. The length that is prepended is +// the length of elements that are 32 bytes long each +func appendLength32(input []byte) []byte { + inputLen := make([]byte, 8) + binary.LittleEndian.PutUint64(inputLen, uint64(len(input)/32)) + return append(inputLen, input...) +} + func (r *RLN) Sha256(data []byte) (MerkleNode, error) { lenPrefData := appendLength(data) @@ -233,6 +243,56 @@ func (r *RLN) GenerateProof(data []byte, key IdentityCredential, index Membershi }, nil } +// Returns a RLN proof with a custom witness, so no tree is required in the RLN instance +// to calculate such proof. The witness can be created with GetMerkleProof data +// input [ id_secret_hash<32> | num_elements<8> | path_elements | num_indexes<8> | path_indexes | x<32> | epoch<32> | rln_identifier<32> ] +// output [ proof<128> | root<32> | epoch<32> | share_x<32> | share_y<32> | nullifier<32> | rln_identifier<32> ] +func (r *RLN) GenerateRLNProofWithWitness(witness RLNWitnessInput) (*RateLimitProof, error) { + + proofBytes, err := r.w.GenerateRLNProofWithWitness(witness.serialize()) + if err != nil { + return nil, err + } + + if len(proofBytes) != 320 { + return nil, errors.New("invalid proof generated") + } + + // parse the proof as [ proof<128> | root<32> | epoch<32> | share_x<32> | share_y<32> | nullifier<32> | rln_identifier<32> ] + proofOffset := 128 + rootOffset := proofOffset + 32 + epochOffset := rootOffset + 32 + shareXOffset := epochOffset + 32 + shareYOffset := shareXOffset + 32 + nullifierOffset := shareYOffset + 32 + rlnIdentifierOffset := nullifierOffset + 32 + + var zkproof ZKSNARK + var proofRoot, shareX, shareY MerkleNode + var epochR Epoch + var nullifier Nullifier + var rlnIdentifier RLNIdentifier + + copy(zkproof[:], proofBytes[0:proofOffset]) + copy(proofRoot[:], proofBytes[proofOffset:rootOffset]) + copy(epochR[:], proofBytes[rootOffset:epochOffset]) + copy(shareX[:], proofBytes[epochOffset:shareXOffset]) + copy(shareY[:], proofBytes[shareXOffset:shareYOffset]) + copy(nullifier[:], proofBytes[shareYOffset:nullifierOffset]) + copy(rlnIdentifier[:], proofBytes[nullifierOffset:rlnIdentifierOffset]) + + return &RateLimitProof{ + Proof: zkproof, + MerkleRoot: proofRoot, + Epoch: epochR, + ShareX: shareX, + ShareY: shareY, + Nullifier: nullifier, + RLNIdentifier: rlnIdentifier, + }, nil + +} + func serialize32(roots [][32]byte) []byte { var result []byte for _, r := range roots { @@ -292,7 +352,7 @@ func (r *RLN) Verify(data []byte, proof RateLimitProof, roots ...[32]byte) (bool return false, err } - return bool(res), nil + return res, nil } // RecoverIDSecret returns an IDSecret having obtained before two proofs @@ -406,58 +466,10 @@ func (r *RLN) GetMerkleProof(index MembershipIndex) (MerkleProof, error) { return MerkleProof{}, err } - // Check if we can read the first byte - if len(proofBytes) < 8 { - return MerkleProof{}, errors.New(fmt.Sprintf("wrong output size: %d", len(proofBytes))) - } - var result MerkleProof - var numElements big.Int - var numIndexes big.Int - - offset := 0 - - // Get amounf of elements in the proof - numElements.SetBytes(revert(proofBytes[offset : offset+8])) - offset += 8 - - // With numElements we can determine the expected length of the proof. - expectedLen := 8 + int(32*numElements.Uint64()) + 8 + int(numElements.Uint64()) - if len(proofBytes) != expectedLen { - return MerkleProof{}, errors.New(fmt.Sprintf("wrong output size expected: %d, current: %d", - expectedLen, - len(proofBytes))) - } - - result.PathElements = make([]MerkleNode, numElements.Uint64()) - - for i := uint64(0); i < numElements.Uint64(); i++ { - copy(result.PathElements[i][:], proofBytes[offset:offset+32]) - offset += 32 - } - - // Get amount of indexes in the path - numIndexes.SetBytes(revert(proofBytes[offset : offset+8])) - offset += 8 - - // Both numElements and numIndexes shall be equal and match the tree depth. - if numIndexes.Uint64() != numElements.Uint64() { - return MerkleProof{}, errors.New(fmt.Sprintf("amount of values in path and indexes do not match: %s vs %s", - numElements.String(), numIndexes.String())) - } - - // TODO: Depth check, but currently not accesible - - result.PathIndexes = make([]uint8, numIndexes.Uint64()) - - for i := uint64(0); i < numIndexes.Uint64(); i++ { - result.PathIndexes[i] = proofBytes[offset] - offset += 1 - } - - if offset != len(proofBytes) { - return MerkleProof{}, errors.New( - fmt.Sprintf("error parsing proof read: %d, length; %d", offset, len(proofBytes))) + err = result.deserialize(proofBytes) + if err != nil { + return MerkleProof{}, err } return result, nil diff --git a/rln/rln_test.go b/rln/rln_test.go index 3a3fc65..f751cdb 100644 --- a/rln/rln_test.go +++ b/rln/rln_test.go @@ -350,6 +350,157 @@ func (s *RLNSuite) TestGetMerkleProof() { } } +func (s *RLNSuite) TestGenerateRLNProofWithWitness_VerifiesOK() { + treeSize := 20 + + rln, err := NewRLN() + s.NoError(err) + + treeElements := make([]IdentityCredential, 0) + + // Create a Merkle tree with random members + for i := 0; i < treeSize; i++ { + memberKeys, err := rln.MembershipKeyGen() + s.NoError(err) + + err = rln.InsertMember(memberKeys.IDCommitment) + s.NoError(err) + treeElements = append(treeElements, *memberKeys) + } + + // We generate proofs with a custom witness aquired outside zerokit for diferent indexes + for _, memberIndex := range []uint{0, 10, 13, 15} { + root, err := rln.GetMerkleRoot() + s.NoError(err) + + // We provide out custom witness + merkleProof, err := rln.GetMerkleProof(memberIndex) + s.NoError(err) + + message := []byte("some rln protected message") + epoch := ToEpoch(1000) + + rlnWitness := CreateWitness( + treeElements[memberIndex].IDSecretHash, + message, + epoch, + merkleProof) + + // Generate a proof with our custom witness (Merkle Path of the memberIndex) + proofRes1, err := rln.GenerateRLNProofWithWitness(rlnWitness) + s.NoError(err) + verified1, err := rln.Verify(message, *proofRes1, root) + s.NoError(err) + s.True(verified1) + + // Generate a proof without our custom witness, to ensure they match + proofRes2, err := rln.GenerateProof(message, treeElements[memberIndex], MembershipIndex(memberIndex), epoch) + s.NoError(err) + + // Ensure we have the same root + s.Equal(root, proofRes1.MerkleRoot) + + // Proof generate with custom witness match the proof generate with the witness + // from zerokit. Proof itself is not asserted, can be different. + s.Equal(proofRes1.MerkleRoot, proofRes2.MerkleRoot) + s.Equal(proofRes1.Epoch, proofRes2.Epoch) + s.Equal(proofRes1.ShareX, proofRes2.ShareX) + s.Equal(proofRes1.ShareY, proofRes2.ShareY) + s.Equal(proofRes1.Nullifier, proofRes2.Nullifier) + s.Equal(proofRes1.RLNIdentifier, proofRes2.RLNIdentifier) + } +} + +func (s *RLNSuite) TestGenerateRLNProofWithWitness_VerifiesNOK() { + treeSize := 20 + + rln, err := NewRLN() + s.NoError(err) + + treeElements := make([]IdentityCredential, 0) + + // Create a Merkle tree with random members + for i := 0; i < treeSize; i++ { + memberKeys, err := rln.MembershipKeyGen() + s.NoError(err) + + err = rln.InsertMember(memberKeys.IDCommitment) + s.NoError(err) + treeElements = append(treeElements, *memberKeys) + } + + // We generate proofs with a custom witness aquired outside zerokit for diferent indexes + for _, memberIndex := range []uint{0, 10, 13, 15} { + root, err := rln.GetMerkleRoot() + s.NoError(err) + + // We provide out custom witness + merkleProof, err := rln.GetMerkleProof(memberIndex) + s.NoError(err) + + message := []byte("some rln protected message") + epoch := ToEpoch(1000) + + rlnWitness1 := CreateWitness( + treeElements[memberIndex].IDSecretHash, + message, + epoch, + merkleProof) + + // Generate a proof with our custom witness (Merkle Path of the memberIndex) + proofRes1, err := rln.GenerateRLNProofWithWitness(rlnWitness1) + s.NoError(err) + + // 1) Message changed, does not verify + verified1, err := rln.Verify([]byte("different message"), *proofRes1, root) + s.NoError(err) + s.False(verified1) + + // 2) Different epoch, does not verify + proofRes1.Epoch = ToEpoch(999) + verified2, err := rln.Verify(message, *proofRes1, root) + s.NoError(err) + s.False(verified2) + + // 3) Merkle proof in provided witness is wrong, does not verify + merkleProof.PathElements[0] = [32]byte{0x11} + rlnWitness2 := CreateWitness( + treeElements[memberIndex].IDSecretHash, + message, + epoch, + merkleProof) + + proofRes3, err := rln.GenerateRLNProofWithWitness(rlnWitness2) + s.NoError(err) + + verified3, err := rln.Verify(message, *proofRes3, root) + s.NoError(err) + s.False(verified3) + + // 4) Membership does not match the index (and not part of tree), does not verify + merkleProof4, err := rln.GetMerkleProof(memberIndex) + s.NoError(err) + + // Membership that does not match the index + memberKeys, err := rln.MembershipKeyGen() + s.NoError(err) + + // Proof proves memberIndex inclusion, but provided membership is different + rlnWitness4 := CreateWitness( + memberKeys.IDSecretHash, + []byte("some rln protected message"), + ToEpoch(999), + merkleProof4) + + proofRes4, err := rln.GenerateRLNProofWithWitness(rlnWitness4) + s.NoError(err) + + verified4, err := rln.Verify(message, *proofRes4, root) + s.NoError(err) + s.False(verified4) + } +} + func (s *RLNSuite) TestEpochConsistency() { // check edge cases var epoch uint64 = math.MaxUint64 diff --git a/rln/serialize.go b/rln/serialize.go index 87e3093..b10664b 100644 --- a/rln/serialize.go +++ b/rln/serialize.go @@ -1,6 +1,11 @@ package rln -import "encoding/binary" +import ( + "encoding/binary" + "errors" + "fmt" + "math/big" +) // serialize converts a RateLimitProof and the data to a byte seq // this conversion is used in the proofGen function @@ -42,3 +47,85 @@ func (r RateLimitProof) serialize() []byte { proofBytes = append(proofBytes, r.RLNIdentifier[:]...) return proofBytes } + +func (r *RLNWitnessInput) serialize() []byte { + output := make([]byte, 0) + + output = append(output, r.IDSecretHash[:]...) + output = append(output, r.MerkleProof.serialize()...) + output = append(output, r.X[:]...) + output = append(output, r.Epoch[:]...) + output = append(output, r.RlnIdentifier[:]...) + + return output +} + +func (r *RLNWitnessInput) deserialize(b []byte) error { + + return errors.New("not implemented") +} + +func (r *MerkleProof) serialize() []byte { + output := make([]byte, 0) + + output = append(output, appendLength32(Flatten(r.PathElements))...) + output = append(output, appendLength(r.PathIndexes)...) + + return output +} + +func (r *MerkleProof) deserialize(b []byte) error { + + // Check if we can read the first byte + if len(b) < 8 { + return errors.New(fmt.Sprintf("wrong input size: %d", len(b))) + } + + var numElements big.Int + var numIndexes big.Int + + offset := 0 + + // Get amounf of elements in the proof + numElements.SetBytes(revert(b[offset : offset+8])) + offset += 8 + + // With numElements we can determine the expected length of the proof. + expectedLen := 8 + int(32*numElements.Uint64()) + 8 + int(numElements.Uint64()) + if len(b) != expectedLen { + return errors.New(fmt.Sprintf("wrong input size expected: %d, current: %d", + expectedLen, + len(b))) + } + + r.PathElements = make([]MerkleNode, numElements.Uint64()) + + for i := uint64(0); i < numElements.Uint64(); i++ { + copy(r.PathElements[i][:], b[offset:offset+32]) + offset += 32 + } + + // Get amount of indexes in the path + numIndexes.SetBytes(revert(b[offset : offset+8])) + offset += 8 + + // Both numElements and numIndexes shall be equal and match the tree depth. + if numIndexes.Uint64() != numElements.Uint64() { + return errors.New(fmt.Sprintf("amount of values in path and indexes do not match: %s vs %s", + numElements.String(), numIndexes.String())) + } + + r.PathIndexes = make([]uint8, numIndexes.Uint64()) + + for i := uint64(0); i < numIndexes.Uint64(); i++ { + r.PathIndexes[i] = b[offset] + offset += 1 + } + + if offset != len(b) { + return errors.New( + fmt.Sprintf("error parsing proof read: %d, length; %d", offset, len(b))) + } + + return nil +} diff --git a/rln/serialize_test.go b/rln/serialize_test.go new file mode 100644 index 0000000..78a97c5 --- /dev/null +++ b/rln/serialize_test.go @@ -0,0 +1,64 @@ +package rln + +import ( + "math/rand" + "testing" + + "github.com/stretchr/testify/require" +) + +func random32() [32]byte { + var randomBytes [32]byte + _, _ = rand.Read(randomBytes[:]) + return randomBytes +} + +func TestMerkleProofSerDe(t *testing.T) { + + for _, testSize := range []int{0, 1, 8, 16, 20} { + mProof := MerkleProof{ + PathElements: []MerkleNode{}, + PathIndexes: []uint8{}, + } + + for i := 0; i < testSize; i++ { + mProof.PathElements = append(mProof.PathElements, random32()) + mProof.PathIndexes = append(mProof.PathIndexes, uint8(i%2)) + } + + // Check the size is the expected + ser := mProof.serialize() + require.Equal(t, 8+testSize*32+testSize+8, len(ser)) + + // Deserialize and check its matches the original + desProof := MerkleProof{} + err := desProof.deserialize(ser) + require.NoError(t, err) + require.Equal(t, mProof, desProof) + } +} + +func TestRLNWitnessInputSerDe(t *testing.T) { + depth := 20 + + mProof := MerkleProof{ + PathElements: []MerkleNode{}, + PathIndexes: []uint8{}, + } + + for i := 0; i < depth; i++ { + mProof.PathElements = append(mProof.PathElements, random32()) + mProof.PathIndexes = append(mProof.PathIndexes, uint8(i%2)) + } + + witness := RLNWitnessInput{ + IDSecretHash: random32(), + MerkleProof: mProof, + X: [32]byte{0x00}, + Epoch: ToEpoch(10), + RlnIdentifier: [32]byte{0x00}, + } + + ser := witness.serialize() + require.Equal(t, 32+8+depth*32+depth+8+32+32+32, len(ser)) +} diff --git a/rln/types.go b/rln/types.go index 02eed31..7d8f0dc 100644 --- a/rln/types.go +++ b/rln/types.go @@ -68,6 +68,15 @@ type MerkleProof struct { PathIndexes []uint8 `json:"pathIndexes"` } +// Equivalent: https://github.com/vacp2p/zerokit/blob/v0.3.5/rln/src/protocol.rs#L33-L40 +type RLNWitnessInput struct { + IDSecretHash IDSecretHash `json:"identitySecretHash"` + MerkleProof MerkleProof `json:"merkleProof"` + X [32]byte `json:"x"` + Epoch Epoch `json:"epoch"` + RlnIdentifier RLNIdentifier `json:"rlnIdentifier"` +} + type TreeDepth int const ( diff --git a/rln/utils.go b/rln/utils.go index 42e767d..d87313d 100644 --- a/rln/utils.go +++ b/rln/utils.go @@ -2,9 +2,29 @@ package rln import ( "encoding/hex" + "hash" "math/big" + "sync" + + "github.com/consensys/gnark-crypto/ecc/bn254/fr" + "golang.org/x/crypto/sha3" ) +func CreateWitness( + idSecretHash IDSecretHash, + data []byte, + epoch [32]byte, + merkleProof MerkleProof) RLNWitnessInput { + + return RLNWitnessInput{ + IDSecretHash: idSecretHash, + MerkleProof: merkleProof, + X: HashToBN255(data), + Epoch: epoch, + RlnIdentifier: RLN_IDENTIFIER, + } +} + func ToIdentityCredentials(groupKeys [][]string) ([]IdentityCredential, error) { // groupKeys is sequence of membership key tuples in the form of (identity key, identity commitment) all in the hexadecimal format // the toIdentityCredentials proc populates a sequence of IdentityCredentials using the supplied groupKeys @@ -56,6 +76,14 @@ func Bytes128(b []byte) [128]byte { return result } +func Flatten(b [][32]byte) []byte { + result := make([]byte, len(b)*32) + for i, v := range b { + copy(result[i*32:(i+1)*32], v[:]) + } + return result +} + func ToBytes32LE(hexStr string) ([32]byte, error) { b, err := hex.DecodeString(hexStr) @@ -98,3 +126,58 @@ func Bytes32ToBigInt(value [32]byte) *big.Int { result.SetBytes(b) return result } + +// Keccak functions take from here. To avoid unnecessary dependency to go-ethereum. +// https://github.com/ethereum/go-ethereum/blob/v1.13.11/crypto/crypto.go#L62-L84 + +// KeccakState wraps sha3.state. In addition to the usual hash methods, it also supports +// Read to get a variable amount of data from the hash state. Read is faster than Sum +// because it doesn't copy the internal state, but also modifies the internal state. +type KeccakState interface { + hash.Hash + Read([]byte) (int, error) +} + +// Avoids multiple allocations if used frequently +var keccak256Pool = sync.Pool{New: func() interface{} { + return NewKeccakState() +}} + +// NewKeccakState creates a new KeccakState +func NewKeccakState() KeccakState { + return sha3.NewLegacyKeccak256().(KeccakState) +} + +// Keccak256 calculates and returns the Keccak256 hash of the input data. +func Keccak256(data ...[]byte) []byte { + b := make([]byte, 32) + h, ok := keccak256Pool.Get().(KeccakState) + if !ok { + h = NewKeccakState() + } + defer keccak256Pool.Put(h) + h.Reset() + for _, b := range data { + h.Write(b) + } + + h.Read(b) + return b +} + +// Hashes a byte array to a field element in BN254, as used by zerokit. +// Equivalent to: https://github.com/vacp2p/zerokit/blob/v0.3.4/rln/src/hashers.rs +func HashToBN255(data []byte) [32]byte { + // Hash is fixed to 32 bytes + hashed := Keccak256(data[:]) + + // Convert to field element + var frBN254 fr.Element + frBN254.Unmarshal(revert(hashed)) + frBN254Bytes := frBN254.Bytes() + + // Return fixed size + fixexLen := [32]byte{} + copy(fixexLen[:], revert(frBN254Bytes[:])) + return fixexLen +} diff --git a/rln/utils_test.go b/rln/utils_test.go index f44440e..c7610c2 100644 --- a/rln/utils_test.go +++ b/rln/utils_test.go @@ -19,3 +19,35 @@ func TestBigInt(t *testing.T) { newValue := Bytes32ToBigInt(b32Value) require.True(t, bytes.Equal(newValue.Bytes(), value.Bytes())) } + +func TestFlatten(t *testing.T) { + in1 := [][32]byte{[32]byte{}} + in2 := [][32]byte{[32]byte{0x00}, [32]byte{0x01}} + in3 := [][32]byte{[32]byte{0x01, 0x02, 0x03}, [32]byte{0x04, 0x05, 0x06}} + + expected1 := []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0} + expected2 := []byte{ + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0} + expected3 := []byte{ + 0x1, 0x2, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x4, 0x5, 0x6, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0} + + out1 := Flatten(in1) + require.Equal(t, expected1, out1) + + out2 := Flatten(in2) + require.Equal(t, expected2, out2) + + out3 := Flatten(in3) + require.Equal(t, expected3, out3) +} +func TestHashToBN255(t *testing.T) { + // Inputs for proof generation + msg := []byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} + + out := HashToBN255(msg) + require.Equal(t, + [32]byte{69, 7, 140, 46, 26, 131, 147, 30, 161, 68, 2, 5, 234, 195, 227, 223, 119, 187, 116, 97, 153, 70, 71, 254, 60, 149, 54, 109, 77, 79, 105, 20}, + out) +}