From 1ab5161d795c415e98dc836e34e6dc40b2f79ece Mon Sep 17 00:00:00 2001 From: vctt Date: Thu, 13 Nov 2025 00:04:45 -0300 Subject: [PATCH] add check for wr with passed csv --- ponggame/interface.go | 4 ++ server/referee.go | 35 ++++++++++++++ server/server.go | 105 ++++++++++++++++++++++++++++++++++++++++++ server/waitingroom.go | 7 +++ 4 files changed, 151 insertions(+) diff --git a/ponggame/interface.go b/ponggame/interface.go index ae9d689..e1cb323 100644 --- a/ponggame/interface.go +++ b/ponggame/interface.go @@ -120,6 +120,10 @@ type WaitingRoom struct { HostID *clientintf.UserID Players []*Player BetAmount int64 + // Chain height when the room was opened (used for CSV expiry checks). + OpenedHeight int64 + // CSV lock duration inherited from the host escrow at room creation. + CSVBlocks uint32 } func (wr *WaitingRoom) Marshal() *pong.WaitingRoom { diff --git a/server/referee.go b/server/referee.go index ea83e71..8743206 100644 --- a/server/referee.go +++ b/server/referee.go @@ -73,6 +73,36 @@ func (s *Server) trackEscrow(ctx context.Context, es *escrowSession, ch <-chan c player := es.player es.mu.Unlock() + expired, currentHeight, expiryHeight, csv, err := s.escrowExpired(ctx, es) + if err != nil { + s.log.Warnf("trackEscrow: failed expiry check for escrow %s: %v", es.escrowID, err) + } else if expired { + if player == nil { + player = s.gameManager.PlayerSessions.GetPlayer(es.ownerUID) + } + msg := fmt.Sprintf("escrow expired (csv=%d, opened_height=%d, expiry_height=%d, current_height=%d)", csv, es.openedHeight, expiryHeight, currentHeight) + if player != nil { + _ = s.notify(player, &pong.NtfnStreamResponse{ + NotificationType: pong.NotificationType_MESSAGE, + Message: msg, + }) + } + if es.unsubW != nil { + es.unsubW() + es.unsubW = nil + } + es.clearPreSigns() + es.mu.Lock() + es.player = nil + es.mu.Unlock() + s.removeEscrowFromRooms(es.escrowID) + s.escrowsMu.Lock() + delete(s.escrows, es.escrowID) + s.escrowsMu.Unlock() + s.log.Infof("trackEscrow: stopped tracking escrow %s owner=%s due to csv expiry", es.escrowID, es.ownerUID) + return + } + // Emit a single structured update when funded and state changed. // Only send notifications if player is still bound (escrow not cleaned up). ownerID := es.ownerUID.String() @@ -216,6 +246,11 @@ func (s *Server) OpenEscrow(ctx context.Context, req *pong.OpenEscrowRequest) (* pkScriptHex: pkScriptHex, player: player, } + height, err := s.currentBestBlockHeight(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to fetch current block height: %v", err) + } + es.openedHeight = height s.escrowsMu.Lock() if s.escrows == nil { s.escrows = make(map[string]*escrowSession) diff --git a/server/server.go b/server/server.go index c3e54e1..f05a260 100644 --- a/server/server.go +++ b/server/server.go @@ -10,6 +10,7 @@ import ( "os" "path/filepath" "sync" + "sync/atomic" "time" "github.com/companyzero/bisonrelay/zkidentity" @@ -97,6 +98,7 @@ type escrowSession struct { csvBlocks uint32 redeemScriptHex string pkScriptHex string + openedHeight int64 // ----------------- runtime state (protected by mu) ------------- mu sync.RWMutex @@ -141,6 +143,9 @@ type Server struct { // chain watcher for tip + mempool watcher *chainwatcher.ChainWatcher + // Cached best block height updated from dcrd notifications. + bestBlockHeight atomic.Int64 + // Escrow-first funding state escrowsMu sync.RWMutex escrows map[string]*escrowSession @@ -198,6 +203,7 @@ func NewServer(id *zkidentity.ShortID, cfg ServerConfig) (*Server, error) { }, adaptorSecret: cfg.AdaptorSecret, } + s.bestBlockHeight.Store(-1) if cfg.ReadyTimeoutSeconds <= 0 { return nil, fmt.Errorf("readytimeoutseconds cfg param must be greater than 0") @@ -251,6 +257,11 @@ func NewServer(id *zkidentity.ShortID, cfg ServerConfig) (*Server, error) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) go func() { defer cancel(); s.watcher.ProcessBlockConnected(ctx) }() } + go func() { + if _, err := s.refreshBestBlockHeight(context.Background()); err != nil { + s.log.Warnf("refresh best block height: %v", err) + } + }() }, } @@ -261,6 +272,10 @@ func NewServer(id *zkidentity.ShortID, cfg ServerConfig) (*Server, error) { s.dcrd = c s.log.Infof("Connected to dcrd at %s", cfg.DcrdHostPort) + if _, err := s.refreshBestBlockHeight(context.Background()); err != nil { + s.log.Warnf("initial block height fetch failed: %v", err) + } + // Start chain watcher to keep tip and mempool for watched scripts s.watcher = chainwatcher.NewChainWatcher(s.log, s.dcrd) @@ -633,6 +648,82 @@ func (s *Server) removeWaitingRoom(wr *ponggame.WaitingRoom, msg string) { s.log.Debugf("Waiting room %s removed (%s)", wr.ID, msg) } +func (s *Server) refreshBestBlockHeight(parent context.Context) (int64, error) { + if s.dcrd == nil { + return 0, fmt.Errorf("dcrd rpc client not configured") + } + if parent == nil { + parent = context.Background() + } + ctx, cancel := context.WithTimeout(parent, 5*time.Second) + defer cancel() + height, err := s.dcrd.GetBlockCount(ctx) + if err != nil { + return 0, err + } + s.bestBlockHeight.Store(height) + return height, nil +} + +func (s *Server) currentBestBlockHeight(parent context.Context) (int64, error) { + if h := s.bestBlockHeight.Load(); h >= 0 { + return h, nil + } + return s.refreshBestBlockHeight(parent) +} + +func (s *Server) waitingRoomExpired(parent context.Context, wr *ponggame.WaitingRoom) (bool, int64, int64, uint32, error) { + if wr == nil { + return false, 0, 0, 0, fmt.Errorf("waiting room is nil") + } + wr.RLock() + opened := wr.OpenedHeight + csv := wr.CSVBlocks + wr.RUnlock() + if opened == 0 || csv == 0 { + return false, 0, 0, csv, nil + } + currentHeight, err := s.currentBestBlockHeight(parent) + if err != nil { + return false, 0, 0, csv, err + } + expiryHeight := opened + int64(csv) + return currentHeight > expiryHeight, currentHeight, expiryHeight, csv, nil +} + +func (s *Server) removeEscrowFromRooms(escrowID string) { + if escrowID == "" { + return + } + s.roomEscrowsMu.Lock() + defer s.roomEscrowsMu.Unlock() + for uid, rooms := range s.roomEscrows { + for roomID, eid := range rooms { + if eid == escrowID { + delete(rooms, roomID) + } + } + if len(rooms) == 0 { + delete(s.roomEscrows, uid) + } + } +} + +func (s *Server) escrowExpired(parent context.Context, es *escrowSession) (bool, int64, int64, uint32, error) { + if es == nil { + return false, 0, 0, 0, fmt.Errorf("escrow session is nil") + } + if es.openedHeight == 0 || es.csvBlocks == 0 { + return false, 0, 0, es.csvBlocks, nil + } + currentHeight, err := s.currentBestBlockHeight(parent) + if err != nil { + return false, 0, 0, es.csvBlocks, err + } + expiryHeight := es.openedHeight + int64(es.csvBlocks) + return currentHeight > expiryHeight, currentHeight, expiryHeight, es.csvBlocks, nil +} + // preSignSnapshot returns a consistent snapshot of the presign state while // holding a read lock only once. It includes the bound input id, the list of // input ids present in presign contexts, whether all contexts agree on the @@ -669,6 +760,20 @@ func (s *Server) manageWaitingRoom(ctx context.Context, wr *ponggame.WaitingRoom return ctx.Err() case <-ticker.C: + if !s.isF2P { + expired, currentHeight, expiryHeight, csv, err := s.waitingRoomExpired(ctx, wr) + if err != nil { + s.log.Warnf("waiting room %s expiry check failed: %v", wr.ID, err) + } else if expired { + wr.RLock() + opened := wr.OpenedHeight + wr.RUnlock() + msg := fmt.Sprintf("waiting room expired (csv=%d, opened_height=%d, expiry_height=%d, current_height=%d)", csv, opened, expiryHeight, currentHeight) + s.removeWaitingRoom(wr, msg) + return nil + } + } + players := wr.ReadyPlayers() if len(players) < 2 { continue diff --git a/server/waitingroom.go b/server/waitingroom.go index bd5f4a0..512db4d 100644 --- a/server/waitingroom.go +++ b/server/waitingroom.go @@ -123,11 +123,18 @@ func (s *Server) CreateWaitingRoom(ctx context.Context, req *pong.CreateWaitingR return nil, fmt.Errorf("require funded escrow to create room (0-conf ok): %w", err) } + currentHeight, err := s.refreshBestBlockHeight(ctx) + if err != nil { + return nil, fmt.Errorf("failed to fetch current block height: %w", err) + } + // Create room; bet uses host escrow's betAtoms. wr, err := ponggame.NewWaitingRoom(hostPlayer, int64(es.betAtoms)) if err != nil { return nil, fmt.Errorf("failed to create waiting room: %v", err) } + wr.OpenedHeight = currentHeight + wr.CSVBlocks = es.csvBlocks hostPlayer.WR = wr // Bind host escrow to room.