func TestNameTxs(t *testing.T) { state, privAccounts, _ := RandGenesisState(3, true, 1000, 1, true, 1000) types.MinNameRegistrationPeriod = 5 startingBlock := state.LastBlockHeight // try some bad names. these should all fail names := []string{"", "\n", "123#$%", "\x00", string([]byte{20, 40, 60, 80}), "baffledbythespectacleinallofthisyouseeehesaidwithouteyes", "no spaces please"} data := "something about all this just doesn't feel right." fee := int64(1000) numDesiredBlocks := 5 for _, name := range names { amt := fee + int64(numDesiredBlocks)*types.NameCostPerByte*types.NameCostPerBlock*types.BaseEntryCost(name, data) tx, _ := types.NewNameTx(state, privAccounts[0].PubKey, name, data, amt, fee) tx.Sign(state.ChainID, privAccounts[0]) if err := execTxWithState(state, tx, true); err == nil { t.Fatalf("Expected invalid name error from %s", name) } } // try some bad data. these should all fail name := "hold_it_chum" datas := []string{"cold&warm", "!@#$%^&*()", "<<<>>>>", "because why would you ever need a ~ or a & or even a % in a json file? make your case and we'll talk"} for _, data := range datas { amt := fee + int64(numDesiredBlocks)*types.NameCostPerByte*types.NameCostPerBlock*types.BaseEntryCost(name, data) tx, _ := types.NewNameTx(state, privAccounts[0].PubKey, name, data, amt, fee) tx.Sign(state.ChainID, privAccounts[0]) if err := execTxWithState(state, tx, true); err == nil { t.Fatalf("Expected invalid data error from %s", data) } } validateEntry := func(t *testing.T, entry *types.NameRegEntry, name, data string, addr []byte, expires int) { if entry == nil { t.Fatalf("Could not find name %s", name) } if bytes.Compare(entry.Owner, addr) != 0 { t.Fatalf("Wrong owner. Got %X expected %X", entry.Owner, addr) } if data != entry.Data { t.Fatalf("Wrong data. Got %s expected %s", entry.Data, data) } if name != entry.Name { t.Fatalf("Wrong name. Got %s expected %s", entry.Name, name) } if expires != entry.Expires { t.Fatalf("Wrong expiry. Got %d, expected %d", entry.Expires, expires) } } // try a good one, check data, owner, expiry name = "looking_good/karaoke_bar" data = "on this side of neptune there are 1234567890 people: first is OMNIVORE. Or is it. Ok this is pretty restrictive. No exclamations :(. Faces tho :')" amt := fee + int64(numDesiredBlocks)*types.NameCostPerByte*types.NameCostPerBlock*types.BaseEntryCost(name, data) tx, _ := types.NewNameTx(state, privAccounts[0].PubKey, name, data, amt, fee) tx.Sign(state.ChainID, privAccounts[0]) if err := execTxWithState(state, tx, true); err != nil { t.Fatal(err) } entry := state.GetNameRegEntry(name) validateEntry(t, entry, name, data, privAccounts[0].Address, startingBlock+numDesiredBlocks) // fail to update it as non-owner, in same block tx, _ = types.NewNameTx(state, privAccounts[1].PubKey, name, data, amt, fee) tx.Sign(state.ChainID, privAccounts[1]) if err := execTxWithState(state, tx, true); err == nil { t.Fatal("Expected error") } // update it as owner, just to increase expiry, in same block // NOTE: we have to resend the data or it will clear it (is this what we want?) tx, _ = types.NewNameTx(state, privAccounts[0].PubKey, name, data, amt, fee) tx.Sign(state.ChainID, privAccounts[0]) if err := execTxWithStateNewBlock(state, tx, true); err != nil { t.Fatal(err) } entry = state.GetNameRegEntry(name) validateEntry(t, entry, name, data, privAccounts[0].Address, startingBlock+numDesiredBlocks*2) // update it as owner, just to increase expiry, in next block tx, _ = types.NewNameTx(state, privAccounts[0].PubKey, name, data, amt, fee) tx.Sign(state.ChainID, privAccounts[0]) if err := execTxWithStateNewBlock(state, tx, true); err != nil { t.Fatal(err) } entry = state.GetNameRegEntry(name) validateEntry(t, entry, name, data, privAccounts[0].Address, startingBlock+numDesiredBlocks*3) // fail to update it as non-owner state.LastBlockHeight = entry.Expires - 1 tx, _ = types.NewNameTx(state, privAccounts[1].PubKey, name, data, amt, fee) tx.Sign(state.ChainID, privAccounts[1]) if err := execTxWithState(state, tx, true); err == nil { t.Fatal("Expected error") } // once expires, non-owner succeeds state.LastBlockHeight = entry.Expires tx, _ = types.NewNameTx(state, privAccounts[1].PubKey, name, data, amt, fee) tx.Sign(state.ChainID, privAccounts[1]) if err := execTxWithState(state, tx, true); err != nil { t.Fatal(err) } entry = state.GetNameRegEntry(name) validateEntry(t, entry, name, data, privAccounts[1].Address, state.LastBlockHeight+numDesiredBlocks) // update it as new owner, with new data (longer), but keep the expiry! data = "In the beginning there was no thing, not even the beginning. It hadn't been here, no there, nor for that matter anywhere, not especially because it had not to even exist, let alone to not. Nothing especially odd about that." oldCredit := amt - fee numDesiredBlocks = 10 amt = fee + (int64(numDesiredBlocks)*types.NameCostPerByte*types.NameCostPerBlock*types.BaseEntryCost(name, data) - oldCredit) tx, _ = types.NewNameTx(state, privAccounts[1].PubKey, name, data, amt, fee) tx.Sign(state.ChainID, privAccounts[1]) if err := execTxWithState(state, tx, true); err != nil { t.Fatal(err) } entry = state.GetNameRegEntry(name) validateEntry(t, entry, name, data, privAccounts[1].Address, state.LastBlockHeight+numDesiredBlocks) // test removal amt = fee data = "" tx, _ = types.NewNameTx(state, privAccounts[1].PubKey, name, data, amt, fee) tx.Sign(state.ChainID, privAccounts[1]) if err := execTxWithStateNewBlock(state, tx, true); err != nil { t.Fatal(err) } entry = state.GetNameRegEntry(name) if entry != nil { t.Fatal("Expected removed entry to be nil") } // create entry by key0, // test removal by key1 after expiry name = "looking_good/karaoke_bar" data = "some data" amt = fee + int64(numDesiredBlocks)*types.NameCostPerByte*types.NameCostPerBlock*types.BaseEntryCost(name, data) tx, _ = types.NewNameTx(state, privAccounts[0].PubKey, name, data, amt, fee) tx.Sign(state.ChainID, privAccounts[0]) if err := execTxWithState(state, tx, true); err != nil { t.Fatal(err) } entry = state.GetNameRegEntry(name) validateEntry(t, entry, name, data, privAccounts[0].Address, state.LastBlockHeight+numDesiredBlocks) state.LastBlockHeight = entry.Expires amt = fee data = "" tx, _ = types.NewNameTx(state, privAccounts[1].PubKey, name, data, amt, fee) tx.Sign(state.ChainID, privAccounts[1]) if err := execTxWithStateNewBlock(state, tx, true); err != nil { t.Fatal(err) } entry = state.GetNameRegEntry(name) if entry != nil { t.Fatal("Expected removed entry to be nil") } }
// If the tx is invalid, an error will be returned. // Unlike ExecBlock(), state will not be altered. func ExecTx(blockCache *BlockCache, tx types.Tx, runCall bool, evc events.Fireable) (err error) { // TODO: do something with fees fees := int64(0) _s := blockCache.State() // hack to access validators and block height // Exec tx switch tx := tx.(type) { case *types.SendTx: accounts, err := getInputs(blockCache, tx.Inputs) if err != nil { return err } // ensure all inputs have send permissions if !hasSendPermission(blockCache, accounts) { return fmt.Errorf("At least one input lacks permission for SendTx") } // add outputs to accounts map // if any outputs don't exist, all inputs must have CreateAccount perm accounts, err = getOrMakeOutputs(blockCache, accounts, tx.Outputs) if err != nil { return err } signBytes := account.SignBytes(_s.ChainID, tx) inTotal, err := validateInputs(accounts, signBytes, tx.Inputs) if err != nil { return err } outTotal, err := validateOutputs(tx.Outputs) if err != nil { return err } if outTotal > inTotal { return types.ErrTxInsufficientFunds } fee := inTotal - outTotal fees += fee // Good! Adjust accounts adjustByInputs(accounts, tx.Inputs) adjustByOutputs(accounts, tx.Outputs) for _, acc := range accounts { blockCache.UpdateAccount(acc) } // if the evc is nil, nothing will happen if evc != nil { for _, i := range tx.Inputs { evc.FireEvent(types.EventStringAccInput(i.Address), tx) } for _, o := range tx.Outputs { evc.FireEvent(types.EventStringAccOutput(o.Address), tx) } } return nil case *types.CallTx: var inAcc, outAcc *account.Account // Validate input inAcc = blockCache.GetAccount(tx.Input.Address) if inAcc == nil { log.Debug(Fmt("Can't find in account %X", tx.Input.Address)) return types.ErrTxInvalidAddress } createAccount := len(tx.Address) == 0 if createAccount { if !hasCreateContractPermission(blockCache, inAcc) { return fmt.Errorf("Account %X does not have Create permission", tx.Input.Address) } } else { if !hasCallPermission(blockCache, inAcc) { return fmt.Errorf("Account %X does not have Call permission", tx.Input.Address) } } // pubKey should be present in either "inAcc" or "tx.Input" if err := checkInputPubKey(inAcc, tx.Input); err != nil { log.Debug(Fmt("Can't find pubkey for %X", tx.Input.Address)) return err } signBytes := account.SignBytes(_s.ChainID, tx) err := validateInput(inAcc, signBytes, tx.Input) if err != nil { log.Debug(Fmt("validateInput failed on %X: %v", tx.Input.Address, err)) return err } if tx.Input.Amount < tx.Fee { log.Debug(Fmt("Sender did not send enough to cover the fee %X", tx.Input.Address)) return types.ErrTxInsufficientFunds } if !createAccount { // Validate output if len(tx.Address) != 20 { log.Debug(Fmt("Destination address is not 20 bytes %X", tx.Address)) return types.ErrTxInvalidAddress } // this may be nil if we are still in mempool and contract was created in same block as this tx // but that's fine, because the account will be created properly when the create tx runs in the block // and then this won't return nil. otherwise, we take their fee // it may also be nil if its an snative (not a "real" account) outAcc = blockCache.GetAccount(tx.Address) } log.Debug(Fmt("Out account: %v", outAcc)) // Good! value := tx.Input.Amount - tx.Fee inAcc.Sequence += 1 if runCall { var ( gas int64 = tx.GasLimit err error = nil caller *vm.Account = toVMAccount(inAcc) callee *vm.Account = nil code []byte = nil txCache = NewTxCache(blockCache) params = vm.Params{ BlockHeight: int64(_s.LastBlockHeight), BlockHash: LeftPadWord256(_s.LastBlockHash), BlockTime: _s.LastBlockTime.Unix(), GasLimit: 10000000, } ) // get or create callee if !createAccount { if outAcc == nil || len(outAcc.Code) == 0 { // check if its an snative if _, ok := vm.RegisteredSNativeContracts[LeftPadWord256(tx.Address)]; ok { // set the outAcc (simply a placeholder until we reach the call) outAcc = &account.Account{Address: tx.Address} } else { // if you call an account that doesn't exist // or an account with no code then we take fees (sorry pal) // NOTE: it's fine to create a contract and call it within one // block (nonce will prevent re-ordering of those txs) // but to create with one account and call with another // you have to wait a block to avoid a re-ordering attack // that will take your fees inAcc.Balance -= tx.Fee blockCache.UpdateAccount(inAcc) if outAcc == nil { log.Debug(Fmt("Cannot find destination address %X. Deducting fee from caller", tx.Address)) } else { log.Debug(Fmt("Attempting to call an account (%X) with no code. Deducting fee from caller", tx.Address)) } return types.ErrTxInvalidAddress } } callee = toVMAccount(outAcc) code = callee.Code log.Debug(Fmt("Calling contract %X with code %X", callee.Address, callee.Code)) } else { callee = txCache.CreateAccount(caller) log.Debug(Fmt("Created new account %X", callee.Address)) code = tx.Data } log.Debug(Fmt("Code for this contract: %X", code)) txCache.UpdateAccount(caller) // because we bumped nonce txCache.UpdateAccount(callee) // so the txCache knows about the callee and the create and/or transfer takes effect vmach := vm.NewVM(txCache, params, caller.Address, types.TxID(_s.ChainID, tx)) vmach.SetFireable(evc) // NOTE: Call() transfers the value from caller to callee iff call succeeds. ret, err := vmach.Call(caller, callee, code, tx.Data, value, &gas) exception := "" if err != nil { exception = err.Error() // Failure. Charge the gas fee. The 'value' was otherwise not transferred. log.Debug(Fmt("Error on execution: %v", err)) inAcc.Balance -= tx.Fee blockCache.UpdateAccount(inAcc) // Throw away 'txCache' which holds incomplete updates (don't sync it). } else { log.Debug("Successful execution") // Success if createAccount { callee.Code = ret } txCache.Sync() } // Create a receipt from the ret and whether errored. log.Info("VM call complete", "caller", caller, "callee", callee, "return", ret, "err", err) // Fire Events for sender and receiver // a separate event will be fired from vm for each additional call if evc != nil { evc.FireEvent(types.EventStringAccInput(tx.Input.Address), types.EventMsgCallTx{tx, ret, exception}) evc.FireEvent(types.EventStringAccOutput(tx.Address), types.EventMsgCallTx{tx, ret, exception}) } } else { // The mempool does not call txs until // the proposer determines the order of txs. // So mempool will skip the actual .Call(), // and only deduct from the caller's balance. inAcc.Balance -= value if createAccount { inAcc.Sequence += 1 } blockCache.UpdateAccount(inAcc) } return nil case *types.NameTx: var inAcc *account.Account // Validate input inAcc = blockCache.GetAccount(tx.Input.Address) if inAcc == nil { log.Debug(Fmt("Can't find in account %X", tx.Input.Address)) return types.ErrTxInvalidAddress } // check permission if !hasNamePermission(blockCache, inAcc) { return fmt.Errorf("Account %X does not have Name permission", tx.Input.Address) } // pubKey should be present in either "inAcc" or "tx.Input" if err := checkInputPubKey(inAcc, tx.Input); err != nil { log.Debug(Fmt("Can't find pubkey for %X", tx.Input.Address)) return err } signBytes := account.SignBytes(_s.ChainID, tx) err := validateInput(inAcc, signBytes, tx.Input) if err != nil { log.Debug(Fmt("validateInput failed on %X: %v", tx.Input.Address, err)) return err } // fee is in addition to the amount which is used to determine the TTL if tx.Input.Amount < tx.Fee { log.Debug(Fmt("Sender did not send enough to cover the fee %X", tx.Input.Address)) return types.ErrTxInsufficientFunds } // validate the input strings if err := tx.ValidateStrings(); err != nil { log.Debug(err.Error()) return types.ErrTxInvalidString } value := tx.Input.Amount - tx.Fee // let's say cost of a name for one block is len(data) + 32 costPerBlock := types.NameCostPerBlock * types.NameCostPerByte * tx.BaseEntryCost() expiresIn := int(value / costPerBlock) lastBlockHeight := _s.LastBlockHeight log.Debug("New NameTx", "value", value, "costPerBlock", costPerBlock, "expiresIn", expiresIn, "lastBlock", lastBlockHeight) // check if the name exists entry := blockCache.GetNameRegEntry(tx.Name) if entry != nil { var expired bool // if the entry already exists, and hasn't expired, we must be owner if entry.Expires > lastBlockHeight { // ensure we are owner if bytes.Compare(entry.Owner, tx.Input.Address) != 0 { log.Debug(Fmt("Sender %X is trying to update a name (%s) for which he is not owner", tx.Input.Address, tx.Name)) return types.ErrIncorrectOwner } } else { expired = true } // no value and empty data means delete the entry if value == 0 && len(tx.Data) == 0 { // maybe we reward you for telling us we can delete this crap // (owners if not expired, anyone if expired) log.Debug("Removing namereg entry", "name", entry.Name) blockCache.RemoveNameRegEntry(entry.Name) } else { // update the entry by bumping the expiry // and changing the data if expired { if expiresIn < types.MinNameRegistrationPeriod { return errors.New(Fmt("Names must be registered for at least %d blocks", types.MinNameRegistrationPeriod)) } entry.Expires = lastBlockHeight + expiresIn entry.Owner = tx.Input.Address log.Debug("An old namereg entry has expired and been reclaimed", "name", entry.Name, "expiresIn", expiresIn, "owner", entry.Owner) } else { // since the size of the data may have changed // we use the total amount of "credit" oldCredit := int64(entry.Expires-lastBlockHeight) * types.BaseEntryCost(entry.Name, entry.Data) credit := oldCredit + value expiresIn = int(credit / costPerBlock) if expiresIn < types.MinNameRegistrationPeriod { return errors.New(Fmt("Names must be registered for at least %d blocks", types.MinNameRegistrationPeriod)) } entry.Expires = lastBlockHeight + expiresIn log.Debug("Updated namereg entry", "name", entry.Name, "expiresIn", expiresIn, "oldCredit", oldCredit, "value", value, "credit", credit) } entry.Data = tx.Data blockCache.UpdateNameRegEntry(entry) } } else { if expiresIn < types.MinNameRegistrationPeriod { return errors.New(Fmt("Names must be registered for at least %d blocks", types.MinNameRegistrationPeriod)) } // entry does not exist, so create it entry = &types.NameRegEntry{ Name: tx.Name, Owner: tx.Input.Address, Data: tx.Data, Expires: lastBlockHeight + expiresIn, } log.Debug("Creating namereg entry", "name", entry.Name, "expiresIn", expiresIn) blockCache.UpdateNameRegEntry(entry) } // TODO: something with the value sent? // Good! inAcc.Sequence += 1 inAcc.Balance -= value blockCache.UpdateAccount(inAcc) // TODO: maybe we want to take funds on error and allow txs in that don't do anythingi? return nil case *types.BondTx: valInfo := blockCache.State().GetValidatorInfo(tx.PubKey.Address()) if valInfo != nil { // TODO: In the future, check that the validator wasn't destroyed, // add funds, merge UnbondTo outputs, and unbond validator. return errors.New("Adding coins to existing validators not yet supported") } accounts, err := getInputs(blockCache, tx.Inputs) if err != nil { return err } // add outputs to accounts map // if any outputs don't exist, all inputs must have CreateAccount perm // though outputs aren't created until unbonding/release time canCreate := hasCreateAccountPermission(blockCache, accounts) for _, out := range tx.UnbondTo { acc := blockCache.GetAccount(out.Address) if acc == nil && !canCreate { return fmt.Errorf("At least one input does not have permission to create accounts") } } bondAcc := blockCache.GetAccount(tx.PubKey.Address()) if !hasBondPermission(blockCache, bondAcc) { return fmt.Errorf("The bonder does not have permission to bond") } if !hasBondOrSendPermission(blockCache, accounts) { return fmt.Errorf("At least one input lacks permission to bond") } signBytes := account.SignBytes(_s.ChainID, tx) inTotal, err := validateInputs(accounts, signBytes, tx.Inputs) if err != nil { return err } if err := tx.PubKey.ValidateBasic(); err != nil { return err } if !tx.PubKey.VerifyBytes(signBytes, tx.Signature) { return types.ErrTxInvalidSignature } outTotal, err := validateOutputs(tx.UnbondTo) if err != nil { return err } if outTotal > inTotal { return types.ErrTxInsufficientFunds } fee := inTotal - outTotal fees += fee // Good! Adjust accounts adjustByInputs(accounts, tx.Inputs) for _, acc := range accounts { blockCache.UpdateAccount(acc) } // Add ValidatorInfo _s.SetValidatorInfo(&ValidatorInfo{ Address: tx.PubKey.Address(), PubKey: tx.PubKey, UnbondTo: tx.UnbondTo, FirstBondHeight: _s.LastBlockHeight + 1, FirstBondAmount: outTotal, }) // Add Validator added := _s.BondedValidators.Add(&Validator{ Address: tx.PubKey.Address(), PubKey: tx.PubKey, BondHeight: _s.LastBlockHeight + 1, VotingPower: outTotal, Accum: 0, }) if !added { // SOMETHING HAS GONE HORRIBLY WRONG panic("Failed to add validator") } if evc != nil { evc.FireEvent(types.EventStringBond(), tx) } return nil case *types.UnbondTx: // The validator must be active _, val := _s.BondedValidators.GetByAddress(tx.Address) if val == nil { return types.ErrTxInvalidAddress } // Verify the signature signBytes := account.SignBytes(_s.ChainID, tx) if !val.PubKey.VerifyBytes(signBytes, tx.Signature) { return types.ErrTxInvalidSignature } // tx.Height must be greater than val.LastCommitHeight if tx.Height <= val.LastCommitHeight { return errors.New("Invalid unbond height") } // Good! _s.unbondValidator(val) if evc != nil { evc.FireEvent(types.EventStringUnbond(), tx) } return nil case *types.RebondTx: // The validator must be inactive _, val := _s.UnbondingValidators.GetByAddress(tx.Address) if val == nil { return types.ErrTxInvalidAddress } // Verify the signature signBytes := account.SignBytes(_s.ChainID, tx) if !val.PubKey.VerifyBytes(signBytes, tx.Signature) { return types.ErrTxInvalidSignature } // tx.Height must be in a suitable range minRebondHeight := _s.LastBlockHeight - (validatorTimeoutBlocks / 2) maxRebondHeight := _s.LastBlockHeight + 2 if !((minRebondHeight <= tx.Height) && (tx.Height <= maxRebondHeight)) { return errors.New(Fmt("Rebond height not in range. Expected %v <= %v <= %v", minRebondHeight, tx.Height, maxRebondHeight)) } // Good! _s.rebondValidator(val) if evc != nil { evc.FireEvent(types.EventStringRebond(), tx) } return nil case *types.DupeoutTx: // Verify the signatures _, accused := _s.BondedValidators.GetByAddress(tx.Address) if accused == nil { _, accused = _s.UnbondingValidators.GetByAddress(tx.Address) if accused == nil { return types.ErrTxInvalidAddress } } voteASignBytes := account.SignBytes(_s.ChainID, &tx.VoteA) voteBSignBytes := account.SignBytes(_s.ChainID, &tx.VoteB) if !accused.PubKey.VerifyBytes(voteASignBytes, tx.VoteA.Signature) || !accused.PubKey.VerifyBytes(voteBSignBytes, tx.VoteB.Signature) { return types.ErrTxInvalidSignature } // Verify equivocation // TODO: in the future, just require one vote from a previous height that // doesn't exist on this chain. if tx.VoteA.Height != tx.VoteB.Height { return errors.New("DupeoutTx heights don't match") } if tx.VoteA.Round != tx.VoteB.Round { return errors.New("DupeoutTx rounds don't match") } if tx.VoteA.Type != tx.VoteB.Type { return errors.New("DupeoutTx types don't match") } if bytes.Equal(tx.VoteA.BlockHash, tx.VoteB.BlockHash) { return errors.New("DupeoutTx blockhashes shouldn't match") } // Good! (Bad validator!) _s.destroyValidator(accused) if evc != nil { evc.FireEvent(types.EventStringDupeout(), tx) } return nil default: // SANITY CHECK (binary decoding should catch bad tx types // before they get here panic("Unknown Tx type") } }