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????

This commit is contained in:
Jared Wasinger 2024-12-06 14:56:43 +07:00 committed by Felix Lange
parent 44420cbb36
commit 4821fbae99
2 changed files with 198 additions and 9 deletions

View File

@ -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{})
}

View File

@ -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