signer/fourbyte: add support for nested types in selectors (#24407)
This replaces the simple selector parser in signer/fourbyte with one that can actually handle most types. The new parser is added in accounts/abi to also make it useable elsewhere.
This commit is contained in:
		
							parent
							
								
									8fddf27a98
								
							
						
					
					
						commit
						37f9d25ba0
					
				
							
								
								
									
										152
									
								
								accounts/abi/selector_parser.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										152
									
								
								accounts/abi/selector_parser.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,152 @@ | ||||
| package abi | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| ) | ||||
| 
 | ||||
| type SelectorMarshaling struct { | ||||
| 	Name   string               `json:"name"` | ||||
| 	Type   string               `json:"type"` | ||||
| 	Inputs []ArgumentMarshaling `json:"inputs"` | ||||
| } | ||||
| 
 | ||||
| func isDigit(c byte) bool { | ||||
| 	return c >= '0' && c <= '9' | ||||
| } | ||||
| 
 | ||||
| func isAlpha(c byte) bool { | ||||
| 	return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') | ||||
| } | ||||
| 
 | ||||
| func isIdentifierSymbol(c byte) bool { | ||||
| 	return c == '$' || c == '_' | ||||
| } | ||||
| 
 | ||||
| func parseToken(unescapedSelector string, isIdent bool) (string, string, error) { | ||||
| 	if len(unescapedSelector) == 0 { | ||||
| 		return "", "", fmt.Errorf("empty token") | ||||
| 	} | ||||
| 	firstChar := unescapedSelector[0] | ||||
| 	position := 1 | ||||
| 	if !(isAlpha(firstChar) || (isIdent && isIdentifierSymbol(firstChar))) { | ||||
| 		return "", "", fmt.Errorf("invalid token start: %c", firstChar) | ||||
| 	} | ||||
| 	for position < len(unescapedSelector) { | ||||
| 		char := unescapedSelector[position] | ||||
| 		if !(isAlpha(char) || isDigit(char) || (isIdent && isIdentifierSymbol(char))) { | ||||
| 			break | ||||
| 		} | ||||
| 		position++ | ||||
| 	} | ||||
| 	return unescapedSelector[:position], unescapedSelector[position:], nil | ||||
| } | ||||
| 
 | ||||
| func parseIdentifier(unescapedSelector string) (string, string, error) { | ||||
| 	return parseToken(unescapedSelector, true) | ||||
| } | ||||
| 
 | ||||
| func parseElementaryType(unescapedSelector string) (string, string, error) { | ||||
| 	parsedType, rest, err := parseToken(unescapedSelector, false) | ||||
| 	if err != nil { | ||||
| 		return "", "", fmt.Errorf("failed to parse elementary type: %v", err) | ||||
| 	} | ||||
| 	// handle arrays
 | ||||
| 	for len(rest) > 0 && rest[0] == '[' { | ||||
| 		parsedType = parsedType + string(rest[0]) | ||||
| 		rest = rest[1:] | ||||
| 		for len(rest) > 0 && isDigit(rest[0]) { | ||||
| 			parsedType = parsedType + string(rest[0]) | ||||
| 			rest = rest[1:] | ||||
| 		} | ||||
| 		if len(rest) == 0 || rest[0] != ']' { | ||||
| 			return "", "", fmt.Errorf("failed to parse array: expected ']', got %c", unescapedSelector[0]) | ||||
| 		} | ||||
| 		parsedType = parsedType + string(rest[0]) | ||||
| 		rest = rest[1:] | ||||
| 	} | ||||
| 	return parsedType, rest, nil | ||||
| } | ||||
| 
 | ||||
| func parseCompositeType(unescapedSelector string) ([]interface{}, string, error) { | ||||
| 	if len(unescapedSelector) == 0 || unescapedSelector[0] != '(' { | ||||
| 		return nil, "", fmt.Errorf("expected '(', got %c", unescapedSelector[0]) | ||||
| 	} | ||||
| 	parsedType, rest, err := parseType(unescapedSelector[1:]) | ||||
| 	if err != nil { | ||||
| 		return nil, "", fmt.Errorf("failed to parse type: %v", err) | ||||
| 	} | ||||
| 	result := []interface{}{parsedType} | ||||
| 	for len(rest) > 0 && rest[0] != ')' { | ||||
| 		parsedType, rest, err = parseType(rest[1:]) | ||||
| 		if err != nil { | ||||
| 			return nil, "", fmt.Errorf("failed to parse type: %v", err) | ||||
| 		} | ||||
| 		result = append(result, parsedType) | ||||
| 	} | ||||
| 	if len(rest) == 0 || rest[0] != ')' { | ||||
| 		return nil, "", fmt.Errorf("expected ')', got '%s'", rest) | ||||
| 	} | ||||
| 	return result, rest[1:], nil | ||||
| } | ||||
| 
 | ||||
