libtorrent icon indicating copy to clipboard operation
libtorrent copied to clipboard

Assertion failed on a magnet link with `!p.is_seed()` on `master` branch

Open baseplate-admin opened this issue 5 months ago • 8 comments

Hi, I have this magnet link

magnet:?xt=urn:btih:F9B4F6C8D8E1F8B13BB2468D1945A904285CE3C2&dn=Call+of+Duty%3A+Vanguard+%28v1.26+Campaign%2FZombies+%2B+Bonus+OST%2C+MULTi13%29+%5BFitGirl+Repack%2C+Selective+Download+-+from+41.8+GB%5D&tr=udp%3A%2F%2Fopentor.net%3A6969&tr=udp%3A%2F%2Ftracker.torrent.eu.org%3A451%2Fannounce&tr=udp%3A%2F%2Ftracker.theoks.net%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.ccp.ovh%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337%2Fannounce&tr=http%3A%2F%2Ftracker.opentrackr.org%3A1337%2Fannounce&tr=udp%3A%2F%2Fopen.stealth.si%3A80%2Fannounce&tr=https%3A%2F%2Ftracker.tamersunion.org%3A443%2Fannounce&tr=udp%3A%2F%2Fexplodie.org%3A6969%2Fannounce&tr=http%3A%2F%2Ftracker.bt4g.com%3A2095%2Fannounce&tr=udp%3A%2F%2Fbt2.archive.org%3A6969%2Fannounce&tr=udp%3A%2F%2Fbt1.archive.org%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.filemail.com%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker1.bt.moack.co.kr%3A80%2Fannounce&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337%2Fannounce&tr=http%3A%2F%2Ftracker.openbittorrent.com%3A80%2Fannounce&tr=udp%3A%2F%2Fopentracker.i2p.rocks%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.internetwarriors.net%3A1337%2Fannounce&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969%2Fannounce&tr=udp%3A%2F%2Fcoppersurfer.tk%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.zer0day.to%3A1337%2Fannounce
Please include the following information:

version: 2.1.0.0-283cea598

file: '..\..\src\torrent.cpp'
line: 8998
function: check_invariant
expression: !p.is_seed()

stack:
 0: 00007FF8F98649A7 PyInit_libtorrent +1001079

baseplate-admin avatar Jul 17 '25 06:07 baseplate-admin

Minimal code to reproduce this issue:

import asyncio
import libtorrent as lt

MAGNET_LINK = r"magnet:?xt=urn:btih:F9B4F6C8D8E1F8B13BB2468D1945A904285CE3C2&dn=Call+of+Duty%3A+Vanguard+%28v1.26+Campaign%2FZombies+%2B+Bonus+OST%2C+MULTi13%29+%5BFitGirl+Repack%2C+Selective+Download+-+from+41.8+GB%5D&tr=udp%3A%2F%2Fopentor.net%3A6969&tr=udp%3A%2F%2Ftracker.torrent.eu.org%3A451%2Fannounce&tr=udp%3A%2F%2Ftracker.theoks.net%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.ccp.ovh%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337%2Fannounce&tr=http%3A%2F%2Ftracker.opentrackr.org%3A1337%2Fannounce&tr=udp%3A%2F%2Fopen.stealth.si%3A80%2Fannounce&tr=https%3A%2F%2Ftracker.tamersunion.org%3A443%2Fannounce&tr=udp%3A%2F%2Fexplodie.org%3A6969%2Fannounce&tr=http%3A%2F%2Ftracker.bt4g.com%3A2095%2Fannounce&tr=udp%3A%2F%2Fbt2.archive.org%3A6969%2Fannounce&tr=udp%3A%2F%2Fbt1.archive.org%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.filemail.com%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker1.bt.moack.co.kr%3A80%2Fannounce&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337%2Fannounce&tr=http%3A%2F%2Ftracker.openbittorrent.com%3A80%2Fannounce&tr=udp%3A%2F%2Fopentracker.i2p.rocks%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.internetwarriors.net%3A1337%2Fannounce&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969%2Fannounce&tr=udp%3A%2F%2Fcoppersurfer.tk%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.zer0day.to%3A1337%2Fannounce"


async def wait_for_metadata(handle, timeout=20):
    for i in range(timeout * 10):
        if handle.has_metadata():
            return True
        await asyncio.sleep(0.1)
    return False


async def main():
    ses = lt.session()
    ses.listen_on(6881, 6891)

    params = lt.parse_magnet_uri(MAGNET_LINK)
    params.save_path = "./downloads"
    params.flags |= lt.torrent_flags.paused  # Add paused

    handle = ses.add_torrent(params)

    print("Added torrent, waiting for metadata...")

    has_meta = await wait_for_metadata(handle)
    if not has_meta:
        print("Timeout waiting for metadata")
        return

    print("Metadata fetched!")

    # Already paused, but just in case
    handle.pause()

    print("Torrent paused.")

    # Print status info
    status = handle.status()
    print(
        f"Torrent status: state={status.state}, is_seed={handle.is_seed()}, num_peers={status.num_peers}"
    )


if __name__ == "__main__":
    asyncio.run(main())

baseplate-admin avatar Jul 17 '25 06:07 baseplate-admin

setup script:

import json
import os
import platform
import re
import shutil
import sys
import zipfile
from io import BytesIO
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen


def get_os_name_for_asset():
    system = platform.system().lower()
    if system == "darwin":
        return "macos"
    elif system == "linux":
        return "ubuntu"  # Linux builds use ubuntu naming here
    elif system == "windows":
        return "windows"
    else:
        raise RuntimeError(f"Unsupported OS: {system}")


def get_python_minor_version():
    return sys.version_info.minor


def get_venv_site_packages():
    prefix = sys.prefix  # venv root when inside venv
    py_version = f"python{sys.version_info.major}.{sys.version_info.minor}"

    candidate_paths = [
        os.path.join(prefix, "lib", py_version, "site-packages"),  # Unix/macOS
        os.path.join(prefix, "Lib", "site-packages"),  # Windows
    ]
    for path in candidate_paths:
        if os.path.isdir(path):
            return path
    return prefix


def find_libtorrent_folder(root_dir):
    for dirpath, dirnames, filenames in os.walk(root_dir):
        if "libtorrent" in dirnames:
            return os.path.join(dirpath, "libtorrent")
    return None


def copy_folder(src_folder, dst_folder):
    print(f"Copying from {src_folder} to {dst_folder} ...")
    if os.path.exists(dst_folder):
        shutil.rmtree(dst_folder)
    shutil.copytree(src_folder, dst_folder)


def fetch_json(url):
    print(f"Fetching JSON from {url} ...")
    req = Request(url, headers={"User-Agent": "python-urllib"})
    try:
        with urlopen(req) as resp:
            data = resp.read()
            return json.loads(data.decode())
    except HTTPError as e:
        print(f"HTTP error: {e.code} {e.reason}")
    except URLError as e:
        print(f"URL error: {e.reason}")
    return None


def download_file(url):
    print(f"Downloading {url} ...")
    req = Request(url, headers={"User-Agent": "python-urllib"})
    try:
        with urlopen(req) as resp:
            return resp.read()
    except HTTPError as e:
        print(f"HTTP error: {e.code} {e.reason}")
    except URLError as e:
        print(f"URL error: {e.reason}")
    return None


def main():
    os_name = get_os_name_for_asset()
    py_minor = get_python_minor_version()
    pattern = re.compile(
        rf"python-bindings-{os_name}-latest-py3\.{py_minor}-build\.zip"
    )
    print(f"Looking for assets matching OS='{os_name}', Python 3.{py_minor}")

    GITHUB_API_RELEASES_URL = (
        "https://api.github.com/repos/baseplate-admin/libtorrent-python/releases/latest"
    )

    release_data = fetch_json(GITHUB_API_RELEASES_URL)
    if not release_data:
        print("Failed to fetch release data.")
        return

    assets = release_data.get("assets", [])
    matching_asset = None

    for asset in assets:
        name = asset.get("name", "")
        if pattern.fullmatch(name):
            matching_asset = asset
            break

    if not matching_asset:
        print("No matching asset found for pattern:", pattern.pattern)
        return

    print(f"Found asset: {matching_asset['name']}")
    download_url = matching_asset["browser_download_url"]

    zip_bytes = download_file(download_url)
    if not zip_bytes:
        print("Failed to download the asset.")
        return

    tmp_extract_dir = "tmp_extract"
    if os.path.exists(tmp_extract_dir):
        shutil.rmtree(tmp_extract_dir)
    os.makedirs(tmp_extract_dir)

    with zipfile.ZipFile(BytesIO(zip_bytes)) as z:
        print(f"Extracting to {tmp_extract_dir} ...")
        z.extractall(tmp_extract_dir)

    libtorrent_src = find_libtorrent_folder(tmp_extract_dir)
    if not libtorrent_src:
        print("Error: 'libtorrent' folder not found in extracted content.")
        return

    site_packages_path = get_venv_site_packages()
    print("Detected site-packages folder:", site_packages_path)

    dst = os.path.join(site_packages_path, "libtorrent")
    copy_folder(libtorrent_src, dst)

    shutil.rmtree(tmp_extract_dir)

    print("Done.")


