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
ab *abi.ABI
Pattern string
Deps []*MetaData
}
func (m *MetaData) GetAbi() (*abi.ABI, error) {

View file

@ -5,6 +5,7 @@ import (
"fmt"
"github.com/ethereum/go-ethereum/accounts/abi"
"go/format"
"regexp"
"strings"
"text/template"
"unicode"
@ -190,6 +191,17 @@ func (cb *contractBinder) bindError(original abi.Error) error {
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
// 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
@ -271,23 +283,9 @@ func BindV2(types []string, abis []string, bytecodes []string, pkg string, libs
Structs: b.structs,
}
contractsBins := make(map[string]string)
for typ, contract := range data.Contracts {
pattern := invertedLibs[typ]
contractsBins[pattern] = contract.InputBin
}
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
for _, depPattern := range parseLibraryDeps(contract.InputBin) {
data.Contracts[typ].Libraries[libs[depPattern]] = depPattern
}
}
buffer := new(bytes.Buffer)

View file

@ -3,7 +3,7 @@ package bind
import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"regexp"
"maps"
"strings"
)
@ -45,100 +45,6 @@ func (d *DeploymentResult) Accumulate(other *DeploymentResult) {
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
// the address and a pending transaction, or an error if the deployment failed.
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.
// 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.
if node.overrideAddr != nil {
if _, ok := d.deployedAddrs[metadata.Pattern]; ok {
return nil
}
// if this contract/library depends on other libraries deploy them (and their dependencies) first
for _, childNode := range node.children {
if err := d.linkAndDeploy(childNode); err != nil {
for _, dep := range metadata.Deps {
if err := d.linkAndDeploy(dep); err != nil {
return err
}
}
// if we just deployed any prerequisite contracts, link their deployed addresses into the bytecode to produce
// a deployer bytecode for this contract.
deployerCode := node.unlinkedCode
for _, child := range node.children {
linkAddr := d.deployedAddrs[child.pattern]
if child.overrideAddr != nil {
linkAddr = *child.overrideAddr
}
deployerCode = strings.ReplaceAll(deployerCode, "__$"+child.pattern+"$__", strings.ToLower(linkAddr.String()[2:]))
deployerCode := metadata.Bin
for _, dep := range metadata.Deps {
linkAddr := d.deployedAddrs[dep.Pattern]
deployerCode = strings.ReplaceAll(deployerCode, "__$"+dep.Pattern+"$__", strings.ToLower(linkAddr.String()[2:]))
}
// 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 {
return err
}
d.deployedAddrs[node.pattern] = addr
d.deployerTxs[node.pattern] = tx
d.deployedAddrs[metadata.Pattern] = addr
d.deployerTxs[metadata.Pattern] = tx
return nil
}
@ -207,23 +110,16 @@ func newDepTreeDeployer(deploy DeployFn) *depTreeDeployer {
// libraries. If an error occurs, only contracts which were successfully
// deployed are returned in the result.
func LinkAndDeploy(deployParams *DeploymentParams, deploy DeployFn) (res *DeploymentResult, err error) {
unlinkedContracts := make(map[string]string)
accumRes := &DeploymentResult{
Txs: make(map[string]*types.Transaction),
Addrs: make(map[string]common.Address),
}
for _, meta := range deployParams.contracts {
unlinkedContracts[meta.Pattern] = meta.Bin[2:]
}
treeBuilder := newDepTreeBuilder(deployParams.overrides, unlinkedContracts)
deps, _ := treeBuilder.BuildDepTrees()
for _, tr := range deps {
deployer := newDepTreeDeployer(deploy)
deployer := newDepTreeDeployer(deploy)
for _, contract := range deployParams.contracts {
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()
accumRes.Accumulate(res)
if err != nil {

View file

@ -94,6 +94,36 @@ type linkTestCaseInput 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) {
testAddr := crypto.PubkeyToAddress(testKey.PublicKey)
var testAddrNonce uint64
@ -138,24 +168,26 @@ func testLinkCase(t *testing.T, tcInput linkTestCaseInput) {
return contractAddr, nil, nil
}
var contracts []*MetaData
var contracts map[string]*MetaData
overrides := make(map[string]common.Address)
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 {
contracts = append(contracts, &MetaData{
contracts[pattern] = &MetaData{
Bin: "0x" + bin,
Pattern: pattern,
})
}
}
contractsList := linkDeps(contracts)
for pattern, override := range tc.overrides {
overrides[pattern] = override
}
deployParams := NewDeploymentParams(contracts, nil, overrides)
deployParams := NewDeploymentParams(contractsList, nil, overrides)
res, err := LinkAndDeploy(deployParams, mockDeploy)
if err != nil {
t.Fatalf("got error from LinkAndDeploy: %v\n", err)

View file

@ -33,12 +33,6 @@ var (
{{end}}
{{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.
// {{.Type}}MetaData contains all meta data concerning the {{.Type}} contract.
var {{.Type}}MetaData = &bind.MetaData{
@ -47,6 +41,9 @@ var (
{{if .InputBin -}}
Bin: "0x{{.InputBin}}",
{{end}}
{{if .Libraries -}}{{range $name, $pattern := .Libraries}}
{{$name}}MetaData,
{{end}}{{end}}
}
// {{.Type}} is an auto generated Go binding around an Ethereum contract.