Changed the way transactions are being added to the transaction pool
This commit is contained in:
parent
a3559c5e1b
commit
6dc46d3341
|
@ -314,7 +314,30 @@ out:
|
|||
// sm.eth.EventMux().Post(NewBlockEvent{block})
|
||||
// otherwise process and don't emit anything
|
||||
if len(blocks) > 0 {
|
||||
chainManager := self.eth.ChainManager()
|
||||
chainman := self.eth.ChainManager()
|
||||
|
||||
err := chainman.InsertChain(blocks)
|
||||
if err != nil {
|
||||
poollogger.Debugln(err)
|
||||
|
||||
self.Reset()
|
||||
|
||||
if self.peer != nil && self.peer.conn != nil {
|
||||
poollogger.Debugf("Punishing peer for supplying bad chain (%v)\n", self.peer.conn.RemoteAddr())
|
||||
}
|
||||
|
||||
// This peer gave us bad hashes and made us fetch a bad chain, therefor he shall be punished.
|
||||
self.eth.BlacklistPeer(self.peer)
|
||||
self.peer.StopWithReason(DiscBadPeer)
|
||||
self.td = ethutil.Big0
|
||||
self.peer = nil
|
||||
|
||||
for _, block := range blocks {
|
||||
self.Remove(block.Hash())
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
// Test and import
|
||||
bchain := chain.NewChain(blocks)
|
||||
_, err := chainManager.TestChain(bchain)
|
||||
|
@ -338,6 +361,7 @@ out:
|
|||
self.Remove(block.Hash())
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -246,7 +246,7 @@ func (sm *BlockManager) ProcessWithParent(block, parent *Block) (td *big.Int, me
|
|||
return
|
||||
}
|
||||
|
||||
state.Update(nil)
|
||||
state.Update(ethutil.Big0)
|
||||
|
||||
if !block.State().Cmp(state) {
|
||||
err = fmt.Errorf("invalid merkle root. received=%x got=%x", block.Root(), state.Root())
|
||||
|
|
|
@ -321,6 +321,24 @@ func NewChain(blocks Blocks) *BlockChain {
|
|||
return chain
|
||||
}
|
||||
|
||||
// This function assumes you've done your checking. No checking is done at this stage anymore
|
||||
func (self *ChainManager) InsertChain(chain Blocks) error {
|
||||
for _, block := range chain {
|
||||
td, messages, err := self.Ethereum.BlockManager().Process(block)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
self.add(block)
|
||||
self.SetTotalDifficulty(td)
|
||||
self.Ethereum.EventMux().Post(NewBlockEvent{block})
|
||||
self.Ethereum.EventMux().Post(messages)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
// This function assumes you've done your checking. No checking is done at this stage anymore
|
||||
func (self *ChainManager) InsertChain(chain *BlockChain) {
|
||||
for e := chain.Front(); e != nil; e = e.Next() {
|
||||
|
@ -338,7 +356,9 @@ func (self *ChainManager) InsertChain(chain *BlockChain) {
|
|||
chainlogger.Infof("Imported %d blocks. #%v (%x) / %#v (%x)", chain.Len(), front.Number, front.Hash()[0:4], back.Number, back.Hash()[0:4])
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
func (self *ChainManager) TestChain(chain *BlockChain) (td *big.Int, err error) {
|
||||
self.workingChain = chain
|
||||
defer func() { self.workingChain = nil }()
|
||||
|
@ -381,3 +401,4 @@ func (self *ChainManager) TestChain(chain *BlockChain) (td *big.Int, err error)
|
|||
|
||||
return
|
||||
}
|
||||
*/
|
||||
|
|
|
@ -79,12 +79,7 @@ func (tx *Transaction) IsContract() bool {
|
|||
|
||||
func (tx *Transaction) CreationAddress(state *state.State) []byte {
|
||||
// Generate a new address
|
||||
addr := crypto.Sha3(ethutil.NewValue([]interface{}{tx.Sender(), tx.Nonce}).Encode())[12:]
|
||||
//for i := uint64(0); state.GetStateObject(addr) != nil; i++ {
|
||||
// addr = crypto.Sha3(ethutil.NewValue([]interface{}{tx.Sender(), tx.Nonce + i}).Encode())[12:]
|
||||
//}
|
||||
|
||||
return addr
|
||||
return crypto.Sha3(ethutil.NewValue([]interface{}{tx.Sender(), tx.Nonce}).Encode())[12:]
|
||||
}
|
||||
|
||||
func (tx *Transaction) Signature(key []byte) []byte {
|
||||
|
|
|
@ -114,7 +114,6 @@ func (pool *TxPool) ValidateTransaction(tx *Transaction) error {
|
|||
}
|
||||
|
||||
// Get the sender
|
||||
//sender := pool.Ethereum.BlockManager().procState.GetAccount(tx.Sender())
|
||||
sender := pool.Ethereum.BlockManager().CurrentState().GetAccount(tx.Sender())
|
||||
|
||||
totAmount := new(big.Int).Set(tx.Value)
|
||||
|
@ -136,6 +135,34 @@ func (pool *TxPool) ValidateTransaction(tx *Transaction) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (self *TxPool) Add(tx *Transaction) error {
|
||||
hash := tx.Hash()
|
||||
foundTx := FindTx(self.pool, func(tx *Transaction, e *list.Element) bool {
|
||||
return bytes.Compare(tx.Hash(), hash) == 0
|
||||
})
|
||||
|
||||
if foundTx != nil {
|
||||
return fmt.Errorf("Known transaction (%x)", hash[0:4])
|
||||
}
|
||||
|
||||
err := self.ValidateTransaction(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
self.addTransaction(tx)
|
||||
|
||||
tmp := make([]byte, 4)
|
||||
copy(tmp, tx.Recipient)
|
||||
|
||||
txplogger.Debugf("(t) %x => %x (%v) %x\n", tx.Sender()[:4], tmp, tx.Value, tx.Hash())
|
||||
|
||||
// Notify the subscribers
|
||||
self.Ethereum.EventMux().Post(TxPreEvent{tx})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pool *TxPool) queueHandler() {
|
||||
out:
|
||||
for {
|
||||
|
@ -172,9 +199,11 @@ out:
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
func (pool *TxPool) QueueTransaction(tx *Transaction) {
|
||||
pool.queueChan <- tx
|
||||
}
|
||||
*/
|
||||
|
||||
func (pool *TxPool) CurrentTransactions() []*Transaction {
|
||||
pool.mutex.Lock()
|
||||
|
|
|
@ -26,7 +26,6 @@ import (
|
|||
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||
"github.com/ethereum/go-ethereum/ethutil"
|
||||
"github.com/ethereum/go-ethereum/logger"
|
||||
"github.com/ethereum/go-ethereum/xeth"
|
||||
)
|
||||
|
||||
type plugin struct {
|
||||
|
@ -46,12 +45,12 @@ func (gui *Gui) LogPrint(level logger.LogLevel, msg string) {
|
|||
}
|
||||
*/
|
||||
}
|
||||
func (gui *Gui) Transact(recipient, value, gas, gasPrice, d string) (*xeth.JSReceipt, error) {
|
||||
func (gui *Gui) Transact(recipient, value, gas, gasPrice, d string) (string, error) {
|
||||
var data string
|
||||
if len(recipient) == 0 {
|
||||
code, err := ethutil.Compile(d, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return "", err
|
||||
}
|
||||
data = ethutil.Bytes2Hex(code)
|
||||
} else {
|
||||
|
|
|
@ -127,7 +127,11 @@ func (self *UiLib) PastPeers() *ethutil.List {
|
|||
|
||||
func (self *UiLib) ImportTx(rlpTx string) {
|
||||
tx := chain.NewTransactionFromBytes(ethutil.Hex2Bytes(rlpTx))
|
||||
self.eth.TxPool().QueueTransaction(tx)
|
||||
//self.eth.TxPool().QueueTransaction(tx)
|
||||
err := self.eth.TxPool().Add(tx)
|
||||
if err != nil {
|
||||
guilogger.Infoln("import tx failed ", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (self *UiLib) EvalJavascriptFile(path string) {
|
||||
|
@ -305,7 +309,7 @@ func mapToTxParams(object map[string]interface{}) map[string]string {
|
|||
return conv
|
||||
}
|
||||
|
||||
func (self *UiLib) Transact(params map[string]interface{}) (*xeth.JSReceipt, error) {
|
||||
func (self *UiLib) Transact(params map[string]interface{}) (string, error) {
|
||||
object := mapToTxParams(params)
|
||||
|
||||
return self.JSXEth.Transact(
|
||||
|
|
|
@ -62,6 +62,16 @@ func S256(x *big.Int) *big.Int {
|
|||
}
|
||||
}
|
||||
|
||||
func FirstBitSet(v *big.Int) *big.Int {
|
||||
for i := 0; i < v.BitLen(); i++ {
|
||||
if v.Bit(i) > 0 {
|
||||
return big.NewInt(int64(i))
|
||||
}
|
||||
}
|
||||
|
||||
return big.NewInt(int64(v.BitLen()))
|
||||
}
|
||||
|
||||
// Big to bytes
|
||||
//
|
||||
// Returns the bytes of a big integer with the size specified by **base**
|
||||
|
|
|
@ -203,7 +203,7 @@ func (self *Miner) mine() {
|
|||
// Accumulate the rewards included for this block
|
||||
blockManager.AccumelateRewards(block.State(), block, parent)
|
||||
|
||||
block.State().Update(nil)
|
||||
block.State().Update(ethutil.Big0)
|
||||
|
||||
minerlogger.Infof("Mining on block. Includes %v transactions", len(transactions))
|
||||
|
||||
|
@ -211,12 +211,13 @@ func (self *Miner) mine() {
|
|||
nonce := self.pow.Search(block, self.powQuitCh)
|
||||
if nonce != nil {
|
||||
block.Nonce = nonce
|
||||
lchain := chain.NewChain(chain.Blocks{block})
|
||||
_, err := chainMan.TestChain(lchain)
|
||||
//lchain := chain.NewChain(chain.Blocks{block})
|
||||
//_, err := chainMan.TestChain(lchain)
|
||||
err := chainMan.InsertChain(chain.Blocks{block})
|
||||
if err != nil {
|
||||
minerlogger.Infoln(err)
|
||||
} else {
|
||||
chainMan.InsertChain(lchain)
|
||||
//chainMan.InsertChain(lchain)
|
||||
//self.eth.EventMux().Post(chain.NewBlockEvent{block})
|
||||
self.eth.Broadcast(wire.MsgBlockTy, []interface{}{block.Value().Val})
|
||||
|
||||
|
|
8
peer.go
8
peer.go
|
@ -430,7 +430,13 @@ func (p *Peer) HandleInbound() {
|
|||
// processing when a new block is found
|
||||
for i := 0; i < msg.Data.Len(); i++ {
|
||||
tx := chain.NewTransactionFromValue(msg.Data.Get(i))
|
||||
p.ethereum.TxPool().QueueTransaction(tx)
|
||||
err := p.ethereum.TxPool().Add(tx)
|
||||
if err != nil {
|
||||
peerlogger.Infoln(err)
|
||||
} else {
|
||||
peerlogger.Infof("tx OK (%x)\n", tx.Hash()[0:4])
|
||||
}
|
||||
//p.ethereum.TxPool().QueueTransaction(tx)
|
||||
}
|
||||
case wire.MsgGetPeersTy:
|
||||
// Peer asked for list of connected peers
|
||||
|
|
|
@ -249,7 +249,6 @@ func (s *State) Reset() {
|
|||
continue
|
||||
}
|
||||
|
||||
//stateObject.state.Reset()
|
||||
stateObject.Reset()
|
||||
}
|
||||
|
||||
|
@ -281,6 +280,7 @@ func (self *State) Update(gasUsed *big.Int) {
|
|||
var deleted bool
|
||||
|
||||
// Refund any gas that's left
|
||||
// XXX THIS WILL CHANGE IN POC8
|
||||
uhalf := new(big.Int).Div(gasUsed, ethutil.Big2)
|
||||
for addr, refs := range self.refund {
|
||||
for _, ref := range refs {
|
||||
|
@ -289,6 +289,7 @@ func (self *State) Update(gasUsed *big.Int) {
|
|||
self.GetStateObject([]byte(addr)).AddBalance(refund.Mul(refund, ref.price))
|
||||
}
|
||||
}
|
||||
self.refund = make(map[string][]refund)
|
||||
|
||||
for _, stateObject := range self.stateObjects {
|
||||
if stateObject.remove {
|
||||
|
|
|
@ -214,7 +214,7 @@ func (c *StateObject) ConvertGas(gas, price *big.Int) error {
|
|||
func (self *StateObject) SetGasPool(gasLimit *big.Int) {
|
||||
self.gasPool = new(big.Int).Set(gasLimit)
|
||||
|
||||
statelogger.DebugDetailf("%x: fuel (+ %v)", self.Address(), self.gasPool)
|
||||
statelogger.Debugf("%x: gas (+ %v)", self.Address(), self.gasPool)
|
||||
}
|
||||
|
||||
func (self *StateObject) BuyGas(gas, price *big.Int) error {
|
||||
|
|
|
@ -151,7 +151,7 @@ func (self *DebugVm) RunClosure(closure *Closure) (ret []byte, err error) {
|
|||
// Stack checks only
|
||||
case ISZERO, CALLDATALOAD, POP, JUMP, NOT: // 1
|
||||
require(1)
|
||||
case ADD, SUB, DIV, SDIV, MOD, SMOD, EXP, LT, GT, SLT, SGT, EQ, AND, OR, XOR, BYTE: // 2
|
||||
case ADD, SUB, DIV, SDIV, MOD, SMOD, LT, GT, SLT, SGT, EQ, AND, OR, XOR, BYTE: // 2
|
||||
require(2)
|
||||
case ADDMOD, MULMOD: // 3
|
||||
require(3)
|
||||
|
@ -169,6 +169,15 @@ func (self *DebugVm) RunClosure(closure *Closure) (ret []byte, err error) {
|
|||
gas.Set(GasLog)
|
||||
addStepGasUsage(new(big.Int).Mul(big.NewInt(int64(n)), GasLog))
|
||||
addStepGasUsage(new(big.Int).Add(mSize, mStart))
|
||||
case EXP:
|
||||
require(2)
|
||||
|
||||
expGas := ethutil.FirstBitSet(stack.data[stack.Len()-2])
|
||||
expGas.Div(expGas, u256(8))
|
||||
expGas.Sub(u256(32), expGas)
|
||||
expGas.Add(expGas, u256(1))
|
||||
|
||||
gas.Set(expGas)
|
||||
// Gas only
|
||||
case STOP:
|
||||
gas.Set(ethutil.Big0)
|
||||
|
|
|
@ -177,7 +177,50 @@ func (self *JSXEth) FromNumber(str string) string {
|
|||
return ethutil.BigD(ethutil.Hex2Bytes(str)).String()
|
||||
}
|
||||
|
||||
func (self *JSXEth) Transact(key, toStr, valueStr, gasStr, gasPriceStr, codeStr string) (*JSReceipt, error) {
|
||||
func (self *JSXEth) Transact(key, toStr, valueStr, gasStr, gasPriceStr, codeStr string) (string, error) {
|
||||
var (
|
||||
to []byte
|
||||
value = ethutil.NewValue(valueStr)
|
||||
gas = ethutil.NewValue(gasStr)
|
||||
gasPrice = ethutil.NewValue(gasPriceStr)
|
||||
data []byte
|
||||
)
|
||||
|
||||
if ethutil.IsHex(codeStr) {
|
||||
data = ethutil.Hex2Bytes(codeStr[2:])
|
||||
} else {
|
||||
data = ethutil.Hex2Bytes(codeStr)
|
||||
}
|
||||
|
||||
if ethutil.IsHex(toStr) {
|
||||
to = ethutil.Hex2Bytes(toStr[2:])
|
||||
} else {
|
||||
to = ethutil.Hex2Bytes(toStr)
|
||||
}
|
||||
|
||||
var keyPair *crypto.KeyPair
|
||||
var err error
|
||||
if ethutil.IsHex(key) {
|
||||
keyPair, err = crypto.NewKeyPairFromSec([]byte(ethutil.Hex2Bytes(key[2:])))
|
||||
} else {
|
||||
keyPair, err = crypto.NewKeyPairFromSec([]byte(ethutil.Hex2Bytes(key)))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
tx, err := self.XEth.Transact(keyPair, to, value, gas, gasPrice, data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if chain.IsContractAddr(to) {
|
||||
return ethutil.Bytes2Hex(tx.CreationAddress(nil)), nil
|
||||
}
|
||||
|
||||
return ethutil.Bytes2Hex(tx.Hash()), nil
|
||||
|
||||
/*
|
||||
var hash []byte
|
||||
var contractCreation bool
|
||||
if len(toStr) == 0 {
|
||||
|
@ -192,17 +235,6 @@ func (self *JSXEth) Transact(key, toStr, valueStr, gasStr, gasPriceStr, codeStr
|
|||
}
|
||||
}
|
||||
|
||||
var keyPair *crypto.KeyPair
|
||||
var err error
|
||||
if ethutil.IsHex(key) {
|
||||
keyPair, err = crypto.NewKeyPairFromSec([]byte(ethutil.Hex2Bytes(key[2:])))
|
||||
} else {
|
||||
keyPair, err = crypto.NewKeyPairFromSec([]byte(ethutil.Hex2Bytes(key)))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var (
|
||||
value = ethutil.Big(valueStr)
|
||||
|
@ -237,11 +269,15 @@ func (self *JSXEth) Transact(key, toStr, valueStr, gasStr, gasPriceStr, codeStr
|
|||
}
|
||||
|
||||
return NewJSReciept(contractCreation, tx.CreationAddress(self.World().State()), tx.Hash(), keyPair.Address()), nil
|
||||
*/
|
||||
}
|
||||
|
||||
func (self *JSXEth) PushTx(txStr string) (*JSReceipt, error) {
|
||||
tx := chain.NewTransactionFromBytes(ethutil.Hex2Bytes(txStr))
|
||||
self.obj.TxPool().QueueTransaction(tx)
|
||||
err := self.obj.TxPool().Add(tx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewJSReciept(tx.CreatesContract(), tx.CreationAddress(self.World().State()), tx.Hash(), tx.Sender()), nil
|
||||
}
|
||||
|
||||
|
|
68
xeth/pipe.go
68
xeth/pipe.go
|
@ -6,7 +6,6 @@ package xeth
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/chain"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
|
@ -93,7 +92,7 @@ func (self *XEth) Exists(addr []byte) bool {
|
|||
return self.World().Get(addr) != nil
|
||||
}
|
||||
|
||||
func (self *XEth) TransactString(key *crypto.KeyPair, rec string, value, gas, price *ethutil.Value, data []byte) ([]byte, error) {
|
||||
func (self *XEth) TransactString(key *crypto.KeyPair, rec string, value, gas, price *ethutil.Value, data []byte) (*chain.Transaction, error) {
|
||||
// Check if an address is stored by this address
|
||||
var hash []byte
|
||||
addr := self.World().Config().Get("NameReg").StorageString(rec).Bytes()
|
||||
|
@ -108,55 +107,62 @@ func (self *XEth) TransactString(key *crypto.KeyPair, rec string, value, gas, pr
|
|||
return self.Transact(key, hash, value, gas, price, data)
|
||||
}
|
||||
|
||||
func (self *XEth) Transact(key *crypto.KeyPair, rec []byte, value, gas, price *ethutil.Value, data []byte) ([]byte, error) {
|
||||
func (self *XEth) Transact(key *crypto.KeyPair, to []byte, value, gas, price *ethutil.Value, data []byte) (*chain.Transaction, error) {
|
||||
var hash []byte
|
||||
var contractCreation bool
|
||||
if rec == nil {
|
||||
if chain.IsContractAddr(to) {
|
||||
contractCreation = true
|
||||
} else {
|
||||
// Check if an address is stored by this address
|
||||
addr := self.World().Config().Get("NameReg").Storage(to).Bytes()
|
||||
if len(addr) > 0 {
|
||||
hash = addr
|
||||
} else {
|
||||
hash = to
|
||||
}
|
||||
}
|
||||
|
||||
var tx *chain.Transaction
|
||||
// Compile and assemble the given data
|
||||
if contractCreation {
|
||||
script, err := ethutil.Compile(string(data), false)
|
||||
tx = chain.NewContractCreationTx(value.BigInt(), gas.BigInt(), price.BigInt(), data)
|
||||
} else {
|
||||
tx = chain.NewTransactionMessage(hash, value.BigInt(), gas.BigInt(), price.BigInt(), data)
|
||||
}
|
||||
|
||||
state := self.blockManager.TransState()
|
||||
nonce := state.GetNonce(key.Address())
|
||||
|
||||
tx.Nonce = nonce
|
||||
tx.Sign(key.PrivateKey)
|
||||
err := self.obj.TxPool().Add(tx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tx = chain.NewContractCreationTx(value.BigInt(), gas.BigInt(), price.BigInt(), script)
|
||||
} else {
|
||||
data := ethutil.StringToByteFunc(string(data), func(s string) (ret []byte) {
|
||||
slice := strings.Split(s, "\n")
|
||||
for _, dataItem := range slice {
|
||||
d := ethutil.FormatData(dataItem)
|
||||
ret = append(ret, d...)
|
||||
}
|
||||
return
|
||||
})
|
||||
|
||||
tx = chain.NewTransactionMessage(hash, value.BigInt(), gas.BigInt(), price.BigInt(), data)
|
||||
}
|
||||
|
||||
acc := self.blockManager.TransState().GetOrNewStateObject(key.Address())
|
||||
tx.Nonce = acc.Nonce
|
||||
acc.Nonce += 1
|
||||
self.blockManager.TransState().UpdateStateObject(acc)
|
||||
|
||||
tx.Sign(key.PrivateKey)
|
||||
self.obj.TxPool().QueueTransaction(tx)
|
||||
state.SetNonce(key.Address(), nonce+1)
|
||||
|
||||
if contractCreation {
|
||||
addr := tx.CreationAddress(self.World().State())
|
||||
pipelogger.Infof("Contract addr %x\n", addr)
|
||||
|
||||
return addr, nil
|
||||
}
|
||||
|
||||
return tx.Hash(), nil
|
||||
return tx, nil
|
||||
|
||||
//acc := self.blockManager.TransState().GetOrNewStateObject(key.Address())
|
||||
//self.obj.TxPool().QueueTransaction(tx)
|
||||
|
||||
//acc.Nonce += 1
|
||||
//self.blockManager.TransState().UpdateStateObject(acc)
|
||||
|
||||
}
|
||||
|
||||
func (self *XEth) PushTx(tx *chain.Transaction) ([]byte, error) {
|
||||
self.obj.TxPool().QueueTransaction(tx)
|
||||
err := self.obj.TxPool().Add(tx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//self.obj.TxPool().QueueTransaction(tx)
|
||||
if tx.Recipient == nil {
|
||||
addr := tx.CreationAddress(self.World().State())
|
||||
pipelogger.Infof("Contract addr %x\n", addr)
|
||||
|
|
Loading…
Reference in New Issue