| func parseType(unescapedSelector string) (interface{}, string, error) { | ||||
| 	if len(unescapedSelector) == 0 { | ||||
| 		return nil, "", fmt.Errorf("empty type") | ||||
| 	} | ||||
| 	if unescapedSelector[0] == '(' { | ||||
| 		return parseCompositeType(unescapedSelector) | ||||
| 	} else { | ||||
| 		return parseElementaryType(unescapedSelector) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func assembleArgs(args []interface{}) ([]ArgumentMarshaling, error) { | ||||
| 	arguments := make([]ArgumentMarshaling, 0) | ||||
| 	for i, arg := range args { | ||||
| 		// generate dummy name to avoid unmarshal issues
 | ||||
| 		name := fmt.Sprintf("name%d", i) | ||||
| 		if s, ok := arg.(string); ok { | ||||
| 			arguments = append(arguments, ArgumentMarshaling{name, s, s, nil, false}) | ||||
| 		} else if components, ok := arg.([]interface{}); ok { | ||||
| 			subArgs, err := assembleArgs(components) | ||||
| 			if err != nil { | ||||
| 				return nil, fmt.Errorf("failed to assemble components: %v", err) | ||||
| 			} | ||||
| 			arguments = append(arguments, ArgumentMarshaling{name, "tuple", "tuple", subArgs, false}) | ||||
| 		} else { | ||||
| 			return nil, fmt.Errorf("failed to assemble args: unexpected type %T", arg) | ||||
| 		} | ||||
| 	} | ||||
| 	return arguments, nil | ||||
| } | ||||
| 
 | ||||
| // ParseSelector converts a method selector into a struct that can be JSON encoded
 | ||||
| // and consumed by other functions in this package.
 | ||||
| // Note, although uppercase letters are not part of the ABI spec, this function
 | ||||
| // still accepts it as the general format is valid.
 | ||||
| func ParseSelector(unescapedSelector string) (SelectorMarshaling, error) { | ||||
| 	name, rest, err := parseIdentifier(unescapedSelector) | ||||
| 	if err != nil { | ||||
| 		return SelectorMarshaling{}, fmt.Errorf("failed to parse selector '%s': %v", unescapedSelector, err) | ||||
| 	} | ||||
| 	args := []interface{}{} | ||||
| 	if len(rest) >= 2 && rest[0] == '(' && rest[1] == ')' { | ||||
| 		rest = rest[2:] | ||||
| 	} else { | ||||
| 		args, rest, err = parseCompositeType(rest) | ||||
| 		if err != nil { | ||||
| 			return SelectorMarshaling{}, fmt.Errorf("failed to parse selector '%s': %v", unescapedSelector, err) | ||||
| 		} | ||||
| 	} | ||||
| 	if len(rest) > 0 { | ||||
| 		return SelectorMarshaling{}, fmt.Errorf("failed to parse selector '%s': unexpected string '%s'", unescapedSelector, rest) | ||||
| 	} | ||||
| 
 | ||||
| 	// Reassemble the fake ABI and constuct the JSON
 | ||||
| 	fakeArgs, err := assembleArgs(args) | ||||
| 	if err != nil { | ||||
| 		return SelectorMarshaling{}, fmt.Errorf("failed to parse selector: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	return SelectorMarshaling{name, "function", fakeArgs}, nil | ||||
| } | ||||
							
								
								
									
										54
									
								
								accounts/abi/selector_parser_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								accounts/abi/selector_parser_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,54 @@ | ||||
| package abi | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"log" | ||||
| 	"reflect" | ||||
| 	"testing" | ||||
| ) | ||||
| 
 | ||||
| func TestParseSelector(t *testing.T) { | ||||
| 	mkType := func(types ...interface{}) []ArgumentMarshaling { | ||||
| 		var result []ArgumentMarshaling | ||||
| 		for i, typeOrComponents := range types { | ||||
| 			name := fmt.Sprintf("name%d", i) | ||||
| 			if typeName, ok := typeOrComponents.(string); ok { | ||||
| 				result = append(result, ArgumentMarshaling{name, typeName, typeName, nil, false}) | ||||
| 			} else if components, ok := typeOrComponents.([]ArgumentMarshaling); ok { | ||||
| 				result = append(result, ArgumentMarshaling{name, "tuple", "tuple", components, false}) | ||||
| 			} else { | ||||
| 				log.Fatalf("unexpected type %T", typeOrComponents) | ||||
| 			} | ||||
| 		} | ||||
| 		return result | ||||
| 	} | ||||
| 	tests := []struct { | ||||
| 		input string | ||||
| 		name  string | ||||
| 		args  []ArgumentMarshaling | ||||
| 	}{ | ||||
| 		{"noargs()", "noargs", []ArgumentMarshaling{}}, | ||||
| 		{"simple(uint256,uint256,uint256)", "simple", mkType("uint256", "uint256", "uint256")}, | ||||
| 		{"other(uint256,address)", "other", mkType("uint256", "address")}, | ||||
| 		{"withArray(uint256[],address[2],uint8[4][][5])", "withArray", mkType("uint256[]", "address[2]", "uint8[4][][5]")}, | ||||
| 		{"singleNest(bytes32,uint8,(uint256,uint256),address)", "singleNest", mkType("bytes32", "uint8", mkType("uint256", "uint256"), "address")}, | ||||
| 		{"multiNest(address,(uint256[],uint256),((address,bytes32),uint256))", "multiNest", | ||||
| 			mkType("address", mkType("uint256[]", "uint256"), mkType(mkType("address", "bytes32"), "uint256"))}, | ||||
| 	} | ||||
| 	for i, tt := range tests { | ||||
| 		selector, err := ParseSelector(tt.input) | ||||
| 		if err != nil { | ||||
| 			t.Errorf("test %d: failed to parse selector '%v': %v", i, tt.input, err) | ||||
| 		} | ||||
| 		if selector.Name != tt.name { | ||||
| 			t.Errorf("test %d: unexpected function name: '%s' != '%s'", i, selector.Name, tt.name) | ||||
| 		} | ||||
| 
 | ||||
| 		if selector.Type != "function" { | ||||
| 			t.Errorf("test %d: unexpected type: '%s' != '%s'", i, selector.Type, "function") | ||||
| 		} | ||||
| 		if !reflect.DeepEqual(selector.Inputs, tt.args) { | ||||
| 			t.Errorf("test %d: unexpected args: '%v' != '%v'", i, selector.Inputs, tt.args) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @ -20,7 +20,6 @@ import ( | ||||
| 	"bytes" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/ethereum/go-ethereum/accounts/abi" | ||||
| @ -75,42 +74,15 @@ func verifySelector(selector string, calldata []byte) (*decodedCallData, error) | ||||
| 	return parseCallData(calldata, string(abidata)) | ||||
| } | ||||
| 
 | ||||
| // selectorRegexp is used to validate that a 4byte database selector corresponds
 | ||||
| // to a valid ABI function declaration.
 | ||||
| //
 | ||||
| // Note, although uppercase letters are not part of the ABI spec, this regexp
 | ||||
| // still accepts it as the general format is valid. It will be rejected later
 | ||||
| // by the type checker.
 | ||||
| var selectorRegexp = regexp.MustCompile(`^([^\)]+)\(([A-Za-z0-9,\[\]]*)\)`) | ||||
| 
 | ||||
| // parseSelector converts a method selector into an ABI JSON spec. The returned
 | ||||
| // data is a valid JSON string which can be consumed by the standard abi package.
 | ||||
| func parseSelector(unescapedSelector string) ([]byte, error) { | ||||
| 	// Define a tiny fake ABI struct for JSON marshalling
 | ||||
| 	type fakeArg struct { | ||||
| 		Type string `json:"type"` | ||||
| 	selector, err := abi.ParseSelector(unescapedSelector) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to parse selector: %v", err) | ||||
| 	} | ||||
| 	type fakeABI struct { | ||||
| 		Name   string    `json:"name"` | ||||
| 		Type   string    `json:"type"` | ||||
| 		Inputs []fakeArg `json:"inputs"` | ||||
| 	} | ||||
| 	// Validate the unescapedSelector and extract it's components
 | ||||
| 	groups := selectorRegexp.FindStringSubmatch(unescapedSelector) | ||||
| 	if len(groups) != 3 { | ||||
| 		return nil, fmt.Errorf("invalid selector %q (%v matches)", unescapedSelector, len(groups)) | ||||
| 	} | ||||
| 	name := groups[1] | ||||
| 	args := groups[2] | ||||
| 
 | ||||
| 	// Reassemble the fake ABI and constuct the JSON
 | ||||
| 	arguments := make([]fakeArg, 0) | ||||
| 	if len(args) > 0 { | ||||
| 		for _, arg := range strings.Split(args, ",") { | ||||
| 			arguments = append(arguments, fakeArg{arg}) | ||||
| 		} | ||||
| 	} | ||||
| 	return json.Marshal([]fakeABI{{name, "function", arguments}}) | ||||
| 	return json.Marshal([]abi.SelectorMarshaling{selector}) | ||||
| } | ||||
| 
 | ||||
| // parseCallData matches the provided call data against the ABI definition and
 | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user