if __name__ == "__main__":
    main()

baseplate-admin avatar Jul 17 '25 06:07 baseplate-admin

Is it a regression?

The issue is non-existant on pypi libtorrent version 2.0.11.0, but exists with 2.1.0.0

baseplate-admin avatar Jul 17 '25 07:07 baseplate-admin

I have narrowed it down:

import asyncio
import libtorrent as lt
import tempfile

MAGNET_LINK = r"magnet:?xt=urn:btih:F9B4F6C8D8E1F8B13BB2468D1945A904285CE3C2&dn=Call+of+Duty%3A+Vanguard+%28v1.26+Campaign%2FZombies+%2B+Bonus+OST%2C+MULTi13%29+%5BFitGirl+Repack%2C+Selective+Download+-+from+41.8+GB%5D&tr=udp%3A%2F%2Fopentor.net%3A6969&tr=udp%3A%2F%2Ftracker.torrent.eu.org%3A451%2Fannounce&tr=udp%3A%2F%2Ftracker.theoks.net%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.ccp.ovh%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337%2Fannounce&tr=http%3A%2F%2Ftracker.opentrackr.org%3A1337%2Fannounce&tr=udp%3A%2F%2Fopen.stealth.si%3A80%2Fannounce&tr=https%3A%2F%2Ftracker.tamersunion.org%3A443%2Fannounce&tr=udp%3A%2F%2Fexplodie.org%3A6969%2Fannounce&tr=http%3A%2F%2Ftracker.bt4g.com%3A2095%2Fannounce&tr=udp%3A%2F%2Fbt2.archive.org%3A6969%2Fannounce&tr=udp%3A%2F%2Fbt1.archive.org%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.filemail.com%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker1.bt.moack.co.kr%3A80%2Fannounce&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337%2Fannounce&tr=http%3A%2F%2Ftracker.openbittorrent.com%3A80%2Fannounce&tr=udp%3A%2F%2Fopentracker.i2p.rocks%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.internetwarriors.net%3A1337%2Fannounce&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969%2Fannounce&tr=udp%3A%2F%2Fcoppersurfer.tk%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.zer0day.to%3A1337%2Fannounce"

async def main():
    session = lt.session()
    session.apply_settings({"listen_interfaces": "0.0.0.0:6881", "enable_dht": True})

    with tempfile.TemporaryDirectory() as save_path:
        params = lt.parse_magnet_uri(MAGNET_LINK)
        params.save_path = save_path
        params.storage_mode = lt.storage_mode_t.storage_mode_sparse
        params.flags |= lt.torrent_flags.auto_managed
        handle = session.add_torrent(params)

        print("Waiting for metadata...")
        while not handle.status().has_metadata:
            await asyncio.sleep(1)

        print("Metadata fetched!")

        # === Metadata output ===
        info = handle.get_torrent_info()

        print(f"Torrent name: {info.name()}")
        print(f"Total size: {info.total_size() / (1024**3):.2f} GB")
        print(f"Piece size: {info.piece_length() // 1024} KB")
        print(f"Number of pieces: {info.num_pieces()}")
        print(f"Info hash (SHA1): {str(info.info_hash())}")
        print(f"Files:")
        for f in info.files():
            print(f" - {f.path} ({f.size / (1024**2):.2f} MB)")


if __name__ == "__main__":
    asyncio.run(main())

This sometimes fails, sometimes it passes.

Example of two runs:

(backend) PS C:\Programming\bittorrent-client\backend> python .\test.py
Waiting for metadata...
Metadata fetched!
(backend) PS C:\Programming\bittorrent-client\backend> python .\test.py
Waiting for metadata...
assertion failed. Please file a bugreport at https://github.com/arvidn/libtorrent/issues
Please include the following information:

version: 2.1.0.0-283cea598

file: '..\..\src\torrent.cpp'
line: 9003
function: check_invariant
expression: !p.is_seed()

stack:
 0: 00007FF8FBFB4D67 PyInit_libtorrent +1002039

(backend) PS C:\Programming\bittorrent-client\backend> python .\test.py
Waiting for metadata...
Metadata fetched!
C:\Programming\bittorrent-client\backend\test.py:28: DeprecationWarning: get_torrent_info() is deprecated
  info = handle.get_torrent_info()
Torrent name: Call of Duty - Vanguard [FitGirl Repack]
Total size: 60.43 GB
Piece size: 4096 KB
Number of pieces: 15472
Info hash (SHA1): f9b4f6c8d8e1f8b13bb2468d1945a904285ce3c2
Files:
C:\Programming\bittorrent-client\backend\test.py:36: DeprecationWarning: files() is deprecated
  for f in info.files():
C:\Programming\bittorrent-client\backend\test.py:36: DeprecationWarning: __iter__ is deprecated
  for f in info.files():
C:\Programming\bittorrent-client\backend\test.py:37: DeprecationWarning: file_entry is deprecated
  print(f" - {f.path} ({f.size / (1024**2):.2f} MB)")
 - Call of Duty - Vanguard [FitGirl Repack]\fg-01.bin (32753.40 MB)
 - Call of Duty - Vanguard [FitGirl Repack]\fg-02.bin (4966.37 MB)
 - Call of Duty - Vanguard [FitGirl Repack]\fg-03.bin (3209.79 MB)
 - Call of Duty - Vanguard [FitGirl Repack]\fg-04.bin (1568.91 MB)
 - Call of Duty - Vanguard [FitGirl Repack]\fg-05.bin (316.99 MB)
 - Call of Duty - Vanguard [FitGirl Repack]\fg-06.bin (0.26 MB)
 - Call of Duty - Vanguard [FitGirl Repack]\fg-optional-arabic-and-chinese.bin (157.18 MB)
 - Call of Duty - Vanguard [FitGirl Repack]\fg-optional-bonus-soundtrack.bin (158.26 MB)
 - Call of Duty - Vanguard [FitGirl Repack]\fg-optional-brazilian.bin (1650.77 MB)
 - Call of Duty - Vanguard [FitGirl Repack]\fg-optional-french.bin (1529.62 MB)
 - Call of Duty - Vanguard [FitGirl Repack]\fg-optional-german.bin (1648.76 MB)
 - Call of Duty - Vanguard [FitGirl Repack]\fg-optional-italian.bin (1626.05 MB)
 - Call of Duty - Vanguard [FitGirl Repack]\fg-optional-japanese.bin (3889.91 MB)
 - Call of Duty - Vanguard [FitGirl Repack]\fg-optional-korean.bin (1775.95 MB)
 - Call of Duty - Vanguard [FitGirl Repack]\fg-optional-mexican.bin (1657.28 MB)
 - Call of Duty - Vanguard [FitGirl Repack]\fg-optional-polish.bin (1635.27 MB)
 - Call of Duty - Vanguard [FitGirl Repack]\fg-optional-russian.bin (1674.94 MB)
 - Call of Duty - Vanguard [FitGirl Repack]\fg-optional-spanish.bin (1659.48 MB)
 - Call of Duty - Vanguard [FitGirl Repack]\MD5\fitgirl-bins.md5 (0.00 MB)
 - Call of Duty - Vanguard [FitGirl Repack]\MD5\QuickSFV.EXE (0.10 MB)
 - Call of Duty - Vanguard [FitGirl Repack]\MD5\QuickSFV.ini (0.00 MB)
 - Call of Duty - Vanguard [FitGirl Repack]\setup.exe (5.96 MB)
 - Call of Duty - Vanguard [FitGirl Repack]\Verify BIN files before installation.bat (0.00 MB)

baseplate-admin avatar Jul 17 '25 09:07 baseplate-admin

Patching the line out, libtorrent works as expected,

https://github.com/baseplate-admin/libtorrent-python/blob/main/patches/fix.patch

baseplate-admin avatar Jul 17 '25 15:07 baseplate-admin

that link is broken. can you post the patch directly instead?

arvidn avatar Jul 19 '25 19:07 arvidn

# This patch removes an assert that checks `!p.is_seed()` in non-seed peers.
# Reason: The check was redundant and caused false positives in edge cases.
# Check: https://github.com/arvidn/libtorrent/issues/7988


diff --git a/src/torrent.cpp b/src/torrent.cpp
index 9d046ab9a..a608187bd 100644
--- a/src/torrent.cpp
+++ b/src/torrent.cpp
@@ -8998,10 +8998,7 @@ namespace {
 				{
 					++seeds;
 				}
-				else
-				{
-					TORRENT_ASSERT(!p.is_seed());
-				}
+				
 			}
 
 			for (auto const& j : p.request_queue())

baseplate-admin avatar Jul 20 '25 04:07 baseplate-admin

Hey, sorry for broken link, i just removed the else statement

baseplate-admin avatar Jul 20 '25 04:07 baseplate-admin