Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions ponggame/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
35 changes: 35 additions & 0 deletions server/referee.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand Down
105 changes: 105 additions & 0 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"os"
"path/filepath"
"sync"
"sync/atomic"
"time"

"github.com/companyzero/bisonrelay/zkidentity"
Expand Down Expand Up @@ -97,6 +98,7 @@ type escrowSession struct {
csvBlocks uint32
redeemScriptHex string
pkScriptHex string
openedHeight int64

// ----------------- runtime state (protected by mu) -------------
mu sync.RWMutex
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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)
}
}()
},
}

Expand All @@ -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)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions server/waitingroom.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down