Esempio n. 1
0
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")
	}
}
Esempio n. 2
0
// 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")
	}
}