mirror of https://github.com/poanetwork/quorum.git
Merge branch '1812-permission-rpc-api' of https://github.com/vsmk98/quorum into 1812-permission-rpc-api
Merging with Amal's API changes
This commit is contained in:
commit
78c79e59a2
|
@ -33,15 +33,15 @@ import (
|
|||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||
"github.com/ethereum/go-ethereum/console"
|
||||
"github.com/ethereum/go-ethereum/controls/cluster"
|
||||
"github.com/ethereum/go-ethereum/controls/permission"
|
||||
"github.com/ethereum/go-ethereum/core/quorum"
|
||||
"github.com/ethereum/go-ethereum/eth"
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
"github.com/ethereum/go-ethereum/internal/debug"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/metrics"
|
||||
"github.com/ethereum/go-ethereum/node"
|
||||
"github.com/ethereum/go-ethereum/controls/permission"
|
||||
"github.com/ethereum/go-ethereum/controls/cluster"
|
||||
"github.com/ethereum/go-ethereum/core/quorum"
|
||||
"gopkg.in/urfave/cli.v1"
|
||||
)
|
||||
|
||||
|
@ -363,7 +363,7 @@ func startNode(ctx *cli.Context, stack *node.Node) {
|
|||
func startQuorumPermissionService(ctx *cli.Context, stack *node.Node) {
|
||||
if permEnabled := ctx.GlobalBool(utils.EnableNodePermissionFlag.Name); permEnabled {
|
||||
// start the permissions management service
|
||||
pc, err := permission.NewQuorumPermissionCtrl(stack, ctx.GlobalBool(utils.RaftModeFlag.Name))
|
||||
pc, err := permission.NewQuorumPermissionCtrl(stack, ctx.GlobalBool(utils.RaftModeFlag.Name))
|
||||
if err != nil {
|
||||
utils.Fatalf("Failed to start Quorum Permission contract service: %v", err)
|
||||
}
|
||||
|
@ -390,6 +390,6 @@ func startQuorumPermissionService(ctx *cli.Context, stack *node.Node) {
|
|||
utils.Fatalf("Failed to attach to self: %v", err)
|
||||
}
|
||||
stateReader := ethclient.NewClient(rpcClient)
|
||||
qapi.Init(stateReader, stack.InstanceDir())
|
||||
qapi.Init(stateReader)
|
||||
log.Info("Permission API initialized")
|
||||
}
|
||||
|
|
|
@ -1,46 +1,64 @@
|
|||
package quorum
|
||||
|
||||
import (
|
||||
"github.com/ethereum/go-ethereum/core"
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"github.com/ethereum/go-ethereum/accounts/abi/bind"
|
||||
"github.com/ethereum/go-ethereum/params"
|
||||
"github.com/ethereum/go-ethereum/p2p/discover"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
pbind "github.com/ethereum/go-ethereum/controls/bind"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"strings"
|
||||
"github.com/ethereum/go-ethereum/accounts/abi/bind"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
pbind "github.com/ethereum/go-ethereum/controls/bind"
|
||||
"github.com/ethereum/go-ethereum/core"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/p2p/discover"
|
||||
"github.com/ethereum/go-ethereum/params"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
var defaultGasLimit = uint64(4700000)
|
||||
var defaultGasLimit = uint64(470000000)
|
||||
var defaultGasPrice = big.NewInt(0)
|
||||
|
||||
type PermAction int
|
||||
|
||||
const (
|
||||
ProposeNode PermAction = iota
|
||||
ApproveNode
|
||||
DeactivateNode
|
||||
ApproveDeactivateNode
|
||||
AddVoter
|
||||
RemoveVoter
|
||||
)
|
||||
|
||||
type OrgKeyAction int
|
||||
|
||||
const (
|
||||
AddOrgKey OrgKeyAction = iota
|
||||
RemoveOrgKey
|
||||
)
|
||||
|
||||
type PermissionAPI struct {
|
||||
txPool *core.TxPool
|
||||
ethClnt *ethclient.Client
|
||||
am *accounts.Manager
|
||||
trnOpt *bind.TransactOpts
|
||||
acntMgr *accounts.Manager
|
||||
txOpt *bind.TransactOpts
|
||||
permContr *pbind.Permissions
|
||||
clustContr *pbind.Cluster
|
||||
}
|
||||
|
||||
func NewPermissionAPI(tp *core.TxPool, am *accounts.Manager) *PermissionAPI {
|
||||
pa := &PermissionAPI{tp, nil, am, nil, nil, nil}
|
||||
return pa
|
||||
type txArgs struct {
|
||||
from common.Address
|
||||
voter common.Address
|
||||
nodeId string
|
||||
orgId string
|
||||
keyId string
|
||||
}
|
||||
|
||||
func (p *PermissionAPI) Init(ethClnt *ethclient.Client, datadir string) error {
|
||||
func NewPermissionAPI(tp *core.TxPool, am *accounts.Manager) *PermissionAPI {
|
||||
return &PermissionAPI{tp, nil, am, nil, nil, nil}
|
||||
}
|
||||
|
||||
func (p *PermissionAPI) Init(ethClnt *ethclient.Client) error {
|
||||
p.ethClnt = ethClnt
|
||||
key, kerr := getKeyFromKeyStore(datadir)
|
||||
if kerr != nil {
|
||||
log.Error("error reading key file", "err", kerr)
|
||||
return kerr
|
||||
}
|
||||
permContr, err := pbind.NewPermissions(params.QuorumPermissionsContract, p.ethClnt)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -51,226 +69,165 @@ func (p *PermissionAPI) Init(ethClnt *ethclient.Client, datadir string) error {
|
|||
return err
|
||||
}
|
||||
p.clustContr = clustContr
|
||||
auth, err := bind.NewTransactor(strings.NewReader(key), "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.trnOpt = auth
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *PermissionAPI) AddVoter(addr common.Address) bool {
|
||||
acct := accounts.Account{Address: addr}
|
||||
w, err := s.am.Find(acct)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
ps := s.newPermSession1(w, acct)
|
||||
nonce := s.txPool.Nonce(acct.Address)
|
||||
ps.TransactOpts.Nonce = new(big.Int).SetUint64(nonce)
|
||||
|
||||
tx, err := ps.AddVoter(addr)
|
||||
if err != nil {
|
||||
log.Warn("Failed to add voter", "err", err)
|
||||
return false
|
||||
}
|
||||
txHash := tx.Hash()
|
||||
log.Info("Transaction pending", "tx hash", string(txHash[:]))
|
||||
return true
|
||||
func (s *PermissionAPI) AddVoter(from common.Address, vaddr common.Address) bool {
|
||||
return s.executePermAction(AddVoter, txArgs{voter: vaddr, from: from})
|
||||
}
|
||||
|
||||
func (s *PermissionAPI) RemoveVoter(addr common.Address) bool {
|
||||
ps := s.newPermSession()
|
||||
tx, err := ps.RemoveVoter(addr)
|
||||
if err != nil {
|
||||
log.Warn("Failed to remove voter", "err", err)
|
||||
return false
|
||||
}
|
||||
txHash := tx.Hash()
|
||||
log.Info("Transaction pending", "tx hash", string(txHash[:]))
|
||||
return true
|
||||
func (s *PermissionAPI) RemoveVoter(from common.Address, vaddr common.Address) bool {
|
||||
return s.executePermAction(RemoveVoter, txArgs{voter: vaddr, from: from})
|
||||
}
|
||||
|
||||
func (s *PermissionAPI) ProposeNode(from common.Address, nodeId string) bool {
|
||||
acct := accounts.Account{Address: from}
|
||||
w, err := s.am.Find(acct)
|
||||
return s.executePermAction(ProposeNode, txArgs{nodeId: nodeId, from: from})
|
||||
}
|
||||
|
||||
func (s *PermissionAPI) ApproveNode(from common.Address, nodeId string) bool {
|
||||
return s.executePermAction(ApproveNode, txArgs{nodeId: nodeId, from: from})
|
||||
}
|
||||
|
||||
func (s *PermissionAPI) DeactivateNode(from common.Address, nodeId string) bool {
|
||||
return s.executePermAction(DeactivateNode, txArgs{nodeId: nodeId, from: from})
|
||||
}
|
||||
|
||||
func (s *PermissionAPI) ApproveDeactivateNode(from common.Address, nodeId string) bool {
|
||||
return s.executePermAction(ApproveDeactivateNode, txArgs{nodeId: nodeId, from: from})
|
||||
}
|
||||
|
||||
func (s *PermissionAPI) RemoveOrgKey(from common.Address, orgId string, pvtKey string) bool {
|
||||
return s.executeOrgKeyAction(RemoveOrgKey, txArgs{from: from, orgId: orgId, keyId: pvtKey})
|
||||
}
|
||||
|
||||
func (s *PermissionAPI) AddOrgKey(from common.Address, orgId string, pvtKey string) bool {
|
||||
return s.executeOrgKeyAction(AddOrgKey, txArgs{from: from, orgId: orgId, keyId: pvtKey})
|
||||
}
|
||||
|
||||
func (s *PermissionAPI) executePermAction(action PermAction, args txArgs) bool {
|
||||
fromAcct, w, err := s.validateAccount(args.from)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
ps := s.newPermSession(w, fromAcct)
|
||||
var tx *types.Transaction
|
||||
|
||||
ps := s.newPermSession1(w, acct)
|
||||
switch action {
|
||||
case AddVoter:
|
||||
tx, err = ps.AddVoter(args.voter)
|
||||
case RemoveVoter:
|
||||
tx, err = ps.RemoveVoter(args.voter)
|
||||
case ProposeNode:
|
||||
node, err := discover.ParseNode(args.nodeId)
|
||||
if err != nil {
|
||||
log.Error("invalid node id: %v", err)
|
||||
return false
|
||||
}
|
||||
enodeID := node.ID.String()
|
||||
ipAddr := node.IP.String()
|
||||
port := fmt.Sprintf("%v", node.TCP)
|
||||
discPort := fmt.Sprintf("%v", node.UDP)
|
||||
raftPort := fmt.Sprintf("%v", node.RaftPort)
|
||||
ipAddrPort := ipAddr + ":" + port
|
||||
|
||||
tx, err = ps.ProposeNode(enodeID, ipAddrPort, discPort, raftPort)
|
||||
case ApproveNode:
|
||||
node, err := discover.ParseNode(args.nodeId)
|
||||
if err != nil {
|
||||
log.Error("invalid node id: %v", err)
|
||||
return false
|
||||
}
|
||||
enodeID := node.ID.String()
|
||||
tx, err = ps.ApproveNode(enodeID)
|
||||
case DeactivateNode:
|
||||
node, err := discover.ParseNode(args.nodeId)
|
||||
if err != nil {
|
||||
log.Error("invalid node id: %v", err)
|
||||
return false
|
||||
}
|
||||
enodeID := node.ID.String()
|
||||
tx, err = ps.DeactivateNode(enodeID)
|
||||
case ApproveDeactivateNode:
|
||||
node, err := discover.ParseNode(args.nodeId)
|
||||
if err != nil {
|
||||
log.Error("invalid node id: %v", err)
|
||||
return false
|
||||
}
|
||||
enodeID := node.ID.String()
|
||||
//TODO change to approve deactivate node
|
||||
tx, err = ps.DeactivateNode(enodeID)
|
||||
|
||||
}
|
||||
if err != nil {
|
||||
log.Error("Failed to execute permission action", "action", action, "err", err)
|
||||
return false
|
||||
}
|
||||
log.Debug("executed permission action", "action", action, "tx", tx)
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *PermissionAPI) executeOrgKeyAction(action OrgKeyAction, args txArgs) bool {
|
||||
fromAcct, w, err := s.validateAccount(args.from)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
ps := s.newClusterSession(w, fromAcct)
|
||||
var tx *types.Transaction
|
||||
|
||||
switch action {
|
||||
case AddOrgKey:
|
||||
tx, err = ps.AddOrgKey(args.orgId, args.keyId)
|
||||
case RemoveOrgKey:
|
||||
tx, err = ps.DeleteOrgKey(args.orgId, args.keyId)
|
||||
}
|
||||
if err != nil {
|
||||
log.Error("Failed to execute orgKey action", "action", action, "err", err)
|
||||
return false
|
||||
}
|
||||
log.Debug("executed orgKey action", "action", action, "tx", tx)
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *PermissionAPI) validateAccount(from common.Address) (accounts.Account, accounts.Wallet, error) {
|
||||
acct := accounts.Account{Address: from}
|
||||
w, err := s.acntMgr.Find(acct)
|
||||
if err != nil {
|
||||
return acct, nil, err
|
||||
}
|
||||
return acct, w, nil
|
||||
}
|
||||
|
||||
func (s *PermissionAPI) newPermSession(w accounts.Wallet, acct accounts.Account) *pbind.PermissionsSession {
|
||||
transactOpts := bind.NewWalletTransactor(w, acct)
|
||||
ps := &pbind.PermissionsSession{
|
||||
Contract: s.permContr,
|
||||
CallOpts: bind.CallOpts{
|
||||
Pending: true,
|
||||
},
|
||||
TransactOpts: bind.TransactOpts{
|
||||
From: acct.Address,
|
||||
GasLimit: defaultGasLimit,
|
||||
GasPrice: defaultGasPrice,
|
||||
Signer: transactOpts.Signer,
|
||||
},
|
||||
}
|
||||
nonce := s.txPool.Nonce(acct.Address)
|
||||
ps.TransactOpts.Nonce = new(big.Int).SetUint64(nonce)
|
||||
|
||||
node, err := discover.ParseNode(nodeId)
|
||||
if err != nil {
|
||||
log.Error("invalid node id: %v", err)
|
||||
return false
|
||||
}
|
||||
enodeID := node.ID.String()
|
||||
ipAddr := node.IP.String()
|
||||
port := fmt.Sprintf("%v", node.TCP)
|
||||
discPort := fmt.Sprintf("%v", node.UDP)
|
||||
raftPort := fmt.Sprintf("%v", node.RaftPort)
|
||||
ipAddrPort := ipAddr + ":" + port
|
||||
|
||||
tx, err := ps.ProposeNode(enodeID, ipAddrPort, discPort, raftPort)
|
||||
if err != nil {
|
||||
log.Warn("Failed to propose node", "err", err)
|
||||
log.Error("Failed to propose node: %v", err)
|
||||
return false
|
||||
}
|
||||
txHash := tx.Hash()
|
||||
statusMsg := fmt.Sprintf("Transaction pending tx hash %s", string(txHash[:]))
|
||||
log.Debug(statusMsg)
|
||||
return true
|
||||
return ps
|
||||
}
|
||||
|
||||
func (s *PermissionAPI) ApproveNode(nodeId string) bool {
|
||||
node, err := discover.ParseNode(nodeId)
|
||||
if err != nil {
|
||||
log.Error("invalid node id: %v", err)
|
||||
return false
|
||||
}
|
||||
enodeID := node.ID.String()
|
||||
|
||||
ps := s.newPermSession()
|
||||
tx, err := ps.ApproveNode(enodeID)
|
||||
if err != nil {
|
||||
log.Warn("Failed to propose node", "err", err)
|
||||
return false
|
||||
}
|
||||
txHash := tx.Hash()
|
||||
log.Debug("Transaction pending", "tx hash", string(txHash[:]))
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *PermissionAPI) DeactivateNode(nodeId string) bool {
|
||||
node, err := discover.ParseNode(nodeId)
|
||||
if err != nil {
|
||||
log.Error("invalid node id: %v", err)
|
||||
return false
|
||||
}
|
||||
enodeID := node.ID.String()
|
||||
|
||||
ps := s.newPermSession()
|
||||
tx, err := ps.DeactivateNode(enodeID)
|
||||
if err != nil {
|
||||
log.Warn("Failed to propose node", "err", err)
|
||||
return false
|
||||
}
|
||||
txHash := tx.Hash()
|
||||
log.Debug("Transaction pending", "tx hash", string(txHash[:]))
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *PermissionAPI) ApproveDeactivateNode(nodeId string) bool {
|
||||
node, err := discover.ParseNode(nodeId)
|
||||
if err != nil {
|
||||
log.Error("invalid node id: %v", err)
|
||||
return false
|
||||
}
|
||||
enodeID := node.ID.String()
|
||||
|
||||
ps := s.newPermSession()
|
||||
//TODO change it to approveDeactivate node once contract is updated
|
||||
tx, err := ps.DeactivateNode(enodeID)
|
||||
if err != nil {
|
||||
log.Warn("Failed to propose node", "err", err)
|
||||
return false
|
||||
}
|
||||
txHash := tx.Hash()
|
||||
log.Debug("Transaction pending", "tx hash", string(txHash[:]))
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *PermissionAPI) newPermSession() *pbind.PermissionsSession {
|
||||
return &pbind.PermissionsSession{
|
||||
Contract: s.permContr,
|
||||
CallOpts: bind.CallOpts{
|
||||
Pending: true,
|
||||
},
|
||||
TransactOpts: bind.TransactOpts{
|
||||
From: s.trnOpt.From,
|
||||
Signer: s.trnOpt.Signer,
|
||||
GasLimit: defaultGasLimit,
|
||||
GasPrice: defaultGasPrice,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PermissionAPI) newPermSession1(w accounts.Wallet, acct accounts.Account) *pbind.PermissionsSession {
|
||||
auth := bind.NewWalletTransactor(w, acct)
|
||||
return &pbind.PermissionsSession{
|
||||
Contract: s.permContr,
|
||||
CallOpts: bind.CallOpts{
|
||||
Pending: true,
|
||||
},
|
||||
TransactOpts: bind.TransactOpts{
|
||||
From: acct.Address,
|
||||
GasLimit: defaultGasLimit,
|
||||
GasPrice: defaultGasPrice,
|
||||
Signer: auth.Signer,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PermissionAPI) newClusterSession() *pbind.ClusterSession {
|
||||
func (s *PermissionAPI) newClusterSession(w accounts.Wallet, acct accounts.Account) *pbind.ClusterSession {
|
||||
transactOpts := bind.NewWalletTransactor(w, acct)
|
||||
return &pbind.ClusterSession{
|
||||
Contract: s.clustContr,
|
||||
CallOpts: bind.CallOpts{
|
||||
Pending: true,
|
||||
},
|
||||
TransactOpts: bind.TransactOpts{
|
||||
From: acct.Address,
|
||||
GasLimit: defaultGasLimit,
|
||||
GasPrice: defaultGasPrice,
|
||||
Signer: transactOpts.Signer,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PermissionAPI) AddOrgKey(orgId string, pvtKey string) bool {
|
||||
cs := s.newClusterSession()
|
||||
tx, err := cs.AddOrgKey(orgId, pvtKey)
|
||||
if err != nil {
|
||||
log.Warn("Failed to add org key", "err", err)
|
||||
return false
|
||||
}
|
||||
txHash := tx.Hash()
|
||||
log.Info("Transaction pending", "tx hash", string(txHash[:]))
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *PermissionAPI) RemoveOrgKey(orgId string, pvtKey string) bool {
|
||||
cs := s.newClusterSession()
|
||||
tx, err := cs.DeleteOrgKey(orgId, pvtKey)
|
||||
if err != nil {
|
||||
log.Warn("Failed to remove org key", "err", err)
|
||||
return false
|
||||
}
|
||||
txHash := tx.Hash()
|
||||
log.Info("Transaction pending", "tx hash", string(txHash[:]))
|
||||
return true
|
||||
}
|
||||
|
||||
func getKeyFromKeyStore(datadir string) (string, error) {
|
||||
|
||||
files, err := ioutil.ReadDir(filepath.Join(datadir, "keystore"))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// HACK: here we always use the first key as transactor
|
||||
var keyPath string
|
||||
for _, f := range files {
|
||||
keyPath = filepath.Join(datadir, "keystore", f.Name())
|
||||
break
|
||||
}
|
||||
keyBlob, err := ioutil.ReadFile(keyPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
n := len(keyBlob)
|
||||
|
||||
return string(keyBlob[:n]), nil
|
||||
}
|
||||
|
|
|
@ -686,14 +686,14 @@ web3._extend({
|
|||
new web3._extend.Method({
|
||||
name: 'addVoter',
|
||||
call: 'quorum_addVoter',
|
||||
params: 1,
|
||||
inputFormatter: [web3._extend.formatters.inputAddressFormatter]
|
||||
params: 2,
|
||||
inputFormatter: [web3._extend.formatters.inputAddressFormatter,web3._extend.formatters.inputAddressFormatter]
|
||||
}),
|
||||
new web3._extend.Method({
|
||||
name: 'removeVoter',
|
||||
call: 'quorum_removeVoter',
|
||||
params: 1,
|
||||
inputFormatter: [web3._extend.formatters.inputAddressFormatter]
|
||||
params: 2,
|
||||
inputFormatter: [web3._extend.formatters.inputAddressFormatter,web3._extend.formatters.inputAddressFormatter]
|
||||
}),
|
||||
new web3._extend.Method({
|
||||
name: 'proposeNode',
|
||||
|
@ -704,27 +704,32 @@ web3._extend({
|
|||
new web3._extend.Method({
|
||||
name: 'approveNode',
|
||||
call: 'quorum_approveNode',
|
||||
params: 1
|
||||
params: 2,
|
||||
inputFormatter: [web3._extend.formatters.inputAddressFormatter,null]
|
||||
}),
|
||||
new web3._extend.Method({
|
||||
name: 'deactivateNode',
|
||||
call: 'quorum_deactivateNode',
|
||||
params: 1
|
||||
params: 2,
|
||||
inputFormatter: [web3._extend.formatters.inputAddressFormatter,null]
|
||||
}),
|
||||
new web3._extend.Method({
|
||||
name: 'approveDeactivateNode',
|
||||
call: 'quorum_approveDeactivateNode',
|
||||
params: 1
|
||||
params: 2,
|
||||
inputFormatter: [web3._extend.formatters.inputAddressFormatter,null]
|
||||
}),
|
||||
new web3._extend.Method({
|
||||
name: 'addOrgKey',
|
||||
call: 'quorum_addOrgKey',
|
||||
params: 2
|
||||
params: 3,
|
||||
inputFormatter: [web3._extend.formatters.inputAddressFormatter,null,null]
|
||||
}),
|
||||
new web3._extend.Method({
|
||||
name: 'removeOrgKey',
|
||||
call: 'quorum_removeOrgKey',
|
||||
params: 2
|
||||
params: 3,
|
||||
inputFormatter: [web3._extend.formatters.inputAddressFormatter,null,null]
|
||||
}),
|
||||
]
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue