{"id":171,"date":"2024-07-11T20:31:10","date_gmt":"2024-07-11T20:31:10","guid":{"rendered":"https:\/\/santiagomarquezsolis.com\/?p=171"},"modified":"2024-08-16T20:33:37","modified_gmt":"2024-08-16T20:33:37","slug":"understanding-the-bitcoin-handshake-using-rust","status":"publish","type":"post","link":"https:\/\/santiagomarquezsolis.com\/index.php\/2024\/07\/11\/understanding-the-bitcoin-handshake-using-rust\/","title":{"rendered":"Understanding the Bitcoin handshake using Rust"},"content":{"rendered":"<p id=\"ember1290\" class=\"ember-view reader-text-block__paragraph\">A few days ago, I had a brief technical chat with a friend who finds the world of distributed systems fascinating, regarding the Bitcoin handshake process. Although our conversation initially revolved around the necessity of achieving consensus within systems, we soon focused on the necessary step for one system to start communicating with another, known as the handshake. We only touched on a few aspects, but later, with more time on my hands and out of curiosity, I decided to delve deeper into the Bitcoin network handshake. I realized it would be cool to try to implement it in Rust, more as a theoretical exercise than anything else.<\/p>\n<p id=\"ember1291\" class=\"ember-view reader-text-block__paragraph\">Since I enjoyed this topic, I got to work, and this little article is the result. The first thing to note is that the handshake is a fundamental process in communication between distributed systems, including computer networks and communication protocols like Bitcoin. The handshake has its roots in the early days of computing and telecommunications. In the first computer networks and telecommunications systems, the handshake was a fundamental mechanism for establishing connections between devices. As computer networks evolved, the handshake became a standard component of many communication protocols. For example, the TCP (Transmission Control Protocol), developed in the 1970s, uses a three-way handshake process to establish reliable connections between devices on a network. With the advent of the Internet, the handshake became even more important in security protocols (like SSL\/TLS (Secure Sockets Layer \/ Transport Layer Security).<\/p>\n<p id=\"ember1292\" class=\"ember-view reader-text-block__paragraph\">Here are some key reasons why the handshake is crucial (the following is gathered from ChatGPT, let&#8217;s not kid ourselves by saying that everything published is 100% original, although reviewed and corrected, as it sometimes makes significant errors):<\/p>\n<ol>\n<li>Establishing a Secure Connection: The handshake ensures that two systems can establish a secure and reliable connection before exchanging data. In the context of Bitcoin, it is vital to ensure that connecting nodes are using compatible protocol versions and can exchange data securely.<\/li>\n<li>Compatibility and Synchronization: During the handshake, systems exchange information about their software versions and capabilities. This ensures that both systems understand the same protocols and can synchronize correctly. In Bitcoin, this step is essential for blockchain synchronization between nodes.<\/li>\n<li>Authentication: The handshake process can include authentication mechanisms that allow systems to verify each other&#8217;s identity. This is crucial to prevent attacks from malicious entities attempting to intercept or manipulate communication.<\/li>\n<li>Parameter Negotiation: The handshake allows systems to negotiate communication parameters, such as data packet size, compression and encryption methods, and other technical details that optimize communication efficiency and security.<\/li>\n<li>Data Integrity: By establishing a reliable and secure connection, the handshake helps ensure that transmitted data is not corrupted or intercepted. In Bitcoin, this ensures that transactions and blocks exchanged between nodes are accurate and verifiable.<\/li>\n<\/ol>\n<p id=\"ember1294\" class=\"ember-view reader-text-block__paragraph\">Now, the million-dollar question: how many steps does the Bitcoin handshake protocol have? To answer this, we would need to consult the Bitcoin Developer Reference and some of the BIPs dedicated to this topic, including BIP14 (versioning protocol) and BIP37 (bloom filtering for lightweight clients). It&#8217;s a lot of work to read them and gather all the documentation, so from my point of view, I&#8217;ve summarized all this into a nine-step process, with its implementation in Rust.<\/p>\n<p id=\"ember1295\" class=\"ember-view reader-text-block__paragraph\">You will see that the source code I share on Github has several approaches (5 plus the final one), and the <a class=\"app-aware-link \" href=\"https:\/\/github.com\/santiagomarquezsolis-me\/rust-bitcoin-handshake\" target=\"_self\" data-test-app-aware-link=\"\" rel=\"noopener\">README.md<\/a> only covers the latest implementation (<a class=\"app-aware-link \" href=\"https:\/\/github.com\/santiagomarquezsolis-me\/rust-bitcoin-handshake\/blob\/main\/src\/main.rs\" target=\"_self\" data-test-app-aware-link=\"\" rel=\"noopener\">main.rs<\/a>). The most important crates we will use are the following: Here&#8217;s a summary of each:<\/p>\n<ul>\n<li>bitcoin: A library for Bitcoin-related functionalities.<\/li>\n<li>hex: A library for encoding and decoding hexadecimal strings.<\/li>\n<li>rand: A library for random number generation.<\/li>\n<li>trust_dns_resolver: A library for DNS resolution.<\/li>\n<li>tokio: An asynchronous runtime for Rust, enabling efficient handling of I\/O and other asynchronous operations.<\/li>\n<\/ul>\n<h3 id=\"ember1297\" class=\"ember-view reader-text-block__heading-3\">DNS Process to Select a Bitcoin Peer<\/h3>\n<p id=\"ember1298\" class=\"ember-view reader-text-block__paragraph\">If you want to perform the handshake, you need to connect with a peer, sometimes if you have a trustful peer, you can use its IP, but is a better approach to use the Bitcoin seed DNS to retrieve this information. The code utilizes the trust-dns-resolver crate to perform DNS resolution. Here&#8217;s an explanation of how this process works:<\/p>\n<h3 id=\"ember1299\" class=\"ember-view reader-text-block__heading-3\">1. Reading the Configuration<\/h3>\n<p id=\"ember1300\" class=\"ember-view reader-text-block__paragraph\">The first step is to read the configuration file config.toml, which contains the DNS seeds. These seeds are domain names that resolve to the IP addresses of Bitcoin nodes.<\/p>\n<pre class=\"reader-text-block__code-block\">#[derive(Deserialize)]\r\nstruct Config {\r\n    nodes: Nodes,\r\n}\r\n\r\n#[derive(Deserialize)]\r\nstruct Nodes {\r\n    seeds: Vec&lt;String&gt;,\r\n}\r\n\r\nfn read_config() -&gt; Config {\r\n    let config_content = fs::read_to_string(\"config.toml\").expect(\"Failed to read config file\");\r\n    toml::from_str(&amp;config_content).expect(\"Failed to parse config file\")\r\n}<\/pre>\n<p id=\"ember1301\" class=\"ember-view reader-text-block__paragraph\">In this example, the config.toml file might look like this:<\/p>\n<pre class=\"reader-text-block__code-block\">[nodes]\r\nseeds = [\"seed.bitcoin.sipa.be\", \"dnsseed.bluematt.me\"]<\/pre>\n<h3 id=\"ember1302\" class=\"ember-view reader-text-block__heading-3\">2. Selecting a Seed<\/h3>\n<p id=\"ember1303\" class=\"ember-view reader-text-block__paragraph\">Once the configuration is read, a seed is randomly chosen from the list of seeds provided.<\/p>\n<pre class=\"reader-text-block__code-block\">let config = read_config();\r\nlet seeds = &amp;config.nodes.seeds;\r\n\r\nlet seed = seeds.choose(&amp;mut rand::thread_rng()).expect(\"No seed nodes found\");<\/pre>\n<h3 id=\"ember1304\" class=\"ember-view reader-text-block__heading-3\">3. Resolving the Seed to IP Addresses<\/h3>\n<p id=\"ember1305\" class=\"ember-view reader-text-block__paragraph\">The selected seed (a DNS name) is then resolved to a list of IP addresses using the trust-dns-resolver crate. This involves querying the DNS server for A (IPv4) or AAAA (IPv6) records associated with the seed domain name.<\/p>\n<pre class=\"reader-text-block__code-block\">use trust_dns_resolver::config::*;\r\nuse trust_dns_resolver::AsyncResolver;\r\n\r\nasync fn resolve_bitcoin_peers(seed: &amp;str) -&gt; Vec&lt;SocketAddr&gt; {\r\n    let resolver = AsyncResolver::tokio(ResolverConfig::default(), ResolverOpts::default()).unwrap();\r\n    let response = resolver.lookup_ip(seed).await.unwrap();\r\n    let mut peers = Vec::new();\r\n    for ip in response.iter() {\r\n        let socket_addr = SocketAddr::new(ip, 8333);  \/\/ 8333 is the default port for Bitcoin\r\n        peers.push(socket_addr);\r\n    }\r\n    peers\r\n}<\/pre>\n<p id=\"ember1306\" class=\"ember-view reader-text-block__paragraph\">Here\u2019s what happens in detail:<\/p>\n<ol>\n<li><strong>Resolver Configuration<\/strong>: An asynchronous DNS resolver is created with default configurations.<\/li>\n<li><strong>DNS Query<\/strong>: The lookup_ip method queries the DNS server for IP addresses associated with the seed.<\/li>\n<li><strong>IP Collection<\/strong>: The IP addresses returned by the DNS server are collected into a vector of SocketAddr structures, with each IP address paired with the default Bitcoin port 8333.<\/li>\n<\/ol>\n<h3 id=\"ember1308\" class=\"ember-view reader-text-block__heading-3\">4. Connecting to a Peer<\/h3>\n<p id=\"ember1309\" class=\"ember-view reader-text-block__paragraph\">With the list of resolved IP addresses, the next step is to attempt to connect to these peers. This involves trying to establish a TCP connection with each IP address.<\/p>\n<pre class=\"reader-text-block__code-block\">use tokio::net::TcpStream;\r\nuse tokio::time::timeout;\r\nuse std::time::Duration;\r\n\r\nasync fn connect_to_peer(peer: SocketAddr) -&gt; Result&lt;TcpStream, ()&gt; {\r\n    match timeout(Duration::from_secs(5), TcpStream::connect(peer)).await {\r\n        Ok(Ok(stream)) =&gt; {\r\n            println!(\"Connected to peer: {}\", peer);\r\n            Ok(stream)\r\n        }\r\n        _ =&gt; {\r\n            println!(\"Failed to connect to peer: {}\", peer);\r\n            Err(())\r\n        }\r\n    }\r\n}<\/pre>\n<p id=\"ember1310\" class=\"ember-view reader-text-block__paragraph\">OK. now we can connect with a peer and begin our handshake \ud83d\ude42<\/p>\n<h2 id=\"ember1311\" class=\"ember-view reader-text-block__heading-2\">My 9 steps for the handshake<\/h2>\n<h3 id=\"ember1312\" class=\"ember-view reader-text-block__heading-3\">1. Create the version Message<\/h3>\n<p id=\"ember1313\" class=\"ember-view reader-text-block__paragraph\">The first step is to create a version message that contains information about the node, such as the protocol version, services supported, timestamp, receiver&#8217;s address, sender&#8217;s address, nonce, user agent, start height, and relay flag.<\/p>\n<pre class=\"reader-text-block__code-block\">fn create_version_message() -&gt; Vec&lt;u8&gt; {\r\n    let mut buf = Vec::new();\r\n\r\n    \/\/ Version\r\n    WriteBytesExt::write_u32::&lt;LittleEndian&gt;(&amp;mut buf, 70015).unwrap();\r\n\r\n    \/\/ Services\r\n    WriteBytesExt::write_u64::&lt;LittleEndian&gt;(&amp;mut buf, 0).unwrap();\r\n\r\n    \/\/ Timestamp\r\n    WriteBytesExt::write_u64::&lt;LittleEndian&gt;(&amp;mut buf, Utc::now().timestamp() as u64).unwrap();\r\n\r\n    \/\/ Addr_recv (Receiver's address)\r\n    WriteBytesExt::write_u64::&lt;LittleEndian&gt;(&amp;mut buf, 0).unwrap();\r\n    let ip = Ipv4Addr::new(0, 0, 0, 0).octets();\r\n    buf.extend_from_slice(&amp;[0; 10]);\r\n    buf.extend_from_slice(&amp;ip);\r\n    WriteBytesExt::write_u16::&lt;BigEndian&gt;(&amp;mut buf, 8333).unwrap();\r\n\r\n    \/\/ Addr_from (Sender's address)\r\n    WriteBytesExt::write_u64::&lt;LittleEndian&gt;(&amp;mut buf, 0).unwrap();\r\n    let ip = Ipv4Addr::new(0, 0, 0, 0).octets();\r\n    buf.extend_from_slice(&amp;[0; 10]);\r\n    buf.extend_from_slice(&amp;ip);\r\n    WriteBytesExt::write_u16::&lt;BigEndian&gt;(&amp;mut buf, 8333).unwrap();\r\n\r\n    \/\/ Nonce\r\n    WriteBytesExt::write_u64::&lt;LittleEndian&gt;(&amp;mut buf, rand::random()).unwrap();\r\n\r\n    \/\/ User Agent\r\n    let user_agent = \"\/Satoshi:0.7.2\/\".as_bytes();\r\n    WriteBytesExt::write_u8(&amp;mut buf, user_agent.len() as u8).unwrap();\r\n    buf.extend_from_slice(user_agent);\r\n\r\n    \/\/ Start Height\r\n    WriteBytesExt::write_u32::&lt;LittleEndian&gt;(&amp;mut buf, 0).unwrap();\r\n\r\n    \/\/ Relay\r\n    WriteBytesExt::write_u8(&amp;mut buf, 1).unwrap();\r\n\r\n    buf\r\n}<\/pre>\n<h3 id=\"ember1314\" class=\"ember-view reader-text-block__heading-3\">2. Send the version Message<\/h3>\n<p id=\"ember1315\" class=\"ember-view reader-text-block__paragraph\">Once the version message is created, it is sent to the peer.<\/p>\n<pre class=\"reader-text-block__code-block\">stream.write_all(&amp;version_message).await.unwrap();<\/pre>\n<h3 id=\"ember1316\" class=\"ember-view reader-text-block__heading-3\">3. Receive the version Message from the Peer<\/h3>\n<p id=\"ember1317\" class=\"ember-view reader-text-block__paragraph\">The node waits to receive a version message from the peer. A timeout is used to ensure the process does not hang indefinitely.<\/p>\n<pre class=\"reader-text-block__code-block\">let mut buf = [0; 1024];\r\nmatch timeout(Duration::from_secs(10), stream.read(&amp;mut buf)).await {\r\n    Ok(Ok(n)) if n &gt; 0 =&gt; {\r\n        let peer_version_message = &amp;buf[..n];\r\n        decode_version_message(peer_version_message);<\/pre>\n<h3 id=\"ember1318\" class=\"ember-view reader-text-block__heading-3\">4. Verify Protocol Version Compatibility<\/h3>\n<p id=\"ember1319\" class=\"ember-view reader-text-block__paragraph\">After receiving the version message, the node checks if the peer&#8217;s protocol version is compatible.<\/p>\n<pre class=\"reader-text-block__code-block\">if !is_version_compatible(peer_version_message) {\r\n    return Err(());\r\n}<\/pre>\n<h3 id=\"ember1320\" class=\"ember-view reader-text-block__heading-3\">5. Verify Supported Services<\/h3>\n<p id=\"ember1321\" class=\"ember-view reader-text-block__paragraph\">The node also checks if the services supported by the peer are compatible.<\/p>\n<pre class=\"reader-text-block__code-block\">if !are_services_compatible(peer_version_message) {\r\n    return Err(());\r\n}\r\n7. Send the verack Message<\/pre>\n<p id=\"ember1322\" class=\"ember-view reader-text-block__paragraph\">If all checks are passed, the node sends a verack message to acknowledge the version message.<\/p>\n<h3 id=\"ember1323\" class=\"ember-view reader-text-block__heading-3\">6. Check for Loopback Connection<\/h3>\n<p id=\"ember1324\" class=\"ember-view reader-text-block__paragraph\">To avoid connecting to itself, the node verifies if the nonce in the version message indicates a loopback connection.<\/p>\n<pre class=\"reader-text-block__code-block\">if is_loopback_connection(peer_version_message) {\r\n    return Err(());\r\n}<\/pre>\n<h3 id=\"ember1325\" class=\"ember-view reader-text-block__heading-3\">7. Send the verack Message<\/h3>\n<p id=\"ember1326\" class=\"ember-view reader-text-block__paragraph\">If all checks are passed, the node sends a verack message to acknowledge the version message.<\/p>\n<pre class=\"reader-text-block__code-block\">let verack_message = create_verack_message();\r\nstream.write_all(&amp;verack_message).await.unwrap();<\/pre>\n<h3 id=\"ember1327\" class=\"ember-view reader-text-block__heading-3\">8. Receive the verack Message from the Peer<\/h3>\n<p id=\"ember1328\" class=\"ember-view reader-text-block__paragraph\">The node waits to receive a verack message from the peer, confirming that the peer acknowledges the version message.<\/p>\n<pre class=\"reader-text-block__code-block\">match timeout(Duration::from_secs(5), stream.read(&amp;mut buf)).await {\r\n    Ok(Ok(n)) if n &gt; 0 =&gt; {\r\n        let peer_verack_message = &amp;buf[..n];\r\n        decode_verack_message(peer_verack_message);\r\n        Ok(())\r\n    }\r\n    _ =&gt; {\r\n        Err(())\r\n    }\r\n}<\/pre>\n<h3 id=\"ember1329\" class=\"ember-view reader-text-block__heading-3\">9. Handshake Completed<\/h3>\n<p id=\"ember1330\" class=\"ember-view reader-text-block__paragraph\">If the node successfully receives and decodes the verack message, the handshake is considered complete, and the node can now communicate with the peer.<\/p>\n<pre class=\"reader-text-block__code-block\">println!(\"Handshake completed successfully\");<\/pre>\n<h3 id=\"ember1331\" class=\"ember-view reader-text-block__heading-3\">Detailed Explanation of Helper Functions<\/h3>\n<h3 id=\"ember1332\" class=\"ember-view reader-text-block__heading-3\">decode_version_message<\/h3>\n<p id=\"ember1333\" class=\"ember-view reader-text-block__paragraph\">This function decodes the version message received from the peer, extracting various fields such as version, services, timestamp, addresses, nonce, user agent, start height, and relay flag.<\/p>\n<pre class=\"reader-text-block__code-block\">fn decode_version_message(message: &amp;[u8]) {\r\n    let mut rdr = Cursor::new(message);\r\n\r\n    let version = ReadBytesExt::read_u32::&lt;LittleEndian&gt;(&amp;mut rdr).unwrap();\r\n    let services = ReadBytesExt::read_u64::&lt;LittleEndian&gt;(&amp;mut rdr).unwrap();\r\n    let timestamp = ReadBytesExt::read_u64::&lt;LittleEndian&gt;(&amp;mut rdr).unwrap();\r\n\r\n    \/\/ Receiver's address\r\n    let addr_recv_services = ReadBytesExt::read_u64::&lt;LittleEndian&gt;(&amp;mut rdr).unwrap();\r\n    let addr_recv_ip = {\r\n        let mut buf = [0u8; 16];\r\n        Read::read_exact(&amp;mut rdr, &amp;mut buf).unwrap();\r\n        buf\r\n    };\r\n    let addr_recv_port = ReadBytesExt::read_u16::&lt;BigEndian&gt;(&amp;mut rdr).unwrap();\r\n\r\n    \/\/ Sender's address\r\n    let addr_from_services = ReadBytesExt::read_u64::&lt;LittleEndian&gt;(&amp;mut rdr).unwrap();\r\n    let addr_from_ip = {\r\n        let mut buf = [0u8; 16];\r\n        Read::read_exact(&amp;mut rdr, &amp;mut buf).unwrap();\r\n        buf\r\n    };\r\n    let addr_from_port = ReadBytesExt::read_u16::&lt;BigEndian&gt;(&amp;mut rdr).unwrap();\r\n\r\n    let nonce = ReadBytesExt::read_u64::&lt;LittleEndian&gt;(&amp;mut rdr).unwrap();\r\n\r\n    \/\/ User agent (variable length)\r\n    let user_agent_length = ReadBytesExt::read_u8(&amp;mut rdr).unwrap();\r\n    let mut user_agent = vec![0u8; user_agent_length as usize];\r\n    Read::read_exact(&amp;mut rdr, &amp;mut user_agent).unwrap();\r\n\r\n    let start_height = ReadBytesExt::read_u32::&lt;LittleEndian&gt;(&amp;mut rdr).unwrap();\r\n    let relay = ReadBytesExt::read_u8(&amp;mut rdr).unwrap_or(1);\r\n\r\n    println!(\"Decoded version message:\");\r\n    println!(\"Version: {}\", version);\r\n    println!(\"Services: {}\", services);\r\n    println!(\"Timestamp: {}\", timestamp);\r\n    println!(\r\n        \"Receiver Address: {}:{} with services {}\",\r\n        addr_recv_ip.iter().map(|b| b.to_string()).collect::&lt;Vec&lt;_&gt;&gt;().join(\".\"),\r\n        addr_recv_port,\r\n        addr_recv_services\r\n    );\r\n    println!(\r\n        \"Sender Address: {}:{} with services {}\",\r\n        addr_from_ip.iter().map(|b| b.to_string()).collect::&lt;Vec&lt;_&gt;&gt;().join(\".\"),\r\n        addr_from_port,\r\n        addr_from_services\r\n    );\r\n    println!(\"Nonce: {}\", nonce);\r\n    println!(\"User Agent: {}\", String::from_utf8(user_agent).unwrap_or_default());\r\n    println!(\"Start Height: {}\", start_height);\r\n    println!(\"Relay: {}\", relay);\r\n}<\/pre>\n<h3 id=\"ember1334\" class=\"ember-view reader-text-block__heading-3\">decode_verack_message<\/h3>\n<p id=\"ember1335\" class=\"ember-view reader-text-block__paragraph\">The verack message is simple and doesn&#8217;t need much decoding. The function just prints out the received message.<\/p>\n<pre class=\"reader-text-block__code-block\">fn decode_verack_message(message: &amp;[u8]) {\r\n    println!(\"Decoded verack message: {:?}\", message);\r\n}<\/pre>\n<h2 id=\"ember1336\" class=\"ember-view reader-text-block__heading-2\">GitHub Repository<\/h2>\n<p id=\"ember1337\" class=\"ember-view reader-text-block__paragraph\">You can download the code in this <a class=\"app-aware-link \" href=\"https:\/\/github.com\/santiagomarquezsolis-me\/rust-bitcoin-handshake\" target=\"_self\" data-test-app-aware-link=\"\" rel=\"noopener\">link<\/a>, and you are free to use and modify it \ud83d\ude09 or if you see any errors please let me know to correct it.<\/p>\n<h2 id=\"ember1338\" class=\"ember-view reader-text-block__heading-2\">Conclusion<\/h2>\n<p id=\"ember1339\" class=\"ember-view reader-text-block__paragraph\">These nine steps form the core of the Bitcoin handshake protocol, ensuring that nodes can establish secure and compatible connections. By following this guide, you&#8217;ve built a Rust application that effectively handles this complex process, showcasing the power of Rust for network programming and asynchronous I\/O.<\/p>\n<p id=\"ember1340\" class=\"ember-view reader-text-block__paragraph\">This concludes the detailed explanation of the handshake steps in the Bitcoin protocol as implemented in the provided Rust code.<\/p>\n<p id=\"ember1341\" class=\"ember-view reader-text-block__paragraph\">Happy coding \ud83d\ude09<\/p>\n","protected":false},"excerpt":{"rendered":"<p>A few days ago, I had a brief technical chat with a friend who finds the world of distributed systems fascinating, regarding the Bitcoin handshake process. Although our conversation initially revolved around the necessity of achieving consensus within systems, we soon focused on the necessary step for one system to start communicating with another, known [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":172,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[60,43,2,12],"tags":[13,72,69],"class_list":["post-171","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-bitcoin","category-blockchain","category-blog","category-cripto","tag-bitcoin","tag-programacion","tag-rust"],"jetpack_featured_media_url":"https:\/\/santiagomarquezsolis.com\/wp-content\/uploads\/2024\/08\/1720722617775.png","_links":{"self":[{"href":"https:\/\/santiagomarquezsolis.com\/index.php\/wp-json\/wp\/v2\/posts\/171","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/santiagomarquezsolis.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/santiagomarquezsolis.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/santiagomarquezsolis.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/santiagomarquezsolis.com\/index.php\/wp-json\/wp\/v2\/comments?post=171"}],"version-history":[{"count":1,"href":"https:\/\/santiagomarquezsolis.com\/index.php\/wp-json\/wp\/v2\/posts\/171\/revisions"}],"predecessor-version":[{"id":173,"href":"https:\/\/santiagomarquezsolis.com\/index.php\/wp-json\/wp\/v2\/posts\/171\/revisions\/173"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/santiagomarquezsolis.com\/index.php\/wp-json\/wp\/v2\/media\/172"}],"wp:attachment":[{"href":"https:\/\/santiagomarquezsolis.com\/index.php\/wp-json\/wp\/v2\/media?parent=171"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/santiagomarquezsolis.com\/index.php\/wp-json\/wp\/v2\/categories?post=171"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/santiagomarquezsolis.com\/index.php\/wp-json\/wp\/v2\/tags?post=171"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}