package privval import ( "errors" "fmt" "io" "net" "time" "github.com/tendermint/go-amino" "github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/crypto/ed25519" cmn "github.com/tendermint/tendermint/libs/common" "github.com/tendermint/tendermint/libs/log" p2pconn "github.com/tendermint/tendermint/p2p/conn" "github.com/tendermint/tendermint/types" ) const ( defaultAcceptDeadlineSeconds = 3 defaultConnDeadlineSeconds = 3 defaultConnHeartBeatSeconds = 30 defaultConnWaitSeconds = 60 defaultDialRetries = 10 ) // Socket errors. var ( ErrDialRetryMax = errors.New("dialed maximum retries") ErrConnWaitTimeout = errors.New("waited for remote signer for too long") ErrConnTimeout = errors.New("remote signer timed out") ErrUnexpectedResponse = errors.New("received unexpected response") ) var ( acceptDeadline = time.Second * defaultAcceptDeadlineSeconds connDeadline = time.Second * defaultConnDeadlineSeconds connHeartbeat = time.Second * defaultConnHeartBeatSeconds ) // SocketPVOption sets an optional parameter on the SocketPV. type SocketPVOption func(*SocketPV) // SocketPVAcceptDeadline sets the deadline for the SocketPV listener. // A zero time value disables the deadline. func SocketPVAcceptDeadline(deadline time.Duration) SocketPVOption { return func(sc *SocketPV) { sc.acceptDeadline = deadline } } // SocketPVConnDeadline sets the read and write deadline for connections // from external signing processes. func SocketPVConnDeadline(deadline time.Duration) SocketPVOption { return func(sc *SocketPV) { sc.connDeadline = deadline } } // SocketPVHeartbeat sets the period on which to check the liveness of the // connected Signer connections. func SocketPVHeartbeat(period time.Duration) SocketPVOption { return func(sc *SocketPV) { sc.connHeartbeat = period } } // SocketPVConnWait sets the timeout duration before connection of external // signing processes are considered to be unsuccessful. func SocketPVConnWait(timeout time.Duration) SocketPVOption { return func(sc *SocketPV) { sc.connWaitTimeout = timeout } } // SocketPV implements PrivValidator, it uses a socket to request signatures // from an external process. type SocketPV struct { cmn.BaseService addr string acceptDeadline time.Duration connDeadline time.Duration connHeartbeat time.Duration connWaitTimeout time.Duration privKey ed25519.PrivKeyEd25519 conn net.Conn listener net.Listener } // Check that SocketPV implements PrivValidator. var _ types.PrivValidator = (*SocketPV)(nil) // NewSocketPV returns an instance of SocketPV. func NewSocketPV( logger log.Logger, socketAddr string, privKey ed25519.PrivKeyEd25519, ) *SocketPV { sc := &SocketPV{ addr: socketAddr, acceptDeadline: acceptDeadline, connDeadline: connDeadline, connHeartbeat: connHeartbeat, connWaitTimeout: time.Second * defaultConnWaitSeconds, privKey: privKey, } sc.BaseService = *cmn.NewBaseService(logger, "SocketPV", sc) return sc } // GetAddress implements PrivValidator. func (sc *SocketPV) GetAddress() types.Address { addr, err := sc.getAddress() if err != nil { panic(err) } return addr } // Address is an alias for PubKey().Address(). func (sc *SocketPV) getAddress() (cmn.HexBytes, error) { p, err := sc.getPubKey() if err != nil { return nil, err } return p.Address(), nil } // GetPubKey implements PrivValidator. func (sc *SocketPV) GetPubKey() crypto.PubKey { pubKey, err := sc.getPubKey() if err != nil { panic(err) } return pubKey } func (sc *SocketPV) getPubKey() (crypto.PubKey, error) { err := writeMsg(sc.conn, &PubKeyMsg{}) if err != nil { return nil, err } res, err := readMsg(sc.conn) if err != nil { return nil, err } return res.(*PubKeyMsg).PubKey, nil } // SignVote implements PrivValidator. func (sc *SocketPV) SignVote(chainID string, vote *types.Vote) error { err := writeMsg(sc.conn, &SignVoteRequest{Vote: vote}) if err != nil { return err } res, err := readMsg(sc.conn) if err != nil { return err } resp, ok := res.(*SignedVoteResponse) if !ok { return ErrUnexpectedResponse } if resp.Error != nil { return fmt.Errorf("remote error occurred: code: %v, description: %s", resp.Error.Code, resp.Error.Description) } *vote = *resp.Vote return nil } // SignProposal implements PrivValidator. func (sc *SocketPV) SignProposal( chainID string, proposal *types.Proposal, ) error { err := writeMsg(sc.conn, &SignProposalRequest{Proposal: proposal}) if err != nil { return err } res, err := readMsg(sc.conn) if err != nil { return err } resp, ok := res.(*SignedProposalResponse) if !ok { return ErrUnexpectedResponse } if resp.Error != nil { return fmt.Errorf("remote error occurred: code: %v, description: %s", resp.Error.Code, resp.Error.Description) } *proposal = *resp.Proposal return nil } // SignHeartbeat implements PrivValidator. func (sc *SocketPV) SignHeartbeat( chainID string, heartbeat *types.Heartbeat, ) error { err := writeMsg(sc.conn, &SignHeartbeatRequest{Heartbeat: heartbeat}) if err != nil { return err } res, err := readMsg(sc.conn) if err != nil { return err } resp, ok := res.(*SignedHeartbeatResponse) if !ok { return ErrUnexpectedResponse } if resp.Error != nil { return fmt.Errorf("remote error occurred: code: %v, description: %s", resp.Error.Code, resp.Error.Description) } *heartbeat = *resp.Heartbeat return nil } // OnStart implements cmn.Service. func (sc *SocketPV) OnStart() error { if err := sc.listen(); err != nil { err = cmn.ErrorWrap(err, "failed to listen") sc.Logger.Error( "OnStart", "err", err, ) return err } conn, err := sc.waitConnection() if err != nil { err = cmn.ErrorWrap(err, "failed to accept connection") sc.Logger.Error( "OnStart", "err", err, ) return err } sc.conn = conn return nil } // OnStop implements cmn.Service. func (sc *SocketPV) OnStop() { if sc.conn != nil { if err := sc.conn.Close(); err != nil { err = cmn.ErrorWrap(err, "failed to close connection") sc.Logger.Error( "OnStop", "err", err, ) } } if sc.listener != nil { if err := sc.listener.Close(); err != nil { err = cmn.ErrorWrap(err, "failed to close listener") sc.Logger.Error( "OnStop", "err", err, ) } } } func (sc *SocketPV) acceptConnection() (net.Conn, error) { conn, err := sc.listener.Accept() if err != nil { if !sc.IsRunning() { return nil, nil // Ignore error from listener closing. } return nil, err } conn, err = p2pconn.MakeSecretConnection(conn, sc.privKey) if err != nil { return nil, err } return conn, nil } func (sc *SocketPV) listen() error { ln, err := net.Listen(cmn.ProtocolAndAddress(sc.addr)) if err != nil { return err } sc.listener = newTCPTimeoutListener( ln, sc.acceptDeadline, sc.connDeadline, sc.connHeartbeat, ) return nil } // waitConnection uses the configured wait timeout to error if no external // process connects in the time period. func (sc *SocketPV) waitConnection() (net.Conn, error) { var ( connc = make(chan net.Conn, 1) errc = make(chan error, 1) ) go func(connc chan<- net.Conn, errc chan<- error) { conn, err := sc.acceptConnection() if err != nil { errc <- err return } connc <- conn }(connc, errc) select { case conn := <-connc: return conn, nil case err := <-errc: if _, ok := err.(timeoutError); ok { return nil, cmn.ErrorWrap(ErrConnWaitTimeout, err.Error()) } return nil, err case <-time.After(sc.connWaitTimeout): return nil, ErrConnWaitTimeout } } //--------------------------------------------------------- // RemoteSignerOption sets an optional parameter on the RemoteSigner. type RemoteSignerOption func(*RemoteSigner) // RemoteSignerConnDeadline sets the read and write deadline for connections // from external signing processes. func RemoteSignerConnDeadline(deadline time.Duration) RemoteSignerOption { return func(ss *RemoteSigner) { ss.connDeadline = deadline } } // RemoteSignerConnRetries sets the amount of attempted retries to connect. func RemoteSignerConnRetries(retries int) RemoteSignerOption { return func(ss *RemoteSigner) { ss.connRetries = retries } } // RemoteSigner implements PrivValidator by dialing to a socket. type RemoteSigner struct { cmn.BaseService addr string chainID string connDeadline time.Duration connRetries int privKey ed25519.PrivKeyEd25519 privVal types.PrivValidator conn net.Conn } // NewRemoteSigner returns an instance of RemoteSigner. func NewRemoteSigner( logger log.Logger, chainID, socketAddr string, privVal types.PrivValidator, privKey ed25519.PrivKeyEd25519, ) *RemoteSigner { rs := &RemoteSigner{ addr: socketAddr, chainID: chainID, connDeadline: time.Second * defaultConnDeadlineSeconds, connRetries: defaultDialRetries, privKey: privKey, privVal: privVal, } rs.BaseService = *cmn.NewBaseService(logger, "RemoteSigner", rs) return rs } // OnStart implements cmn.Service. func (rs *RemoteSigner) OnStart() error { conn, err := rs.connect() if err != nil { err = cmn.ErrorWrap(err, "connect") rs.Logger.Error("OnStart", "err", err) return err } go rs.handleConnection(conn) return nil } // OnStop implements cmn.Service. func (rs *RemoteSigner) OnStop() { if rs.conn == nil { return } if err := rs.conn.Close(); err != nil { rs.Logger.Error("OnStop", "err", cmn.ErrorWrap(err, "closing listener failed")) } } func (rs *RemoteSigner) connect() (net.Conn, error) { for retries := rs.connRetries; retries > 0; retries-- { // Don't sleep if it is the first retry. if retries != rs.connRetries { time.Sleep(rs.connDeadline) } conn, err := cmn.Connect(rs.addr) if err != nil { err = cmn.ErrorWrap(err, "connection failed") rs.Logger.Error( "connect", "addr", rs.addr, "err", err, ) continue } if err := conn.SetDeadline(time.Now().Add(connDeadline)); err != nil { err = cmn.ErrorWrap(err, "setting connection timeout failed") rs.Logger.Error( "connect", "err", err, ) continue } conn, err = p2pconn.MakeSecretConnection(conn, rs.privKey) if err != nil { err = cmn.ErrorWrap(err, "encrypting connection failed") rs.Logger.Error( "connect", "err", err, ) continue } return conn, nil } return nil, ErrDialRetryMax } func (rs *RemoteSigner) handleConnection(conn net.Conn) { for { if !rs.IsRunning() { return // Ignore error from listener closing. } req, err := readMsg(conn) if err != nil { if err != io.EOF { rs.Logger.Error("handleConnection", "err", err) } return } var res SocketPVMsg switch r := req.(type) { case *PubKeyMsg: var p crypto.PubKey p = rs.privVal.GetPubKey() res = &PubKeyMsg{p} case *SignVoteRequest: err = rs.privVal.SignVote(rs.chainID, r.Vote) if err != nil { res = &SignedVoteResponse{nil, &RemoteSignerError{0, err.Error()}} } else { res = &SignedVoteResponse{r.Vote, nil} } case *SignProposalRequest: err = rs.privVal.SignProposal(rs.chainID, r.Proposal) if err != nil { res = &SignedProposalResponse{nil, &RemoteSignerError{0, err.Error()}} } else { res = &SignedProposalResponse{r.Proposal, nil} } case *SignHeartbeatRequest: err = rs.privVal.SignHeartbeat(rs.chainID, r.Heartbeat) if err != nil { res = &SignedHeartbeatResponse{nil, &RemoteSignerError{0, err.Error()}} } else { res = &SignedHeartbeatResponse{r.Heartbeat, nil} } default: err = fmt.Errorf("unknown msg: %v", r) } if err != nil { // only log the error; we'll reply with an error in res rs.Logger.Error("handleConnection", "err", err) } err = writeMsg(conn, res) if err != nil { rs.Logger.Error("handleConnection", "err", err) return } } } //--------------------------------------------------------- // SocketPVMsg is sent between RemoteSigner and SocketPV. type SocketPVMsg interface{} func RegisterSocketPVMsg(cdc *amino.Codec) { cdc.RegisterInterface((*SocketPVMsg)(nil), nil) cdc.RegisterConcrete(&PubKeyMsg{}, "tendermint/socketpv/PubKeyMsg", nil) cdc.RegisterConcrete(&SignVoteRequest{}, "tendermint/socketpv/SignVoteRequest", nil) cdc.RegisterConcrete(&SignedVoteResponse{}, "tendermint/socketpv/SignedVoteResponse", nil) cdc.RegisterConcrete(&SignProposalRequest{}, "tendermint/socketpv/SignProposalRequest", nil) cdc.RegisterConcrete(&SignedProposalResponse{}, "tendermint/socketpv/SignedProposalResponse", nil) cdc.RegisterConcrete(&SignHeartbeatRequest{}, "tendermint/socketpv/SignHeartbeatRequest", nil) cdc.RegisterConcrete(&SignedHeartbeatResponse{}, "tendermint/socketpv/SignedHeartbeatResponse", nil) } // PubKeyMsg is a PrivValidatorSocket message containing the public key. type PubKeyMsg struct { PubKey crypto.PubKey } // SignVoteRequest is a PrivValidatorSocket message containing a vote. type SignVoteRequest struct { Vote *types.Vote } // SignedVoteResponse is a PrivValidatorSocket message containing a signed vote along with a potenial error message. type SignedVoteResponse struct { Vote *types.Vote Error *RemoteSignerError } // SignProposalRequest is a PrivValidatorSocket message containing a Proposal. type SignProposalRequest struct { Proposal *types.Proposal } type SignedProposalResponse struct { Proposal *types.Proposal Error *RemoteSignerError } // SignHeartbeatRequest is a PrivValidatorSocket message containing a Heartbeat. type SignHeartbeatRequest struct { Heartbeat *types.Heartbeat } type SignedHeartbeatResponse struct { Heartbeat *types.Heartbeat Error *RemoteSignerError } // RemoteSignerError allows (remote) validators to include meaningful error descriptions in their reply. type RemoteSignerError struct { // TODO(ismail): create an enum of known errors Code int Description string } func readMsg(r io.Reader) (msg SocketPVMsg, err error) { const maxSocketPVMsgSize = 1024 * 10 _, err = cdc.UnmarshalBinaryReader(r, &msg, maxSocketPVMsgSize) if _, ok := err.(timeoutError); ok { err = cmn.ErrorWrap(ErrConnTimeout, err.Error()) } return } func writeMsg(w io.Writer, msg interface{}) (err error) { _, err = cdc.MarshalBinaryWriter(w, msg) if _, ok := err.(timeoutError); ok { err = cmn.ErrorWrap(ErrConnTimeout, err.Error()) } return }