Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ABIGEN v2 #26782

Open
wants to merge 101 commits into
base: master
Choose a base branch
from
Open

ABIGEN v2 #26782

wants to merge 101 commits into from

Conversation

s1na
Copy link
Contributor

@s1na s1na commented Feb 28, 2023

This PR adds a new version of abigen which will co-exist parallel to the existing version for a while. To generate v2 bindings provide the --v2 flag to abigen cmd.

Summary

The main point of abigen v2 is having a lightweight generated binding which allows for more composability. This is possible now thanks to Go generics. "only" methods to pack and unpack call and return data are generated for a contract. As well as unpacking of logs.

To interact with the contract a library is available with functions such as Call, Transact, FilterLogs, etc. These take in the packed calldata, or a function pointer to unpack return data.

Features

The new version is at feature-parity with v1 at a much lower generated binding size. The only missing feature as of now is sessions.

Example

V1 and v2 bindings for a sample contract are available here: https://gist.github.com/s1na/05f2d241b07372b41ba1747ce6e098b7. A sample script using v2 is available in main.go.

@s1na s1na changed the title cmd,accounts/abi: abigen v2 cmd, accounts/abi: abigen v2 Feb 28, 2023
Copy link
Member

@MariusVanDerWijden MariusVanDerWijden left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks pretty cool! I don't like the *2 naming, but I guess Felix likes it, so I won't argue to hard against it. I'm going to do some more tests and then approve

accounts/abi/bind/lib.go Outdated Show resolved Hide resolved
accounts/abi/bind/lib.go Outdated Show resolved Hide resolved
@MariusVanDerWijden
Copy link
Member

MariusVanDerWijden commented Mar 1, 2023

Something is broken here:

func (_{{$contract.Type}} *{{$contract.Type}}) Unpack{{.Normalized.Name}}(data []byte) ({{if .Structured}}struct{ {{range .Normalized.Outputs}}{{.Name}} {{bindtype .Type $structs}};{{end}} },{{else}}{{range .Normalized.Outputs}}{{bindtype .Type $structs}},{{end}}{{end}} error) {
			out, err := _{{$contract.Type}}.abi.Unpack("{{.Original.Name}}", data)
			{{if .Structured}}
			outstruct := new(struct{ {{range .Normalized.Outputs}} {{.Name}} {{bindtype .Type $structs}}; {{end}} })
			if err != nil {
				return *outstruct, err
			}
			{{range $i, $t := .Normalized.Outputs}} 
			outstruct.{{.Name}} = *abi.ConvertType(out[{{$i}}], new({{bindtype .Type $structs}})).(*{{bindtype .Type $structs}}){{end}}

			return *outstruct, err
			{{else}}
			if err != nil {
				return {{range $i, $_ := .Normalized.Outputs}}*new({{bindtype .Type $structs}}), {{end}} err
			}
			{{range $i, $t := .Normalized.Outputs}}
			out{{$i}} := *abi.ConvertType(out[{{$i}}], new({{bindtype .Type $structs}})).(*{{bindtype .Type $structs}}){{end}}
			
			return {{range $i, $t := .Normalized.Outputs}}out{{$i}}, {{end}} err
			{{end}}
		}

With this contract it produces invalid code:

contract Eventer {
   
    event TestInt8(int8 indexed out1, int8 indexed out2);
    event AnonEvent(address, address);
    
    function getEvent() public {
        // set to 2,3 for functioning filter
        emit TestInt8(-2, -3);
    }

    function anonEvent() public {
        emit AnonEvent(msg.sender, msg.sender);
    }

    function fail() public {
        require(false, "error string");
    }
}

Produced code:

func (_Eventer *Eventer) UnpackGetEvent(data []byte) (struct{}, error) {
	out, err := _Eventer.abi.Unpack("getEvent", data)

	outstruct := new(struct{})
	if err != nil {
		return *outstruct, err
	}

	return *outstruct, err

}

(out is never used)

I think the issue is that you generate Unpacking functions for solidity functions that have no return value.
I think those functions can just be skipped

@MariusVanDerWijden
Copy link
Member

/home/matematik/go/src/github.com/go-snippets/geth-test/array.go:8:2: "fmt" imported and not used

You should probably do with fmt the same thing as with the other imports:
_ = fmt.Printf so that go doesn't break if fmt is not used

