wip: embed library relations in metadata

This commit is contained in:
Jared Wasinger 2024-12-18 21:07:24 +07:00 committed by Felix Lange
parent c89c24ce37
commit eb10c47793
5 changed files with 71 additions and 147 deletions

View file

@ -95,6 +95,7 @@ type MetaData struct {
ABI string ABI string
ab *abi.ABI ab *abi.ABI
Pattern string Pattern string
Deps []*MetaData
} }
func (m *MetaData) GetAbi() (*abi.ABI, error) { func (m *MetaData) GetAbi() (*abi.ABI, error) {

View file

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/abi"
"go/format" "go/format"
"regexp"
"strings" "strings"
"text/template" "text/template"
"unicode" "unicode"
@ -190,6 +191,17 @@ func (cb *contractBinder) bindError(original abi.Error) error {
return nil return nil
} }
func parseLibraryDeps(unlinkedCode string) (res []string) {
reMatchSpecificPattern, err := regexp.Compile(`__\$([a-f0-9]+)\$__`)
if err != nil {
panic(err)
}
for _, match := range reMatchSpecificPattern.FindAllStringSubmatch(unlinkedCode, -1) {
res = append(res, match[1])
}
return res
}
// BindV2 generates a Go wrapper around a contract ABI. This wrapper isn't meant // BindV2 generates a Go wrapper around a contract ABI. This wrapper isn't meant
// to be used as is in client code, but rather as an intermediate struct which // to be used as is in client code, but rather as an intermediate struct which
// enforces compile time type safety and naming convention as opposed to having to // enforces compile time type safety and naming convention as opposed to having to
@ -271,23 +283,9 @@ func BindV2(types []string, abis []string, bytecodes []string, pkg string, libs
Structs: b.structs, Structs: b.structs,
} }
contractsBins := make(map[string]string)
for typ, contract := range data.Contracts { for typ, contract := range data.Contracts {
pattern := invertedLibs[typ] for _, depPattern := range parseLibraryDeps(contract.InputBin) {
contractsBins[pattern] = contract.InputBin data.Contracts[typ].Libraries[libs[depPattern]] = depPattern
}
builder := newDepTreeBuilder(nil, contractsBins)
roots, deps := builder.BuildDepTrees()
allNodes := append(roots, deps...)
for _, dep := range allNodes {
contractType := libs[dep.pattern]
for subDepPattern, _ := range dep.Flatten() {
if subDepPattern == dep.pattern {
// don't include the dep as a dependency of itself
continue
}
subDepType := libs[subDepPattern]
data.Contracts[contractType].Libraries[subDepType] = subDepPattern
} }
} }
buffer := new(bytes.Buffer) buffer := new(bytes.Buffer)

View file

