diff --git a/go/carmen/database_test.go b/go/carmen/database_test.go index cee1d5b12..1557f65a2 100644 --- a/go/carmen/database_test.go +++ b/go/carmen/database_test.go @@ -23,6 +23,7 @@ import ( "testing" "time" + "github.com/Fantom-foundation/Carmen/go/database/mpt" "github.com/Fantom-foundation/Carmen/go/database/mpt/io" "github.com/Fantom-foundation/Carmen/go/state/gostate" @@ -1216,26 +1217,35 @@ func TestDatabase_GetProof_Extract_SubProofs(t *testing.T) { } // extract storage nodes only - gotStorageElements, _, complete := recovered.GetStorageElements(root, addr, keys...) - if !complete { - t.Errorf("proof is not complete") - } - // first block's account has no storage, others do - if j > 0 && len(gotStorageElements) == 0 { - t.Errorf("no storage elements") - } + allStorageElements := []Bytes{} + for _, key := range keys { + gotStorageElements, complete := recovered.GetStorageElements(root, addr, key) + if !complete { + t.Errorf("proof is not complete") + } + // first block's account has no storage, others do + if j > 0 && len(gotStorageElements) == 0 { + t.Errorf("no storage elements") + } + + // both proofs must be distinct + for _, accountElement := range gotAccount.GetElements() { + for _, storageElement := range gotStorageElements { + if accountElement == storageElement { + t.Errorf("account and storage proofs must be distinct") + } + } + } - // both proofs must be distinct - for _, accountElement := range gotAccount.GetElements() { for _, storageElement := range gotStorageElements { - if accountElement == storageElement { - t.Errorf("account and storage proofs must be distinct") + if storageElement != mpt.EmptyNodeEthereumEncoding { + allStorageElements = append(allStorageElements, storageElement) } } } // putting nodes together must provide the original proof - merged := CreateWitnessProofFromNodes(append(gotStorageElements, gotAccount.GetElements()...)...) + merged := CreateWitnessProofFromNodes(append(allStorageElements, gotAccount.GetElements()...)...) gotElements := merged.GetElements() wantElements := shadowProofs[addr].GetElements() diff --git a/go/carmen/example_test.go b/go/carmen/example_test.go index 374de9ee6..93c3a9b75 100644 --- a/go/carmen/example_test.go +++ b/go/carmen/example_test.go @@ -481,10 +481,10 @@ func ExampleWitnessProof_GetStorageElements() { // ------- Recover Proof and Split into Account and Storage Proof ------- proof := carmen.CreateWitnessProofFromNodes(elements...) - accountProof, accountComplete := proof.Extract(rootHash, carmen.Address{1}) - storageElements, storageRoot, storageComplete := proof.GetStorageElements(rootHash, carmen.Address{1}, carmen.Key{1}) + accountProofElements, storageRoot, accountComplete := proof.GetAccountElements(rootHash, carmen.Address{1}) + storageElements, storageComplete := proof.GetStorageElements(rootHash, carmen.Address{1}, carmen.Key{1}) - fmt.Printf("Account proof is complete: %v and has %d elements\n", accountComplete, len(accountProof.GetElements())) + fmt.Printf("Account proof is complete: %v and has %d elements\n", accountComplete, len(accountProofElements)) fmt.Printf("Storage proof is complete: %v and has %d elements and root: %v\n", storageComplete, len(storageElements), storageRoot) // Output: Account proof is complete: true and has 1 elements diff --git a/go/carmen/proof.go b/go/carmen/proof.go index e8d594db7..dd7556f0d 100644 --- a/go/carmen/proof.go +++ b/go/carmen/proof.go @@ -48,15 +48,18 @@ type WitnessProof interface { // GetElements returns serialised elements of the witness proof. GetElements() []Bytes - // GetStorageElements returns serialised elements of the witness proof for a given account - // and selected storage locations from this proof. + // GetAccountElements returns serialised elements of the witness proof for a selected account and + // the root of the account's storage trie. The final return parameter indicates whether everything that + // was requested could be covered. If so, it is set to true, otherwise it is set to false. + GetAccountElements(root Hash, address Address) ([]Bytes, Hash, bool) + + // GetStorageElements returns serialised elements of the witness proof for a selected + // storage location within an account. // The resulting elements contains only the storage part of the account. - // For this reason, the second parameter of this method returns the storage root for this storage - // as any proving and other operations on the resulting proof must be done related to the storage root. // This method returns a copy that contains only the data necessary for proving storage keys. // The third return parameter indicates whether everything that was requested could be covered. // If so, it is set to true, otherwise it is set to false. - GetStorageElements(root Hash, address Address, keys ...Key) ([]Bytes, Hash, bool) + GetStorageElements(root Hash, address Address, key Key) ([]Bytes, bool) // GetBalance extracts a balance from the witness proof for the input root hash and the address. // If the witness proof contains the requested account for the input address for the given root hash, it returns its balance. @@ -117,16 +120,16 @@ func (w witnessProof) GetElements() []Bytes { return w.proof.GetElements() } -func (w witnessProof) GetStorageElements(root Hash, address Address, keys ...Key) ([]Bytes, Hash, bool) { - commonKeys := make([]common.Key, len(keys)) - for i, k := range keys { - commonKeys[i] = common.Key(k) - } - - resProof, storageRoot, complete := w.proof.GetStorageElements(common.Hash(root), common.Address(address), commonKeys...) +func (w witnessProof) GetAccountElements(root Hash, address Address) ([]Bytes, Hash, bool) { + resProof, storageRoot, complete := w.proof.GetAccountElements(common.Hash(root), common.Address(address)) return resProof, Hash(storageRoot), complete } +func (w witnessProof) GetStorageElements(root Hash, address Address, key Key) ([]Bytes, bool) { + resProof, complete := w.proof.GetStorageElements(common.Hash(root), common.Address(address), common.Key(key)) + return resProof, complete +} + func (w witnessProof) IsValid() bool { return w.proof.IsValid() } diff --git a/go/common/witness/proof.go b/go/common/witness/proof.go index d3177c3ae..98c58dc6e 100644 --- a/go/common/witness/proof.go +++ b/go/common/witness/proof.go @@ -40,15 +40,18 @@ type Proof interface { // GetElements returns serialised elements of the witness proof. GetElements() []immutable.Bytes - // GetStorageElements returns serialised elements of the witness proof for a given account - // and selected storage locations from this proof. + // GetAccountElements returns serialised elements of the witness proof for a selected account and + // the root of the account's storage trie. The final return parameter indicates whether everything that + // was requested could be covered. If so, it is set to true, otherwise it is set to false. + GetAccountElements(root common.Hash, address common.Address) ([]immutable.Bytes, common.Hash, bool) + + // GetStorageElements returns serialised elements of the witness proof for a selected + // storage location within an account. // The resulting elements contains only the storage part of the account. - // For this reason, the second parameter of this method returns the storage root for this storage - // as any proving and other operations on the resulting proof must be done related to the storage root. // This method returns a copy that contains only the data necessary for proving storage keys. // The third return parameter indicates whether everything that was requested could be covered. // If so, it is set to true, otherwise it is set to false. - GetStorageElements(root common.Hash, address common.Address, keys ...common.Key) ([]immutable.Bytes, common.Hash, bool) + GetStorageElements(root common.Hash, address common.Address, key common.Key) ([]immutable.Bytes, bool) // GetBalance extracts a balance from the witness proof for the input root hash and the address. // If the witness proof contains the requested account for the input address for the given root hash, it returns its balance. diff --git a/go/common/witness/proof_mocks.go b/go/common/witness/proof_mocks.go index 27f9743da..71b456387 100644 --- a/go/common/witness/proof_mocks.go +++ b/go/common/witness/proof_mocks.go @@ -8,6 +8,15 @@ // On the date above, in accordance with the Business Source License, use of // this software will be governed by the GNU Lesser General Public License v3. +// Code generated by MockGen. DO NOT EDIT. +// Source: proof.go +// +// Generated by this command: +// +// mockgen -source proof.go -destination proof_mocks.go -package witness +// + +// Package witness is a generated GoMock package. package witness import ( @@ -93,6 +102,22 @@ func (mr *MockProofMockRecorder) Extract(root, address any, keys ...any) *gomock return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Extract", reflect.TypeOf((*MockProof)(nil).Extract), varargs...) } +// GetAccountElements mocks base method. +func (m *MockProof) GetAccountElements(root common.Hash, address common.Address) ([]immutable.Bytes, common.Hash, bool) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAccountElements", root, address) + ret0, _ := ret[0].([]immutable.Bytes) + ret1, _ := ret[1].(common.Hash) + ret2, _ := ret[2].(bool) + return ret0, ret1, ret2 +} + +// GetAccountElements indicates an expected call of GetAccountElements. +func (mr *MockProofMockRecorder) GetAccountElements(root, address any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountElements", reflect.TypeOf((*MockProof)(nil).GetAccountElements), root, address) +} + // GetBalance mocks base method. func (m *MockProof) GetBalance(root common.Hash, address common.Address) (amount.Amount, bool, error) { m.ctrl.T.Helper() @@ -172,24 +197,18 @@ func (mr *MockProofMockRecorder) GetState(root, address, key any) *gomock.Call { } // GetStorageElements mocks base method. -func (m *MockProof) GetStorageElements(root common.Hash, address common.Address, keys ...common.Key) ([]immutable.Bytes, common.Hash, bool) { +func (m *MockProof) GetStorageElements(root common.Hash, address common.Address, key common.Key) ([]immutable.Bytes, bool) { m.ctrl.T.Helper() - varargs := []any{root, address} - for _, a := range keys { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "GetStorageElements", varargs...) + ret := m.ctrl.Call(m, "GetStorageElements", root, address, key) ret0, _ := ret[0].([]immutable.Bytes) - ret1, _ := ret[1].(common.Hash) - ret2, _ := ret[2].(bool) - return ret0, ret1, ret2 + ret1, _ := ret[1].(bool) + return ret0, ret1 } // GetStorageElements indicates an expected call of GetStorageElements. -func (mr *MockProofMockRecorder) GetStorageElements(root, address any, keys ...any) *gomock.Call { +func (mr *MockProofMockRecorder) GetStorageElements(root, address, key any) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]any{root, address}, keys...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetStorageElements", reflect.TypeOf((*MockProof)(nil).GetStorageElements), varargs...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetStorageElements", reflect.TypeOf((*MockProof)(nil).GetStorageElements), root, address, key) } // IsValid mocks base method. diff --git a/go/database/mpt/hasher.go b/go/database/mpt/hasher.go index 51e1c8367..753c54eeb 100644 --- a/go/database/mpt/hasher.go +++ b/go/database/mpt/hasher.go @@ -19,6 +19,7 @@ import ( "sync" "github.com/Fantom-foundation/Carmen/go/common" + "github.com/Fantom-foundation/Carmen/go/common/immutable" "github.com/Fantom-foundation/Carmen/go/database/mpt/rlp" "github.com/Fantom-foundation/Carmen/go/database/mpt/shared" ) @@ -245,7 +246,8 @@ func makeEthereumLikeHasher() hasher { type ethHasher struct{} -var EmptyNodeEthereumHash = common.Keccak256(rlp.Encode(rlp.String{})) +var EmptyNodeEthereumEncoding = immutable.NewBytes(rlp.Encode(rlp.String{})) +var EmptyNodeEthereumHash = common.Keccak256(EmptyNodeEthereumEncoding.ToBytes()) func (h ethHasher) updateHashes( ref *NodeReference, diff --git a/go/database/mpt/proof.go b/go/database/mpt/proof.go index 7706d1ddc..4d0149f02 100644 --- a/go/database/mpt/proof.go +++ b/go/database/mpt/proof.go @@ -14,12 +14,13 @@ import ( "bytes" "errors" "fmt" - "github.com/Fantom-foundation/Carmen/go/common/immutable" - "github.com/Fantom-foundation/Carmen/go/common/witness" "slices" "sort" "strings" + "github.com/Fantom-foundation/Carmen/go/common/immutable" + "github.com/Fantom-foundation/Carmen/go/common/witness" + "github.com/Fantom-foundation/Carmen/go/common" "github.com/Fantom-foundation/Carmen/go/common/amount" "github.com/Fantom-foundation/Carmen/go/common/tribool" @@ -268,23 +269,40 @@ func (p WitnessProof) GetElements() []immutable.Bytes { return res } -func (p WitnessProof) GetStorageElements(root common.Hash, address common.Address, keys ...common.Key) ([]immutable.Bytes, common.Hash, bool) { - visitor := &proofCollectingVisitor{} +func (p WitnessProof) GetAccountElements(root common.Hash, address common.Address) ([]immutable.Bytes, common.Hash, bool) { + visitor := &proofPathRecordingVisitor{} found, complete, err := visitWitnessPathTo(p.proofDb, root, addressToHashedNibbles(address), visitor) - if err != nil || !found { - return []immutable.Bytes{}, common.Hash{}, complete + if err != nil || !complete { + return []immutable.Bytes{}, common.Hash{}, false + } + storageHash := EmptyNodeEthereumHash + if found { + storageHash = visitor.visitedAccount.storageHash + } + return visitor.path, storageHash, complete +} + +func (p WitnessProof) GetStorageElements(root common.Hash, address common.Address, key common.Key) ([]immutable.Bytes, bool) { + visitor := &proofPathRecordingVisitor{} + found, complete, err := visitWitnessPathTo(p.proofDb, root, addressToHashedNibbles(address), visitor) + if !complete || err != nil { + return []immutable.Bytes{}, false } + // If the account does not exist, its storage is empty, and this can be proven. + if !found { + return []immutable.Bytes{EmptyNodeEthereumEncoding}, true + } + + // If an account was found, a storage proof can be extracted. storageRoot := visitor.visitedAccount.storageHash - visitor.visited = make(proofDb) - for _, key := range keys { - _, keyComplete, err := visitWitnessPathTo(p.proofDb, storageRoot, keyToHashedPathNibbles(key), visitor) - if err != nil || !keyComplete { - complete = false - } + visitor.path = nil + _, keyComplete, err := visitWitnessPathTo(p.proofDb, storageRoot, keyToHashedPathNibbles(key), visitor) + if err != nil { + return []immutable.Bytes{}, false } - return WitnessProof{visitor.visited}.GetElements(), storageRoot, complete + return visitor.path, keyComplete } // proofExtractionVisitor is a visitor that visits MPT nodes and creates a witness proof. @@ -487,9 +505,20 @@ func (v *proofCollectingVisitor) Visit(hash common.Hash, rlpNode rlpEncodedNode, if !isEmbedded && v.visited != nil { v.visited[hash] = rlpNode } +} + +type proofPathRecordingVisitor struct { + path []immutable.Bytes // all visited RLP encoded nodes + visitedAccount decodedAccountNode // the last visited account node +} + +func (v *proofPathRecordingVisitor) Visit(hash common.Hash, rlpNode rlpEncodedNode, node Node, isEmbedded bool) { if account, ok := node.(*decodedAccountNode); ok { v.visitedAccount = *account } + if !isEmbedded { + v.path = append(v.path, immutable.NewBytes(rlpNode)) + } } // createNibblesFromKeyPrefix creates a nibble path from the input key and the number of nibbles. diff --git a/go/database/mpt/proof_test.go b/go/database/mpt/proof_test.go index 57f6255ae..a82ebc654 100644 --- a/go/database/mpt/proof_test.go +++ b/go/database/mpt/proof_test.go @@ -14,12 +14,12 @@ import ( "encoding/hex" "errors" "fmt" - "github.com/Fantom-foundation/Carmen/go/common/immutable" "reflect" "testing" "github.com/Fantom-foundation/Carmen/go/common" "github.com/Fantom-foundation/Carmen/go/common/amount" + "github.com/Fantom-foundation/Carmen/go/common/immutable" "github.com/Fantom-foundation/Carmen/go/database/mpt/shared" "go.uber.org/mock/gomock" "golang.org/x/exp/maps" @@ -761,6 +761,101 @@ func TestWitnessProof_Extract_EmbeddedNode_In_Proof(t *testing.T) { } } +func TestCreateWitnessProof_GetAccountElements(t *testing.T) { + ctrl := gomock.NewController(t) + + address := common.Address{1} + key := common.Key{2} + + ctxt := newNodeContextWithConfig(t, ctrl, S5LiveConfig) + addressNibbles := AddressToNibblePath(address, ctxt) + + desc := &Branch{ + children: Children{ + addressNibbles[0]: &Branch{ + children: Children{ + addressNibbles[1]: &Extension{ + path: addressNibbles[2:50], + next: &Tag{"A", &Account{ + address: address, + pathLength: 14, + info: AccountInfo{Nonce: common.Nonce{1}}, + storage: &Value{key: key, length: 32, value: common.Value{0x12}}, + }}, + }, + }, + }, + }, + } + + root, _ := ctxt.Build(desc) + + proof, err := CreateWitnessProof(ctxt, &root, address) + if err != nil { + t.Fatalf("failed to create proof: %v", err) + } + + if !proof.IsValid() { + t.Fatalf("proof is not valid") + } + + _, accountNode := ctxt.Get("A") + accountHandle := accountNode.GetViewHandle() + storageHashWant := accountHandle.Get().(*AccountNode).storageHash + accountHandle.Release() + + hash, _ := ctxt.getHashFor(&root) + + t.Run("Extract present account", func(t *testing.T) { + elements, storageHash, complete := proof.GetAccountElements(hash, address) + if !complete { + t.Fatalf("proof is not complete") + } + if got, want := storageHash, storageHashWant; got != want { + t.Errorf("unexpected storage hash: got %v, want %v", got, want) + } + reconstructed := CreateWitnessProofFromNodes(elements) + if !reconstructed.IsValid() { + t.Fatalf("reconstructed proof is not valid") + } + nonce, complete, err := reconstructed.GetNonce(hash, address) + if err != nil { + t.Fatalf("failed to get nonce: %v", err) + } + if !complete { + t.Fatalf("nonce not found") + } + if got, want := nonce, (common.Nonce{1}); got != want { + t.Errorf("unexpected nonce: got %v, want %v", got, want) + } + }) + + t.Run("Extract missing account", func(t *testing.T) { + address := common.Address{2} + elements, storageHash, complete := proof.GetAccountElements(hash, address) + if !complete { + t.Fatalf("proof is not complete") + } + if got, want := storageHash, EmptyNodeEthereumHash; got != want { + t.Errorf("unexpected storage hash: got %v, want %v", got, want) + } + reconstructed := CreateWitnessProofFromNodes(elements) + if !reconstructed.IsValid() { + t.Fatalf("reconstructed proof is not valid") + } + nonce, complete, err := reconstructed.GetNonce(hash, address) + if err != nil { + t.Fatalf("failed to get nonce: %v", err) + } + if !complete { + t.Fatalf("nonce not found") + } + if got, want := nonce, (common.Nonce{0}); got != want { + t.Errorf("unexpected nonce: got %v, want %v", got, want) + } + }) +} + func TestCreateWitnessProof_GetStorageElements(t *testing.T) { ctrl := gomock.NewController(t) @@ -825,7 +920,12 @@ func TestCreateWitnessProof_GetStorageElements(t *testing.T) { } t.Run("Extract storage", func(t *testing.T) { - storageElements, storageHash, complete := proof.GetStorageElements(hash, address, key) + _, storageHash, complete := proof.GetAccountElements(hash, address) + if !complete { + t.Fatalf("proof is not complete") + } + + storageElements, complete := proof.GetStorageElements(hash, address, key) if !complete { t.Fatalf("proof is not complete") } @@ -854,7 +954,12 @@ func TestCreateWitnessProof_GetStorageElements(t *testing.T) { }) t.Run("Extract empty storage", func(t *testing.T) { - storageElements, storageHash, complete := proof.GetStorageElements(hash, address, common.Key{}) + _, storageHash, complete := proof.GetAccountElements(hash, address) + if !complete { + t.Fatalf("proof is not complete") + } + + storageElements, complete := proof.GetStorageElements(hash, address, common.Key{}) if !complete { t.Fatalf("proof is not complete") } @@ -884,7 +989,12 @@ func TestCreateWitnessProof_GetStorageElements(t *testing.T) { }) t.Run("Extract empty account", func(t *testing.T) { - storageElements, storageHash, complete := proof.GetStorageElements(hash, common.Address{}, common.Key{}) + _, storageHash, complete := proof.GetAccountElements(hash, common.Address{}) + if !complete { + t.Fatalf("proof is not complete") + } + + storageElements, complete := proof.GetStorageElements(hash, common.Address{}, common.Key{}) if !complete { t.Fatalf("proof is not complete") } @@ -893,11 +1003,14 @@ func TestCreateWitnessProof_GetStorageElements(t *testing.T) { t.Fatalf("storage proof is not valid") } - if got, want := storageHash, (common.Hash{}); got != want { + if got, want := storageHash, EmptyNodeEthereumHash; got != want { t.Errorf("unexpected storage hash: got %v, want %v", got, want) } - if got, want := storageProof, (WitnessProof{}); !want.Equals(got) { + emptyStorageProof := make(proofDb) + emptyStorageProof[EmptyNodeEthereumHash] = rlpEncodedNode(EmptyNodeEthereumEncoding.ToBytes()) + + if got, want := storageProof, (WitnessProof{emptyStorageProof}); !want.Equals(got) { t.Errorf("unexpected storage proof: got %v, want %v", got, want) } }) @@ -914,7 +1027,7 @@ func TestCreateWitnessProof_GetStorageElements(t *testing.T) { proofCopy := CreateWitnessProofFromNodes(proof.GetElements()) delete(proofCopy.proofDb, common.Keccak256(rlp)) - _, _, complete := proofCopy.GetStorageElements(hash, address, common.Key{}) + _, complete := proofCopy.GetStorageElements(hash, address, common.Key{}) if complete { t.Errorf("proof should not be complete") } @@ -932,7 +1045,7 @@ func TestCreateWitnessProof_GetStorageElements(t *testing.T) { proofCopy := CreateWitnessProofFromNodes(proof.GetElements()) proofCopy.proofDb[common.Keccak256(rlp)] = []byte{0xAA, 0xBB, 0xCC, 0xDD} - _, _, complete := proofCopy.GetStorageElements(hash, address, common.Key{}) + _, complete := proofCopy.GetStorageElements(hash, address, common.Key{}) if complete { t.Errorf("proof should not be complete") } @@ -1240,7 +1353,7 @@ func TestWitnessProof_String(t *testing.T) { "0x0300000000000000000000000000000000000000000000000000000000000000->0x0c\n" + "0x0400000000000000000000000000000000000000000000000000000000000000->0x0d\n" - if got, want := fmt.Sprintf("%s", WitnessProof{proof}), str; got != want { + if got, want := (WitnessProof{proof}.String()), str; got != want { t.Errorf("unexpected string: got %v, want %v", got, want) } }