@s1na
Copy link
Contributor Author

s1na commented Mar 1, 2023

Something is broken here:

Aha, we don't need an unpack method when there are no return params.

Copy link
Contributor

@fjl fjl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add some tests, and more documentation.

@geoknee
Copy link

geoknee commented Jul 19, 2023

@s1na are you still actively working on this PR? It is of interest to our project so we would be willing to devote some time to helping getting it over the line.

Our use case is: we are building a state channel client in Go. Currently, it manages a private key, and acts as a signer. It uses the abigen output to sign and launch transactions. What we want is to optionally have our client not manage a private key, and instead simply prepare (or "pack") the transaction and give it to the user to sign and send. Since we would like to maintain the coupling between or .sol source files are our go code, a neat way to achieve this is some changes to abigen such as those proposed on this PR.

One suggestion I would have is to maintain backward compatibility with the current version of abigen by simply adding the extra PackFoo (etc) functions to the existing output? Then you wouldn't need to worry about putting it all under a "v2" namespace or even adding those Transact helper functions...

@s1na
Copy link
Contributor Author

s1na commented Jul 19, 2023

Hi @geoknee, great to see your interest in this PR. I'm not actively working on this, although I plan to finish it at some point. Your help is appreciated.

I want to first address

One suggestion I would have is to maintain backward compatibility with the current version of abigen by simply adding the extra PackFoo (etc) functions to the existing output? Then you wouldn't need to worry about putting it all under a "v2" namespace or even adding those Transact helper functions...

We really want to get v2 out instead of packing more features into v1.

I saw you have already generated v2 bindings for your contract. It'd be already great to hear if things are working and you've had any friction points.

The biggest outstanding point is to add a test suite for v2.

@geoknee
Copy link

geoknee commented Jul 20, 2023

We really want to get v2 out instead of packing more features into v1.

Fair enough, we can use both side by side in our project without too much pain.

I saw you have already generated v2 bindings for your contract. It'd be already great to hear if things are working and you've had any friction points.

I did spot a couple of problems:

  1. One which I suspect is a bug -- some unexpected db field which looks like it could be from your example contract sneaking into our bindings:

statechannels/go-nitro@a1e9dd7

I pushed a fix here s1na#10.

  1. I get an error when trying to generate bindings for an ERC20 Token that is part of our setup. This has also been thrown up in some tests I started to sketch out https://github.com/s1na/go-ethereum/pull/11/files#r1269362198

The biggest outstanding point is to add a test suite for v2.

How should this look like? Is there an existing test for v1 which we can emulate?
I started hacking with the existing tests to extend their coverage. Keen to get feedback on that approach before devoting the time to finishing that off.

"context"
"encoding/json"
"github.com/ethereum/go-ethereum/accounts/abi/bind/backends"
"github.com/ethereum/go-ethereum/accounts/abi/bind/testdata/v2_generated_testcase"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Build fails on this import atm

@jwasinger
Copy link
Contributor

@MariusVanDerWijden I have a few things I am trying to wrap with this. It's in a bit of a messy state, and I'd ask to hold off on review until I can get this finished (couple of hours).