@ -3,7 +3,7 @@ package bind
import ( import (
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"regexp" "maps"
"strings" "strings"
) )
@ -45,100 +45,6 @@ func (d *DeploymentResult) Accumulate(other *DeploymentResult) {
maps.Copy(d.Addrs, other.Addrs) maps.Copy(d.Addrs, other.Addrs)
} }
// depTreeBuilder turns a set of unlinked contracts libraries into a set of one
// or more dependency trees.
type depTreeBuilder struct {
overrides map[string]common.Address
// map of pattern to unlinked contract bytecode (for libraries or contracts)
contracts map[string]string
// map of pattern to subtree represented by contract
subtrees map[string]*depTreeNode
// map of nodes that aren't referenced by other dependencies (these can be libraries too if user is doing lib-only deployment)
roots map[string]struct{}
}
// depTreeNode represents a node (contract) in a dependency tree. it contains its unlinked code, and references to any
// library contracts that it requires. If it is specified as an override, it contains the address where it has already
// been deployed at.
type depTreeNode struct {
pattern string
unlinkedCode string
children []*depTreeNode
overrideAddr *common.Address
}
// Flatten returns the subtree into a map of pattern -> unlinked contract bytecode.
func (n *depTreeNode) Flatten() (res map[string]string) {
res = map[string]string{n.pattern: n.unlinkedCode}
for _, child := range n.children {
subtree := child.Flatten()
for k, v := range subtree {
res[k] = v
}
}
return res
}
// buildDepTrees is the internal version of BuildDepTrees that recursively calls itself.
func (d *depTreeBuilder) buildDepTrees(pattern, contract string) {
// if the node is in the subtree set already, it has already been fully recursed/built so we can bail out.
if _, ok := d.subtrees[pattern]; ok {
return
}
node := &depTreeNode{
pattern: pattern,
unlinkedCode: contract,
}
if addr, ok := d.overrides[pattern]; ok {
node.overrideAddr = &addr
}
// iterate each referenced library in the unlinked code, recurse and build its subtree.
reMatchSpecificPattern, err := regexp.Compile(`__\$([a-f0-9]+)\$__`)
if err != nil {
panic(err)
}
for _, match := range reMatchSpecificPattern.FindAllStringSubmatch(contract, -1) {
depPattern := match[1]
d.buildDepTrees(depPattern, d.contracts[depPattern])
node.children = append(node.children, d.subtrees[depPattern])
// this library can't be a root dependency if it is referenced by other contracts.
delete(d.roots, depPattern)
}
d.subtrees[pattern] = node
}
// BuildDepTrees will compute a set of dependency trees from a set of unlinked contracts. The root of each tree
// corresponds to a contract/library that is not referenced as a dependency anywhere else. Children of each node are
// its library dependencies. It returns nodes that are roots of a dependency tree and nodes that aren't.
func (d *depTreeBuilder) BuildDepTrees() (roots []*depTreeNode, nonRoots []*depTreeNode) {
// before the trees of dependencies are known, consider that any provided contract could be a root.
for pattern, _ := range d.contracts {
d.roots[pattern] = struct{}{}
}
for pattern, contract := range d.contracts {
d.buildDepTrees(pattern, contract)
}
for pattern, _ := range d.contracts {
if _, ok := d.roots[pattern]; ok {
roots = append(roots, d.subtrees[pattern])
} else {
nonRoots = append(nonRoots, d.subtrees[pattern])
}
}
return roots, nonRoots
}
func newDepTreeBuilder(overrides map[string]common.Address, contracts map[string]string) *depTreeBuilder {
return &depTreeBuilder{
overrides: overrides,
contracts: contracts,
subtrees: make(map[string]*depTreeNode),
roots: make(map[string]struct{}),
}
}
// DeployFn deploys a contract given a deployer and optional input. It returns // DeployFn deploys a contract given a deployer and optional input. It returns
// the address and a pending transaction, or an error if the deployment failed. // the address and a pending transaction, or an error if the deployment failed.
type DeployFn func(input, deployer []byte) (common.Address, *types.Transaction, error) type DeployFn func(input, deployer []byte) (common.Address, *types.Transaction, error)
@ -154,37 +60,34 @@ type depTreeDeployer struct {
// linkAndDeploy recursively deploys a contract and its dependencies: starting by linking/deploying its dependencies. // linkAndDeploy recursively deploys a contract and its dependencies: starting by linking/deploying its dependencies.
// The deployment result (deploy addresses/txs or an error) is stored in the depTreeDeployer object. // The deployment result (deploy addresses/txs or an error) is stored in the depTreeDeployer object.
func (d *depTreeDeployer) linkAndDeploy(node *depTreeNode) error { func (d *depTreeDeployer) linkAndDeploy(metadata *MetaData) error {
// don't deploy contracts specified as overrides. don't deploy their dependencies. // don't deploy contracts specified as overrides. don't deploy their dependencies.
if node.overrideAddr != nil { if _, ok := d.deployedAddrs[metadata.Pattern]; ok {
return nil return nil
} }
// if this contract/library depends on other libraries deploy them (and their dependencies) first // if this contract/library depends on other libraries deploy them (and their dependencies) first
for _, childNode := range node.children { for _, dep := range metadata.Deps {
if err := d.linkAndDeploy(childNode); err != nil { if err := d.linkAndDeploy(dep); err != nil {
return err return err
} }
} }
// if we just deployed any prerequisite contracts, link their deployed addresses into the bytecode to produce // if we just deployed any prerequisite contracts, link their deployed addresses into the bytecode to produce
// a deployer bytecode for this contract. // a deployer bytecode for this contract.
deployerCode := node.unlinkedCode deployerCode := metadata.Bin
for _, child := range node.children { for _, dep := range metadata.Deps {
linkAddr := d.deployedAddrs[child.pattern] linkAddr := d.deployedAddrs[dep.Pattern]
if child.overrideAddr != nil { deployerCode = strings.ReplaceAll(deployerCode, "__$"+dep.Pattern+"$__", strings.ToLower(linkAddr.String()[2:]))
linkAddr = *child.overrideAddr
}
deployerCode = strings.ReplaceAll(deployerCode, "__$"+child.pattern+"$__", strings.ToLower(linkAddr.String()[2:]))
} }
// Finally, deploy the contract. // Finally, deploy the contract.
addr, tx, err := d.deploy(d.input[node.pattern], common.Hex2Bytes(deployerCode)) addr, tx, err := d.deploy(d.input[metadata.Pattern], common.Hex2Bytes(deployerCode))
if err != nil { if err != nil {
return err return err
} }
d.deployedAddrs[node.pattern] = addr d.deployedAddrs[metadata.Pattern] = addr
d.deployerTxs[node.pattern] = tx d.deployerTxs[metadata.Pattern] = tx
return nil return nil
} }
@ -207,23 +110,16 @@ func newDepTreeDeployer(deploy DeployFn) *depTreeDeployer {
// libraries. If an error occurs, only contracts which were successfully // libraries. If an error occurs, only contracts which were successfully
// deployed are returned in the result. // deployed are returned in the result.
func LinkAndDeploy(deployParams *DeploymentParams, deploy DeployFn) (res *DeploymentResult, err error) { func LinkAndDeploy(deployParams *DeploymentParams, deploy DeployFn) (res *DeploymentResult, err error) {
unlinkedContracts := make(map[string]string)
accumRes := &DeploymentResult{ accumRes := &DeploymentResult{
Txs: make(map[string]*types.Transaction), Txs: make(map[string]*types.Transaction),
Addrs: make(map[string]common.Address), Addrs: make(map[string]common.Address),
} }
for _, meta := range deployParams.contracts { deployer := newDepTreeDeployer(deploy)
unlinkedContracts[meta.Pattern] = meta.Bin[2:] for _, contract := range deployParams.contracts {
}
treeBuilder := newDepTreeBuilder(deployParams.overrides, unlinkedContracts)
deps, _ := treeBuilder.BuildDepTrees()
for _, tr := range deps {
deployer := newDepTreeDeployer(deploy)
if deployParams.inputs != nil { if deployParams.inputs != nil {
deployer.input = map[string][]byte{tr.pattern: deployParams.inputs[tr.pattern]} deployer.input = map[string][]byte{contract.Pattern: deployParams.inputs[contract.Pattern]}
} }
err := deployer.linkAndDeploy(tr) err := deployer.linkAndDeploy(contract)
res := deployer.result() res := deployer.result()
accumRes.Accumulate(res) accumRes.Accumulate(res)
if err != nil { if err != nil {

View file

@ -94,6 +94,36 @@ type linkTestCaseInput struct {
expectDeployed map[rune]struct{} expectDeployed map[rune]struct{}
} }
// linkDeps will return a set of root dependencies and their sub-dependencies connected via the Deps field
func linkDeps(deps map[string]*MetaData) []*MetaData {
roots := make(map[string]struct{})
for pattern, _ := range deps {
roots[pattern] = struct{}{}
}
connectedDeps := make(map[string]MetaData)
for pattern, dep := range deps {
connectedDeps[pattern] = __linkDeps(*dep, deps, &roots)
}
rootMetadatas := []*MetaData{}
for pattern, _ := range roots {
dep := connectedDeps[pattern]
rootMetadatas = append(rootMetadatas, &dep)
}
return rootMetadatas
}
func __linkDeps(metadata MetaData, depMap map[string]*MetaData, roots *map[string]struct{}) MetaData {
linked := metadata
depPatterns := parseLibraryDeps(metadata.Bin)
for _, pattern := range depPatterns {
delete(*roots, pattern)
connectedDep := __linkDeps(*depMap[pattern], depMap, roots)
linked.Deps = append(linked.Deps, &connectedDep)
}
return linked
}
func testLinkCase(t *testing.T, tcInput linkTestCaseInput) { func testLinkCase(t *testing.T, tcInput linkTestCaseInput) {
testAddr := crypto.PubkeyToAddress(testKey.PublicKey) testAddr := crypto.PubkeyToAddress(testKey.PublicKey)
var testAddrNonce uint64 var testAddrNonce uint64
@ -138,24 +168,26 @@ func testLinkCase(t *testing.T, tcInput linkTestCaseInput) {
return contractAddr, nil, nil return contractAddr, nil, nil
} }
var contracts []*MetaData var contracts map[string]*MetaData
overrides := make(map[string]common.Address) overrides := make(map[string]common.Address)
for pattern, bin := range tc.contractCodes { for pattern, bin := range tc.contractCodes {
contracts = append(contracts, &MetaData{Pattern: pattern, Bin: "0x" + bin}) contracts[pattern] = &MetaData{Pattern: pattern, Bin: "0x" + bin}
} }
for pattern, bin := range tc.libCodes { for pattern, bin := range tc.libCodes {
contracts = append(contracts, &MetaData{ contracts[pattern] = &MetaData{
Bin: "0x" + bin, Bin: "0x" + bin,
Pattern: pattern, Pattern: pattern,
}) }
} }
contractsList := linkDeps(contracts)
for pattern, override := range tc.overrides { for pattern, override := range tc.overrides {
overrides[pattern] = override overrides[pattern] = override
} }
deployParams := NewDeploymentParams(contracts, nil, overrides) deployParams := NewDeploymentParams(contractsList, nil, overrides)
res, err := LinkAndDeploy(deployParams, mockDeploy) res, err := LinkAndDeploy(deployParams, mockDeploy)
if err != nil { if err != nil {
t.Fatalf("got error from LinkAndDeploy: %v\n", err) t.Fatalf("got error from LinkAndDeploy: %v\n", err)

View file

@ -33,12 +33,6 @@ var (
{{end}} {{end}}
{{range $contract := .Contracts}} {{range $contract := .Contracts}}
var {{$contract.Type}}LibraryDeps = []*bind.MetaData{
{{range $name, $pattern := .Libraries -}}
{{$name}}MetaData,
{{ end}}
}
// TODO: convert this type to value type after everything works. // TODO: convert this type to value type after everything works.
// {{.Type}}MetaData contains all meta data concerning the {{.Type}} contract. // {{.Type}}MetaData contains all meta data concerning the {{.Type}} contract.
var {{.Type}}MetaData = &bind.MetaData{ var {{.Type}}MetaData = &bind.MetaData{
@ -47,6 +41,9 @@ var (
{{if .InputBin -}} {{if .InputBin -}}
Bin: "0x{{.InputBin}}", Bin: "0x{{.InputBin}}",
{{end}} {{end}}
{{if .Libraries -}}{{range $name, $pattern := .Libraries}}
{{$name}}MetaData,
{{end}}{{end}}
} }
// {{.Type}} is an auto generated Go binding around an Ethereum contract. // {{.Type}} is an auto generated Go binding around an Ethereum contract.