diff --git a/api/censuses.go b/api/censuses.go index 9447d1654..b8615112d 100644 --- a/api/censuses.go +++ b/api/censuses.go @@ -945,15 +945,15 @@ func (a *API) censusVerifyHandler(msg *apirest.APIdata, ctx *httprouter.HTTPCont } } - valid, err := ref.Tree().VerifyProof(leafKey, cdata.Value, cdata.CensusProof, cdata.CensusRoot) - if err != nil { + if err := ref.Tree().VerifyProof(leafKey, cdata.Value, cdata.CensusProof, cdata.CensusRoot); err != nil { + if strings.Contains(err.Error(), "calculated vs expected root mismatch") { + return ctx.Send(nil, apirest.HTTPstatusBadRequest) + } return ErrCensusProofVerificationFailed.WithErr(err) } - if !valid { - return ctx.Send(nil, apirest.HTTPstatusBadRequest) - } + response := Census{ - Valid: valid, + Valid: true, } var data []byte if data, err = json.Marshal(&response); err != nil { diff --git a/censustree/censustree.go b/censustree/censustree.go index 7a5ce433f..78fb09b7a 100644 --- a/censustree/censustree.go +++ b/censustree/censustree.go @@ -162,12 +162,12 @@ func (t *Tree) Get(key []byte) ([]byte, error) { // VerifyProof verifies a census proof. // If the census is indexed key can be nil (value provides the key already). // If root is nil the last merkle root is used for verify. -func (t *Tree) VerifyProof(key, value, proof, root []byte) (bool, error) { +func (t *Tree) VerifyProof(key, value, proof, root []byte) error { var err error if root == nil { root, err = t.Root() if err != nil { - return false, fmt.Errorf("cannot get tree root: %w", err) + return fmt.Errorf("cannot get tree root: %w", err) } } // If the provided key is longer than the defined maximum length truncate it @@ -176,7 +176,10 @@ func (t *Tree) VerifyProof(key, value, proof, root []byte) (bool, error) { if len(leafKey) > DefaultMaxKeyLen { leafKey = leafKey[:DefaultMaxKeyLen] } - return t.tree.VerifyProof(leafKey, value, proof, root) + if err := t.tree.VerifyProof(leafKey, value, proof, root); err != nil { + return err + } + return nil } // GenProof generates a census proof for the provided key. diff --git a/censustree/censustree_test.go b/censustree/censustree_test.go index 7064e5965..224b5b95c 100644 --- a/censustree/censustree_test.go +++ b/censustree/censustree_test.go @@ -110,9 +110,8 @@ func TestWeightedProof(t *testing.T) { root, err := censusTree.Root() qt.Assert(t, err, qt.IsNil) - verified, err := censusTree.VerifyProof(userKey, value, siblings, root) + err = censusTree.VerifyProof(userKey, value, siblings, root) qt.Assert(t, err, qt.IsNil) - qt.Assert(t, verified, qt.IsTrue) } func TestGetCensusWeight(t *testing.T) { diff --git a/tree/arbo/addbatch_test.go b/tree/arbo/addbatch_test.go index 33ba97dae..dba686a79 100644 --- a/tree/arbo/addbatch_test.go +++ b/tree/arbo/addbatch_test.go @@ -998,14 +998,12 @@ func TestAddKeysWithEmptyValues(t *testing.T) { // check with empty array root, err := tree.Root() c.Assert(err, qt.IsNil) - verif, err := CheckProof(tree.hashFunction, keys[9], []byte{}, root, siblings) + err = CheckProof(tree.hashFunction, keys[9], []byte{}, root, siblings) c.Assert(err, qt.IsNil) - c.Check(verif, qt.IsTrue) // check with array with only 1 zero - verif, err = CheckProof(tree.hashFunction, keys[9], []byte{0}, root, siblings) + err = CheckProof(tree.hashFunction, keys[9], []byte{0}, root, siblings) c.Assert(err, qt.IsNil) - c.Check(verif, qt.IsTrue) // check with array with 32 zeroes e32 := []byte{ @@ -1013,12 +1011,10 @@ func TestAddKeysWithEmptyValues(t *testing.T) { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, } c.Assert(len(e32), qt.Equals, 32) - verif, err = CheckProof(tree.hashFunction, keys[9], e32, root, siblings) + err = CheckProof(tree.hashFunction, keys[9], e32, root, siblings) c.Assert(err, qt.IsNil) - c.Check(verif, qt.IsTrue) // check with array with value!=0 returns false at verification - verif, err = CheckProof(tree.hashFunction, keys[9], []byte{0, 1}, root, siblings) - c.Assert(err, qt.IsNil) - c.Check(verif, qt.IsFalse) + err = CheckProof(tree.hashFunction, keys[9], []byte{0, 1}, root, siblings) + c.Assert(err, qt.ErrorMatches, "calculated vs expected root mismatch") } diff --git a/tree/arbo/circomproofs.go b/tree/arbo/circomproofs.go index 94a4aacbb..afb0797d3 100644 --- a/tree/arbo/circomproofs.go +++ b/tree/arbo/circomproofs.go @@ -1,7 +1,10 @@ package arbo import ( + "bytes" "encoding/json" + "fmt" + "slices" ) // CircomVerifierProof contains the needed data to check a Circom Verifier Proof @@ -89,3 +92,33 @@ func (t *Tree) GenerateCircomVerifierProof(k []byte) (*CircomVerifierProof, erro return &cp, nil } + +// CalculateProofNodes calculates the chain of hashes in the path of the proof. +// In the returned list, first item is the root, and last item is the hash of the leaf. +func (cvp CircomVerifierProof) CalculateProofNodes(hashFunc HashFunction) ([][]byte, error) { + paddedSiblings := slices.Clone(cvp.Siblings) + for k, v := range paddedSiblings { + if bytes.Equal(v, []byte{0}) { + paddedSiblings[k] = make([]byte, hashFunc.Len()) + } + } + packedSiblings, err := PackSiblings(hashFunc, paddedSiblings) + if err != nil { + return nil, err + } + return CalculateProofNodes(hashFunc, cvp.Key, cvp.Value, packedSiblings, cvp.OldKey, (cvp.Fnc == 1)) +} + +// CheckProof verifies the given proof. The proof verification depends on the +// HashFunction passed as parameter. +// Returns nil if the proof is valid, or an error otherwise. +func (cvp CircomVerifierProof) CheckProof(hashFunc HashFunction) error { + hashes, err := cvp.CalculateProofNodes(hashFunc) + if err != nil { + return err + } + if !bytes.Equal(hashes[0], cvp.Root) { + return fmt.Errorf("calculated vs expected root mismatch") + } + return nil +} diff --git a/tree/arbo/proof.go b/tree/arbo/proof.go index ad19f671d..4cdfce7c4 100644 --- a/tree/arbo/proof.go +++ b/tree/arbo/proof.go @@ -3,6 +3,7 @@ package arbo import ( "bytes" "encoding/binary" + "encoding/hex" "fmt" "math" "slices" @@ -159,30 +160,115 @@ func bytesToBitmap(b []byte) []bool { // CheckProof verifies the given proof. The proof verification depends on the // HashFunction passed as parameter. -func CheckProof(hashFunc HashFunction, k, v, root, packedSiblings []byte) (bool, error) { +// Returns nil if the proof is valid, or an error otherwise. +func CheckProof(hashFunc HashFunction, k, v, root, packedSiblings []byte) error { + hashes, err := CalculateProofNodes(hashFunc, k, v, packedSiblings, nil, false) + if err != nil { + return err + } + if !bytes.Equal(hashes[0], root) { + return fmt.Errorf("calculated vs expected root mismatch") + } + return nil +} + +// CalculateProofNodes calculates the chain of hashes in the path of the given proof. +// In the returned list, first item is the root, and last item is the hash of the leaf. +func CalculateProofNodes(hashFunc HashFunction, k, v, packedSiblings, oldKey []byte, exclusion bool) ([][]byte, error) { siblings, err := UnpackSiblings(hashFunc, packedSiblings) if err != nil { - return false, err + return nil, err } keyPath := make([]byte, int(math.Ceil(float64(len(siblings))/float64(8)))) copy(keyPath, k) + path := getPath(len(siblings), keyPath) - key, _, err := newLeafValue(hashFunc, k, v) - if err != nil { - return false, err + key := slices.Clone(k) + + if exclusion { + if slices.Equal(k, oldKey) { + return nil, fmt.Errorf("exclusion proof invalid, key and oldKey are equal") + } + // we'll prove the path to the existing key (passed as oldKey) + key = slices.Clone(oldKey) } - path := getPath(len(siblings), keyPath) + hash, _, err := newLeafValue(hashFunc, key, v) + if err != nil { + return nil, err + } + hashes := [][]byte{hash} for i, sibling := range slices.Backward(siblings) { if path[i] { - key, _, err = newIntermediate(hashFunc, sibling, key) + hash, _, err = newIntermediate(hashFunc, sibling, hash) } else { - key, _, err = newIntermediate(hashFunc, key, sibling) + hash, _, err = newIntermediate(hashFunc, hash, sibling) + } + if err != nil { + return nil, err + } + hashes = append(hashes, hash) + } + slices.Reverse(hashes) + return hashes, nil +} + +// CheckProofBatch verifies a batch of N proofs pairs (old and new). The proof verification depends on the +// HashFunction passed as parameter. +// Returns nil if the batch is valid, or an error otherwise. +// +// TODO: doesn't support removing leaves (newProofs can only update or add new leaves) +func CheckProofBatch(hashFunc HashFunction, oldProofs, newProofs []*CircomVerifierProof) error { + newBranches := make(map[string]int) + newSiblings := make(map[string]int) + + if len(oldProofs) != len(newProofs) { + return fmt.Errorf("batch of proofs incomplete") + } + + if len(oldProofs) == 0 { + return fmt.Errorf("empty batch") + } + + for i := range oldProofs { + // Map all old branches + oldNodes, err := oldProofs[i].CalculateProofNodes(hashFunc) + if err != nil { + return fmt.Errorf("old proof invalid: %w", err) } + // and check they are valid + if !bytes.Equal(oldProofs[i].Root, oldNodes[0]) { + return fmt.Errorf("old proof invalid: root doesn't match") + } + + // Map all new branches + newNodes, err := newProofs[i].CalculateProofNodes(hashFunc) if err != nil { - return false, err + return fmt.Errorf("new proof invalid: %w", err) + } + // and check they are valid + if !bytes.Equal(newProofs[i].Root, newNodes[0]) { + return fmt.Errorf("new proof invalid: root doesn't match") + } + + for level, hash := range newNodes { + newBranches[hex.EncodeToString(hash)] = level + } + + for level := range newProofs[i].Siblings { + if !slices.Equal(oldProofs[i].Siblings[level], newProofs[i].Siblings[level]) { + // since in newBranch the root is level 0, we shift siblings to level + 1 + newSiblings[hex.EncodeToString(newProofs[i].Siblings[level])] = level + 1 + } } } - return bytes.Equal(key, root), nil + + for hash, level := range newSiblings { + if newBranches[hash] != newSiblings[hash] { + return fmt.Errorf("sibling %s (at level %d) changed but there's no proof why", hash, level) + } + } + + return nil } diff --git a/tree/arbo/proof_test.go b/tree/arbo/proof_test.go new file mode 100644 index 000000000..d095b6dac --- /dev/null +++ b/tree/arbo/proof_test.go @@ -0,0 +1,150 @@ +package arbo + +import ( + "math/big" + "slices" + "testing" + + qt "github.com/frankban/quicktest" + "go.vocdoni.io/dvote/db/metadb" +) + +func TestCheckProofBatch(t *testing.T) { + database := metadb.NewTest(t) + c := qt.New(t) + + keyLen := 1 + maxLevels := keyLen * 8 + tree, err := NewTree(Config{ + Database: database, MaxLevels: maxLevels, + HashFunction: HashFunctionBlake3, + }) + c.Assert(err, qt.IsNil) + + censusRoot := []byte("01234567890123456789012345678901") + ballotMode := []byte("1234") + + err = tree.Add(BigIntToBytesLE(keyLen, big.NewInt(0x01)), censusRoot) + c.Assert(err, qt.IsNil) + + err = tree.Add(BigIntToBytesLE(keyLen, big.NewInt(0x02)), ballotMode) + c.Assert(err, qt.IsNil) + + var oldProofs, newProofs []*CircomVerifierProof + + for i := int64(0x00); i <= int64(0x04); i++ { + proof, err := tree.GenerateCircomVerifierProof(BigIntToBytesLE(keyLen, big.NewInt(i))) + c.Assert(err, qt.IsNil) + oldProofs = append(oldProofs, proof) + } + + censusRoot[0] = byte(0x02) + ballotMode[0] = byte(0x02) + + err = tree.Update(BigIntToBytesLE(keyLen, big.NewInt(0x01)), censusRoot) + c.Assert(err, qt.IsNil) + + err = tree.Update(BigIntToBytesLE(keyLen, big.NewInt(0x02)), ballotMode) + c.Assert(err, qt.IsNil) + + err = tree.Add(BigIntToBytesLE(keyLen, big.NewInt(0x03)), ballotMode) + c.Assert(err, qt.IsNil) + + for i := int64(0x00); i <= int64(0x04); i++ { + proof, err := tree.GenerateCircomVerifierProof(BigIntToBytesLE(keyLen, big.NewInt(i))) + c.Assert(err, qt.IsNil) + newProofs = append(newProofs, proof) + } + + // passing all proofs should be OK: + // proof 1 + 2 + 3 are required + // proof 0 and 4 are of unchanged keys, but the new siblings are explained by the other proofs + err = CheckProofBatch(HashFunctionBlake3, oldProofs, newProofs) + c.Assert(err, qt.IsNil) + + // omitting proof 0 and 4 (unchanged keys) should also be OK + err = CheckProofBatch(HashFunctionBlake3, oldProofs[1:4], newProofs[1:4]) + c.Assert(err, qt.IsNil) + + // providing an empty batch should not pass + err = CheckProofBatch(HashFunctionBlake3, []*CircomVerifierProof{}, []*CircomVerifierProof{}) + c.Assert(err, qt.ErrorMatches, "empty batch") + + // length mismatch + err = CheckProofBatch(HashFunctionBlake3, oldProofs, newProofs[:1]) + c.Assert(err, qt.ErrorMatches, "batch of proofs incomplete") + + // providing just proof 0 (unchanged key) should not pass since siblings can't be explained + err = CheckProofBatch(HashFunctionBlake3, oldProofs[:1], newProofs[:1]) + c.Assert(err, qt.ErrorMatches, ".*changed but there's no proof why.*") + + // providing just proof 0 (unchanged key) and an add, should fail + err = CheckProofBatch(HashFunctionBlake3, oldProofs[:1], newProofs[3:4]) + c.Assert(err, qt.ErrorMatches, ".*changed but there's no proof why.*") + + // omitting proof 3 should fail (since changed siblings in other proofs can't be explained) + err = CheckProofBatch(HashFunctionBlake3, oldProofs[:3], newProofs[:3]) + c.Assert(err, qt.ErrorMatches, ".*changed but there's no proof why.*") + + // the next 4 are mangling proofs to simulate other unexplained changes in the tree, all of these should fail + badProofs := deepClone(oldProofs) + badProofs[0].Root = []byte("01234567890123456789012345678900") + err = CheckProofBatch(HashFunctionBlake3, badProofs, newProofs) + c.Assert(err, qt.ErrorMatches, "old proof invalid: root doesn't match") + + badProofs = deepClone(oldProofs) + badProofs[0].Siblings[0] = []byte("01234567890123456789012345678900") + err = CheckProofBatch(HashFunctionBlake3, badProofs, newProofs) + c.Assert(err, qt.ErrorMatches, "old proof invalid: root doesn't match") + + badProofs = deepClone(newProofs) + badProofs[0].Root = []byte("01234567890123456789012345678900") + err = CheckProofBatch(HashFunctionBlake3, oldProofs, badProofs) + c.Assert(err, qt.ErrorMatches, "new proof invalid: root doesn't match") + + badProofs = deepClone(newProofs) + badProofs[0].Siblings[0] = []byte("01234567890123456789012345678900") + err = CheckProofBatch(HashFunctionBlake3, oldProofs, badProofs) + c.Assert(err, qt.ErrorMatches, "new proof invalid: root doesn't match") + + // also test exclusion proofs: + // exclusion proof of key 0x04 can't be used to prove exclusion of 0x01, 0x03 or 0x05 obviously + badProofs = deepClone(oldProofs) + badProofs[4].Key = []byte{0x01} + err = CheckProofBatch(HashFunctionBlake3, oldProofs[4:], badProofs[4:]) + c.Assert(err, qt.ErrorMatches, "new proof invalid: root doesn't match") + badProofs[4].Key = []byte{0x03} + err = CheckProofBatch(HashFunctionBlake3, oldProofs[4:], badProofs[4:]) + c.Assert(err, qt.ErrorMatches, "new proof invalid: root doesn't match") + badProofs[4].Key = []byte{0x05} + err = CheckProofBatch(HashFunctionBlake3, oldProofs[4:], badProofs[4:]) + c.Assert(err, qt.ErrorMatches, "new proof invalid: root doesn't match") + // also can't prove key 0x02 exclusion (since that leaf exists and is indeed the starting point of the proof) + badProofs[4].Key = []byte{0x02} + err = CheckProofBatch(HashFunctionBlake3, oldProofs[4:], badProofs[4:]) + c.Assert(err, qt.ErrorMatches, "new proof invalid: exclusion proof invalid, key and oldKey are equal") + // but exclusion proof of key 0x04 can also prove exclusion of the whole prefix (0x00, 0x08, 0x0c, 0x10, etc) + badProofs[4].Key = []byte{0x00} + err = CheckProofBatch(HashFunctionBlake3, oldProofs[4:], badProofs[4:]) + c.Assert(err, qt.IsNil) + badProofs[4].Key = []byte{0x08} + err = CheckProofBatch(HashFunctionBlake3, oldProofs[4:], badProofs[4:]) + c.Assert(err, qt.IsNil) + badProofs[4].Key = []byte{0x0c} + err = CheckProofBatch(HashFunctionBlake3, oldProofs[4:], badProofs[4:]) + c.Assert(err, qt.IsNil) + badProofs[4].Key = []byte{0x10} + err = CheckProofBatch(HashFunctionBlake3, oldProofs[4:], badProofs[4:]) + c.Assert(err, qt.IsNil) +} + +func deepClone(src []*CircomVerifierProof) []*CircomVerifierProof { + dst := slices.Clone(src) + for i := range src { + proof := *src[i] + dst[i] = &proof + + dst[i].Siblings = slices.Clone(src[i].Siblings) + } + return dst +} diff --git a/tree/arbo/tree_test.go b/tree/arbo/tree_test.go index 7980e1a93..ac298e3aa 100644 --- a/tree/arbo/tree_test.go +++ b/tree/arbo/tree_test.go @@ -539,9 +539,8 @@ func TestGenProofAndVerify(t *testing.T) { root, err := tree.Root() c.Assert(err, qt.IsNil) - verif, err := CheckProof(tree.hashFunction, k, v, root, siblings) + err = CheckProof(tree.hashFunction, k, v, root, siblings) c.Assert(err, qt.IsNil) - c.Check(verif, qt.IsTrue) } func TestDumpAndImportDump(t *testing.T) { @@ -933,16 +932,14 @@ func TestKeyLen(t *testing.T) { root, err := tree.Root() c.Assert(err, qt.IsNil) - verif, err := CheckProof(tree.HashFunction(), kAux, vAux, root, packedSiblings) + err = CheckProof(tree.HashFunction(), kAux, vAux, root, packedSiblings) c.Assert(err, qt.IsNil) - c.Assert(verif, qt.IsTrue) // use a similar key but with one zero, expect that CheckProof fails on // the verification kAux = append(kAux, 0) - verif, err = CheckProof(tree.HashFunction(), kAux, vAux, root, packedSiblings) - c.Assert(err, qt.IsNil) - c.Assert(verif, qt.IsFalse) + err = CheckProof(tree.HashFunction(), kAux, vAux, root, packedSiblings) + c.Assert(err, qt.ErrorMatches, "calculated vs expected root mismatch") } func TestKeyLenBiggerThan32(t *testing.T) { diff --git a/tree/tree.go b/tree/tree.go index 8a5fc217e..dc491be7a 100644 --- a/tree/tree.go +++ b/tree/tree.go @@ -229,13 +229,13 @@ func (t *Tree) GenProof(rTx db.Reader, key []byte) ([]byte, []byte, error) { // VerifyProof checks the proof for the given key, value and root, using the // passed hash function -func VerifyProof(hashFunc arbo.HashFunction, key, value, proof, root []byte) (bool, error) { +func VerifyProof(hashFunc arbo.HashFunction, key, value, proof, root []byte) error { return arbo.CheckProof(hashFunc, key, value, root, proof) } // VerifyProof checks the proof for the given key, value and root, using the // hash function of the Tree -func (t *Tree) VerifyProof(key, value, proof, root []byte) (bool, error) { +func (t *Tree) VerifyProof(key, value, proof, root []byte) error { return VerifyProof(t.tree.HashFunction(), key, value, proof, root) } diff --git a/tree/tree_test.go b/tree/tree_test.go index 2e9d7a242..8be16b5bb 100644 --- a/tree/tree_test.go +++ b/tree/tree_test.go @@ -84,9 +84,8 @@ func TestGenProof(t *testing.T) { root, err := tree.Root(wTx) qt.Assert(t, err, qt.IsNil) - verif, err := tree.VerifyProof(k, v, proof, root) + err = tree.VerifyProof(k, v, proof, root) qt.Assert(t, err, qt.IsNil) - qt.Assert(t, verif, qt.IsTrue) err = wTx.Commit() qt.Assert(t, err, qt.IsNil) diff --git a/vochain/transaction/proofs/arboproof/arboproof.go b/vochain/transaction/proofs/arboproof/arboproof.go index f9e933d4a..689b73bd8 100644 --- a/vochain/transaction/proofs/arboproof/arboproof.go +++ b/vochain/transaction/proofs/arboproof/arboproof.go @@ -52,8 +52,8 @@ func (*ProofVerifierArbo) Verify(process *models.Process, envelope *models.VoteE key = key[:censustree.DefaultMaxKeyLen] } } - valid, err := tree.VerifyProof(hashFunc, key, p.AvailableWeight, p.Siblings, process.CensusRoot) - if !valid || err != nil { + + if err := tree.VerifyProof(hashFunc, key, p.AvailableWeight, p.Siblings, process.CensusRoot); err != nil { return false, nil, err } // Legacy: support p.LeafWeight == nil, assume then value=1 diff --git a/vochain/vote_test.go b/vochain/vote_test.go index cf04fcec9..cbee6cfde 100644 --- a/vochain/vote_test.go +++ b/vochain/vote_test.go @@ -46,9 +46,8 @@ func testCreateKeysAndBuildWeightedZkCensus(t *testing.T, size int, weight *big. _, proof, err := tr.GenProof(k.Address().Bytes()) qt.Check(t, err, qt.IsNil) proofs = append(proofs, proof) - valid, err := tr.VerifyProof(k.Address().Bytes(), encWeight, proof, root) + err = tr.VerifyProof(k.Address().Bytes(), encWeight, proof, root) qt.Check(t, err, qt.IsNil) - qt.Check(t, valid, qt.IsTrue) } return keys, root, proofs }