Comment on lines 226 to 231
err := deployer.linkAndDeploy(tr)
res := deployer.result()
accumRes.Accumulate(res)
if err != nil {
return accumRes, err
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
err := deployer.linkAndDeploy(tr)
res := deployer.result()
accumRes.Accumulate(res)
if err != nil {
return accumRes, err
}
if err := deployer.linkAndDeploy(tr); err != nil {
return accumRes, err
}
accumRes.Accumulate(deployer.result())

Afaict, if it linkAndDeploy exits with errors, there's nothing new to add to the accumulated result, so might aswell write it a bit more go-idiomatic

Copy link
Contributor

@jwasinger jwasinger Dec 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the call to linkAndDeploy deploys multiple contracts and then fails at some point, we want the accumulated result to include whatever succeeded.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. I didn't see that there's a recursive linkAndDeploy internally, which might add things and still return with error later.

Addrs: make(map[string]common.Address),
}
for _, meta := range deployParams.contracts {
unlinkedContracts[meta.Pattern] = meta.Bin[2:]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we fully in control of the inputs here? Do we know with 100% certainty that the meta.Bin starts with 0x-prefix?

Copy link
Contributor

@jwasinger jwasinger Dec 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume that users are sourcing everything from the output of solidity abi compilation (and it does prefix the contract bin with 0x). That's why I haven't yet added more defensive checks throughout this PR.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the contract deployer can be empty in the ABI. I can see no reason why that would ever be a valid result of the solidity compilation process.

Comment on lines 221 to 224
for _, tr := range deps {
deployer := newDepTreeDeployer(deploy)
if deployParams.inputs != nil {
deployer.input = map[string][]byte{tr.pattern: deployParams.inputs[tr.pattern]}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is it named tr? It's definitely not obvious to a casual reader.
And what does pattern signify, really? Please document or name it more appropriate. For me, it's a bit black magic what is happening here, with the setting of deployer.input to a map, involving the pattern, if the deployParams.inputs is non-nil.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tr represents the root of a deployment tree here: it's a contract/library that isn't a dependency of any other contracts.

a contract's pattern is a legitimate solidity concept (reference): it's the "34 character prefix of the hex encoding of the keccak256 hash of the fully qualified library name". A unique identifier for each contract/library.

I think for clarity, it probably deserves its own type (even if just a typedef on string).

Comment on lines 57 to 58
overrideAddrs map[string]common.Address
deployedAddrs map[string]common.Address
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can drop overrideAddrs, and shove everything into deployedAddrs. The deployerTxs can be used if the user wants to know which happened now and which happened previously

Copy link
Contributor

@jwasinger jwasinger Dec 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WDYT about filtering the overrides out of the DeploymentResult at the end of LinkAndDeploy based on their non-presence in the map of deployed txs?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something like that, yes.
Here are some suggested changes: holiman@4ad21ba
I wanted to try it out in code to see if it worked. Seems to, so far

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^ as you can see in that commit, I don't see any need for Accumulate to exist? It's accumulated in the deployer

return rootMetadatas
}

func __linkDeps(metadata MetaData, depMap map[string]*MetaData, roots *map[string]struct{}) MetaData {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's up with this name? Why such a strange prefix?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's the recursing part of linkDeps. I didn't know what to call it. That code needs a bit of love (documentation/renaming where appropriate) but I'm trying to port the v1 bind tests over to v2 atm.

Copy link
Contributor

@holiman holiman Dec 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, that's now how we name things in go (the underscores). I'm sure you can figure out a more descriptive name

Comment on lines 71 to 86
// if this contract/library depends on other libraries deploy them (and their dependencies) first
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 := metadata.Bin
for _, dep := range metadata.Deps {
linkAddr, ok := d.deployedAddrs[dep.Pattern]
if !ok {
linkAddr = d.overrideAddrs[dep.Pattern]
}
deployerCode = strings.ReplaceAll(deployerCode, "__$"+dep.Pattern+"$__", strings.ToLower(linkAddr.String()[2:]))
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can combine these two steps, and also make it look nicer if you change the signature to return common.Address, error:

	// if this contract/library depends on other libraries deploy them (and their dependencies) first
	deployerCode := metadata.Bin
	for _, dep := range metadata.Deps {
		addr, err := d.linkAndDeploy(dep)
		if err != nil {
			return common.Address{}, err
		}
		// link their deployed addresses into the bytecode to produce
		deployerCode = strings.ReplaceAll(deployerCode, "__$"+dep.Pattern+"$__", strings.ToLower(addr.String()[2:]))
	}

(still store the addr/tx as previous, though)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See second commit in 62a7806

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool. I'm going to pull these in to the branch.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW, I wouldn't mind if you squash things a bit, perhaps squash sequences of commits from one author into one commit.

Comment on lines 129 to 131
if deployParams.inputs != nil {
deployer.input = map[string][]byte{contract.Pattern: deployParams.inputs[contract.Pattern]}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This clause is not covered by testing (commenting it out doesn't cause failures). And it's not surprising, because the clause doesn't make sense.
On each iteration of a contract, it overwrites the input-map with the current contract input.
Why not just copy/clone the deployParams.inputs map to the deployer, already when creating the deployer?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Afaict you can turn it into

func LinkAndDeploy(deployParams *DeploymentParams, deploy DeployFn) (res *DeploymentResult, err error) {
	deployer := newDepTreeDeployer(deployParams.overrides, deploy, deployParams.inputs)
	for _, contract := range deployParams.contracts {
		if _ err := deployer.linkAndDeploy(contract); err != nil {
			return deployer.result(), err
		}
	}
	return deployer.result(), nil

Comment on lines 46 to 48
func (b *contractBinder) RegisterCallIdentifier(id string) (string, error) {
return b.registerIdentifier(b.callIdentifiers, id)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WHy is this public accessor needed? Are users expected to use this? In what scenario?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's a public method on a private type. Will move it to lowercase.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's a public method on a private type. Will move it to lowercase.

i.e. I didn't mean it for public consumption. a mistake on my part.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, if it's not for public, is it even needed? doesn't save a whole lot of typing really

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not really needed no.

LangGo: abi.ToCamelCase,
}
// conform to Go naming conventions.
var methodNormalizer = abi.ToCamelCase
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed at all (now that you removed the lang-dependent map). It's never actually changed, is it? So why not just use abi.ToCamelCase and keep it simple?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same with capitalise below, really

Comment on lines +91 to +92
normalized.Inputs = make([]abi.Argument, len(original.Inputs))
copy(normalized.Inputs, original.Inputs)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
normalized.Inputs = make([]abi.Argument, len(original.Inputs))
copy(normalized.Inputs, original.Inputs)
normalized.Inputs = slices.Clone(original.Inputs)

And same for outputs.


// normalizeErrorOrEventFields normalizes errors/events for emitting through bindings:
// Any anonymous fields are given generated names.
func (cb *contractBinder) normalizeErrorOrEventFields(originalInputs abi.Arguments) abi.Arguments {
Copy link
Contributor

@holiman holiman Dec 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method does multiple things. Howver, it only uses cb in order to do this:

		if hasStruct(input.Type) {
			cb.binder.BindStructType(input.Type)
		}

Hence, would it be possible to change into

// normalizeErrorOrEventFields normalizes errors/events for emitting through bindings:
// Any anonymous fields are given generated names.
func (cb *contractBinder) normalizeErrorOrEventFields(originalInputs abi.Arguments) abi.Arguments {
	args := normalizeArgs(originalInputs)
	// Soup up the struct types
	for _, input := range args {
		if hasStruct(input.Type) {
			cb.binder.BindStructType(input.Type)
		}
	}
	return args
}

func normalizeArgs(originalInputs abi.Arguments) abi.Arguments {
...

?
Would make it more testable

Comment on lines 149 to 153
if !used[capitalise(normalizedArguments[i].Name)] {
used[capitalise(normalizedArguments[i].Name)] = true
break
}
normalizedArguments[i].Name = fmt.Sprintf("%s%d", normalizedArguments[i].Name, index)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks slightly wrong to me. We check for capitalized version of the name, and then iterate on changing the name. But we don't actually capitalize the name. So the used-check uses the capitalized version, but non-capitalized is set as name.

It's a big confusing, and looks like a bug

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't it be something like this?

func normalizeArguments(args abi.Arguments) abi.Arguments {
	args := slices.Clone(args)
	used := make(map[string]bool)
	for i, input := range args {
		if input.Name == "" || isKeyWord(input.Name) {
			args[i].Name = fmt.Sprintf("arg%d", i)
		}
		name := capitalise(args[i].Name)
		for index := 0; ; index++ {
			if !used[name] {
				used[name] = true
				break
			}
			// try next
			name = capitalise(fmt.Sprintf("%s%d", args[i].Name, index))
		}
		args[i].Name = name
	}
	return args
}

}

// bindEvent normalizes an event and registers it to be emitted in the bindings.
func (cb *contractBinder) bindEvent(original abi.Event) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move bindEvent and bindError so they are right below bindMethod

type tmplDataV2 struct {
Package string // Name of the package to place the generated file in
Contracts map[string]*tmplContractV2 // List of contracts to generate into this file
Libraries map[string]string // Map of the contract's name to link pattern
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the only thing that's different which warranted addition of templDataV2.

…t fails before the fix, and also also a few more test cases for args normalization.
…t the backing-array provided by the API user from being mutated). re-enable deployment-with-overrides test
…the results of feeding v1 binding test ABIs through the v2 generator.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants