diff --git a/manager/eris-mint/evm/native.go b/manager/eris-mint/evm/native.go index ed4b4f378c93a2a7be83031f59ac6c17c7bc22cd..ab9e0c9720ee40c14f30f788cdd86bd03f5609dc 100644 --- a/manager/eris-mint/evm/native.go +++ b/manager/eris-mint/evm/native.go @@ -40,6 +40,8 @@ func registerNativeContracts() { type NativeContract func(appState AppState, caller *Account, input []byte, gas *int64) (output []byte, err error) +type FuncID [4]byte + /* Removed due to C dependency func ecrecoverFunc(appState AppState, caller *Account, input []byte, gas *int64) (output []byte, err error) { // Deduct gas diff --git a/manager/eris-mint/evm/snative.go b/manager/eris-mint/evm/snative.go index 0c03827eab16463537c693d07a88e66896bf1491..95021ed0d6d8d066b34d538ee913c3350f1b5ddb 100644 --- a/manager/eris-mint/evm/snative.go +++ b/manager/eris-mint/evm/snative.go @@ -1,104 +1,344 @@ package vm import ( - "encoding/hex" "fmt" "github.com/eris-ltd/eris-db/common/sanity" "github.com/eris-ltd/eris-db/manager/eris-mint/evm/sha3" ptypes "github.com/eris-ltd/eris-db/permission/types" . "github.com/eris-ltd/eris-db/word256" + + "bytes" + "strings" ) //------------------------------------------------------------------------------------------------ // Registered SNative contracts -var PermissionsContract = "permissions_contract" +type SNativeContractDescription struct { + Comment string + Name string + functions map[FuncID]SNativeFuncDescription +} + +type SNativeFuncDescription struct { + Comment string + Name string + Args []SolidityArg + Return SolidityReturn + PermFlag ptypes.PermFlag + F NativeContract +} + +type SolidityType string + +type SolidityArg struct { + Name string + Type SolidityType +} + +type SolidityReturn struct { + Name string + Type SolidityType +} + +const ( + // We don't need to be exhaustive here, just make what we used strongly typed + SolidityAddress SolidityType = "address" + SolidityInt SolidityType = "int" + SolidityUint64 SolidityType = "uint64" + SolidityBytes32 SolidityType = "bytes32" + SolidityString SolidityType = "string" + SolidityBool SolidityType = "bool" +) func registerSNativeContracts() { - registeredNativeContracts[LeftPadWord256([]byte(PermissionsContract))] = permissionsContract + for _, contract := range SNativeContracts() { + registeredNativeContracts[contract.Address()] = contract.Dispatch + } +} + +// Returns a map of all SNative contracts defined indexed by name +func SNativeContracts() map[string]SNativeContractDescription { + contracts := []SNativeContractDescription{ + NewSNativeContract(` + * Interface for managing Secure Native authorizations. + * @dev This Solidity interface describes the functions exposed by the SNative permissions layer in the Monax blockchain (ErisDB). + `, + "permissions_contract", + SNativeFuncDescription{` + * @notice Adds a role to an account + * @param _account account + * @param _role role + * @return result whether role was added + `, + "add_role", + []SolidityArg{ + arg("_account", SolidityAddress), + arg("_role", SolidityBytes32), + }, + ret("result", SolidityBool), + ptypes.AddRole, + add_role}, + + SNativeFuncDescription{` + * @notice Indicates whether an account has a role + * @param _account account + * @param _role role + * @return result whether account has role + `, + "has_role", + []SolidityArg{ + arg("_account", SolidityAddress), + arg("_role", SolidityBytes32), + }, + ret("result", SolidityBool), + ptypes.HasRole, + has_role}, - /* - // we could expose these but we moved permission and args checks into the permissionsContract - // so calling them would be unsafe ... - registeredNativeContracts[LeftPadWord256([]byte("has_base"))] = has_base - registeredNativeContracts[LeftPadWord256([]byte("set_base"))] = set_base - registeredNativeContracts[LeftPadWord256([]byte("unset_base"))] = unset_base - registeredNativeContracts[LeftPadWord256([]byte("set_global"))] = set_global - registeredNativeContracts[LeftPadWord256([]byte("has_role"))] = has_role - registeredNativeContracts[LeftPadWord256([]byte("add_role"))] = add_role - registeredNativeContracts[LeftPadWord256([]byte("rm_role"))] = rm_role - */ + SNativeFuncDescription{` + * @notice Removes a role from an account + * @param _account account + * @param _role role + * @return result whether role was removed + `, + "rm_role", + []SolidityArg{ + arg("_account", SolidityAddress), + arg("_role", SolidityBytes32), + }, + ret("result", SolidityBool), + ptypes.RmRole, + rm_role}, + + SNativeFuncDescription{` + * @notice Sets a base authorization for an account + * @param _account account + * @param _authorization base authorization + * @param _value value of base authorization + * @return result value passed in + `, + "set_base", + []SolidityArg{arg("_account", SolidityAddress), + arg("_authorization", SolidityInt), + arg("_value", SolidityInt)}, + ret("result", SolidityBool), + ptypes.SetBase, + set_base}, + + SNativeFuncDescription{` + * @notice Indicates whether an account has a base authorization + * @param _account account + * @param _authorization base authorization + * @return result whether account has base authorization set + `, + "has_base", + []SolidityArg{arg("_account", SolidityAddress), + arg("_authorization", SolidityInt)}, + ret("result", SolidityBool), + ptypes.HasBase, + has_base}, + + SNativeFuncDescription{` + * @notice Sets a base authorization for an account to the global (default) value of the base authorization + * @param _account account + * @param _authorization base authorization + * @return authorization base authorization passed in + `, + "unset_base", + []SolidityArg{arg("_account", SolidityAddress), + arg("_authorization", SolidityInt)}, + ret("authorization", SolidityInt), + ptypes.UnsetBase, + unset_base}, + + SNativeFuncDescription{` + * @notice Sets global (default) value for a base authorization + * @param _account account + * @param _authorization base authorization + * @param _value value of base authorization + * @return authorization base authorization passed in + `, + "set_global", + []SolidityArg{arg("_account", SolidityAddress), + arg("_authorization", SolidityInt), + arg("_value", SolidityInt)}, + ret("authorization", SolidityInt), + ptypes.SetGlobal, + set_global}, + ), + } + + contractMap := make(map[string]SNativeContractDescription, len(contracts)) + for _, contract := range contracts { + contractMap[contract.Name] = contract + } + return contractMap } //----------------------------------------------------------------------------- // snative are native contracts that can access and modify an account's permissions -type SNativeFuncDescription struct { - Name string - NArgs int - PermFlag ptypes.PermFlag - F NativeContract +func NewSNativeContract(comment, name string, functions ...SNativeFuncDescription) SNativeContractDescription { + fs := make(map[FuncID]SNativeFuncDescription, len(functions)) + for _, f := range functions { + fid := f.ID() + otherF, ok := fs[fid] + if ok { + panic(fmt.Errorf("Function with ID %x already defined: %s", fid, + otherF)) + } + fs[fid] = f + } + return SNativeContractDescription{ + Comment: comment, + Name: name, + functions: fs, + } } -/* The solidity interface used to generate the abi function ids below -contract Permissions { - function has_base(address addr, uint64 permFlag) constant returns (bool value) {} - function set_base(address addr, uint64 permFlag, bool value) constant returns (bool val) {} - function unset_base(address addr, uint64 permFlag) constant returns (uint64 pf) {} - function set_global(uint64 permFlag, bool value) constant returns (uint64 pf) {} - function has_role(address addr, string role) constant returns (bool val) {} - function add_role(address addr, string role) constant returns (bool added) {} - function rm_role(address addr, string role) constant returns (bool removed) {} +func (contract *SNativeContractDescription) Address() Word256 { + return LeftPadWord256([]byte(contract.Name)) } -*/ -// function identifiers from the solidity abi -var PermsMap = map[string]SNativeFuncDescription{ - getFuncIdentifiersFromSignature("has_role(address,bytes32)"): SNativeFuncDescription{"has_role", 2, ptypes.HasRole, has_role}, - getFuncIdentifiersFromSignature("unset_base(address,uint64)"): SNativeFuncDescription{"unset_base", 2, ptypes.UnsetBase, unset_base}, - getFuncIdentifiersFromSignature("set_global(uint64,bool)"): SNativeFuncDescription{"set_global", 2, ptypes.SetGlobal, set_global}, - getFuncIdentifiersFromSignature("add_role(address,bytes32)"): SNativeFuncDescription{"add_role", 2, ptypes.AddRole, add_role}, - getFuncIdentifiersFromSignature("set_base(address,uint64,bool"): SNativeFuncDescription{"set_base", 3, ptypes.SetBase, set_base}, - getFuncIdentifiersFromSignature("has_base(address,uint64)"): SNativeFuncDescription{"has_base", 2, ptypes.HasBase, has_base}, - getFuncIdentifiersFromSignature("rm_role(address,bytes32)"): SNativeFuncDescription{"rm_role", 2, ptypes.RmRole, rm_role}, +func (contract *SNativeContractDescription) FunctionByID(id FuncID) (*SNativeFuncDescription, error) { + f, ok := contract.functions[id] + if !ok { + return nil, + fmt.Errorf("Unknown SNative function with ID %x", id) + } + return &f, nil +} + +func (contract *SNativeContractDescription) FunctionByName(name string) (*SNativeFuncDescription, error) { + for _, f := range contract.functions { + if f.Name == name { + return &f, nil + } + } + return nil, fmt.Errorf("Unknown SNative function with name %s", name) } -func getFuncIdentifiersFromSignature(signature string) string { - identifier := sha3.Sha3([]byte(signature)) - return hex.EncodeToString(identifier[:4]) +func (contract *SNativeContractDescription) Functions() []SNativeFuncDescription { + fs := make([]SNativeFuncDescription, 0, len(contract.functions)) + for _, f := range contract.functions { + fs = append(fs, f) + } + return fs } -func permissionsContract(appState AppState, caller *Account, args []byte, gas *int64) (output []byte, err error) { +func (contract *SNativeContractDescription) Dispatch(appState AppState, + caller *Account, args []byte, gas *int64) (output []byte, err error) { if len(args) < 4 { - return nil, fmt.Errorf("permissionsContract expects at least a 4-byte function identifier") + return nil, fmt.Errorf("SNatives dispatch requires a 4-byte function "+ + "identifier but arguments are only %s bytes long", len(args)) } - // map solidity abi function id to snative - funcIDbytes := args[:4] - args = args[4:] - funcID := hex.EncodeToString(funcIDbytes) - d, ok := PermsMap[funcID] - if !ok { - return nil, fmt.Errorf("unknown permissionsContract funcID %s", funcID) + function, err := contract.FunctionByID(firstFourBytes(args)) + if err != nil { + return nil, err } + remainingArgs := args[4:] + // check if we have permission to call this function - if !HasPermission(appState, caller, d.PermFlag) { - return nil, ErrInvalidPermission{caller.Address, d.Name} + if !HasPermission(appState, caller, function.PermFlag) { + return nil, ErrInvalidPermission{caller.Address, function.Name} } // ensure there are enough arguments - if len(args) != d.NArgs*32 { - return nil, fmt.Errorf("%s() takes %d arguments", d.Name) + if len(remainingArgs) != function.NArgs()*32 { + return nil, fmt.Errorf("%s() takes %d arguments", function.Name, + function.NArgs()) } // call the function - return d.F(appState, caller, args, gas) + return function.F(appState, caller, remainingArgs, gas) } -// TODO: catch errors, log em, return 0s to the vm (should some errors cause exceptions though?) +func (contract *SNativeContractDescription) SolidityComment() string { + return solidityComment(contract.Comment) +} + +// Generate solidity code for this SNative contract +func (contract *SNativeContractDescription) Solidity() (string, error) { + buf := new(bytes.Buffer) + err := snativeContractTemplate.Execute(buf, contract) + if err != nil { + return "", err + } + return buf.String(), nil +} + +// +// SNative functions +// +func (function *SNativeFuncDescription) Signature() string { + argTypes := make([]string, len(function.Args)) + for i, arg := range function.Args { + argTypes[i] = string(arg.Type) + } + return fmt.Sprintf("%s(%s)", function.Name, + strings.Join(argTypes, ",")) +} + +func (function *SNativeFuncDescription) ID() FuncID { + return firstFourBytes(sha3.Sha3([]byte(function.Signature()))) +} + +func (function *SNativeFuncDescription) NArgs() int { + return len(function.Args) +} + +func (function *SNativeFuncDescription) SolidityArgList() string { + argList := make([]string, len(function.Args)) + for i, arg := range function.Args { + argList[i] = fmt.Sprintf("%s %s", arg.Type, arg.Name) + } + return strings.Join(argList, ", ") +} +func (function *SNativeFuncDescription) SolidityComment() string { + return solidityComment(function.Comment) +} + +func (function *SNativeFuncDescription) Solidity() (string, error) { + buf := new(bytes.Buffer) + err := snativeFuncTemplate.Execute(buf, function) + if err != nil { + return "", err + } + return buf.String(), nil +} + +func solidityComment(comment string) string { + commentLines := make([]string, 0, 5) + for _, line := range strings.Split(comment, "\n") { + trimLine := strings.TrimLeft(line, " \t\n") + if trimLine != "" { + commentLines = append(commentLines, trimLine) + } + } + return strings.Join(commentLines, "\n") +} + +func arg(name string, solidityType SolidityType) SolidityArg { + return SolidityArg{ + Name: name, + Type: solidityType, + } +} + +func ret(name string, solidityType SolidityType) SolidityReturn { + return SolidityReturn{ + Name: name, + Type: solidityType, + } +} + +// Permission function defintions + +// TODO: catch errors, log em, return 0s to the vm (should some errors cause exceptions though?) func has_base(appState AppState, caller *Account, args []byte, gas *int64) (output []byte, err error) { addr, permNum := returnTwoArgs(args) vmAcc := appState.GetAccount(addr) @@ -249,3 +489,9 @@ func byteFromBool(b bool) byte { } return 0x0 } + +func firstFourBytes(byteSlice []byte) [4]byte { + var bs [4]byte + copy(bs[:], byteSlice[:4]) + return bs +} diff --git a/manager/eris-mint/evm/snative_templates.go b/manager/eris-mint/evm/snative_templates.go new file mode 100644 index 0000000000000000000000000000000000000000..dc47388e2dc515693743148e2d53e4c844ca1532 --- /dev/null +++ b/manager/eris-mint/evm/snative_templates.go @@ -0,0 +1,37 @@ +package vm + +import ( + "text/template" + "fmt" +) + +const snativeContractTemplateText=`/** +{{.SolidityComment}} +*/ +contract {{.Name}} { +{{range .Functions}} +{{.Solidity}} +{{end}} +} +` +const snativeFuncTemplateText=`/** +{{.SolidityComment}} +*/ +function {{.Name}}({{.SolidityArgList}}) constant returns ({{.Return.Type}} {{.Return.Name}});` + +var snativeContractTemplate *template.Template +var snativeFuncTemplate *template.Template + +func init() { + var err error + snativeFuncTemplate, err = template.New("snativeFuncTemplate"). + Parse(snativeFuncTemplateText) + if err != nil { + panic(fmt.Errorf("Couldn't parse SNative function template: %s", err)) + } + snativeContractTemplate, err = template.New("snativeFuncTemplate"). + Parse(snativeContractTemplateText) + if err != nil { + panic(fmt.Errorf("Couldn't parse SNative contract template: %s", err)) + } +} \ No newline at end of file diff --git a/manager/eris-mint/evm/snative_test.go b/manager/eris-mint/evm/snative_test.go new file mode 100644 index 0000000000000000000000000000000000000000..731a52179ee91ff034ca43492a490f0f57966e57 --- /dev/null +++ b/manager/eris-mint/evm/snative_test.go @@ -0,0 +1,88 @@ +package vm + +import ( + "encoding/hex" + "testing" + + "github.com/stretchr/testify/assert" + "fmt" +) + +/* Compiling the Permissions solidity contract at +https://ethereum.github.io/browser-solidity yields: + +3fbf7da5 add_role(address,bytes32) +bb37737a has_base(address,int256) +e8145855 has_role(address,bytes32) +28fd0194 rm_role(address,bytes32) +9ea53314 set_base(address,int256,bool) +d69186a6 set_global(int256,bool) +180d26f2 unset_base(address,int256) +*/ + +func TestPermissionsContract(t *testing.T) { + registerNativeContracts() + contract := SNativeContracts()["permissions_contract"] + + assertContractFunction(t, contract, "3fbf7da5", + "add_role(address,bytes32)") + + assertContractFunction(t, contract, "bb37737a", + "has_base(address,int256)") + + assertContractFunction(t, contract, "054556ac", + "has_role(address,bytes32)") + + assertContractFunction(t, contract, "ded3350a", + "rm_role(address,bytes32)") + + assertContractFunction(t, contract, "c2174d8f", + "set_base(address,int256,bool)") + + assertContractFunction(t, contract, "85f1522b", + "set_global(int256,bool)") + + assertContractFunction(t, contract, "73448c99", + "unset_base(address,int256)") +} + +func TestSNativeFuncTemplate(t *testing.T) { + contract := SNativeContracts()["permissions_contract"] + function, err := contract.FunctionByName("rm_role") + if err != nil { + t.Fatal("Couldn't get function") + } + solidity, err := function.Solidity() + assert.NoError(t, err) + fmt.Println(solidity) +} + +func TestSNativeContractTemplate(t *testing.T) { + contract := SNativeContracts()["permissions_contract"] + solidity, err := contract.Solidity() + assert.NoError(t, err) + fmt.Println(solidity) +} + +// Helpers + +func assertContractFunction(t *testing.T, contract SNativeContractDescription, + funcIDHex string, expectedSignature string) { + function, err := contract.FunctionByID(fourBytesFromHex(t, funcIDHex)) + assert.NoError(t, err, + "Error retrieving SNativeFunctionDescription with ID %s", funcIDHex) + if err == nil { + assert.Equal(t, expectedSignature, function.Signature()) + } +} + +func fourBytesFromHex(t *testing.T, hexString string) [4]byte { + bs, err := hex.DecodeString(hexString) + assert.NoError(t, err, "Could not decode hex string '%s'", hexString) + if len(bs) != 4 { + t.Fatalf("FuncID must be 4 bytes but '%s' is %v bytes", hexString, + len(bs)) + } + return firstFourBytes(bs) +} + diff --git a/manager/eris-mint/state/permissions_test.go b/manager/eris-mint/state/permissions_test.go index fd4dd421935b6a58b0cdaf592b0f61dbd4849722..a264178de5bb0f807cee132bbd9be86b9d12229a 100644 --- a/manager/eris-mint/state/permissions_test.go +++ b/manager/eris-mint/state/permissions_test.go @@ -2,7 +2,6 @@ package state import ( "bytes" - "encoding/hex" "fmt" "strconv" "testing" @@ -29,6 +28,7 @@ func init() { var ( dbBackend = "memdb" dbDir = "" + permissionsContract = vm.SNativeContracts()["permissions_contract"] ) /* @@ -1182,18 +1182,17 @@ func boolToWord256(v bool) Word256 { return LeftPadWord256([]byte{vint}) } -func permNameToFuncID(s string) []byte { - for k, v := range vm.PermsMap { - if s == v.Name { - b, _ := hex.DecodeString(k) - return b - } +func permNameToFuncID(name string) []byte { + function, err := permissionsContract.FunctionByName(name) + if err != nil { + panic("didn't find snative function signature!") } - panic("didn't find snative function signature!") + id := function.ID() + return id[:] } func snativePermTestInputCALL(name string, user *acm.PrivAccount, perm ptypes.PermFlag, val bool) (addr []byte, pF ptypes.PermFlag, data []byte) { - addr = LeftPadWord256([]byte(vm.PermissionsContract)).Postfix(20) + addr = permissionsContract.Address().Postfix(20) switch name { case "has_base", "unset_base": data = LeftPadBytes(user.Address, 32) @@ -1229,7 +1228,7 @@ func snativePermTestInputTx(name string, user *acm.PrivAccount, perm ptypes.Perm } func snativeRoleTestInputCALL(name string, user *acm.PrivAccount, role string) (addr []byte, pF ptypes.PermFlag, data []byte) { - addr = LeftPadWord256([]byte(vm.PermissionsContract)).Postfix(20) + addr = permissionsContract.Address().Postfix(20) data = LeftPadBytes(user.Address, 32) data = append(data, RightPadBytes([]byte(role), 32)...) data = append(permNameToFuncID(name), data...)