From 4821fbae99b1884921cadb6a8f63c7ffab8420f1 Mon Sep 17 00:00:00 2001 From: Jared Wasinger Date: Fri, 6 Dec 2024 14:56:43 +0700 Subject: [PATCH] add test cases for contract linking that I forgot to push earlier. Fix deployment logic (apparently modifying a map in-place during iteration is not safe???? --- accounts/abi/bind/v2/contract_linking_test.go | 184 ++++++++++++++++++ accounts/abi/bind/v2/lib.go | 23 ++- 2 files changed, 198 insertions(+), 9 deletions(-) create mode 100644 accounts/abi/bind/v2/contract_linking_test.go diff --git a/accounts/abi/bind/v2/contract_linking_test.go b/accounts/abi/bind/v2/contract_linking_test.go new file mode 100644 index 0000000000..517afbaa10 --- /dev/null +++ b/accounts/abi/bind/v2/contract_linking_test.go @@ -0,0 +1,184 @@ +package v2 + +import ( + "fmt" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "testing" +) + +type linkTestCase struct { + // map of pattern to unlinked bytecode (for the purposes of tests just contains the patterns of its dependencies) + libCodes map[string]string + contractCodes map[string]string + + overrides map[string]common.Address +} + +func makeLinkTestCase(input map[rune][]rune, overrides map[rune]common.Address) *linkTestCase { + codes := make(map[string]string) + libCodes := make(map[string]string) + contractCodes := make(map[string]string) + + inputMap := make(map[rune]map[rune]struct{}) + // set of solidity patterns for all contracts that are known to be libraries + libs := make(map[string]struct{}) + + // map of test contract id (rune) to the solidity library pattern (hash of that rune) + patternMap := map[rune]string{} + + for contract, deps := range input { + inputMap[contract] = make(map[rune]struct{}) + if _, ok := patternMap[contract]; !ok { + patternMap[contract] = crypto.Keccak256Hash([]byte(string(contract))).String()[2:36] + } + + for _, dep := range deps { + if _, ok := patternMap[dep]; !ok { + patternMap[dep] = crypto.Keccak256Hash([]byte(string(dep))).String()[2:36] + } + codes[patternMap[contract]] = codes[patternMap[contract]] + fmt.Sprintf("__$%s$__", patternMap[dep]) + inputMap[contract][dep] = struct{}{} + libs[patternMap[dep]] = struct{}{} + } + } + overridesPatterns := make(map[string]common.Address) + for contractId, overrideAddr := range overrides { + pattern := crypto.Keccak256Hash([]byte(string(contractId))).String()[2:36] + overridesPatterns[pattern] = overrideAddr + } + + for _, pattern := range patternMap { + if _, ok := libs[pattern]; ok { + // if the library didn't depend on others, give it some dummy code to not bork deployment logic down-the-line + if len(codes[pattern]) == 0 { + libCodes[pattern] = "ff" + } else { + libCodes[pattern] = codes[pattern] + } + } else { + contractCodes[pattern] = codes[pattern] + } + } + + return &linkTestCase{ + libCodes, + contractCodes, + overridesPatterns, + } +} + +func testLinkCase(t *testing.T, input map[rune][]rune, overrides map[rune]common.Address) { + testAddr := crypto.PubkeyToAddress(testKey.PublicKey) + var testAddrNonce uint64 + + tc := makeLinkTestCase(input, overrides) + alreadyDeployed := make(map[common.Address]struct{}) + allContracts := make(map[rune]struct{}) + + for contract, deps := range input { + allContracts[contract] = struct{}{} + for _, dep := range deps { + allContracts[dep] = struct{}{} + } + } + + // TODO: include in link test case: set of contracts that we expect to be deployed at the end. + // generate this in makeLinkTestCase + // ^ overrides are not included in this case. + mockDeploy := func(input []byte, deployer []byte) (common.Address, *types.Transaction, error) { + contractAddr := crypto.CreateAddress(testAddr, testAddrNonce) + testAddrNonce++ + + // assert that this contract only references libs that are known to be deployed or in the override set + for i := 0; i < len(deployer)/20; i += 20 { + var dep common.Address + dep.SetBytes(deployer[i : i+20]) + if _, ok := alreadyDeployed[dep]; !ok { + t.Fatalf("reference to dependent contract that has not yet been deployed: %x\n", dep) + } + } + alreadyDeployed[contractAddr] = struct{}{} + // we don't care about the txs themselves for the sake of the linking tests. so we can return nil for them in the mock deployer + return contractAddr, nil, nil + } + + var ( + contracts []ContractDeployParams + libs []*bind.MetaData + ) + for pattern, bin := range tc.contractCodes { + contracts = append(contracts, ContractDeployParams{ + Meta: &bind.MetaData{Pattern: pattern, Bin: "0x" + bin}, + Input: nil, + }) + } + for pattern, bin := range tc.libCodes { + libs = append(libs, &bind.MetaData{ + Bin: "0x" + bin, + Pattern: pattern, + }) + } + deployParams := DeploymentParams{ + Contracts: contracts, + Libraries: libs, + Overrides: nil, + } + + res, err := LinkAndDeploy(deployParams, mockDeploy) + if err != nil { + t.Fatalf("got error from LinkAndDeploy: %v\n", err) + } + + // TODO: assert that the result consists of the input contracts minus the overrides. + + if len(res.Addrs) != len(allContracts)-len(overrides) { + for val, _ := range allContracts { + fmt.Println(string(val)) + } + t.Fatalf("expected %d contracts to be deployed. got %d\n", len(allContracts)-len(overrides), len(res.Addrs)) + } + + // note that the link-and-deploy functionality assumes that the combined-abi is well-formed. + + // test-case ideas: + // * libraries that are disjount from the rest of dep graph (they don't get deployed) +} + +func TestContractLinking(t *testing.T) { + testLinkCase(t, map[rune][]rune{ + 'a': {'b', 'c', 'd', 'e'}, + 'e': {'f', 'g', 'h', 'i'}}, + map[rune]common.Address{}) + + testLinkCase(t, map[rune][]rune{ + 'a': {'b', 'c', 'd', 'e'}}, + map[rune]common.Address{}) + + // test single contract only without deps + testLinkCase(t, map[rune][]rune{ + 'a': {}}, + map[rune]common.Address{}) + + // test that libraries at different levels of the tree can share deps, + // and that these shared deps will only be deployed once. + testLinkCase(t, map[rune][]rune{ + 'a': {'b', 'c', 'd', 'e'}, + 'e': {'f', 'g', 'h', 'i', 'm'}, + 'i': {'j', 'k', 'l', 'm'}}, + map[rune]common.Address{}) + + // test two contracts can be deployed which don't share deps + testLinkCase(t, map[rune][]rune{ + 'a': {'b', 'c', 'd', 'e'}, + 'f': {'g', 'h', 'i', 'j'}}, + map[rune]common.Address{}) + + // test two contracts can be deployed which share deps + testLinkCase(t, map[rune][]rune{ + 'a': {'b', 'c', 'd', 'e'}, + 'f': {'g', 'c', 'd', 'j'}}, + map[rune]common.Address{}) +} diff --git a/accounts/abi/bind/v2/lib.go b/accounts/abi/bind/v2/lib.go index 415b7f8827..0db3fd3191 100644 --- a/accounts/abi/bind/v2/lib.go +++ b/accounts/abi/bind/v2/lib.go @@ -91,7 +91,8 @@ func linkContract(contract string, linkedLibs map[string]common.Address) (deploy // // contracts that have become fully linked in the current invocation are // returned. -func linkLibs(pending *map[string]string, linked map[string]common.Address) (deployableDeps map[string]string) { +func linkLibs(pending map[string]string, linked map[string]common.Address) (newPending map[string]string, deployableDeps map[string]string) { + newPending = make(map[string]string) reMatchSpecificPattern, err := regexp.Compile("__\\$([a-f0-9]+)\\$__") if err != nil { panic(err) @@ -102,23 +103,25 @@ func linkLibs(pending *map[string]string, linked map[string]common.Address) (dep } deployableDeps = make(map[string]string) - for pattern, dep := range *pending { + for pattern, dep := range pending { + newPending[pattern] = dep // link references to dependent libraries that have been deployed - for _, match := range reMatchSpecificPattern.FindAllStringSubmatch(dep, -1) { + for _, match := range reMatchSpecificPattern.FindAllStringSubmatch(newPending[pattern], -1) { matchingPattern := match[1] addr, ok := linked[matchingPattern] if !ok { continue } - (*pending)[pattern] = strings.ReplaceAll(dep, "__$"+matchingPattern+"$__", addr.String()[2:]) + + newPending[pattern] = strings.ReplaceAll(newPending[pattern], "__$"+matchingPattern+"$__", addr.String()[2:]) } // if the library code became fully linked, move it from pending->linked. - if !reMatchAnyPattern.MatchString((*pending)[pattern]) { - deployableDeps[pattern] = (*pending)[pattern] - delete(*pending, pattern) + if !reMatchAnyPattern.MatchString(newPending[pattern]) { + deployableDeps[pattern] = newPending[pattern] + delete(newPending, pattern) } } - return deployableDeps + return newPending, deployableDeps } // ContractDeployParams represents state needed to deploy a contract: @@ -187,10 +190,12 @@ func LinkAndDeploy(deployParams DeploymentParams, deploy func(input, deployer [] // link and deploy dynamic libraries for { - deployableDeps := linkLibs(&pending, deployed) + var deployableDeps map[string]string + pending, deployableDeps = linkLibs(pending, deployed) if len(deployableDeps) == 0 { break } + deployTxs, deployAddrs, err := deployLibs(deployableDeps, deploy) for pattern, addr := range deployAddrs { deployed[pattern] = addr