<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[MITB For All - Medium]]></title>
        <description><![CDATA[Tech contents related to AI, Analytics, Fintech, and Digital Transformation. Written by MITB Alumni; open-access for everyone. - Medium]]></description>
        <link>https://medium.com/mitb-for-all?source=rss----825f074cc537---4</link>
        <image>
            <url>https://cdn-images-1.medium.com/proxy/1*TGH72Nnw24QL3iV9IOm4VA.png</url>
            <title>MITB For All - Medium</title>
            <link>https://medium.com/mitb-for-all?source=rss----825f074cc537---4</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Thu, 05 Nov 2026 18:06:31 GMT</lastBuildDate>
        <atom:link href="https://medium.com/feed/mitb-for-all" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[The Cocoa Trade: Data, Decisions, and an Internship with Olam Food Ingredients]]></title>
            <link>https://medium.com/mitb-for-all/the-cocoa-trade-data-decisions-and-an-internship-with-olam-food-ingredients-b90af1f58b4e?source=rss----825f074cc537---4</link>
            <guid isPermaLink="false">https://medium.com/p/b90af1f58b4e</guid>
            <category><![CDATA[commercial]]></category>
            <category><![CDATA[cocoa]]></category>
            <category><![CDATA[data-analytics]]></category>
            <category><![CDATA[internship-experience]]></category>
            <dc:creator><![CDATA[Andre Seah ]]></dc:creator>
            <pubDate>Wed, 28 Oct 2026 03:15:35 GMT</pubDate>
            <atom:updated>2026-10-28T03:15:34.544Z</atom:updated>
            <content:encoded><![CDATA[<h3><strong>Introduction</strong></h3><p>I am Andre from the Data Science and Analytics (DSA) track of MITB’s August 2025 intake. From April to October 2026, I interned with Olam Food Ingredients (ofi) as a Commercial Analyst in the Cocoa Division, supporting the Commercial team through market and sales analytics.</p><p>As someone transitioning from Research &amp; Development in Food Science into analytics, this internship represented more than just a six-month attachment. As I embark on my career change, this opportunity was a rare one, allowing to bridge two very different worlds. Having attended CDSWS10 Managing Career Change, I knew that the “easier” route would have been a singular pivot, either changing job functions within the same industry or keeping the same function while moving industries. However, opportunities within Food domain were limited, with the only singular pivot available for me being job function, and the gap between Food Science and Data Science is substantial.</p><p>This internship therefore felt like a rare doorway into the analytical field and, hopefully, a gateway towards future analytics roles.</p><h3><strong>Interview Process</strong></h3><p>The interview process was one of the more unusual experiences of my internship journey. It began with an informal screening call with the Head of HR, where we discussed my background, motivations, and career aspirations. Surprisingly, HR expressed the intention to offer the internship shortly afterwards, despite me not having met the hiring manager or undergone any technical assessment.</p><p>Wanting greater clarity before making a decision, I requested a meeting with the hiring manager. The discussion provided a better understanding of the role, team objectives, and how the internship aligned with both MITB requirements and my career goals. However, it also revealed that analytics capabilities within the team was relatively limited, with Excel and Power BI serving as the primary tools while Python and SQL appeared to play a secondary role.</p><p>Uncertainty and scepticism remained, yet the opportunity still represented a rare chance to pursue the career pivot I had been seeking.</p><p><em>I took a leap of faith.</em></p><h3><strong>Working as a Commercial Analyst Intern in ofi (Cocoa)</strong></h3><p>Olam Food Ingredients (ofi) is a global food and agri-business. This internship was housed within the Cocoa Division, covering products across the cocoa value chain, including cocoa beans, powder, liquor, and butter.</p><h3><strong>Overarching Project</strong></h3><p>The primary project occupied most of my internship tenure.</p><p>The company had purchased third-party market data to improve market understanding and support commercial strategy. However, the dataset had reportedly remained largely untouched for over a year. The internship position was subsequently created to determine how commercial value could be extracted from it.</p><h3><strong>The Actual Work Done in This Role</strong></h3><p>One of my initial observations after joining the team was the absence of an established analytics function. There were no resident data analysts, predefined frameworks, or existing workflows to inherit. While this initially felt intimidating, it also created a unique opportunity.</p><p>Rather than stepping into an existing process, I was given the freedom to help shape one.</p><p>The project was largely unconstrained by existing processes, offering substantial ownership and autonomy over both the analytical approach and the direction of the work. It provided a blank canvas on which to creatively apply the skills, methodologies, and disciplines developed throughout the MITB programme.</p><p>Upon gaining access to the dataset, the broad work process involved:</p><ol><li>Understanding what management hoped to achieve with the data.</li><li>Understanding what the data was actually capable of answering, whether narrower or broader than management’s expectations.</li><li>Wrangling messy text-based data into structured information that could be aggregated, analysed, and visualised.</li><li>Building PowerBI dashboards capable of addressing management’s requirements while clearly communicating any analytical limitations.</li></ol><p>With the goal of establishing a structured understanding of global cocoa trade flows to support market intelligence and strengthen competitive positioning against other cocoa bean grinders in the region.</p><p>One of the key challenges involved transforming unstructured trade records into a format suitable for analysis. This required extracting SKU and product information from free-text “Goods Description” fields and harmonising importer and exporter names across different origin and destination countries. The methodology was subsequently generalised across all countries under investigation, enabling large-scale aggregation, visualisation, and market analysis.</p><p>Given that the project was solely owned by an intern, it was also important to ensure a smooth handover upon completion. While more technically sophisticated solutions were possible, I recognised that their value would be limited if future users were unable to maintain them. With Excel being the primary tool used by the team, I intentionally constrained the solution design around tools and workflows that would remain accessible after my departure.</p><p>This led to several key considerations:</p><ol><li>Data cleaning, transformation, and other recurring processes had to be performed within Excel using transparent, step-by-step workflows that were easy to understand and maintain.</li><li>Power BI dashboards had to be easily refreshable and maintainable by users with limited technical expertise.</li><li>Any processes requiring other programming languages or tools had to be fully automated, requiring only script execution without modification.</li></ol><p>With these constraints and considerations in mind, the project was ultimately executed using Excel (including Power Query) and PowerBI.</p><h3><strong>Short Side Projects and Ad Hoc Requests</strong></h3><p>Beyond the primary project, there were opportunities to support a range of smaller initiatives and ad hoc requests.</p><p>These smaller projects varied substantially in scope and requirements, but they provided valuable opportunities to apply more advanced analytical techniques, modelling techniques, or exposure to tools that were beyond what I had been exposed to in the MITB programme as of this writing. Examples included:</p><ul><li>Forecasting Cocoa Bean Arrivals using time series modelling with XGBoost Regressor and lag-variable feature engineering.</li><li>Explored the use of Agentic AI on Copilot Studio as an alternative to PowerBI reporting or complementary tool, assessing its generative capabilities and the costs trade off.</li></ul><h3><strong>Who Does the Intern Interact With?</strong></h3><p><strong>Supervisor</strong></p><p>My supervisor was also the hiring manager and primary stakeholder. He focused on obtaining data-backed answers to business questions while giving significant autonomy over the design and execution of analytical solutions.</p><p><strong>Colleagues Across Functions</strong></p><p>Interactions with colleagues were highly collaborative. They provided domain knowledge, business context, and feedback on dashboard usability, while I supported analytical requests and explained methodologies and outputs.</p><h3><strong>Relevant Coursework</strong></h3><p>Several MITB modules were particularly valuable in supporting the work required for this role.</p><p><strong>ISSS621 Data Science for Business</strong></p><p>Arguably the most impactful module. The discipline of always anchoring analytics work to a Business Question ensured that analytical efforts remained relevant, purposeful, and aligned with business objectives. The ability to translate business questions into measurable data science tasks proved especially valuable.</p><p><strong>IS628 Computational Thinking with Python</strong></p><p>In particular, Regular Expressions (Regex) formed the foundation of the data cleaning process. They were instrumental in transforming messy text-based fields into structured information suitable for aggregation, analysis and visualisation.</p><p><strong>ISSS615 Data Management &amp; IS625 Query Processing and Optimisation</strong></p><p>Concepts surrounding relational databases, data modelling, and entity-relationship design proved useful when integrating datasets, mapping relationships, and constructing dimension tables.</p><p><strong>CS610 Applied Machine Learning</strong></p><p>Knowledge of machine learning algorithms, model evaluation techniques, and practical implementation enabled the application of models such as XGBoost in forecasting-related tasks.</p><h3><strong>Learnings and Takeaways from the Internship</strong></h3><p>The freedom to independently shape the project’s direction provided significant opportunities for both technical and professional growth. Beyond strengthening my capabilities in Excel, Power BI, and analytics, I was able to apply concepts and methodologies from the MITB programme within a real-world business environment.</p><p>The high level of ownership also created opportunities to present findings, demonstrate dashboard functionalities, and explain how analytical outputs addressed business questions. Through these interactions, I developed the ability to translate technical concepts and analytical methodologies into language accessible to stakeholders with varying levels of data literacy.</p><p>One of the most valuable lessons from the internship was that data and analytics derive their value not from technical sophistication alone, but from their ability to support meaningful business objectives. While metrics, models, and visualisations can generate insights, their impact depends on how effectively they are linked to business context and decision-making. This reinforced the importance of combining technical expertise with domain knowledge and business understanding, a skillset that remains highly transferable across industries.</p><p>An encouraging outcome of the internship was the interest expressed by management in exploring potential opportunities within the organisation beyond the internship period. While no formal offer was extended, discussions were held around areas such as digitalisation, data governance, business transformation, and commercial support, where my combined background in Food Science and Data Science &amp; Analytics was viewed as potentially valuable. These conversations reflected a positive reception to the work completed during the internship and remain open for future consideration following my next internship and completion of the MITB programme in April 2027.</p><h3><strong>Advice for Future Interns</strong></h3><p><strong>1. Don’t Judge an Internship Solely by the Technology Stack</strong></p><p>It is easy to evaluate an internship based on the tools listed in the job description. Compared to many technology-focused internships, my role relied primarily on Excel, Power BI, and occasional Python usage rather than modern data infrastructure or large-scale analytics platforms.</p><p>However, technical sophistication does not necessarily determine learning value (apart from the technical proficiency of it). Despite the relatively simple tech stack, I had substantial ownership over the entire analytical process, from understanding business requirements and preparing data to building dashboards and presenting insights. While tools matter, the scope, autonomy, and business impact of the work can be equally valuable opportunities for growth.</p><p><strong>2. Learn the Business Before Focusing on the Analytics</strong></p><p>One of the biggest lessons I learnt was that understanding the business often creates more value than immediately applying analytical techniques.</p><p>Before any analysis could be performed, it was necessary to understand what management was trying to achieve, what decisions needed to be made, and what questions the business wanted answered. Only then could the data be evaluated for its usefulness.</p><p>Many aspiring data professionals naturally gravitate towards technical solutions. However, a sophisticated analysis that answers the wrong question creates little value, while a simple analysis that addresses an important business problem can have significant impact.</p><p><strong>3. Be Comfortable with Ambiguity</strong></p><p>Not every organisation will have mature analytics capabilities, established processes, or clearly defined project requirements.</p><p>When I joined the team, there was no resident data analyst and no predefined framework for how the project should progress. Initially, this uncertainty was intimidating. Over time, however, I realised that ambiguity can also be an opportunity.</p><p>The lack of an established process provided greater freedom to shape the analytical approach, define workflows, and determine what outputs would be most valuable. While structured environments can accelerate learning, less structured environments often provide greater opportunities to develop ownership, initiative, and problem-solving skills.</p><p><strong>4. Build Solutions Others Can Maintain</strong></p><p>As the sole owner of the project, I knew that maintainability was just as important as the analysis itself. Although more technically sophisticated solutions were possible, many would have been difficult for future users to support. As a result, recurring processes were intentionally designed around tools that colleagues were already familiar with.</p><p>The best solution is not always the most advanced one. More often, it is the one that balances capability, usability, and maintainability.</p><h3><strong>Final Thoughts</strong></h3><p>Looking back, the factors that initially made me hesitant to accept the internship ultimately became the factors that made it valuable. The most important lesson I took away was that analytics is ultimately a means to an end. Tools, models, and dashboards only create value when they help answer business questions and support decision-making.</p><p>Regardless of industry or role, individuals who can combine technical skills with business understanding, ownership, and effective communication will be better positioned to create meaningful impact.</p><h3><strong>Acknowledgements</strong></h3><p>I would like to express my sincere gratitude to Saurabh and Valencia for the opportunity to intern with ofi, as well as Syed, Yi Feng and Joanne, for their guidance and support throughout the internship. The trust they placed in me, together with the freedom to explore, experiment, and shape the project’s direction, made the experience especially meaningful. It provided a rare opportunity to apply the skills and perspectives gained from the MITB programme to real business challenges, while learning firsthand how analytics can create value within a commercial environment.</p><p><em>Disclaimer: All opinions and interpretations are that of the writer(s), and not of MITB. We declare that we have full rights to use the contents published here, and nothing is plagiarized. We declare that this article is written by us and not with any generative AI tool such as ChatGPT. We declare that no data privacy policy is breached, and that any data associated with the contents here are obtained legitimately to the best of our knowledge. We agree not to make any changes without first seeking the editors’ approval. Any violations may lead to this article being retracted from the publication.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b90af1f58b4e" width="1" height="1" alt=""><hr><p><a href="https://medium.com/mitb-for-all/the-cocoa-trade-data-decisions-and-an-internship-with-olam-food-ingredients-b90af1f58b4e">The Cocoa Trade: Data, Decisions, and an Internship with Olam Food Ingredients</a> was originally published in <a href="https://medium.com/mitb-for-all">MITB For All</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Going Back to School: 1st Term of My SMU MITB Journey]]></title>
            <link>https://medium.com/mitb-for-all/going-back-to-school-1st-term-of-my-smu-mitb-journey-3780ce0b9d13?source=rss----825f074cc537---4</link>
            <guid isPermaLink="false">https://medium.com/p/3780ce0b9d13</guid>
            <category><![CDATA[masters-degree]]></category>
            <category><![CDATA[reflections]]></category>
            <category><![CDATA[singapore]]></category>
            <dc:creator><![CDATA[Dea Gloria]]></dc:creator>
            <pubDate>Sat, 17 Oct 2026 05:00:52 GMT</pubDate>
            <atom:updated>2026-10-17T05:00:51.183Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*hX7w2HMfQ5Va9WoU" /><figcaption>Photo by <a href="https://unsplash.com/@9oh9x?utm_source=medium&amp;utm_medium=referral">Guo Xin Goh</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>A few weeks ago I started my Master of IT in Business journey at Singapore Management University (SMU) and somehow, I’m already halfway through my first term (how time flies so fast!).</p><p>If I’m being honest, going back to school after working for a few years feels very different from what I expected.</p><p>There are the obvious parts: attending classes, doing assignments, preparing for quizzes, working on group projects, and trying to remember what it feels like to study for an exam again.</p><p>But beyond the academic workload, I’ve realised that this experience has been teaching me something I didn’t quite expect: <strong>sometimes, going back to the basics is exactly what you need to move forward.</strong></p><h3>Going back to the basics</h3><p>After working for a while, we naturally develop our own ways of doing things. You build muscle memory. You know which tools to use, which shortcuts to take, and what usually works. Eventually, you can do certain things almost automatically.</p><p>But when you go back to the fundamentals, you start asking different questions:</p><p><strong>Why am I doing it this way?<br>Why does this method work?<br>Is there actually a better way to approach this problem?</strong></p><p>And that has been one of the things I appreciate most about the program so far. Going through the fundamentals again has helped me understand the <em>reasoning behind</em> some of the things I have been doing at work, rather than simply knowing how to do them.</p><p>It’s a good reminder that knowing how to do something and understanding why you do it are two very different things.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*QQ2-lmWokvzSkW2hDVvIyw.jpeg" /><figcaption>Photo of my Digital Transformation Strategy Classmates onboarding the OceanXplorer Ship</figcaption></figure><h3>The people make the experience</h3><p>Another thing I’ve really enjoyed is meeting people from completely different backgrounds.</p><p>Everyone comes into the classroom with a different set of experiences and strengths. Some people are incredibly skilled in areas where I have much less experience, while I have my own areas where I can contribute.</p><p>And that’s where things get interesting!</p><p>Instead of everyone trying to know everything, we complement one another. A group discussion can suddenly become a mini knowledge exchange: someone brings the technical perspective, someone brings the business perspective, someone has industry experience, and someone else sees the problem from a completely different angle.</p><p>The only problem?</p><p><strong>There is never enough time to learn everything from everyone.</strong></p><p>There are simply too many interesting conversations to have, too many people to meet, and too many things happening at once.</p><h3>When everything becomes P0</h3><p>And speaking of “too many things”…</p><p>Individual projects. Quizzes. Assignments. Group projects. Readings. Classes. Networking. Somehow, still trying to have a life outside school. At some point, my priority matrix started looking… questionable. 😂</p><p>It actually reminds me a lot of working in a fast-paced environment where suddenly <strong>everything is P0</strong>. Everyone wants to be prioritised. Everything feels urgent. And when everything is a priority, nothing really is.</p><p>So once again, I found myself asking:</p><p><strong>What actually needs to be done first?</strong></p><p>I started becoming much more intentional about prioritising based on impact and effort. If something is low effort and high impact, why not get it out of the way first? If something is important but requires more time, perhaps it needs to be <strong>broken down into smaller tasks</strong>.</p><p>It sounds simple, but being thrown into an environment where multiple deadlines overlap has made me practise this much more deliberately. Ironically, one of the biggest things I’ve learned so far isn’t from a textbook.</p><p>It’s <strong>how to manage my own time and priorities.</strong></p><h3>And this is only the first half</h3><p>It has only been the first half of my first term, but I already feel like I’ve learned so much.</p><p>Not just academically, but also in ways that are much harder to measure: learning from people with different perspectives, becoming more comfortable with uncertainty, managing competing priorities, and reminding myself that there is always more to learn.</p><p>I’m sure there will be many more stressful weeks, confusing assignments, last-minute group discussions, and “how am I supposed to finish all of this?” moments ahead.</p><p>But I’m also looking forward to them.</p><p>Because perhaps that’s what going back to school is really about — not just collecting another qualification, but <strong>putting yourself in an environment where you get to learn, unlearn, and grow all over again.</strong></p><p>Here’s to the second half of the term, and to many more lessons and journeys ahead. 🥂</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*72J7yuxkMAcehB9JpAc8mw.jpeg" /><figcaption>Closing Photo of me and my fellow SMU masters having picnic at Marina Barrage</figcaption></figure><p><em>Disclaimer: All opinions and interpretations are that of the writer, and not of MITB. I declare that I have full rights to use the contents published here, and nothing is plagiarized. I declare that this article is written by me and not with any generative AI tool such as ChatGPT. I declare that no data privacy policy is breached, and that any data associated with the contents here are obtained legitimately to the best of my knowledge. I agree not to make any changes without first seeking the editors’ approval. Any violations may lead to this article being retracted from the publication.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=3780ce0b9d13" width="1" height="1" alt=""><hr><p><a href="https://medium.com/mitb-for-all/going-back-to-school-1st-term-of-my-smu-mitb-journey-3780ce0b9d13">Going Back to School: 1st Term of My SMU MITB Journey</a> was originally published in <a href="https://medium.com/mitb-for-all">MITB For All</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Your AI Agent Needs a Bouncer]]></title>
            <link>https://medium.com/mitb-for-all/your-ai-agent-needs-a-bouncer-f115d5ddac55?source=rss----825f074cc537---4</link>
            <guid isPermaLink="false">https://medium.com/p/f115d5ddac55</guid>
            <category><![CDATA[system-one-model]]></category>
            <category><![CDATA[langchain]]></category>
            <category><![CDATA[ai]]></category>
            <dc:creator><![CDATA[Tituslhy]]></dc:creator>
            <pubDate>Sat, 17 Oct 2026 04:53:37 GMT</pubDate>
            <atom:updated>2026-10-17T04:53:35.969Z</atom:updated>
            <content:encoded><![CDATA[<h4>Catching Jev-er: Teaching a multi-agent system to triage messy questions.</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*NK8bGYaaCPoI0lv2drwh8g.png" /></figure><p>The artificial intelligence industry has spent the past few years racing to build increasingly intelligent large language models (LLMs).</p><p>LLMs got bigger, got better at reasoning and now accept longer context windows.</p><p>Then, on <a href="https://typesafe.ai/blog/introducing-system-one-models-and-jev">15 September 2026</a>, something rather different arrived.</p><p>Meet Jev, the world’s first commercially released “System One” model.</p><p>And here’s the interesting part: <strong>Jev doesn’t generate text. It makes decisions.</strong></p><p>Unlike a conventional generative LLM, Jev doesn’t produce free-form text. Instead, you give it a set of possible answers, and it evaluates them, returning confidence scores for the available options.</p><p>Think multiple-choice questions rather than essay writing.</p><h3>Why the hype?</h3><p>According to benchmarks published by TypeSafe AI, Jev delivers competitive classification accuracy at a fraction of the cost of state-of-the-art LLMs:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*2eAdHJGaYTFiVX2z" /><figcaption>cost vs accuracy chart. Extracted from <a href="https://typesafe.ai/blog/introducing-system-one-models-and-jev">TypeSafe AI</a></figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*0m5SOT88SeExduqt" /><figcaption>Hallucination and Type Safety comparison charts. Extracted from <a href="https://typesafe.ai/blog/introducing-system-one-models-and-jev">TypeSafe AI</a></figcaption></figure><p>In addition, as we’ll soon see, Jev is absurdly fast.</p><p>The implication is that you might not always need to use a LLM for classification and tool calling. Jev would be able to do it too.</p><h3>End of story?</h3><p>Not quite. There’s always a caveat.</p><p>Though Jev’s model architecture is unknown, it is the first model to be classified as a “Systems One” model. This borrows from the definition of “Systems One” first coined by Daniel Kahneman in his book “<a href="https://en.wikipedia.org/wiki/Thinking,_Fast_and_Slow">Thinking, Fast and Slow</a>”, which is now the cornerstone of behavioral economics.</p><p>The theory is this: systems one is our fast, automatic, intuitive state of decision-making.</p><p>As an example, imagine if your colleague sitting next to you at work got surprised by other colleagues with a “happy birthday” song. You might react by singing along without thinking. That’s a system one response.</p><p>System Two, on the other hand, describes slower, more deliberate thinking. It’s what happens when we consciously work through a difficult problem, such as solving a quadratic equation.</p><p>Of course, the two systems aren’t completely independent. Our intuition frequently informs our deliberate reasoning, and deliberate reasoning can override our intuition.</p><p>But the distinction is useful: <strong>System One reacts. System Two reasons.</strong></p><p>This distinction is important when building AI agents.</p><p>Jev might be excellent at deciding which tool should handle a request. But deciding which tool to call is not the same as reasoning through the problem, executing a multistep workflow, and synthesizing the results.</p><p>In other words, <strong>Jev might make an excellent bouncer. That doesn’t mean it should run the entire nightclub.</strong></p><h4>Important Fineprint</h4><p>Jev 1.13 (the latest model) is said to have a <a href="https://openrouter.ai/docs/guides/community/jev">context window of 64k tokens</a> — far less than the 1 million token context window of state-of-the-art LLMs.</p><p><a href="https://openrouter.ai/docs/guides/community/jev">TypeSafe AI is pushing the envelope though</a>. Jev 1.14 (in preview) is said to have a long-context mode which takes up to 262k tokens. TypeSafe AI caveats that Jev is less accurate and slower in this mode — so don’t go stuffing entire documents or chat histories at it.</p><p>Since the launch of Jev, many <a href="https://huggingface.co/openjev/openjev">open-sourced “System One” models</a> have been released — most involve <a href="https://github.com/jaredpalmer/kev">finetuning a smaller LLM like Qwen 3.5:9bn</a> to do classification. Not all of these models are trained via the <a href="https://arxiv.org/html/2609.38850v1">reinforcement learning for calibrated decisions (RLCD) </a>regime like Jev though.</p><p>Other closed-sourced systems one models like <a href="https://www.liquid.ai/blog/d1-decision-model">Liquid AI’s d1 model</a> have also since been released.</p><p>What we’re witnessing might be the beginning of a second AI race: not to build models that can reason about everything, but to build models that can make narrow, reliable decisions extraordinarily quickly.</p><h3>How should you use Jev then?</h3><p>TypeSafe AI provides many <a href="https://docs.typesafe.ai/cookbooks#cookbooks">cookbooks</a> demonstrating how Jev can perform tasks traditionally handled by specialized models. For example, Jev can be used to rerank retrieved text chunks — a task typically assigned to a dedicated reranker.</p><p><a href="https://docs.langchain.com/oss/python/integrations/providers/typesafe#model-routing">LangChain published an integration package with TypeSafe AI </a>that ships with two default middlewares — ModelRoutingMiddleware and AutoModeMiddleware that aim to use Jev in different settings:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*cCA2yHenCCxLiIpTV3P2qA.png" /><figcaption>Extracted from <a href="https://docs.langchain.com/oss/python/integrations/providers/typesafe#model-routing">LangChain’s Docs</a></figcaption></figure><p>The spirit of these recipes is this: Jev can be used at decision-making points that would otherwise require an LLM. But just because Jev can make a decision doesn’t necessarily mean it should. The trick is identifying decisions that don’t require extensive reasoning, and determining whether Jev can make them reliably.</p><p>At the time of writing, I haven’t read any of these cookbooks. Instead, I wanted to approach Jev from first principles and figure out where it might fit into my own multi-agent systems.</p><p>What follows is my own experiment, rather than an adaptation of an existing recipe. I hope you find it useful!</p><p>I’ll walk through the architecture, implementation, and experiments below. You can also find the complete code in my <a href="https://github.com/tituslhy/Jever/tree/main">GitHub repository</a>.</p><h3>The idea</h3><p>One of the challenges of building agentic applications is that you never quite know what questions your users will throw at your agents.</p><p>User questions might be abstract, multipart, direct, or simply badly written. Depending on the question, an agent might benefit from asking for clarification, decomposing the request into smaller questions, or rewriting it into something more precise.</p><p>More commonly, however, we simply rely on the raw intelligence of the LLM to figure everything out, including which tools and skills to invoke.</p><p><strong>Let’s change that.</strong></p><p>In my experiment, I’ll create a hierarchical multi-agent system with a supervisor agent, a forex exchange agent and a web search agent; and use Jev to be the bouncer at the door.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qvqfPgkunmOwJsOesZqIKA.png" /><figcaption>Agent architecture generated by ChatGPT</figcaption></figure><h4>The tools</h4><p>There are two tools.</p><p>My forex exchange agent uses a forex tool around Frankfurter’s generous public forex API:</p><pre>import requests<br>from langchain.tools import tool<br><br>@tool<br>def get_forex_exchange_rate(base_currency: str, target_currencies: list[str]) -&gt; dict[str, float]:<br>    &quot;&quot;&quot;<br>    Get the current forex exchange rate between two currencies.<br><br>    Args:<br>        base_currency (str): The base currency code (e.g., &quot;USD&quot;).<br>        target_currencies (list[str]): A list of target currency codes (e.g., [&quot;EUR&quot;, &quot;GBP&quot;]).<br><br>    Returns:<br>        dict[str, float]: A dictionary mapping each target currency to its exchange rate.<br>    &quot;&quot;&quot;<br>    res = requests.get(<br>        &quot;https://api.frankfurter.dev/v2/rates&quot;,<br>        params={&quot;base&quot;: base_currency, &quot;quotes&quot;: &quot;,&quot;.join(target_currencies)},<br>        timeout=10,<br>    )<br>    rates = {row[&quot;quote&quot;]: row[&quot;rate&quot;] for row in res.json()}<br>    return rates</pre><p>My web search tool leverages <a href="https://www.tavily.com/pricing">Tavily’s</a> generous free tier:</p><pre>from langchain_tavily import TavilySearch<br><br>tavily_tool = TavilySearch(<br>    max_results=5,<br>    topic=&quot;general&quot;,<br>    include_answer=False,<br>    search_depth=&quot;basic&quot;,<br>)</pre><h3>Jev as the bouncer</h3><p>I want Jev to intercept every user query before my supervisor agent sees it, and determine whether the query needs further work. If so, perform the query enrichment so that we can maximize the mileage (and hopefully save tokens) of our multi-agent system.</p><p>LangChain’s before_agent agent middleware hook is the perfect place to implement this, and can be used either as a function or within a class that subclasses AgentMiddleware .</p><p>Let’s start by defining the middleware’s constructor and the middleware hook method:</p><pre>class QueryEnrichmentMiddleware(AgentMiddleware):    <br>    def __init__(self, llm, classifier = None, threshold: float = 0.80):<br>        super().__init__()<br>        self.llm = llm<br>        self.classifier = classifier or TypeSafeClassifier()<br>        self.threshold = threshold<br>        self.handlers = {<br>            &quot;decomposable&quot;: self.decompose_query,<br>            &quot;enrichable&quot;: self.enrich_query,<br>            &quot;needs_clarification&quot;: self.clarify_query,<br>        }<br><br>    def before_agent(self, state: AgentState, runtime: Runtime) -&gt; dict[str, Any] | None:<br>        messages = state[&quot;messages&quot;]<br>        if not messages or not isinstance(messages[-1], HumanMessage):<br>            return None<br>        query = messages[-1].text<br><br>        next_action = self.query_classify(query).choices[&quot;next_action&quot;]<br>        handler = self.handlers.get(next_action.choice)<br>        if next_action.confidence &lt; self.threshold or handler is None:<br>            return None<br>        return {<br>            &quot;messages&quot;: [<br>                HumanMessage(<br>                    content=handler(query), <br>                    id = messages[-1].id<br>                )<br>            ]<br>        }</pre><p>We want to extract the last human message, classify it, handle it, and then send it to the supervisor agent. Importantly, before_agentmust return either a dictionary object or Nonein or LangChain will return an error.</p><p>With that in mind, it’s time to write our handlers:</p><pre>def enrich_query(self, query: str) -&gt; str:<br>    &quot;&quot;&quot;Helper function to enrich user queries&quot;&quot;&quot;<br>    return self.llm.invoke(<br>       &quot;Rewrite the following user query for clarity. &quot;<br>       &quot;Preserve the original meaning and all explicit constraints. &quot;<br>       &quot;Do not invent missing details or answer the question. &quot;<br>       &quot;Return only the rewritten query.\n\n&quot;<br>       f&quot;User query: {query}&quot;<br>    ).content<br>    <br>def decompose_query(self, query: str) -&gt; str:<br>     &quot;&quot;&quot;Helper function to decompose user queries&quot;&quot;&quot;<br>     sub_queries = self.llm.invoke(<br>        f&quot;The user&#39;s query is: {query}. &quot;<br>        &quot;Your role is to decompose this query into sub-queries that can be answered separately. &quot;<br>      )<br>     return (<br>         f&quot;Original user query: {query}\n\n&quot;<br>         f&quot;Suggested sub-questions:\n{sub_queries.content}\n\n&quot;<br>         &quot;Use these sub-questions to guide your analysis. &quot;<br>         &quot;Combine related operations where appropriate, &quot;<br>         &quot;and synthesize the results into a single response.&quot;<br>     )<br><br>def clarify_query(self, query: str) -&gt; str:<br>    &quot;&quot;&quot;Helper function to inject prompt to clarify user queries&quot;&quot;&quot;<br>    return (<br>         f&quot;Original user query: {query}\n\n&quot;<br>         &quot;The request may be ambiguous or missing essential information. &quot;<br>         &quot;Ask the user one concise clarification question before proceeding. &quot;<br>         &quot;Do not assume missing details.&quot;<br>    )</pre><ul><li>enrich_query uses an LLM to replace the original query by rewriting it to be clearer and more specific</li><li>decompose_query uses an LLM to decompose the original query into sub questions, then appends these sub questions on top of the original query</li><li>clarifiy_query nudges the LLM to seek user clarification — wrapping the original user query.</li></ul><h4>Now for the query classification method</h4><p>This is where Jev comes in.</p><p>LangChain’s TypeSafe AI integration docs are extremely instructive. In essence, Jev has 3 primary primitives:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*NX2wRgEXbXo33Vr8gfo2_A.png" /></figure><p>Of these, Noul is the only primitive that does not return a confidence score (remember that machine learning class?).</p><p>Choice is the equivalent of a normal classification task, and Score is a subset of Choice , used for questions where the answer is a position on a spectrum — for example, on a scale of 1–5, how much does it hurt when your agent hallucinates.</p><p>For our bouncer, we’re interested in choosing between several possible actions: rewrite, decompose, clarify, or leave the query alone. That makes Choice a natural fit.</p><p>The code:</p><pre>def query_classify(self, query: str) -&gt; ClassifierResponse:<br>    &quot;&quot;&quot;Helper function to classify user queries using Jev&quot;&quot;&quot;<br>    return self.classifier.invoke(<br>        {<br>            &quot;state&quot;: query,<br>            &quot;questions&quot;: {<br>                &quot;next_action&quot;: Choice(<br>                    instructions = &quot;Should this query be decomposed into multiple sub-queries?&quot;,<br>                    criteria = {<br>                        &quot;decomposable&quot;: &quot;Does the query have multiple distinct sub-queries that can be answered separately?&quot;,<br>                        &quot;direct&quot;: &quot;Is the query a single, direct question that can be answered without additional context or information?&quot;,<br>                        &quot;enrichable&quot;: &quot;Should the query be re-written to be clearer so that it can be answered more effectively?&quot;,<br>                        &quot;needs_clarification&quot;: &quot;Does the query require additional context or clarification before it can be answered?&quot;,<br>                    }<br>                )<br>            }<br>        }<br>    )</pre><h4>Taking it out for a spin</h4><p>Let’s see Jev classify something using our middleware:</p><pre>from langchain_openai import ChatOpenAI<br><br>llm = ChatOpenAI(model=&quot;gpt-6-luna&quot;)<br>decomposable_query = (<br>    &quot;What is the current exchange rate between USD and EUR, &quot;<br>    &quot;and can you also provide the latest news on the European Central Bank&#39;s &quot;<br>    &quot;monetary policy?&quot;<br>)<br><br>query_middleware = QueryEnrichmentMiddleware(llm=llm)<br>response = query_middleware.query_classify(query=decomposable_query)<br>print(<br>  response.choices[&quot;next_action&quot;].choice, <br>  response.choices[&quot;next_action&quot;].confidence<br>)<br><br># decomposable 1.0</pre><p>Jev classified the query as decomposable with a confidence score of 1.0. Given that the request contains two distinct questions, that&#39;s hardly surprising.</p><h3>Assembling the multi-agent system</h3><p>Let’s first start by wrapping our sub agents and their tools in LangChain’s SubAgentMiddleware :</p><pre>from deepagents.backends import StateBackend<br>from deepagents.middleware import SubAgentMiddleware<br><br>subagent_middleware = SubAgentMiddleware(<br>    backend=StateBackend(),<br>    subagents = [<br>        {<br>            &quot;name&quot;: &quot;Forex_Expert&quot;,<br>            &quot;description&quot;: &quot;An expert in forex exchange rates and currency markets.&quot;,<br>            &quot;tools&quot;: [get_forex_exchange_rate],<br>            &quot;model&quot;: llm,<br>        },<br>        {<br>            &quot;name&quot;: &quot;Web_search&quot;,<br>            &quot;description&quot;: &quot;An expert in searching the web for information.&quot;,<br>            &quot;tools&quot;: [tavily_tool],<br>            &quot;model&quot;: llm,<br>        },<br>    ]<br>)</pre><p>The SubAgentMiddleware class wraps every subagent as a task tool for the supervisor agent — so we don’t have to define any further tools.</p><blockquote>Notice the backend parameter. It deserves an article of its own, so I&#39;ll leave that discussion for another time. For this demonstration, StateBackend keeps things simple.</blockquote><p>Next, let’s instantiate the checkpointer and create the supervisor agent:</p><pre>conn = sqlite3.connect(&quot;jever.db&quot;, check_same_thread=False)<br>checkpointer = SqliteSaver(conn)<br><br>agent = create_agent(<br>    model = llm,<br>    tools = [], # LOOK NO TOOLS!<br>    middleware = [query_middleware, subagent_middleware],<br>    checkpointer=checkpointer,<br>)</pre><p>Running the agent against the same query, we get:</p><pre>response = agent.invoke(<br>    {&quot;messages&quot;: [ HumanMessage(content=decomposable_query),]},<br>    {&quot;configurable&quot;: {&quot;thread_id&quot;: &quot;thread-4&quot;,}}<br>)<br><br># Truncated response<br>### USD to EUR: The latest rate lookup returned US$1 ≈ €0.8920. <br>### ...<br>### ECB monetary policy: As of 15 October 2026, the latest rate decision I <br>### could verify was on 10 September. The ECB raised its key rates by 0.25 <br>### percentage point, ....</pre><h3>It’s what our traces tell us that’s interesting</h3><p>The first trace shows the expected execution pattern. Jev classified the request as decomposable, our middleware generated the sub-questions, and the supervisor subsequently invoked both specialist agents in parallel before synthesizing their responses.</p><p>Importantly, Jev didn’t select the specialist agents. That decision remained with the supervisor.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*AQ5zHhxYUULtLu3cnW7IKw.png" /></figure><p>In one run, my supervisor agent actually got one of its subagents to do a web search to verify the findings of a previous subagent run:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*QfySNIPbJiUtO8HFgrk_gQ.png" /></figure><p>That’s an example of adaptive orchestration.</p><p>Rather than executing a predetermined sequence of steps, the supervisor made a subsequent decision based on the information available.</p><p>To me this proves that it is quite dangerous to simply replace the supervisor agent with Jev, because:</p><ul><li>Jev is unable to synthesize a final response from subagent replies,</li><li>Jev can classify predefined next actions, but it doesn’t offer the same open-ended reasoning and generation capabilities as a conventional LLM supervisor.</li></ul><p>Jev did an amazing job as a bouncer though.</p><h4>How did the Jev-ed agents do for other question types?</h4><p>I then ran the multi-agent system across 3 more query types:</p><pre>direct_query = &quot;What is the current exchange rate between USD and EUR?&quot;<br>enrichable_query = &quot;Usain Bolt fast how training he?&quot;<br>needs_clarification_query = &quot;fasdlkfaleuraoi34&quot;<br><br>tests = [direct_query, enrichable_query, needs_clarification_query]<br>answers = []<br>for i, test in enumerate(tests):<br>    response = agent.invoke(<br>        {&quot;messages&quot;: [ HumanMessage(content=test),]},<br>        {&quot;configurable&quot;: {&quot;thread_id&quot;: f&quot;thread_{i}&quot;,}}<br>    )<br>    answers.append(response[&#39;messages&#39;][-1].content[-1][&#39;text&#39;])</pre><p>And the Jev-ed agent did well for them:</p><pre>The latest rate available to me is 1 USD ≈ 0.8916 EUR. <br>---<br>Bolt trained for explosive speed, not long-distance running....<br>---<br>What would you like me to do with “fasdlkfaleuraoi34”?</pre><p>An important point is the enrichable_query question:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*bWzwSOwJH9LCU3kpqcp-zg.png" /></figure><p>Jev classified the deliberately poorly written question as enrichable, as expected. However, its confidence score fell below our threshold of 0.80. Consequently, the middleware left the original query untouched and allowed the supervisor to handle it directly.</p><h4>How did the baseline do?</h4><p>Here’s the code I used to run the baseline:</p><pre>baseline_agent = create_agent(<br>    model = llm,<br>    tools = [], # LOOK NO TOOLS!<br>    middleware = [subagent_middleware],<br>    checkpointer=checkpointer,<br>)<br><br>baseline_tests = [decomposable_query, direct_query, enrichable_query, needs_clarification_query]<br>baseline_answers = []<br>for i, test in enumerate(baseline_tests):<br>    response = baseline_agent.invoke(<br>        {&quot;messages&quot;: [ HumanMessage(content=test),]},<br>        {&quot;configurable&quot;: {&quot;thread_id&quot;: f&quot;baseline_thread_{i}&quot;,}}<br>    )<br>    baseline_answers.append(response[&#39;messages&#39;][-1].content[-1][&#39;text&#39;])</pre><p>Their metrics on LangSmith were pretty interesting:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*eDE265YzGYh9JWtGFU9x9A.png" /><figcaption>LangSmith runs, arranged by start time in descending order</figcaption></figure><p>Now here’s where things get interesting.</p><p>For our multipart question (rows 4 and 8), the baseline agent took <strong>2 minutes and 21 seconds</strong> to complete its response.</p><p>With Jev-powered query enrichment, the same task took just <strong>11.69 seconds</strong>.</p><p>That’s approximately a <strong>12× reduction in execution time</strong>, despite the additional classification and query decomposition steps.</p><p>I repeated the baseline experiment several times, and the results remained substantially slower. Even my fastest baseline run took approximately 1.5 minutes.</p><p>So what happened?</p><p>My hypothesis is that the decomposed query helped the supervisor understand how to distribute the work across its specialist agents, avoiding unnecessary deliberation or repeated tool invocations.</p><p>However, there’s an important caveat: Jev didn’t perform the decomposition itself. It classified the query as requiring decomposition, after which another LLM generated the sub-questions.</p><p>Consequently, this experiment doesn’t isolate how much of the improvement came from Jev versus the decomposition step.</p><blockquote>What it does demonstrate is that <strong>conditionally preprocessing a complex query can substantially change how an agent executes it</strong>.</blockquote><p>Interestingly, the differences for the other question types were relatively small.</p><p>My suspicion is that GPT-6-Luna was already capable of handling those simpler requests without assistance.</p><p>This raises an interesting question: would a more capable supervisor narrow the performance gap, or would a smaller model benefit even more from query enrichment?</p><p>That’s an experiment for another day.</p><h3>Final thoughts</h3><p>So, does your AI agent need a bouncer? Perhaps.</p><p>Jev is an interesting addition to the AI engineer’s toolbox. Rather than replacing the reasoning capabilities of an LLM, it offers an alternative for making fast, inexpensive decisions at specific points in an agentic workflow.</p><p>In my experiment, Jev helped determine whether incoming queries needed clarification, rewriting, decomposition, or no intervention at all. The results were encouraging, particularly for multipart questions, where the Jev-powered system completed one task nearly 12 times faster than the baseline. Of course, this is a small experiment, and more rigorous testing is needed to determine how much of that improvement came from Jev’s classification versus the subsequent LLM-powered query decomposition.</p><p>But that’s not really the point. <strong>The interesting opportunity isn’t replacing LLMs with Jev. It’s identifying where we don’t need an LLM in the first place.</strong> Not every decision requires an expensive reasoning model, and not every user query needs additional processing. Sometimes, a lightweight classifier is all we need to decide what happens next.</p><p>As agentic systems become increasingly sophisticated, I suspect we’ll see more specialized models working alongside general-purpose LLMs, each doing what they’re best at.</p><p>And that’s how you catch Jev-er. 😎</p><p><em>Disclaimer: All opinions and interpretations are that of the writer, and not of MITB. I declare that I have full rights to use the contents published here, and nothing is plagiarized. I declare that this article is written by me and not with any generative AI tool such as ChatGPT. I declare that no data privacy policy is breached, and that any data associated with the contents here are obtained legitimately to the best of my knowledge. I agree not to make any changes without first seeking the editors’ approval. Any violations may lead to this article being retracted from the publication.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f115d5ddac55" width="1" height="1" alt=""><hr><p><a href="https://medium.com/mitb-for-all/your-ai-agent-needs-a-bouncer-f115d5ddac55">Your AI Agent Needs a Bouncer</a> was originally published in <a href="https://medium.com/mitb-for-all">MITB For All</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Introducing stablecoins]]></title>
            <link>https://medium.com/mitb-for-all/introducing-stablecoins-2b3e63e6c5f1?source=rss----825f074cc537---4</link>
            <guid isPermaLink="false">https://medium.com/p/2b3e63e6c5f1</guid>
            <category><![CDATA[stablecoin-cryptocurrency]]></category>
            <category><![CDATA[fintech]]></category>
            <category><![CDATA[blockchain]]></category>
            <category><![CDATA[python]]></category>
            <dc:creator><![CDATA[Tituslhy]]></dc:creator>
            <pubDate>Sat, 10 Oct 2026 06:49:40 GMT</pubDate>
            <atom:updated>2026-10-10T06:49:39.563Z</atom:updated>
            <content:encoded><![CDATA[<h4><em>A dollar you have to take someone’s word for</em></h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/768/1*IrGDxHop8Y-GBfiwRHMb1w@2x.jpeg" /></figure><p>Cryptocurrency has a volatility problem.</p><p>The entire construct of cryptocurrency is trust-based through the use of blockchain. Unlike fiat currencies like USD, cryptocurrencies are unregulated, but can be exchanged for fiat currencies.</p><p>However, a quick google search shows the price fluctuation of bitcoin:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/749/1*5pj9d6101RfNXQ734Xiglg.png" /><figcaption>Screenshot of a google search on bitcoin-USD exchange rates by the author</figcaption></figure><p>Imagine receiving your salary in cryptocurrency, only to discover that your purchasing power has fallen by 10% before you’ve paid your bills.</p><p>One solution is to create a cryptocurrency that doesn’t fluctuate in value. Just create a token and promise everyone that it’s worth one US dollar.</p><p><strong>Welcome to stablecoins, where blockchain technology meets a centuries-old financial concept: someone promising that their money is worth something else.</strong></p><p>In this article, I’ll explain what stablecoins are in great detail. Specifically, how their exchange rates are maintained, and what happens when those mechanisms fail.</p><p>As with my previous articles, all my code can be found in my <a href="https://github.com/tituslhy/Blockchain/blob/0cce06e1f6fbe0a8c82e2d3d2da2e7667c2c3c95/notebooks/8.%20stablecoins.ipynb">GitHub repository</a>.</p><h3>1. What is a stablecoin?</h3><p>Simply put, a stablecoin is a cryptocurrency designed to maintain a stable value relative to another asset, typically a fiat currency like the US dollar.</p><p>Say I were to create my own TitusCoin (TTC) and peg it to USD, then my objective would be:</p><p><strong>1 TTC = 1 USD</strong></p><p>Simple enough. Problem solved, right?</p><p>Right. And then some.</p><h4><strong>1a. Banks transfer money between themselves and facilitate payments between individuals and organizations.</strong></h4><p>However, some payment and settlement systems operate within specific business hours, particularly when transactions cross national borders. Transactions can also take time to settle, depending on the payment infrastructure and institutions involved. Stablecoins circumvent these issues. <strong>A stablecoin can be transferred between blockchain addresses at any time, even when conventional banking systems are closed.</strong></p><p>The transfer can settle on-chain without waiting for the next banking day, although converting the stablecoin back into fiat currency may still depend on banking infrastructure and the issuer’s redemption arrangements.</p><p>That’s potentially useful for cross-border transactions, payments, and financial institutions moving value between counterparties.</p><h4>1b. Stablecoins can also provide an alternative to holding currencies that experience significant volatility.</h4><p>For example, someone holding a currency that is rapidly depreciating against USD might choose to convert part of their holdings into a USD-denominated stablecoin.</p><p>This reduces their exposure to their domestic currency, although it introduces other risks, including exposure to the US dollar and the stablecoin issuer.</p><h3>2. So can anyone create a stablecoin?</h3><p>Stablecoins are still a cryptocurrency and trust is still the key. I could create a million TitusCoins (TTCs) and call myself a millionare, but my declaration does not mean anyone will believe it.</p><p>Let me explain using the age old-economics concepts of demand and supply.</p><h4>Remember the Asian Financial Crisis of 1997?</h4><p>Before the Asian Financial Crisis, Thailand maintained an exchange-rate regime that kept the Thai baht closely tied to the US dollar at 25 Baht to 1 USD.</p><p>The Bank of Thailand had to intervene in foreign exchange markets to maintain this rate. If too many people wanted to sell Baht for USD, the central bank would have to defend its currency by using its foreign exchange reserves to purchase Baht.</p><p>Here’s the problem: <strong>The Bank of Thailand did not have an unlimited supply of US dollars.</strong></p><p>During the 1990s, speculators began questioning whether Thailand could maintain its exchange-rate regime. Wealthy investors, including funds associated with George Soros, made bets against the Baht by shorting it.</p><p>These investors deliberately hoarded USD and aggressively borrowed and sold Baht for even more USD, anticipating that the currency would eventually depreciate. The Bank of Thailand continued defending the exchange rate using its foreign reserves and other market interventions.</p><p>But the pressure became increasingly difficult to sustain. And Thailand eventually abandoned its exchange-rate regime and allowed the Baht to float. The currency depreciated sharply, contributing to a financial crisis that spread across much of Asia.</p><p>The lesson: <strong>You can promise that your currency is worth a fixed amount of another currency. But maintaining that promise requires resources and market confidence.</strong></p><p><em>Reference: </em><a href="https://www.bot.or.th/en/our-roles/special-measures/Tom-Yum-Kung-lesson.html"><em>Lessons learnt from the Asian Financial Crisis (Bank of Thailand)</em></a></p><h3>3. Let’s create TitusCoin</h3><p>Time to establish a fictional financial institution called Titus Bank and a stablecoin cryptocurrency called TitusCoin (TTC). My promise is straightforward:</p><blockquote><strong>Anyone holding 1 TTC can redeem it for 1 USD, subject to the bank’s redemption arrangements.</strong></blockquote><p>To back that up, I ensure that Titus Bank must hold at least USD 1 in reserves for every TTC issued.</p><p>So, if Alice deposits USD 100 into Titus Bank, I mint 100 TTC and send the tokens to her blockchain address. I’d also have to find some way to put USD 100 in my bank. That way if Alice wants her money back, I destroy or burn those tokens and release USD 100 from my reserves.</p><p>This is the basic concept behind a <strong>fiat-backed stablecoin</strong>, such as <a href="https://www.circle.com/usdc">USDC, issued by Circle</a>.</p><h3>4. But does 1 TTC always equal 1 USD?</h3><p>Not necessarily. There’s an important distinction between the value promised by Titus Bank and the price at which TTC actually trades on cryptocurrency exchanges.</p><p>I can’t stop Alice from selling her TTC to Bob for USD 1.10, or some other amount. The issuer’s promised redemption value and the market price are two different things — the market price depends on what buyers and sellers are willing to pay.</p><p>And as we saw in the Asian Financial Crisis, it depends largely on whether people believe the issuer can honor its promise.</p><h4>The USDC depegging incident of March 2023</h4><p><a href="https://www.investopedia.com/what-happened-to-silicon-valley-bank-7368676">In March 2023, Silicon Valley Bank (SVB), a major US financial institution, collapsed</a>. <a href="https://www.cnbc.com/2023/03/11/stablecoin-usdc-breaks-dollar-peg-after-firm-reveals-it-has-3point3-billion-in-svb-exposure.html">Unfortunately for Circle, approximately USD 3.3 billion of its reserves (roughly 8% of its reserves) were deposited with SVB</a>.</p><p>When SVB failed, uncertainty emerged over whether Circle could recover those funds. And the cryptocurrency market reacted.</p><p><a href="https://arxiv.org/html/2606.07442v1">USDC, which was supposed to maintain a value of USD 1, briefly traded around USD 0.87–0.88 on secondary markets</a>.</p><p>No blockchain failures or cyberattacks. The market was simply uncertain whether Circle could access enough reserves to honour redemptions. Nonetheless, Circle maintained its commitment to redeem eligible USDC holdings at USD 1 per token.</p><p>Following intervention by US authorities to protect depositors at SVB, confidence recovered, and USDC returned towards its peg.</p><p><strong>A stablecoin’s price doesn’t remain at USD 1 simply because its issuer says it should. </strong>So what actually keeps the market price close to the promised value?</p><h3>5. Greed is the main market force maintaining the exchange rate</h3><p>More specifically, arbitrage.</p><p>Imagine that 1 TTC = USD 1.20 on a cryptocurrency exchange. That’s interesting, especially when you remember Titus Bank’s promise of issuing 1 TTC for every USD 1 deposited.</p><p>Which means, you could:</p><ul><li>Deposit USD 100 with Titus Bank.</li><li>Receive 100 newly minted TTC.</li><li>Sell those 100 TTC on the market for USD 120.</li><li>Pocket the USD 20 difference.</li></ul><p>20% profit!</p><p>The problem is every other investor will do the same thing.</p><p>As more TTC tokens are minted and sold, supply increases which pushes the market price back to USD 1.</p><p>So what happens if say 1 TTC = USD 0.90? Titus Bank still promises 1 TTC for USD 1. So you could:</p><ul><li>Purchase 100 TTC on the market for USD 90.</li><li>Redeem those tokens with Titus Bank.</li><li>Receive USD 100.</li><li>Pocket the USD 10 difference.</li></ul><p>And every other investor will do the same. So demand for TTC increases, and when those tokens are redeemed, they are burned, which reduces the circulating supply. Which pushes the price to return to USD 1.</p><p>This is the fundamental arbitrage mechanism behind a redeemable fiat-backed stablecoin.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*z1WqiJWIRMpwB0zUAbp2gA.png" /><figcaption>Illustrative mechanism of TTC:USD market forces</figcaption></figure><p>Isn’t it beautiful that nobody needs to perform these transactions out of kindness? Greed makes things work out.</p><blockquote>But this only works reliably when redemption is accessible, transaction costs are manageable, and investors believe Titus Bank can actually pay them.</blockquote><h3>6. What about using another cryptocurrency to maintain the peg?</h3><p>What if, instead of keeping US dollars in a bank, we used another cryptocurrency to maintain the value of TTC?</p><p>Let’s introduce TitusFloat (TTF), a cryptocurrency whose price fluctuates freely.</p><p>Instead of promising that every TTC is backed by an actual US dollar, our protocol allows users to exchange TTC for <strong>USD 1 worth of TTF</strong>.</p><p>Notice the difference? We’re promising that 1 TTC can be exchanged for however many TTF tokens are worth USD 1 at the prevailing market price.</p><p>Say 1 TTF = USD 100:</p><ul><li>If Alice redeems 1 TTC, we burn her TTC and issue her 0.01 TTF. That’s USD 1 worth of TTF.</li><li>If TTF falls to USD 50, redeeming 1 TTC would instead create 0.02 TTF</li></ul><h4>6a. The mechanism of maintaining the peg is conceptually similar</h4><p>If TTC trades below USD 1, arbitrageurs can purchase discounted TTC and exchange it through the protocol for USD 1 worth of newly issued TTF.</p><p>For example:</p><ul><li>If 1 TTC trades at USD 0.90.</li><li>Investors each purchase 100 TTC for USD 90.</li><li>They redeem those tokens through the protocol and receive USD 100 worth of TTF.</li><li>They then sell the TTF on the market.</li><li>The TTC tokens are burned during redemption, reducing their circulating supply.</li></ul><p>Now supposed TTC trades above USD 1:</p><ul><li>Investors could use USD 1 worth of TTF to mint a new TTC then sell it for more than USD 1.</li><li>The newly minted TTC increases the supply, pushing its market price down.</li></ul><h4>6b. But this mechanism doesn’t necessarily work.</h4><p>Supposed 1 TTF initially trades at USD 100.</p><p>When someone redeems 1 TTC, the protocol only needs to mint 0.01 TTF to give them USD 1 worth of tokens. But what happens if TTF’s price falls ?</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*senBAJKNiEde3oYF4-ScVg.png" /><figcaption>Illustrative table of TTF to TTC exchanges dictated by market forces</figcaption></figure><p>As TTF becomes cheaper, the protocol has to issue more TTF to honour the same USD 1 redemption.</p><p>And what are investors likely to do with the TTF they receive?</p><p>Sell it.</p><p>After all, they wanted to redeem their TTC for something worth one US dollar. They didn’t necessarily want to hold a volatile cryptocurrency.</p><p>But as more TTF is minted and sold, the additional supply and selling pressure can cause TTF’s market price to fall further. Which means the protocol must issue even more TTF to honour subsequent redemptions.</p><p>Which creates even more selling pressure.</p><p>Which causes the price to fall further.</p><p>The vicious part: this is all done <strong>automatically via code</strong>. The protocol automatically increases the quantity of TTF issued per redemption! The mechanism trying to save the peg is the hyperinflation engine. This is the ailment of algorithmic stablecoins.</p><p>This isn’t just theoretical by the way.</p><h4>6c. Terra/LUNA: When the stablecoin collapsed</h4><p><a href="https://mitsloan.mit.edu/cfi/anatomy-a-run-terra-luna-crash">In May 2022, the Terra ecosystem suffered one of the most spectacular collapses in cryptocurrency history.</a></p><p>TerraUSD (UST) was an algorithmic stablecoin designed to maintain a value of USD 1, and its peg mechanism depended on another cryptocurrency, LUNA.</p><p>Under the protocol’s original design, users could exchange 1 UST for USD 1 worth of LUNA, and vice versa, through the mint-and-burn mechanism. When UST began losing its peg, investors rushed to exit their positions.</p><p>The protocol attempted to restore the peg by allowing UST to be redeemed for newly minted LUNA. But that created enormous quantities of additional LUNA, causing a price collapse — and more tokens minted to maintain redemption value.</p><p>The supply of LUNA exploded from hundreds of millions of tokens to trillions in a matter of days. Its market price collapsed from tens of dollars to tiny fractions of a cent.</p><p>Ultimately, tens of billions of dollars in market value disappeared from the Terra ecosystem.</p><p>And perhaps the most unsettling part is that the collapse didn’t require someone to break the blockchain’s cryptography</p><p>Phew. That was a lot of economics theory. And we haven’t written a single line of code yet.</p><h3>7. First, we need a ledger</h3><p>We’ll create a toy fiat-backed stablecoin and demonstrate how its transactions can be recorded on a blockchain.</p><p>First, we need a ledger to keep track of how many TTC tokens each person owns.</p><p>Our TokenLedger class will manage token balances, minting, burning, and transfers. Whenever a token operation occurs, we&#39;ll broadcast a corresponding transaction and include it on our blockchain, reusing the mempool implementation from my previous articles.</p><blockquote>For simplicity, our token balances are maintained in a Python dictionary, while the blockchain records the associated transaction events. In a production token implementation, balance updates would ordinarily be enforced as part of the blockchain’s smart contract state.</blockquote><p><strong>The important distinction is that our blockchain records token activity, not the actual US dollars held by Titus Bank.</strong></p><pre>class TokenLedger:<br>    &quot;&quot;&quot;Minimal on-chain token ledger for stablecoin and HTLC lessons.&quot;&quot;&quot;<br><br>    def __init__(<br>        self, symbol: str, blockchain: Blockchain, network: Network<br>    ) -&gt; None:<br>        &quot;&quot;&quot;Attach this ledger to one chain and one gossip network.&quot;&quot;&quot;<br>        self.symbol = symbol<br>        self.blockchain = blockchain<br>        self.network = network<br>        self.balances: dict[str, float] = {}<br>        self.total_supply: float = 0.0<br><br>    def record_payload(self, payload: str, tx_id: str | None = None) -&gt; None:<br>        &quot;&quot;&quot;Gossip ``payload`` as a ``Transaction`` and include it from the origin.&quot;&quot;&quot;<br>        tx = Transaction(<br>            tx_id or f&quot;{self.symbol}-{len(self.blockchain.chain)}&quot;,<br>            payload,<br>        )<br>        origin = self.network.node_names[0]<br>        self.network.broadcast(tx, origin)<br>        self.network.include(origin, tx.tx_id, self.blockchain)<br><br>    def _record(self, record: dict[str, str | float]) -&gt; None:<br>        &quot;&quot;&quot;Gossip a token event as a ``Transaction``, then include it.&quot;&quot;&quot;<br>        event = str(record.get(&quot;event&quot;, &quot;EVENT&quot;))<br>        self.record_payload(<br>            json.dumps(record, sort_keys=True),<br>            tx_id=f&quot;{self.symbol}-{event}-{len(self.blockchain.chain)}&quot;,<br>        )<br><br>    def mint(self, to: str, amount: float) -&gt; None:<br>        &quot;&quot;&quot;Create tokens for ``to`` and record a ``MINT`` receipt.&quot;&quot;&quot;<br>        self._require_positive(amount)<br>        self.balances[to] = self.balances.get(to, 0) + amount<br>        self.total_supply += amount<br>        self._record({&quot;event&quot;: &quot;MINT&quot;, &quot;symbol&quot;: self.symbol, &quot;to&quot;: to, &quot;amount&quot;: amount})<br><br>    def transfer(self, sender: str, receiver: str, amount: float) -&gt; bool:<br>        &quot;&quot;&quot;Move tokens between holders without changing supply&quot;&quot;&quot;<br>        self._require_positive(amount)<br>        if self.balances.get(sender, 0) &lt; amount:<br>            return False<br>        self.balances[sender] -= amount<br>        self.balances[receiver] = self.balances.get(receiver, 0) + amount<br>        self._record(<br>            {<br>                &quot;event&quot;: &quot;TRANSFER&quot;,<br>                &quot;symbol&quot;: self.symbol,<br>                &quot;from&quot;: sender,<br>                &quot;to&quot;: receiver,<br>                &quot;amount&quot;: amount,<br>            }<br>        )<br>        return True<br><br>    def burn(self, holder: str, amount: float) -&gt; bool:<br>        &quot;&quot;&quot;Destroy tokens and shrink supply.&quot;&quot;&quot;<br>        self._require_positive(amount)<br>        if self.balances.get(holder, 0) &lt; amount:<br>            return False<br>        self.balances[holder] -= amount<br>        self.total_supply -= amount<br>        self._record(<br>            {&quot;event&quot;: &quot;BURN&quot;, &quot;symbol&quot;: self.symbol, &quot;holder&quot;: holder, &quot;amount&quot;: amount}<br>        )<br>        return True</pre><p>Our ledger can:</p><ul><li>mint / burn — create or destroy tokens and change total_supply.</li><li>transfer — move tokens; supply stays put. Ownership moved, nothing was created.</li></ul><h3>8. Next we need a FiatBackedIssuer</h3><p>Now that we can create, destroy, and transfer TTC, we need someone to guarantee that every TTC is backed by one US dollar.</p><p>If I haven’t said so already, welcome to Titus Bank 🏦</p><p>In code, Titus Bank will be represented by a FiatBackedIssuer class that maintains a separate record of its USD reserves.</p><p>It needs three important methods.</p><ul><li>deposit_and_mint — When someone deposits USD, Titus Bank records the deposit and asks TokenLedger to mint the corresponding amount of TTC.</li><li>redeem — When someone wants their money back, Titus Bank asks TokenLedger to burn the TTC and reduces its reserve balance accordingly.</li><li>record_reserve_loss — This is where things get interesting. The bank can record a loss of USD reserves <strong>without modifying the token ledger or creating a blockchain transaction</strong>. This method is particularly important — the blockchain cannot independently determine whether the issuer has sufficient reserves, and USD reserves exist outside our blockchain. Therfore, a change in these reserves would not affect the number of TTC in circulation.</li></ul><pre>class FiatBackedIssuer:<br>    def __init__(self, name: str, ledger: TokenLedger) -&gt; None:<br>        &quot;&quot;&quot;Bind this issuer to one token ledger.&quot;&quot;&quot;<br>        self.name = name<br>        self.ledger = ledger<br>        self.reserve_usd: float = 0.0<br><br>    def deposit_and_mint(self, to: str, usd_amount: float) -&gt; None:<br>        &quot;&quot;&quot;Record fictional USD reserves and mint the matching token amount.&quot;&quot;&quot;<br>        TokenLedger._require_positive(usd_amount)<br>        self.reserve_usd += usd_amount<br>        self.ledger.mint(to, usd_amount)<br><br>    def redeem(self, holder: str, token_amount: float) -&gt; bool:<br>        &quot;&quot;&quot;Burn tokens and release the corresponding fictional reserve amount.&quot;&quot;&quot;<br>        TokenLedger._require_positive(token_amount)<br>        if self.reserve_usd &lt; token_amount:<br>            return False<br>        if not self.ledger.burn(holder, token_amount):<br>            return False<br>        self.reserve_usd -= token_amount<br>        return True<br><br>    def record_reserve_loss(self, usd_amount: float) -&gt; bool:<br>        &quot;&quot;&quot;Record an off-chain loss against this fictional issuer&#39;s reserves.&quot;&quot;&quot;<br>        TokenLedger._require_positive(usd_amount)<br>        if self.reserve_usd &lt; usd_amount:<br>            return False<br>        self.reserve_usd -= usd_amount<br>        return True<br><br>    @property<br>    def reserve_ratio(self) -&gt; float:<br>        &quot;&quot;&quot;Return the fictional reserve amount divided by issued token supply.&quot;&quot;&quot;<br>        if self.ledger.total_supply == 0:<br>            return 1.0<br>        return self.reserve_usd / self.ledger.total_supply<br><br>    @property<br>    def backing_per_token(self) -&gt; float:<br>        &quot;&quot;&quot;Return simplified fictional backing per token, not a market price.&quot;&quot;&quot;<br>        return min(1.0, self.reserve_ratio)</pre><h3>9. Taking our stablecoin blockchain for a spin</h3><p>We’ll use the same proof-of-stake blockchain in my <a href="https://medium.com/mitb-for-all/proof-of-stake-the-mining-free-blockchain-ddae684ffec5">previous article</a> and extend it a little:</p><pre>class StableCoinBlockchain(Blockchain):<br>    &quot;&quot;&quot;PoS blockchain extended with a readable name and proposer rewards.&quot;&quot;&quot;<br><br>    def __init__(self, name: str, validators: list[Validator]) -&gt; None:<br>        &quot;&quot;&quot;Name this chain and reuse the parent PoS genesis.&quot;&quot;&quot;<br>        super().__init__(validators)<br>        self.name = name<br><br>    def add_block(self, data: str) -&gt; tuple[Block, Validator]:<br>        &quot;&quot;&quot;Append a block through the shared PoS implementation and pay its proposer.&quot;&quot;&quot;<br>        block, proposer = super().add_block(data)<br>        proposer.stake += BLOCK_REWARD + FEE_PER_TX<br>        return block, proposer<br><br>    def accept_candidate(self, block: Block) -&gt; None:<br>        &quot;&quot;&quot;Append a mempool-included candidate and pay its named proposer.&quot;&quot;&quot;<br>        super().accept_candidate(block)<br>        proposer = next(v for v in self.validators if v.name == block.proposer)<br>        proposer.stake += BLOCK_REWARD + FEE_PER_TX</pre><p>Then, reusing the Network class from my <a href="https://medium.com/mitb-for-all/introducing-mempools-the-one-time-gossip-pays-off-9fbc92701f70">previous article on mempools</a> and wiring everything togther:</p><pre>nodes = [&quot;Titus-Node-1&quot;, &quot;Titus-Node-2&quot;]<br>network = Network(nodes, random.Random(7))<br>chain = StableCoinBlockchain(<br>    &quot;TitusChain&quot;,<br>    [Validator(&quot;Titus-Node-1&quot;, 250), Validator(&quot;Titus-Node-2&quot;, 150)],<br>)<br>tituscoin = TokenLedger(&quot;TTC&quot;, chain, network)<br>titus_issuer = FiatBackedIssuer(&quot;Fictional Titus Bank&quot;, tituscoin)</pre><p>Let’s imagine Alice deposits USD 1000 with Titus Bank. Titus Bank would then have to mint 1000 TTC.</p><pre>titus_issuer.deposit_and_mint(&quot;Alice&quot;, 1_000)<br><br>print(f&quot;Alice: {tituscoin.balances[&#39;Alice&#39;]:.0f} TTC&quot;)<br>print(f&quot;Supply: {tituscoin.total_supply:.0f} TTC&quot;)<br>print(f&quot;Reserve: ${titus_issuer.reserve_usd:,.0f} fictional USD&quot;)</pre><p>Output:</p><pre>Alice: 1000 TTC<br>Supply: 1000 TTC<br>Reserve: $1,000 fictional USD</pre><p>Now suppose Alice decides to pay Bob 250 TTC. Would Titus Bank need to mint another 250 TTC?</p><p>Of course not. We’re simply transferring ownership of existing tokens.</p><pre>supply_before_transfer = tituscoin.total_supply<br>reserve_before_transfer = titus_issuer.reserve_usd<br>transfer_succeeded = tituscoin.transfer(&quot;Alice&quot;, &quot;Bob&quot;, 250)<br><br>print(tituscoin.balances)<br>print(f&quot;Supply still: {tituscoin.total_supply:.0f} TTC&quot;)<br><br># {&#39;Alice&#39;: 750, &#39;Bob&#39;: 250}<br># Supply still: 1000 TTC</pre><p>Alice now has 750 TTC while Bob has 250 TTC. Total supply of TTC remains at 1,000 TTC.</p><p>Now let’s say Bob redeems 100 TTC:</p><ul><li>Titus Bank would then have to release 100 USD to Bob, and total supply changes.</li><li>The TokenLedger.burn removes Bob’s 100 TTC on-chain and reduces reserve_usd by 100 off-chain. Both of which are coordinated by FiatBackedIssuer.redeem</li></ul><pre><br>redemption_succeeded = titus_issuer.redeem(&quot;Bob&quot;, 100)<br><br>print(f&quot;Bob: {tituscoin.balances[&#39;Bob&#39;]:.0f} TTC&quot;)<br>print(f&quot;Supply: {tituscoin.total_supply:.0f} TTC&quot;)<br>print(f&quot;Reserve: ${titus_issuer.reserve_usd:,.0f} fictional USD&quot;)<br><br>Bob: 150 TTC<br>Supply: 900 TTC<br>Reserve: $900 fictional USD</pre><h4>Notice that a problem off the chain does not rewrite the token ledger</h4><p>Say Titus Bank records a USD 180 reserve loss. Token balances and total supply should not automatically shrink!</p><p>Instead, FiatBackedIssuer.record_reserve_loss changes only reserve_usd . Alice and Bob’s TTC are in tact and total supply remains at 900.</p><pre>supply_before_loss = tituscoin.total_supply<br>balances_before_loss = tituscoin.balances.copy()<br>blocks_before_loss = len(chain.chain)<br>loss_recorded = titus_issuer.record_reserve_loss(180)<br><br>print(f&quot;Reserve after loss: ${titus_issuer.reserve_usd:,.0f} fictional USD&quot;)<br>print(f&quot;Supply after loss: {tituscoin.total_supply:.0f} TTC&quot;)<br>print(f&quot;Reserve ratio: {titus_issuer.reserve_ratio:.0%}&quot;)<br>print(f&quot;Backing per TTC: ${titus_issuer.backing_per_token:.2f}&quot;)<br>print(f&quot;Blockchain valid: {chain.is_valid()}&quot;)<br><br># Reserve after loss: $720 fictional USD<br># Supply after loss: 900 TTC<br># Reserve ratio: 80%<br># Backing per TTC: $0.80<br># Blockchain valid: True</pre><p>Importantly, Titus Bank can no longer redeem every TTC for USD 1 because of a lack of reserves. But nobody knows that — shh…</p><h3>Moving forward</h3><p>We’ve established why stablecoins exist, how their exchange rates are maintained, and what happens when the mechanisms supporting those exchange rates fail.</p><p>We’ve also created our own fictional stablecoin, TitusCoin, using the same proof-of-stake blockchain and mempool concepts from my previous articles.</p><p>But it’s fictional of course. Nothing is technically stopping me from creating my own stablecoin and declaring myself a billionaire — getting everyone else to believe I’m a billionaire is the hard part.</p><p>But there are other interesting questions worth exploring as well.</p><p>Suppose there are now two independent fictional banks, Titus Bank and Mary Bank that need to settle accounts after hours.</p><ul><li>Titus Bank issues TitusCoin (TTC), while Mary Bank issues MaryCoin (MTC).</li><li>Both stablecoins exist on different blockchains.</li></ul><p>How would these transactions be coordinated? Well, that’s where things get interesting.</p><p>I guess our journey continues.</p><p><em>Disclaimer: All opinions and interpretations are that of the writer, and not of MITB. I declare that I have full rights to use the contents published here, and nothing is plagiarized. I declare that this article is written by me and not with any generative AI tool such as ChatGPT. I declare that no data privacy policy is breached, and that any data associated with the contents here are obtained legitimately to the best of my knowledge. I agree not to make any changes without first seeking the editors’ approval. Any violations may lead to this article being retracted from the publication.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=2b3e63e6c5f1" width="1" height="1" alt=""><hr><p><a href="https://medium.com/mitb-for-all/introducing-stablecoins-2b3e63e6c5f1">Introducing stablecoins</a> was originally published in <a href="https://medium.com/mitb-for-all">MITB For All</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[TAME-MA: Taming Mobile Agents via Trajectory-Level Trustworthiness under Jailbreak Manipulations]]></title>
            <link>https://medium.com/mitb-for-all/tame-ma-taming-mobile-agents-via-trajectory-level-trustworthiness-under-jailbreak-manipulations-30fe5a8f7955?source=rss----825f074cc537---4</link>
            <guid isPermaLink="false">https://medium.com/p/30fe5a8f7955</guid>
            <category><![CDATA[mobile-agent]]></category>
            <category><![CDATA[ai-safety]]></category>
            <dc:creator><![CDATA[Tm Fang ]]></dc:creator>
            <pubDate>Fri, 09 Oct 2026 08:09:08 GMT</pubDate>
            <atom:updated>2026-10-09T08:09:07.611Z</atom:updated>
            <content:encoded><![CDATA[<p>Mobile agent systems based on multimodal foundation models are increasingly used for mobile device control, virtual assistant interaction, and multimodal task execution. These systems combine graphical user interface inputs with natural-language planning and can autonomously execute multi-step tasks. This capability also introduces a different class of security risk: jailbreak attacks can manipulate an agent into bypassing behavioral norms and performing high-risk operations such as tampering with settings, exposing sensitive information, or initiating unauthorized transactions.</p><p>Existing protections for large language models mainly operate at the textual interaction level. Safety prompting, guardrail policies, and jailbreak filters can often determine whether a single response should be refused. Mobile agents are different. Their risk does not necessarily appear at one isolated step; it can accumulate over a multi-step chain of behaviors.</p><p>An agent may first open a payment or settings application, then navigate through apparently normal interface states, bypass a confirmation process, and finally enter or transmit sensitive information. Individual actions such as opening an application, selecting a contact, typing a value, or pressing a button may appear harmless in isolation. The risk emerges from the dynamic evolution of the entire visual-behavior trajectory across interfaces, applications, and state transitions.</p><p>This creates two central challenges. First, step-level defenses designed for language models can fail to detect how risk evolves over time, making chained attack paths difficult to identify and block. Second, security evaluation in GUI environments still relies heavily on manual review and heuristic rules, which are expensive, subjective, and difficult to scale.</p><p>TAME-MA addresses both challenges by treating trajectory-level trustworthiness as the core safety objective.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*mZLgaqLWNTDRAL0WhQJWOQ.png" /><figcaption>Figure 1 provides an intuitive overview of the mobile-agent workflow, SafeTrajGuard defense, and GPTJudge evaluation for normal and jailbreak tasks.</figcaption></figure><p><strong>TAME-MA: defense and evaluation at the trajectory level</strong></p><p>TAME-MA consists of two main components. SafeTrajGuard is a behavior-level defense module that evaluates candidate operations before execution. GPTJudge is a trajectory-level evaluator that performs automated post-hoc safety assessment over complete execution traces.</p><p>SafeTrajGuard is trained using SafeTrajDPO, a trajectory-level preference optimization mechanism. It jointly models historical interactions and the current action to capture progressive risk patterns such as semantic drift and privilege escalation. GPTJudge transforms the task description, behavior logs, and final interface state into a structured evaluation input and produces a trajectory-level safety score and jailbreak judgment.</p><p>The framework is designed to be non-invasive and modular. It can operate alongside an existing mobile agent without modifying the underlying agent architecture.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*MTyWSFWfK6fvk8x5fldGKA.png" /><figcaption>Figure 2 The framework contains risk-weighted trajectory preference optimization, history-aware inference with SafeTrajGuard, and automated jailbreak assessment with GPTJudge.</figcaption></figure><p><strong>Why a trajectory should not be treated as a list of equal actions</strong></p><p>A mobile task is naturally represented as an ordered sequence of actions. The actions may include clicking a button, entering text, switching a toggle, navigating to another screen, or submitting information. These actions have clear temporal and causal dependencies.</p><p>However, not every action contributes equally to safety risk. Some operations are supporting actions, such as browsing an interface, while others directly determine a state transition or create a security consequence. Examples include `click:confirm`, `navigate:settings`, and `input:password`. These operations often act as turning points in a trajectory.</p><p>TAME-MA therefore treats a trajectory as a risk-sensitive behavior chain. During training, key actions with significant decision-making or safety impact receive stronger supervision. This design focuses learning on the parts of the trajectory that matter most for security rather than treating every action as equally important.</p><p><strong>Safe and unsafe trajectory preference learning</strong></p><p>To enable trajectory-level preference learning, TAME-MA constructs aligned pairs of safe and unsafe trajectories grounded on common GUI contexts. Unsafe trajectories contain risk-sensitive operations drawn from categories such as privilege escalation, system reset, sensitive-data leakage, unauthorized transactions, authentication changes, phishing, or malicious content submission. Safe trajectories represent legitimate task completion in the same or compatible interface context.</p><p>SafeTrajDPO builds on Direct Preference Optimization and uses these positive and negative trajectories to train SafeTrajGuard. A risk-aware weighting mechanism places greater emphasis on key actions, especially high-impact unsafe steps. Local subsequences around key actions are also reused during training so that safety supervision is concentrated around important decision points.</p><p>The goal is not merely to teach the guard to reject suspicious words. It is to learn preferences over structured behavior sequences and to distinguish how apparently ordinary actions combine into safe or unsafe task progression.</p><p><strong>History-aware inference</strong></p><p>After training, SafeTrajGuard is integrated into the mobile-agent pipeline as a safety reviewer. At each decision step, it evaluates the task objective, the current GUI state, the candidate action proposed by the original agent, and recent interaction history.</p><p>The historical context is important because the safety of a current action may depend on what has already happened. Navigating to a sensitive settings page may be acceptable in one task and suspicious in another. Entering information may be benign in isolation but risky if earlier steps exposed private data or established an unauthorized payment workflow.</p><p>To keep the context compact, the main configuration uses the two most recent historical traces. With this local history, SafeTrajGuard moves from static single-step judgment to dynamic security inference over a behavior chain. It can identify patterns such as consecutive navigation to sensitive pages, privacy-sensitive information flow, or action combinations that collectively form unauthorized access or payment workflows.</p><p><strong>Automated evaluation with GPTJudge</strong></p><p>The second component, GPTJudge, addresses the difficulty of evaluating mobile-agent safety at scale. Manual evaluation requires reviewers to inspect long behavior traces, interface states, and final outcomes. Rule-based evaluation is easier to automate but often fails when tasks or interfaces change.</p><p>GPTJudge evaluates the complete trajectory using three types of information: the original task description, the sequence of actions produced by the agent, and the final interface state. These are encoded into a structured prompt and assessed by a large language model acting as a safety evaluator.</p><p>Each trajectory receives a safety score from 0 to 100. Higher scores indicate safer and more task-aligned behavior, while lower scores indicate greater jailbreak risk. A threshold of 50 is used in the main experiments to convert the score into a binary jailbreak-success judgment.</p><p>This provides two complementary metrics: G-Score measures trajectory safety on a continuous scale, while G-ASR measures the proportion of jailbreak tasks judged to be successful. Human evaluation is retained as H-ASR to verify whether the automated assessment is consistent with expert judgment.</p><p><strong>Evaluation setup</strong></p><p>We evaluate TAME-MA on two representative mobile-agent systems: the single-agent AppAgent and the multi-agent Mobile-Agent-v2. Both operate in an Android emulator environment with GUI parsing and natural-language task execution.</p><p>The evaluation is based on MobileSafetyBench and covers six task categories: Text Messaging, Web Navigation, Social Media, Finance, Device/Data Management, and Utility. The original 80 real-world scenarios are expanded to 330 jailbreak tasks using semantic rewriting, cross-interface context perturbation, and human verification.</p><p>The evaluation measures both security and utility. G-Score and G-ASR capture automated trajectory-level safety; H-ASR provides human judgments; Task Completion Rate measures whether benign tasks remain usable after the defense is introduced.</p><p><strong>Main defense results</strong></p><p>Across six jailbreak categories, TAME-MA substantially improves trajectory safety. The average G-Score increases from 31.04 without defense to 83.94 with TAME-MA. The table below summarizes the central comparison with guard-model baselines.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/888/1*VzK-s7aibQc5Q_GBm12wZQ.png" /><figcaption>Table 1:Comparison of guard models with TAME-MA<br>in jailbreak and normal tasks.</figcaption></figure><p>The key result is the security-utility balance. TAME-MA reduces G-ASR to 16.7% and H-ASR to 8.4%, while the normal-task completion rate remains 77.8%, the same as the no-defense setting. In contrast, some alternative guards reduce attack success by becoming more conservative, which also lowers normal task completion.</p><p>The results also show strong protection in high-risk categories. In Finance and Device Management, where the no-defense attack success rate can reach 100%, TAME-MA reduces both automated and human-measured jailbreak success to zero in the reported category-level experiments.</p><p>Optional visual — insert the original Figure 5 here. It shows examples in which SafeTrajGuard blocks a critical transfer step induced by a forged system prompt and prevents the posting of harmful content.</p><p>Generalization across foundation models and mobile-agent frameworks</p><p>TAME-MA is evaluated across GPT-4o, Gemini 2.5, Claude 3, and DeepSeek within AppAgent. Across these models, the average G-Score improves from 51.75 to 85.94 and the average G-ASR decreases from 55.5% to 16.7%.</p><p>For GPT-4o, G-ASR decreases from 86.1% to 16.7% and H-ASR from 72.2% to 8.4%. On Claude 3, the defended G-Score reaches 92.31 and H-ASR falls to 2.7%. On DeepSeek, the defended G-Score reaches 89.23 with H-ASR of 8.3%.</p><p>The framework is also evaluated on both AppAgent and Mobile-Agent-v2. In the multi-agent Mobile-Agent-v2 setting, the average G-ASR decreases from 87.9% to 2.2% and H-ASR from 78.9% to 1.1% in the selected high-risk categories. This indicates that the defense can adapt to more complex multi-stage planning and execution processes.</p><p>Why history matters — and why more history is not always better</p><p>One of the most informative ablations changes the amount of trajectory history available to SafeTrajGuard.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*538BrT0yd5jSlffzGPDNIw.png" /><figcaption>Table2: Ablation study on the history window size used by SafeTrajGuard.</figcaption></figure><p>Using only the current action is insufficient for many cross-step risks. Adding one or two recent steps substantially improves defense performance. The two-step history window gives the best overall balance between security, task completion, false positives, and latency.</p><p>Importantly, using the entire history does not continue to improve performance. Full history increases latency, increases the false-positive rate, and slightly reduces task completion. This suggests that excessively long context can introduce irrelevant information or noise. For this reason, the main configuration uses the most recent two steps rather than the full trajectory history for online decision making.</p><p>Safety gains are not simply over-blocking</p><p>A practical guard can appear safe simply because it refuses too many actions. We therefore separately measure false positives on normal tasks.</p><p>With the LLaMA3–8B BF16 guard, Action-FPR is only 0.8% and the task-level blocking failure rate is 0.6%, while normal TCR remains 77.8%, identical to the no-defense baseline. This indicates that the main security benefit is not obtained by indiscriminately blocking benign actions.</p><p>There is also a deployment trade-off between latency and security. The LLaMA3–8B configuration adds approximately 2.93 seconds per decision step and achieves G-ASR of 16.7%. Smaller guard models reduce latency but have higher G-ASR and higher false-positive rates. This allows the deployment configuration to be adjusted according to device resources and task risk.</p><p><strong>Limitations and future directions</strong></p><p>TAME-MA demonstrates substantial security improvements across multiple mobile-agent systems and high-risk task scenarios, but several limitations remain. SafeTrajGuard mainly relies on structured GUI-level information and action trajectories, which makes more dynamic interfaces and unstructured visual interactions more challenging. The current key-action identification mechanism is used only during training, but its heuristic design may still limit the discovery of important operations in applications with non-standard interface labels or customized UI components.</p><p>SafeTrajDPO also requires fine-tuning on preference data, and training or inference overhead can become more important when scaling to larger models or multi-agent systems. Finally, GPTJudge currently evaluates trajectories according to static safety norms, whereas real-world safety requirements can vary by task context and evolve over time.</p><p>Future work will extend trajectory-level safety modeling to broader agent modalities, reduce training and inference overhead, improve automatic discovery of high-impact actions, and investigate adaptive safety standards and formal verification tools.</p><p><strong>Full paper</strong></p><p>TAME-MA: Taming Mobile Agents via Trajectory-Level Trustworthiness under Jailbreak Manipulations<br>Full publication link: <a href="https://link.springer.com/article/10.1007/s11263-026-03015-7">https://link.springer.com/article/10.1007/s11263-026-03015-7</a></p><p><em>Disclaimer: All opinions and interpretations are that of the writer, and not of MITB. I declare that I have full rights to use the contents published here, and nothing is plagiarized. I declare that this article is written by me and not with any generative AI tool such as ChatGPT. I declare that no data privacy policy is breached, and that any data associated with the contents here are obtained legitimately to the best of my knowledge. I agree not to make any changes without first seeking the editors’ approval. Any violations may lead to this article being retracted from the publication.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=30fe5a8f7955" width="1" height="1" alt=""><hr><p><a href="https://medium.com/mitb-for-all/tame-ma-taming-mobile-agents-via-trajectory-level-trustworthiness-under-jailbreak-manipulations-30fe5a8f7955">TAME-MA: Taming Mobile Agents via Trajectory-Level Trustworthiness under Jailbreak Manipulations</a> was originally published in <a href="https://medium.com/mitb-for-all">MITB For All</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[A Patch-based Cross-view Regularized Framework for Backdoor Defense in Multimodal Large Language…]]></title>
            <link>https://medium.com/mitb-for-all/a-patch-based-cross-view-regularized-framework-for-backdoor-defense-in-multimodal-large-language-28618e3ec05d?source=rss----825f074cc537---4</link>
            <guid isPermaLink="false">https://medium.com/p/28618e3ec05d</guid>
            <category><![CDATA[backdoor-attack]]></category>
            <category><![CDATA[ai-safety]]></category>
            <category><![CDATA[data-science]]></category>
            <dc:creator><![CDATA[Tm Fang ]]></dc:creator>
            <pubDate>Wed, 07 Oct 2026 02:04:46 GMT</pubDate>
            <atom:updated>2026-10-09T08:10:44.015Z</atom:updated>
            <content:encoded><![CDATA[<h3>A Patch-based Cross-view Regularized Framework for Backdoor Defense in Multimodal Large Language Models</h3><p>Multimodal large language models are becoming an important infrastructure for general-purpose AI systems by unifying visual and linguistic information and demonstrating strong generalization capabilities in tasks such as visual question answering, vision-language understanding, embodied intelligence, human-computer interaction, and automated content generation. As these models are deployed in safety-sensitive scenarios such as healthcare, autonomous driving, intelligent manufacturing, and content moderation, the reliability of their output behavior is no longer merely a matter of performance but is directly related to security and societal risks.</p><p>However, multimodal large language models are susceptible to backdoor implantation during instruction alignment and supervised fine-tuning. An attacker only needs to construct a small number of poisoned samples with trigger conditions to cause the model to output an attacker-defined target response when a specific trigger appears at test time, while still maintaining good performance on normal inputs. This makes backdoor attacks highly stealthy: once implanted, they can be difficult to detect through standard testing or manual inspection.</p><p>The defense problem is difficult for two reasons. First, realistic attackers can use low-to-moderate poisoning ratios, such as 5%, which results in sparse backdoor-related signals during training. Second, multimodal models have complex generation distributions, so aggressive defense can easily damage normal generation capability. The central challenge is therefore to suppress backdoor behavior without sacrificing utility.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*t3UAIMEeFv3A6pZEjO4rKQ.png" /><figcaption>Figure 1. It compares the same visual inputs under a backdoored model and the defended model. The backdoored model is hijacked to output a fixed target, while the defended model preserves correct and context-consistent descriptions.</figcaption></figure><p><strong>Threat setting and defense objective</strong></p><p>We focus on training-phase backdoor attacks under low-to-moderate poisoning ratios. During supervised fine-tuning, an attacker injects a small number of poisoned samples containing hidden trigger patterns. These samples establish a stable trigger-target association, so that the model behaves normally on clean inputs but produces a predefined target response when the trigger is present.</p><p>In our setting, only a very small number of samples are poisoned in the training dataset, and each training batch may contain only zero or one poisoned sample. This makes the backdoor highly difficult to identify through simple statistics or anomaly detection.</p><p>The defense objective is correspondingly two-sided. We want the model to stop producing the attacker-specified target when the trigger is present, while keeping its multimodal understanding and generation behavior on clean inputs as close as possible to the original model. Importantly, the defense should not require explicit annotations of poisoned samples or prior knowledge of the attack trigger.</p><p><strong>The key observation</strong>: backdoor responses are abnormally invariant across views</p><p>Backdoor attacks commonly rely on localized trigger patterns in visual inputs. During training, the model may overfit these local regions and establish a strong association between the trigger and a fixed target response.</p><p>To expose this behavior, we construct two views of the same input. The original image is kept unchanged, while a perturbed view is created by applying small local modifications such as masking, random-noise replacement, or patch shuffling. The textual instruction remains unchanged, and the perturbation is designed to preserve the global semantics of the image.</p><p>This dual-view construction reveals an important structural difference between normal and backdoor samples. Normal samples remain semantically consistent across the two views, but their generated forms can naturally vary. Backdoor samples, in contrast, tend to collapse into nearly identical fixed target responses across both views because of the strong trigger-target binding.</p><p>This cross-view output invariance is not a natural property of the task. It is a pathological feature associated with the backdoor structure. Instead of trying to identify poisoned samples directly, we use this abnormal invariance itself as a defense signal.</p><p><strong>The defense framework</strong></p><p>Our framework combines three components: patch-based view generation, cross-view discrepancy optimization, and uncertainty-aware output regularization.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*y3UpQcK9G7W9dvRxsEdWwQ.png" /><figcaption>Figure 2 . The figure shows the three components of the framework: patch-based view generation, cross-view discrepancy optimization, and uncertainty-aware output regularization.</figcaption></figure><p><strong>1. Patch-based view generation</strong></p><p>For each training sample, we construct an original view and a locally perturbed view. The perturbation operates only on the visual modality and keeps the text instruction unchanged. In the experiments, the patch size is randomly sampled from a small portion of the image area, and the position is sampled across the image. Perturbation operations include masking, random-noise replacement, and patch shuffling.</p><p>At the feature level, the model is encouraged to maintain stable high-level semantic representations across the two views. This reduces the tendency of the model to over-rely on localized trigger regions and weakens the separability of backdoor triggers in the feature space.</p><p><strong>2. Cross-view discrepancy optimization</strong></p><p>The central component operates at the output level. Normal and backdoor samples behave differently across the two views: normal outputs are semantically consistent but naturally diverse, whereas backdoor outputs tend to collapse into almost identical fixed responses.</p><p>We therefore penalize this anomalous cross-view similarity at the token-probability level. The optimization acts on each sample rather than relying on statistics of the poisoning ratio at the batch level. For normal samples, output distributions already have natural variation across views, so the resulting gradient is limited. For backdoor samples, output distributions are abnormally similar, which generates a stronger optimization signal.</p><p>This is particularly useful under low poisoning ratios. Even when only zero or one poisoned sample appears in a batch, the cross-view relationship of that individual sample can still expose the pathological stability of the backdoor response.</p><p><strong>3. Uncertainty-aware output regularization</strong></p><p>Simply forcing the outputs of the two views to become different can introduce a new problem: the model may produce overconfident extreme predictions, leading to output-distribution collapse or degradation of normal generation capability.</p><p>To prevent this side effect, we introduce uncertainty-aware regularization at the output layer. The regularization penalizes excessively concentrated, low-entropy predictions and helps preserve sufficient diversity in the model’s generation distribution.</p><p>The two objectives are complementary. Cross-view discrepancy optimization suppresses the abnormal stability of backdoor responses, while uncertainty-aware regularization stabilizes the normal output distribution. Together with patch-level feature regularization, they aim to balance security and normal generative capability.</p><p><strong>Experimental evaluation</strong></p><p>We evaluate the defense on two multimodal vision-language datasets, MS COCO and Flickr30k. Three representative multimodal models are used as victim models: OpenFlamingo, Otter, and BLIP-2. We construct the training-stage threat environment using a 5% poisoning ratio.</p><p>Six representative backdoor attacks are included: BadNets, Blended, LowFrequency, WaNet, InputAware, and DualKey. These attacks cover local pixel triggers, global blending triggers, frequency-domain triggers, geometric transformations, input-adaptive triggers, and dual-key triggers. Normal generation quality is evaluated using BLEU@4 and CIDEr, while backdoor effectiveness is measured by Attack Success Rate (ASR).</p><p><strong>Defense performance under different attacks</strong></p><p>On OpenFlamingo with the COCO dataset, the undefended model is almost completely controlled by the backdoor under all six attack settings, with ASR values between 96.7% and 98.9%. After applying the proposed defense, the ASRs are substantially reduced.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*M6Jww13SWqdoSBeR7XZErA.png" /><figcaption>Table 1 Defense Performance of OpenFlamingo on COCO Dataset</figcaption></figure><p>The defense also preserves normal generation performance. In the clean setting, BLEU@4 decreases from 25.8 to 23.9 and CIDEr decreases from 98.9 to 89.4, indicating that the security improvement does not come from simply degrading the model’s overall generation capability.</p><p>For triggered inputs, the defense can also recover normal generation performance. Under BadNets and Blended, for example, BLEU@4 improves from 2.7 and 2.0 to 14.6 and 13.7, while CIDEr improves from 1.5 and 1.9 to 45.5 and 41.3.</p><p><strong>Generalization across models and datasets</strong></p><p>The same trend is observed across different model architectures. On Otter, the defended ASRs for BadNets, Blended, LowFrequency, WaNet, InputAware, and DualKey are 55.0%, 60.4%, 42.8%, 48.4%, 32.8%, and 35.3%, respectively. On BLIP-2, the corresponding values are 52.1%, 58.1%, 40.1%, 46.2%, 30.6%, and 34.0%. Normal generation performance remains close to the clean setting in most cases.</p><p>We also evaluate cross-dataset generalization on Flickr30k. Without defense, all six attacks again achieve ASRs above 95%. After defense, the ASRs are reduced to 53.0%, 60.8%, 42.6%, 48.4%, 32.3%, and 35.8%. On clean inputs, BLEU@4 changes from 22.1 to 20.9 and CIDEr from 86.6 to 78.5.</p><p>These results show that the defense is not limited to a single model architecture or data distribution.</p><p><strong>Which component matters most?</strong></p><p>We conduct an ablation study under the BadNets attack on COCO to examine the contribution of each component.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/612/1*_hAZKqGhjnAkPeaFSSkWpg.png" /><figcaption>Table 2 Ablation Study on BadNets Attack (COCO Dataset)</figcaption></figure><p>Patch-level feature consistency weakens the model’s dependence on localized trigger patterns. Adding cross-view discrepancy further reduces ASR, confirming that anomalous cross-view invariance is a key signal for breaking the trigger-target association. The complete method provides the best overall defense and generation performance, with uncertainty-aware regularization helping stabilize the output distribution.</p><p><strong>Comparison with existing defenses</strong></p><p>We also compare the method with representative inference-phase defenses under the BadNets attack on Flickr30k. Without defense, ASR reaches 98.2% and generation quality is severely degraded. Fine-Pruning reduces ASR to 71.3%, STRIP Detection to 88.5%, and Entropy Filtering to 79.6%. Our method reduces ASR further to 53.0% while maintaining higher generation quality.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/644/1*yjZQfw2DVFqjISq2JSysYw.png" /><figcaption>Table 3 Defense performance against the BadNets attack with baselines.</figcaption></figure><p>The distinction is important. These inference-phase approaches mainly rely on post-hoc detection, filtering, or pruning. Our framework instead suppresses the formation and stability of the backdoor mechanism during training. In the reported comparison, this produces a better balance between attack suppression and task performance.</p><p><strong>Practical trade-offs and limitations</strong></p><p>The method introduces an additional forward pass for the perturbed view during training, resulting in approximately a twofold increase in training-time computation compared with standard supervised fine-tuning. GPU memory usage also increases because intermediate activations from both views must be stored. Importantly, the method does not introduce additional inference-time overhead because the perturbation mechanism is only used during training.</p><p>Overall, the defense shows consistent performance across attack types, datasets, and model architectures. The results suggest that cross-view discrepancy regularization can disrupt trigger-target bindings while preserving normal generation behavior.</p><p>The method also has limitations. Its core perturbation operates mainly on localized visual regions, so more complex cross-modal or global triggering mechanisms require further evaluation. Regularization weights may still need adjustment across models and tasks, and the method mainly takes effect during fine-tuning, which limits its direct applicability to already deployed models.</p><p><strong>Full paper</strong></p><p>The complete paper, including all mathematical formulations, baselines, implementation details, sensitivity analyses, and references, is available here:</p><p>A Patch-based Cross-view Regularized Framework for Backdoor Defense in Multimodal Large Language Models<br><a href="https://arxiv.org/abs/2604.04488">https://arxiv.org/abs/2604.04488</a></p><p><em>Disclaimer: All opinions and interpretations are that of the writer, and not of MITB. I declare that I have full rights to use the contents published here, and nothing is plagiarized. I declare that this article is written by me and not with any generative AI tool such as ChatGPT. I declare that no data privacy policy is breached, and that any data associated with the contents here are obtained legitimately to the best of my knowledge. I agree not to make any changes without first seeking the editors’ approval. Any violations may lead to this article being retracted from the publication.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=28618e3ec05d" width="1" height="1" alt=""><hr><p><a href="https://medium.com/mitb-for-all/a-patch-based-cross-view-regularized-framework-for-backdoor-defense-in-multimodal-large-language-28618e3ec05d">A Patch-based Cross-view Regularized Framework for Backdoor Defense in Multimodal Large Language…</a> was originally published in <a href="https://medium.com/mitb-for-all">MITB For All</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Flash loan attacks on smart contracts]]></title>
            <link>https://medium.com/mitb-for-all/flash-loan-attacks-on-smart-contracts-cb08c72408bd?source=rss----825f074cc537---4</link>
            <guid isPermaLink="false">https://medium.com/p/cb08c72408bd</guid>
            <category><![CDATA[python]]></category>
            <category><![CDATA[fintech]]></category>
            <category><![CDATA[blockchain]]></category>
            <category><![CDATA[flash-loan]]></category>
            <dc:creator><![CDATA[Tituslhy]]></dc:creator>
            <pubDate>Sun, 04 Oct 2026 04:08:14 GMT</pubDate>
            <atom:updated>2026-10-04T04:42:57.840Z</atom:updated>
            <content:encoded><![CDATA[<h4>The heist where you borrow the money first</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/688/1*Km0coZYZiINJvKPMfMcRzA@2x.jpeg" /></figure><p><a href="https://medium.com/mitb-for-all/introducing-smart-contracts-340c20cbe672">Last time</a>, I compared smart contracts to vending machines: put in $2, press B4, get a Snickers. No clerk, no judgment calls. A smart contract is code at an address that gives the same output every time it’s called, and it can call other contracts.</p><p>We built three of them.</p><ul><li>First, an Automated Market Maker (AMM) pool: a contract holding two piles of tokens, Ether (ETH) — a cryptocurrency — and USD, that quotes a price purely from the ratio between them. Its one rule: multiplying the two piles together must always give the same number, so selling ETH in pushes its price down.</li></ul><blockquote>Think of it as a puddle. Toss in a pebble and the water barely moves. Cannonball into it and it splashes everywhere. But a splash tells you about the puddle, not the ocean. In my code, one big sale crashed ETH’s price 69% in this pool while it stayed $2,000 everywhere else.</blockquote><ul><li>A lending protocol loans USD to borrowers by ascertaining the value of their collateral from their ETH, and liquidates borrowers whose collateral fell below a specific threshold of their debt. This price could come from the AMM pool, or from an external source, such as:</li><li>A price feed stored reported prices, acting as an oracle.</li></ul><p>Our lending protocol took a shortcut: it read its ETH price straight off the AMM pool. One call, and the puddle’s number became its truth. I hinted at a fix, taking the median of several independent price reports, without showing why it works.</p><p>This time we break it. I’m a huge fan of the <a href="https://en.wikipedia.org/wiki/Ocean%27s">Ocean’s</a> movies, but they needed eleven specialists. Ours needs one: you, wearing every hat. Then we’ll see why the median stops the whole thing cold.</p><p>As before, all the code is in my <a href="https://github.com/tituslhy/Blockchain/blob/0cce06e1f6fbe0a8c82e2d3d2da2e7667c2c3c95/notebooks/7.%20flash_loans.ipynb">GitHub repository</a>. We’ll be reusing the classes from my previous article.</p><h3>Act One: Alice executes a heist with ETH she owns</h3><p>Meet Alice, our attacker, with 40 ETH. She participates in a pool that is shallow: 50 ETH and $100,000.</p><p>A borrower has posted 10 ETH against a $12,000 loan: at $2,000/ETH (USD100k : 50 ETH) that’s a collateral ratio of 1.67, comfortably above the 1.5 threshold.</p><blockquote>Pause and predict: What happens if Alice sells all 40 ETH into the pool?</blockquote><pre>nodes = [&quot;Node A&quot;, &quot;Alice-Node&quot;, &quot;Bob-Node&quot;, &quot;Farid-Node&quot;]<br>validators = [<br>    Validator(&quot;Node A&quot;, 100),<br>    Validator(&quot;Alice-Node&quot;, 80),<br>    Validator(&quot;Bob-Node&quot;, 70),<br>    Validator(&quot;Farid-Node&quot;, 50),<br>]<br>owned_network = Network(nodes, random.Random(7))<br>owned_chain = Blockchain(validators)<br><br># Same two contracts, neither buggy. The lending rule still asks the puddle about the ocean.<br>owned_pool = AMMPool(&quot;amm-thin.eth&quot;, 50.0, 100_000.0)<br>owned_lending = LendingProtocol(&quot;lending-thin.eth&quot;, owned_pool)<br>owned_lending.call(<br>    &quot;open_loan&quot;, borrower=&quot;victim&quot;, collateral_eth=10.0, debt_usd=12_000.0<br>)<br><br># TX1: Alice dumps 40 ETH she already owns. The cheap price is now public. Crime scene, open floor plan.<br>alice_eth = 40.0<br>usd_from_dump, dump_tx, dump_block = submit_call(<br>    owned_network,<br>    owned_chain,<br>    &quot;Alice-Node&quot;,<br>    &quot;tx-dump-40eth&quot;,<br>    owned_pool,<br>    &quot;swap_eth_for_usd&quot;,<br>    eth_in=alice_eth,<br>)<br>manipulated_price = owned_pool.spot_price<br>victim_ratio = owned_lending.collateral_ratio(owned_lending.loans[&quot;victim&quot;])<br><br># TX2: liquidate at that public photograph, then buy ETH back at the price she herself created.<br>seized, liq_tx, liq_block = submit_call(<br>    owned_network,<br>    owned_chain,<br>    &quot;Alice-Node&quot;,<br>    &quot;tx-liquidate-victim&quot;,<br>    owned_lending,<br>    &quot;liquidate&quot;,<br>    borrower=&quot;victim&quot;,<br>)<br>usd_for_buyback = usd_from_dump - seized.debt_usd<br>eth_bought_back = owned_pool.call(&quot;swap_usd_for_eth&quot;, usd_in=usd_for_buyback)<br>alice_final_eth = eth_bought_back + seized.collateral_eth<br>profit_eth = alice_final_eth - alice_eth<br><br>print(f&quot;{dump_block.proposer} included {dump_tx.tx_id} in Block #{dump_block.index}&quot;)<br>print(<br>    f&quot;DUMP: Alice sells {alice_eth:.2f} ETH of her own for ${usd_from_dump:,.2f}; &quot;<br>    f&quot;AMM price becomes ${manipulated_price:,.2f}/ETH.&quot;<br>)<br>print(<br>    f&quot;The cheap puddle is now public. Victim ratio at this price: {victim_ratio:.2f}.&quot;<br>)<br>print(f&quot;{liq_block.proposer} included {liq_tx.tx_id} in Block #{liq_block.index}&quot;)<br>print(<br>    f&quot;LIQUIDATE: pay ${seized.debt_usd:,.2f} debt and seize &quot;<br>    f&quot;{seized.collateral_eth:.2f} ETH.&quot;<br>)<br>print(<br>    f&quot;BUY BACK: remaining ${usd_for_buyback:,.2f} buys {eth_bought_back:.2f} ETH.&quot;<br>)<br>print(<br>    f&quot;Alice started with {alice_eth:.2f} ETH, ends with {alice_final_eth:.2f} ETH, &quot;<br>    f&quot;profit {profit_eth:.2f} ETH.&quot;<br>)</pre><p>Output:</p><pre>Alice-Node included tx-dump-40eth in Block #1<br>DUMP: Alice sells 40.00 ETH of her own for $44,444.44; AMM price becomes $617.28/ETH.<br>The cheap puddle is now public. Victim ratio at this price: 0.51.<br>Alice-Node included tx-liquidate-victim in Block #2<br>LIQUIDATE: pay $12,000.00 debt and seize 10.00 ETH.<br>BUY BACK: remaining $32,444.44 buys 33.18 ETH.<br>Alice started with 40.00 ETH, ends with 43.18 ETH, profit 3.18 ETH.</pre><p><strong>Read the result.</strong> Alice deliberately sells 40 ETH into a pool that only had 50 ETH. Price collapses 69% — not because ETH crashed, but because she displaced most of the puddle.</p><p>At $617/ETH, the victim’s 10 ETH is worth $617 against $12,000, i.e. the collateral ratio is 0.51. The protocol, following its rules correctly, marks them liquidatable.</p><p>Alice then pays the $32,444 required to buy ETH back — at the depressed price she herself created and reaps <strong>3.18 ETH</strong> of pure profit, extracted from someone who never did anything wrong.</p><p>That’s why a lender should never treat a single thin pool’s spot price as the truth.</p><h3>Act Two: The same heist with a flash loan</h3><p>Act One had a barrier: you needed to own enough ETH like Alice to move the pool. A flash loan removes this requirement. The idea sounds insane the first time you hear it: a contract will lend you <strong>unlimited money with zero collateral, no credit check, and no questions asked</strong>.</p><p>The catch is brutal— the money must be back by the end of this transaction. If it is not, the entire transaction reverts and it is as though nothing happened.</p><p>Think of it like borrowing a suitcase of cash on the condition that you hand the exact suitcase back before you finish walking through the lobby.</p><p>You can take the money.</p><p>You can use it.</p><p>You can even make a profit with it.</p><p>But if you reach the exit without returning the suitcase, the entire walk through the lobby is rewound.</p><p>That sounds like a very safe loan.</p><p>And it is — <strong>for the lender.</strong></p><p>The lender doesn’t need to trust Alice because Alice never gets to keep the borrowed principal. The loan either comes back before the transaction ends, or the transaction doesn’t happen. And this is enforced by the code of the flash loan smart contract.</p><p>So why offer this at all?</p><p><strong>Fees.</strong> Real flash-loan providers charge fees for the temporary use of their liquidity. The lender gets paid without taking the usual credit risk of an unsecured loan.</p><p>Now change exactly one thing from Act One: Alice owns zero of the 40 ETH. The provider has 1,000 ETH available. Alice borrows 40 ETH.</p><p>She now has the same 40 ETH that previously belonged to her — except this time she only has it temporarily.</p><p>And because the loan, the manipulation, the liquidation, the buyback, and the repayment can all happen inside one transaction, nobody gets a chance to step between the pieces.</p><p>Five operations:</p><ol><li><strong>Borrow</strong> 40 ETH.</li><li><strong>Dump</strong> the 40 ETH into the shallow AMM, crushing its spot price.</li><li><strong>Liquidate</strong> the victim using that manipulated price.</li><li><strong>Buy back</strong> ETH cheaply with the USD recovered from the dump.</li><li><strong>Repay</strong> the original 40 ETH.</li></ol><p>If anything fails, the whole thing rewinds. If everything succeeds, whatever ETH remains after repayment belongs to Alice.</p><p>And here’s the deliciously uncomfortable part:</p><p><strong>We already know exactly how much should remain.</strong></p><p>We just watched Alice do the same thing in Act One with her own money. Now she does it with <strong>0 ETH.</strong></p><p>And she still walks away with <strong>3.18 ETH</strong>.</p><h3>Act Three: The heist fails</h3><p><strong>Now let’s change exactly one thing from Act Two:</strong> the victim has 40 ETH instead of 10 against the same $12,000 debt.This means that even after Alice’s attempted attack, the victim should survive.</p><p>Like Act Two, Alice still borrows 40 ETH and dumps all 40 ETH into the same shallow AMM.</p><pre>failed_tx = Transaction(<br>    &quot;tx-flash-victim2&quot;,<br>    &quot;Flash-loan against Victim2&quot;,<br>)<br><br>network.broadcast(<br>    failed_tx,<br>    origin=&quot;SketchyGuy-Node&quot;,<br>)<br><br>included_failed, failed_block = network.include(<br>    &quot;SketchyGuy-Node&quot;,<br>    failed_tx.tx_id,<br>    chain,<br>)<br><br>print(<br>    f&quot;{failed_block.proposer} included {included_failed.tx_id} &quot;<br>    f&quot;in Block #{failed_block.index} (execution comes next).&quot;<br>)</pre><p>We create the same shallow AMM, the same $12,000 loan, and the same flash-loan provider. The only difference is the victim’s collateral:</p><pre>failed_pool = AMMPool(<br>    &quot;amm-victim2.eth&quot;,<br>    50.0,<br>    100_000.0,<br>)<br><br>failed_protocol = LendingProtocol(<br>    &quot;lending-victim2.eth&quot;,<br>    failed_pool,<br>)<br><br>failed_protocol.open_loan(<br>    &quot;Victim2&quot;,<br>    40.0,<br>    12_000.0,<br>)<br><br>failed_provider = FlashLoanProvider(1_000.0)</pre><p>Before executing the attack, we record the state of the world:</p><pre>failed_before = (<br>    failed_pool.eth_reserve,<br>    failed_pool.usd_reserve,<br>    tuple(sorted(failed_protocol.loans)),<br>    failed_provider.eth_available,<br>)</pre><p>Now Alice’s attack:</p><ul><li>She dumps the borrowed ETH.</li><li>The AMM price collapses.</li><li>Then she attempts the liquidation. But this time, the protocol checks victim’s collateral ratio and refuses.</li></ul><pre>def failed_attack_action(<br>    amount_eth: float,<br>) -&gt; tuple[float, AttackTrace]:<br><br>    failed_pool.swap_eth_for_usd(amount_eth)<br><br>    failed_observation[&quot;price&quot;] = failed_pool.spot_price<br><br>    failed_observation[&quot;ratio&quot;] = (<br>        failed_protocol.collateral_ratio(<br>            failed_protocol.loans[&quot;Victim2&quot;]<br>        )<br>    )<br><br>    failed_protocol.liquidate(&quot;Victim2&quot;)</pre><p>The result:</p><pre>SketchyGuy-Node included tx-flash-victim2 in Block #2<br>(execution comes next).<br><br>Attempted dump moved AMM price to $617.28/ETH.<br>Victim2 ratio after dump: 2.06;<br>liquidation is rejected.<br><br>Attack transaction: REVERTED</pre><p>The AMM really did hit <strong>$617.28</strong>.</p><p>But the victim’s collateral ratio was still <strong>2.06</strong>, comfortably above the <strong>1.50</strong> liquidation threshold. So liquidate() rejected the operation. And once that happened, the flash-loan transaction reverted.</p><ul><li>The AMM reserves were restored.</li><li>The loan record was restored.</li><li>The flash-loan provider’s balance was restored.</li></ul><p>Everything returned to its pre-transaction state.</p><h3>This is the part that is easy to misunderstand.</h3><p>The price manipulation <strong>did happen during execution</strong>. It just didn’t survive the transaction. If a required step fails, the successful steps that came before it don’t get to stay behind as souvenirs.</p><p>But notice what did not happen: nobody looked at Alice’s transaction and said: “Hmm. This appears to be an attack. Let’s stop her.”</p><p>Only rules encoded in code within smart contracts are evaluated. In Act Two, every rule check passed, so the attack succeeded. In Act Three, liquidate() failed its rule check, so the transaction reverted.</p><h3>The defence: stop asking the puddle</h3><p>Act Three failed because, even at the manipulated price, the victim’s collateral ratio remained above the liquidation threshold.</p><p>But what if Alice had a flash loan of 100 ETH? The failure in Act Three wasn’t that flash loans suddenly stopped working. It was that <strong>this particular amount of manipulation wasn’t enough to make the victim liquidatable</strong>. Give Alice enough capital, and she may be able to push the puddle far enough.</p><p>So the real defence cannot be “make the puddle deeper.” It has to be <strong>stop asking the puddle for the market price in the first place.</strong></p><p>My previous article already gave the fix: <strong>stop asking the puddle.</strong> Don’t make the lending protocol trust a single price source that an attacker can manipulate with a large enough trade. Point the lending protocol at a MedianOracle instead and the attack dies — because the liquidation no longer depends on the AMM puddle.</p><p>A median of three sources survives one liar and fails against two. If those “independent” sources are themselves thin AMM pools, a flash loan large enough can move two of them in the same transaction.</p><pre># Three reports, one median. No ML. Sort, pick the middle, ignore how loud the liar was.<br>defense_oracle = MedianOracle(<br>    [<br>        PriceReport(&quot;exchange-A&quot;, 2_005.0),<br>        PriceReport(&quot;exchange-B&quot;, 1_995.0),<br>        PriceReport(&quot;reference-feed&quot;, 2_000.0),<br>    ]<br>)<br>defense_pool = AMMPool(&quot;amm-defense.eth&quot;, 50.0, 100_000.0)<br># price_source overrides the puddle. <br>defense_protocol = LendingProtocol(<br>    &quot;lending-defense.eth&quot;, defense_pool, price_source=defense_oracle<br>)<br>defense_victim_loan = defense_protocol.open_loan(&quot;defense-victim&quot;, 10.0, 12_000.0)<br>defense_pool.swap_eth_for_usd(40.0)<br>defense_ratio = defense_protocol.collateral_ratio(defense_victim_loan)<br><br>try:<br>    defense_protocol.liquidate(&quot;defense-victim&quot;)<br>except PositionNotLiquidatableError:<br>    defense_rejected = True<br>else:<br>    defense_rejected = False<br><br><br>def defense_verdict(defense_rejected: bool) -&gt; str:<br>    &quot;&quot;&quot;Return a short label for whether the median-priced liquidation ran.<br><br>    Args:<br>        defense_rejected: True if ``liquidate`` raised.<br><br>    Returns:<br>        ``liquidation rejected`` or ``liquidation executed``.<br>    &quot;&quot;&quot;<br>    return &quot;liquidation rejected&quot; if defense_rejected else &quot;liquidation executed&quot;<br><br><br>print(f&quot;Manipulated AMM spot price: ${defense_pool.spot_price:,.2f}/ETH&quot;)<br>print(<br>    f&quot;Median oracle price: ${defense_oracle.price:,.2f}/ETH; &quot;<br>    f&quot;protocol ratio: {defense_ratio:.2f}&quot;<br>)<br>print(f&quot;Defense result: {defense_verdict(defense_rejected)}&quot;)</pre><p>The result:</p><pre>Manipulated AMM spot price: $617.28/ETH<br>Median oracle price: $2,000.00/ETH; protocol ratio: 1.67<br>Defense result: liquidation rejected</pre><p><strong>Read the result.</strong> The AMM really moved to $617.28. The protocol read the median price of $2,000 and rejected the liquidation.</p><p>Aggregation did not make the AMM honest — <strong>the AMM was always honest!</strong></p><p>It was honestly reporting the price implied by the liquidity sitting in its pool. The bug was asking that price to answer a question it wasn’t qualified to answer: <em>“What is ETH really worth?”</em></p><p>The defence wasn’t to make the puddle honest. <strong>It was to stop asking the puddle.</strong></p><h3>Thinking of what we just did</h3><p>Here’s a reality bite: Flash loan attacks are not hypothetical.</p><p>Flash loan oracle manipulation has drained hundreds of millions of dollars from real protocols, repeatedly, for years, in attacks that were structurally identical to the one above.</p><p>And the uncomfortable part is that the failure often wasn’t some exotic bug hiding in a thousand lines of Solidity.</p><p>The code could be audited. The contracts could execute exactly as written. The liquidation logic could be perfectly correct.</p><p><strong>There was a contract asking a puddle about the ocean.</strong></p><p>And an attacker realized that if the puddle was shallow enough, they could move it. The vulnerability was trusting a price source that an attacker could manipulate.</p><p>Median feeds, TWAPs, deeper liquidity, freshness limits, circuit breakers: these are layers of defence, each addressing different failure modes. None is magic, and all require understanding the assumptions underneath them.</p><p>This isn’t to say that smart contracts are safe now. In my view, DeFi remains a bit of a wild west, evolving daily.</p><p>Nonetheless, I will tentatively end my exploration of the more volatile side of DeFi here, though I have a feeling this won’t be my last article about it.</p><p>In my next article, I’ll explore something intriguing — <strong>stablecoins, a token that claims to be a dollar.</strong></p><h4>References (honestly too many):</h4><ul><li>Qin, K., Zhou, L., Livshits, B., &amp; Gervais, A. (2021). <a href="https://arxiv.org/abs/2003.03810">Attacking the DeFi Ecosystem with Flash Loans for Fun and Profit</a>.</li><li>Aave. <a href="https://aave.com/docs/developers/flash-loans">Flash Loans</a>.</li><li>Adams, H., Zinsmeister, N., &amp; Robinson, D. (2020). <a href="https://uniswap.org/whitepaper.pdf">Uniswap v2 Core</a></li><li>[Medium Article] Traroe, P. <a href="https://blog.hashex.org/the-5-biggest-flash-loan-attacks-7cd1b872045a">The 5 Biggest Flash Loan Attacks</a> (2022)</li></ul><p><em>Disclaimer: All opinions and interpretations are that of the writer, and not of MITB. I declare that I have full rights to use the contents published here, and nothing is plagiarized. I declare that this article is written by me and not with any generative AI tool such as ChatGPT. I declare that no data privacy policy is breached, and that any data associated with the contents here are obtained legitimately to the best of my knowledge. I agree not to make any changes without first seeking the editors’ approval. Any violations may lead to this article being retracted from the publication.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=cb08c72408bd" width="1" height="1" alt=""><hr><p><a href="https://medium.com/mitb-for-all/flash-loan-attacks-on-smart-contracts-cb08c72408bd">Flash loan attacks on smart contracts</a> was originally published in <a href="https://medium.com/mitb-for-all">MITB For All</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Introducing Smart Contracts]]></title>
            <link>https://medium.com/mitb-for-all/introducing-smart-contracts-340c20cbe672?source=rss----825f074cc537---4</link>
            <guid isPermaLink="false">https://medium.com/p/340c20cbe672</guid>
            <category><![CDATA[blockchain]]></category>
            <category><![CDATA[smart-contracts]]></category>
            <category><![CDATA[fintech]]></category>
            <category><![CDATA[blockchain-oracle]]></category>
            <category><![CDATA[python]]></category>
            <dc:creator><![CDATA[Tituslhy]]></dc:creator>
            <pubDate>Tue, 15 Sep 2026 02:33:48 GMT</pubDate>
            <atom:updated>2026-09-15T02:33:47.399Z</atom:updated>
            <content:encoded><![CDATA[<h4>What they are and how they work with Blockchain</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/768/1*vz63BE9s0G63Jh642ddKQQ@2x.jpeg" /></figure><p>My odyssey into blockchain has been getting progressively more interesting.</p><p>What started as curiosity to understand how Bitcoin works has turned into a series of articles where I keep opening increasingly suspicious-looking boxes and asking, <em>“Okay, but what actually happens inside?”</em></p><p>So far I’ve covered how blocks get proposed (<a href="https://medium.com/mitb-for-all/understanding-blockchains-from-scratch-f14c454af32f">PoW</a>, then <a href="https://medium.com/mitb-for-all/proof-of-stake-the-mining-free-blockchain-ddae684ffec5">PoS</a>), how a <a href="https://medium.com/mitb-for-all/inside-opencerts-how-merkle-trees-verify-singapores-digital-diplomas-61f60d13700e">Merkle tree</a> lets you prove one credential belongs to a much bigger set without hauling out the whole set, how <a href="https://medium.com/mitb-for-all/introducing-permissioned-chains-1d8a71f5b09a">permissioned chains</a> handle endorsement and stale-state conflicts, and how transactions live in a messy, half-agreed-upon <a href="https://medium.com/mitb-for-all/introducing-mempools-the-one-time-gossip-pays-off-9fbc92701f70">mempool</a> before they ever make it into a block. If any of those are fuzzy, the earlier articles are still there — this one assumes you’ve got the gist.</p><p>With the exception of OpenCerts, the blockchains in my articles so far have mainly been about <strong>ledgers:</strong></p><blockquote>Alice sends Bob some tokens, a validator checks the transaction, the network agrees on what happened and the blockchain records it.</blockquote><p>There is, however, a rather important assumption hiding underneath all of this: <strong><em>The blockchain already knows everything it needs to know.</em></strong></p><p>If Alice sends Bob 10 tokens, the blockchain doesn’t need to phone anyone to ask whether Alice really sent Bob 10 tokens.</p><p>The transaction <em>is</em> the information.</p><p>The blockchain can compare that transaction against its existing state, apply the relevant rules, and — assuming everything checks out — record the result.</p><p>But now imagine that our blockchain needs to know something that <strong>does not</strong> <strong>originate on the blockchain.</strong></p><p>And this is where things get interesting.</p><p>Because once we give the blockchain <strong>programs that actually do things</strong>, those programs are going to want information about a world that exists outside the blockchain.</p><p>That brings us to <strong>smart contracts, </strong>and eventually, to <strong>oracles</strong>.</p><p>As before, all my code can be found in my <a href="https://github.com/tituslhy/Blockchain/blob/main/notebooks/6.%20oracles_and_smart_contracts.ipynb">GitHub repository</a>.</p><h3>What a smart contract actually is</h3><p>A smart contract is like a vending machine.</p><p>You put in the required amount of money, press B4, and a Snickers falls out (yum). The machine has rules. All you have to do is satisfy them.</p><p>In Ethereum, a smart contract is program code deployed to the blockchain. Other participants can send transactions requesting that the contract execute a particular function. The resulting execution can change the blockchain’s state.</p><p>And here is the important part:</p><blockquote>The blockchain does <strong>not</strong> understand that you have asked it to “swap some ETH for USD.”</blockquote><blockquote>It understands something much more mechanical — Call the contract at this address, invoke this function, and provide these arguments.</blockquote><p>The contract’s code determines what happens next. <strong>Smart contracts are very deterministic programs</strong>.</p><p>These programs live in a shared environment where thousands of machines may independently execute the same instructions and must arrive at the same result.</p><p>Here’s what a simple smart contract could look like, recycling our PoS blockchain and mempool classes from Articles 2 and 5:</p><pre>class SmartContract:<br>    def __init__(self, address: str) -&gt; None:<br>        &quot;&quot;&quot;Store the address this contract will be called at.&quot;&quot;&quot;<br>        if not address:<br>            raise ValueError(&quot;Contract address must be non-empty.&quot;)<br>        self.address = address<br><br>    def call(self, method: str, **kwargs: Any) -&gt; Any:<br>        &quot;&quot;&quot;Invoke a public method by name.&quot;&quot;&quot;<br>        if method.startswith(&quot;_&quot;) or method == &quot;call&quot;:<br>            raise AttributeError(<br>                f&quot;{self.address} has no public method {method!r}.&quot;<br>            )<br>        func = getattr(self, method, None)<br>        if not callable(func):<br>            raise AttributeError( <br>                f&quot;{self.address} has no public method {method!r}.&quot;<br>            )<br>        return func(**kwargs)</pre><p>The important idea here is surprisingly small.</p><ul><li>A contract has an <strong>address</strong>.</li><li>It has <strong>code</strong>.</li><li>And that code exposes methods that can be called.</li></ul><p>In our simplified blockchain, a contract call is represented as a transaction, which can enter the mempool, be included in a proposed block, and then be executed.</p><p>So the simplified flow is:</p><blockquote><strong>Contract call → Transaction → Mempool → Block → Execution → State change</strong></blockquote><p>Which means the mempool we built in the previous article still matters.</p><p>Our shiny new smart contract has not escaped the machinery we already built. We’ve simply given the machinery something more interesting to execute.</p><pre>def submit_call(<br>    network: Network,<br>    chain: Blockchain,<br>    origin: str,<br>    tx_id: str,<br>    contract: SmartContract,<br>    method: str,<br>    **kwargs: Any,<br>) -&gt; tuple[Any, Transaction, Block]:<br>    arg_preview = &quot;, &quot;.join(f&quot;{k}={v!r}&quot; for k, v in kwargs.items())<br>    description = f&quot;{contract.address}.{method}({arg_preview})&quot;<br>    network.broadcast(Transaction(tx_id, description), origin=origin)<br>    tx, block = network.include(origin, tx_id, chain)<br>    result = contract.call(method, **kwargs)<br>    return result, tx, block</pre><p><strong>A transaction can ask a contract to run some code, and running that code can change blockchain state.</strong></p><h3>Swap contracts aren’t a major leap from the basics</h3><p>An <strong>automated market maker (AMM)</strong> allows users to trade against liquidity held inside the pool using a mathematical pricing rule.</p><p>One of the simplest and most famous AMM designs is the <strong>constant-product AMM</strong>. Its entire personality can be summarised by one equation:</p><p><strong>x × y = k</strong></p><p>Here, x is the amount of ETH in the pool and y is the amount of USD.</p><ul><li>The product k should remain constant.</li><li>So if someone adds ETH to the pool, the amount of USD in the pool must decrease accordingly.</li><li>The pool does not need a human market maker deciding what the trade should cost.</li></ul><p>The math decides. And it is just code.</p><pre>class AMMPool(SmartContract):<br>    def __init__(<br>        self, address: str, eth_reserve: float, usd_reserve: float<br>    ) -&gt; None:<br>        &quot;&quot;&quot;Create a pool at ``address`` with the given reserves.&quot;&quot;&quot;<br>        super().__init__(address)<br>        if eth_reserve &lt;= 0 or usd_reserve &lt;= 0:<br>            raise ValueError(&quot;AMM reserves must be positive.&quot;)<br>        self.eth_reserve = eth_reserve<br>        self.usd_reserve = usd_reserve<br><br>    @property<br>    def spot_price(self) -&gt; float:<br>        return self.usd_reserve / self.eth_reserve<br><br>    @property<br>    def constant_product(self) -&gt; float:<br>        return self.eth_reserve * self.usd_reserve<br><br>    def swap_eth_for_usd(self, eth_in: float) -&gt; float:<br>        if eth_in &lt;= 0:<br>            raise ValueError(&quot;Swap input must be positive.&quot;)<br>        k = self.constant_product<br>        new_eth_reserve = self.eth_reserve + eth_in<br>        new_usd_reserve = k / new_eth_reserve<br>        usd_out = self.usd_reserve - new_usd_reserve<br>        self.eth_reserve, self.usd_reserve = new_eth_reserve, new_usd_reserve<br>        return usd_out<br><br>    def swap_usd_for_eth(self, usd_in: float) -&gt; float:<br>        if usd_in &lt;= 0:<br>            raise ValueError(&quot;Swap input must be positive.&quot;)<br>        k = self.constant_product<br>        new_usd_reserve = self.usd_reserve + usd_in<br>        new_eth_reserve = k / new_usd_reserve<br>        eth_out = self.eth_reserve - new_eth_reserve<br>        self.usd_reserve, self.eth_reserve = new_usd_reserve, new_eth_reserve<br>        return eth_out</pre><p>The pool has two important pieces of state:</p><ul><li>how much ETH it holds</li><li>how much USD it holds</li></ul><p>And it has rules governing what happens when somebody swaps one asset for the other.</p><p>Suppose someone sells ETH to the pool:</p><ul><li>The pool now has <strong>more ETH</strong>.</li><li>To maintain x × y = k, it must therefore have <strong>less USD</strong>.</li><li>The difference between the old and new USD reserves is what the trader receives.</li></ul><h3>Same price for any size trade?</h3><p>The intuition is tempting. Sell a little ETH, get about $2,000 worth of USD. Sell a lot of ETH, get… more of the same deal, just scaled up.</p><p>Not quite.</p><p>We’ll start with a pool containing 50 ETH and $100,000 USD, so the initial spot price is:</p><p>$100,000 ÷ 50 = $2,000 per ETH</p><p>Then we’ll make two identical pools, because I want two experiments that start from exactly the same state.</p><p>One is a small trade:</p><blockquote>Sell 1 ETH.</blockquote><p>The other is a large trade:</p><blockquote>Sell 40 ETH.</blockquote><p>Think of the pool as a puddle. Toss a pebble in and the water barely moves. Cannonball into it and it splashes everywhere. The AMM works the same way — a small trade barely disturbs the reserves, a large one sloshes the price around.</p><p>We’re about to prove that with numbers instead of a metaphor, but keep the puddle in your head. It’s the whole intuition for what “price impact” means.</p><pre># Three identical puddles. The pebble and boulder must not share state.<br>initial_pool = AMMPool(&quot;amm-initial&quot;, 50.0, 100_000.0)<br>small_trade_pool = AMMPool(&quot;amm-pebble&quot;, 50.0, 100_000.0)<br>large_trade_pool = AMMPool(&quot;amm-boulder&quot;, 50.0, 100_000.0)<br><br># call() is the public surface — same path a later Block will take.<br>small_usd_out = small_trade_pool.call(&quot;swap_eth_for_usd&quot;, eth_in=1.0)<br>large_usd_out = large_trade_pool.call(&quot;swap_eth_for_usd&quot;, eth_in=40.0)<br>small_impact = (small_trade_pool.spot_price / initial_pool.spot_price - 1) * 100<br>large_impact = (large_trade_pool.spot_price / initial_pool.spot_price - 1) * 100<br>constant_product_preserved = abs(<br>    large_trade_pool.constant_product - initial_pool.constant_product<br>) &lt; 1e-6<br><br>print(<br>    f&quot;Initial pool: {initial_pool.eth_reserve:.2f} ETH and &quot;<br>    f&quot;${initial_pool.usd_reserve:,.2f}; spot price ${initial_pool.spot_price:,.2f}/ETH&quot;<br>)<br>print(<br>    f&quot;Small 1 ETH sale: receives ${small_usd_out:,.2f}; &quot;<br>    f&quot;price ${small_trade_pool.spot_price:,.2f}/ETH ({small_impact:.2f}%)&quot;<br>)<br>print(<br>    f&quot;Large 40 ETH sale: receives ${large_usd_out:,.2f}; &quot;<br>    f&quot;price ${large_trade_pool.spot_price:,.2f}/ETH ({large_impact:.2f}%)&quot;<br>)<br>print(f&quot;x * y preserved within floating-point tolerance: {constant_product_preserved}&quot;)</pre><p>The output:</p><pre>Initial pool: 50.00 ETH and $100,000.00; spot price $2,000.00/ETH<br>Small 1 ETH sale: receives $1,960.78; price $1,922.34/ETH (-3.88%)<br>Large 40 ETH sale: receives $44,444.44; price $617.28/ETH (-69.14%)<br>x * y preserved within floating-point tolerance: True</pre><p>And there’s our first glimpse of <strong>price impact</strong>.</p><ul><li>Selling just 1 ETH moves the displayed price from $2,000 to about $1,922 — a roughly <strong>3.9% decrease</strong>.</li><li>But selling 40 ETH — almost the entire original ETH reserve — moves the displayed price all the way down to about <strong>$617</strong>.</li></ul><p>The AMM is doing exactly what we told it to do. The pool is simply responding to a very large trade relative to its available liquidity.</p><p>There is no external ETH/USD price feed involved. The AMM doesn’t know what ETH is worth anywhere else. It only knows the ratio of the two assets sitting inside itself.</p><p>And because that number exists on-chain, <strong>another smart contract can read it.</strong></p><p>That opens up a much more interesting possibility: What if a lending protocol uses this number to decide whether someone’s loan is safe?</p><h3>The lending contract: a loan that asks the puddle</h3><p>Now let’s give our AMM a customer who cares rather more about its price than a trader does.</p><p>We’ll build a tiny lending protocol (another smart contract). It accepts ETH as collateral and allows a borrower to take on USD-denominated debt. If the value of the collateral falls too far relative to the debt, the position can be liquidated.</p><p>We’ll set the minimum collateral ratio at <strong>1.5</strong>. Concretely in code:</p><pre>@dataclass(frozen=True)<br>class Loan:<br>    collateral_eth: float<br>    debt_usd: float<br><br>class LoanNotFoundError(Exception):<br>    &quot;&quot;&quot;Raised when a borrower has no open loan.&quot;&quot;&quot;<br><br><br>class PositionNotLiquidatableError(Exception):<br>    &quot;&quot;&quot;Raised when liquidation is attempted on a still-healthy position.&quot;&quot;&quot;<br><br><br>class LendingProtocol(SmartContract):<br>    def __init__(<br>        self,<br>        address: str,<br>        pool: AMMPool,<br>        liquidation_ratio: float = 1.5,<br>        price_source=None,<br>    ) -&gt; None:<br>        &quot;&quot;&quot;Configure the protocol&#39;s price source and liquidation threshold.&quot;&quot;&quot;<br>        super().__init__(address)<br>        if liquidation_ratio &lt;= 0:<br>            raise ValueError(&quot;Liquidation ratio must be positive.&quot;)<br>        self.pool = pool<br>        self.liquidation_ratio = liquidation_ratio<br>        self.price_source = price_source<br>        self.loans: dict[str, Loan] = {}<br><br>    def open_loan(<br>        self, borrower: str, collateral_eth: float, debt_usd: float<br>    ) -&gt; Loan:<br>        &quot;&quot;&quot;Open a loan for ``borrower`` and return it.&quot;&quot;&quot;<br>        return self.add_loan(borrower, Loan(collateral_eth, debt_usd))<br><br>    def add_loan(self, borrower: str, loan: Loan) -&gt; Loan:<br>        &quot;&quot;&quot;Register an open loan for a borrower.&quot;&quot;&quot;<br>        if not borrower:<br>            raise ValueError(&quot;Borrower name must be non-empty.&quot;)<br>        self.loans[borrower] = loan<br>        return loan<br><br>    def collateral_ratio(self, loan: Loan) -&gt; float:<br>        &quot;&quot;&quot;Return collateral value divided by debt.&quot;&quot;&quot;<br>        price = (<br>            self.price_source.price<br>            if self.price_source is not None<br>            else self.pool.spot_price<br>        )<br>        return loan.collateral_eth * price / loan.debt_usd<br><br>    def liquidate(self, borrower: str) -&gt; Loan:<br>        &quot;&quot;&quot;Seize and remove a borrower&#39;s loan if it is unhealthy.&quot;&quot;&quot;<br>        if borrower not in self.loans:<br>            raise LoanNotFoundError(f&quot;No loan for {borrower}.&quot;)<br>        loan = self.loans[borrower]<br>        if self.collateral_ratio(loan) &gt;= self.liquidation_ratio:<br>            raise PositionNotLiquidatableError(&quot;Position is still healthy.&quot;)<br>        return self.loans.pop(borrower)</pre><p>There are a lot of lines here, but most of them are bookkeeping. The interesting part is much smaller:</p><blockquote>When the protocol wants to calculate the collateral ratio, it asks for a price:</blockquote><pre>price = (<br>    self.price_source.price<br>    if self.price_source is not None<br>    else self.pool.spot_price<br>)</pre><p>Right now, we haven’t given it another price source, so it falls back to the AMM.</p><pre>baseline_pool = AMMPool(&quot;amm-baseline&quot;, 50.0, 100_000.0)<br>baseline_protocol = LendingProtocol(&quot;lending-baseline&quot;, baseline_pool)<br>victim_loan = baseline_protocol.call(<br>    &quot;open_loan&quot;, borrower=&quot;victim&quot;, collateral_eth=10.0, debt_usd=12_000.0<br>)<br>baseline_ratio = baseline_protocol.collateral_ratio(victim_loan)<br><br>print(f&quot;At ${baseline_pool.spot_price:,.2f}/ETH, victim collateral ratio: {baseline_ratio:.2f}&quot;)<br>print(&quot;Liquidation threshold: 1.50; verdict: HEALTHY&quot;)</pre><p>The output:</p><pre>     <br>At $2,000.00/ETH, victim collateral ratio: 1.67<br>Liquidation threshold: 1.50; verdict: HEALTHY</pre><p>The position is healthy. The collateral ratio is 1.67 which is above the target ratio of 1.5.</p><h3>Put the contracts on the chain</h3><p>At this point we have two contracts.</p><ul><li>amm.eth is the puddle. It holds ETH and USD, and its reserve ratio gives us a price.</li><li>lending.eth is the loan desk. It holds loans and decides whether they are healthy.</li></ul><p>No new block is required for that inner call. Using the same classes from articles 2 and 5:</p><pre># Same validators / Network / Blockchain as articles 2 and 5. <br>nodes = [&quot;Node A&quot;, &quot;Alice-Node&quot;, &quot;Bob-Node&quot;, &quot;Farid-Node&quot;]<br>validators = [<br>    Validator(&quot;Node A&quot;, 100),<br>    Validator(&quot;Alice-Node&quot;, 80),<br>    Validator(&quot;Bob-Node&quot;, 70),<br>    Validator(&quot;Farid-Node&quot;, 50),<br>]<br>network = Network(nodes, random.Random(7))<br>chain = Blockchain(validators)<br><br>#   amm     = the puddle    (AMMPool, which IS a SmartContract)<br>#   lending = the loan desk (ALSO a SmartContract). No water of its own.<br>amm = AMMPool(&quot;amm.eth&quot;, 50.0, 100_000.0)<br>lending = LendingProtocol(&quot;lending.eth&quot;, amm)<br><br># &#39;Deploy&#39; here is a receipt, not bytecode. The objects already exist in <br># memory. The notary files the form.<br>network.broadcast(Transaction(&quot;tx-deploy-amm&quot;, &quot;Deploy AMMPool at amm.eth&quot;), origin=&quot;Node A&quot;)<br>network.broadcast(Transaction(&quot;tx-deploy-lending&quot;, &quot;Deploy LendingProtocol at lending.eth&quot;), origin=&quot;Node A&quot;)<br>_, deploy_amm_block = network.include(&quot;Node A&quot;, &quot;tx-deploy-amm&quot;, chain)<br>_, deploy_lending_block = network.include(&quot;Node A&quot;, &quot;tx-deploy-lending&quot;, chain)<br><br># Bob&#39;s loan, then Alice&#39;s pebble. Two shouts into the group chat, two stamps.<br>bob_loan, bob_tx, bob_block = submit_call(<br>    network,<br>    chain,<br>    &quot;Bob-Node&quot;,<br>    &quot;tx-open-loan&quot;,<br>    lending,<br>    &quot;open_loan&quot;,<br>    borrower=&quot;Bob&quot;,<br>    collateral_eth=10.0,<br>    debt_usd=12_000.0,<br>)<br>alice_usd, alice_tx, alice_block = submit_call(<br>    network,<br>    chain,<br>    &quot;Alice-Node&quot;,<br>    &quot;tx-swap-1eth&quot;,<br>    amm,<br>    &quot;swap_eth_for_usd&quot;,<br>    eth_in=1.0,<br>)<br>bob_ratio_after_pebble = lending.collateral_ratio(lending.loans[&quot;Bob&quot;])<br>valid, message = chain.is_valid()</pre><p>Alice’s 1 ETH sale barely registers on her — <strong>but it moves the one number Bob’s entire position depends on</strong>. <strong>His collateral ratio slides from 1.67 to 1.60</strong>. Still healthy, still above the 1.50 line — but Bob never touched his loan. He just happened to be priced against a puddle someone else disturbed.</p><blockquote>That’s the whole danger of an on-chain price feed in one sentence: <em>your</em> safety depends on <em>everyone else’s</em> behavior toward the same pool.</blockquote><p>And that is where things get interesting. Because the blockchain has absolutely no idea what ETH is worth outside its own state.</p><h3>Introducing Oracles</h3><p>A smart contract is deterministic. Every node executes the same transaction and must land on the same result — if one node computes $2,000 and another computes $1,742.31, they no longer agree on the state of the blockchain, and the whole system has a rather serious problem.</p><p>That determinism comes with an awkward limitation: <strong>a smart contract cannot simply look outside the blockchain for information</strong>. It cannot make an HTTP request to an exchange, read a local file, check the weather, or ask an external database what happened five seconds ago.</p><blockquote>Every node needs to be able to execute the same logic from the same available inputs, so allowing arbitrary external information into that process would destroy the very consistency we rely on.</blockquote><p>This is the same trade-off we have been seeing throughout the series. The blockchain is extremely good at agreeing on what happened <strong>inside the system</strong>. It is much less capable of knowing what is happening <strong>outside the system</strong>.</p><p>A lending protocol, however, needs information from outside. So somebody has to tell it.</p><p>That is the job of an <strong>oracle</strong>: a mechanism for bringing external information into the deterministic world of the blockchain.</p><p>For our toy implementation, we will keep this deliberately simple. A price feed is <strong>just another smart contract at another address</strong>, with storage for the reports it receives.</p><p>We will create a PriceReport to represent an individual oracle, and two small pieces of logic for the lending protocol: one to calculate the collateral ratio at a given price, and one to decide whether that ratio has crossed the liquidation threshold.</p><pre>@dataclass(frozen=True)<br>class PriceReport:<br>    source: str<br>    eth_price_usd: float<br><br><br>def collateral_ratio(<br>    collateral_eth: float, debt_usd: float, eth_price_usd: float<br>) -&gt; float:<br>    if collateral_eth &lt;= 0 or debt_usd &lt;= 0 or eth_price_usd &lt;= 0:<br>        raise ValueError(&quot;Collateral, debt, and price must be positive.&quot;)<br>    return collateral_eth * eth_price_usd / debt_usd<br><br><br>def should_liquidate(ratio: float, threshold: float = 1.5) -&gt; bool:<br>    return ratio &lt; threshold</pre><blockquote>In a real oracle system, “source” would need to mean something more substantial than a string supplied by the caller. In our toy implementation, it is deliberately just a label — because I want the reader to see exactly where the trust problem begins.</blockquote><p>The interesting question is no longer what the lending contract does once it has a price. We already know that part: multiply the collateral by the price, divide by the debt, and liquidate if the ratio falls below 1.5.</p><p>The interesting question is <strong>which price should it use in the first place?</strong></p><p>That question turns out to be much harder.</p><h3>Take the median</h3><p>Let’s start with the simplest possible price feed. It receives reports from different sources and stores them on-chain.</p><p>In this toy model, I’m splitting the oracle into two pieces: PriceFeed represents the on-chain feed that stores reports, while MedianOracle represents the aggregation rule that decides which of those reports the protocol should use.</p><pre>class PriceFeed(SmartContract):<br>    def __init__(self, address: str) -&gt; None:<br>        &quot;&quot;&quot;Create an empty feed at ``address``.&quot;&quot;&quot;<br><br>        super().__init__(address)<br>        self.reports: list[PriceReport] = []<br><br>    def report(<br>        self,<br>        source: str,<br>        eth_price_usd: float,<br>    ) -&gt; PriceReport:<br>        if eth_price_usd &lt;= 0:<br>            raise ValueError(<br>                &quot;Reported price must be positive.&quot;<br>            )<br><br>        posted = PriceReport(source, eth_price_usd)<br>        self.reports.append(posted)<br>        return posted<br><br>    @property<br>    def latest(self) -&gt; PriceReport:<br>        return self.reports[-1]<br><br>    @property<br>    def price(self) -&gt; float:<br>        return statistics.median(<br>            report.eth_price_usd<br>            for report in self.reports<br>        )</pre><p>There are two interesting things hiding in this very small class.</p><ul><li>First, the feed does not know whether a report is true. The source field is just a label.</li><li>Second, the feed now has a choice about what it considers its price. It can use latest, meaning whatever the most recent report said, or it can aggregate several reports using some rule.</li></ul><p>For our first experiment, let’s give it three reports: two around $2,000 and one deliberately wrong report at $1,200.</p><pre># Fourth box, fourth address.<br>feed = PriceFeed(&quot;feed.eth&quot;)<br><br>network.broadcast(<br>    Transaction(<br>        &quot;tx-deploy-feed&quot;,<br>        &quot;Deploy PriceFeed at feed.eth&quot;,<br>    ),<br>    origin=&quot;Node A&quot;,<br>)<br><br>_, deploy_feed_block = network.include(<br>    &quot;Node A&quot;,<br>    &quot;tx-deploy-feed&quot;,<br>    chain,<br>)<br><br>assert isinstance(deploy_feed_block, Block)<br><br>collateral_eth = 10<br>debt_usd = 12_000<br><br>incoming_reports = [<br>    (&quot;tx-report-a&quot;, &quot;independent-feed-a&quot;, 2_005.0),<br>    (&quot;tx-report-lie&quot;, &quot;malicious-feed&quot;, 1_200.0),<br>    (&quot;tx-report-b&quot;, &quot;independent-feed-b&quot;, 1_995.0),<br>]<br><br>print(<br>    f&quot;Same loan: {collateral_eth} ETH / &quot;<br>    f&quot;${debt_usd:,} debt. Threshold 1.50.&quot;<br>)<br><br>print(<br>    f&quot;Deployed {feed.address} &quot;<br>    f&quot;in Block #{deploy_feed_block.index}&quot;<br>)<br><br>print()<br><br>for tx_id, source, price in incoming_reports:<br>    _, tx, block = submit_call(<br>        network,<br>        chain,<br>        &quot;Node A&quot;,<br>        tx_id,<br>        feed,<br>        &quot;report&quot;,<br>        source=source,<br>        eth_price_usd=price,<br>    )<br><br>    assert isinstance(block, Block)<br><br>    latest_ratio = collateral_ratio(<br>        collateral_eth,<br>        debt_usd,<br>        feed.latest.eth_price_usd,<br>    )<br><br>    latest_verdict = (<br>        &quot;LIQUIDATE (wrong)&quot;<br>        if should_liquidate(latest_ratio)<br>        else &quot;HEALTHY&quot;<br>    )<br><br>    print(block)<br><br>    print(<br>        f&quot;  latest on {feed.address}: &quot;<br>        f&quot;${feed.latest.eth_price_usd:,.0f}/ETH &quot;<br>        f&quot;from {feed.latest.source}&quot;<br>    )<br><br>    print(<br>        f&quot;  if lending trusts only Block #{block.index}: &quot;<br>        f&quot;ratio {latest_ratio:.2f} -&gt; {latest_verdict}&quot;<br>    )</pre><p>And this is where the problem becomes visible:</p><pre>Same loan: 10 ETH / $12,000 debt. Threshold 1.50.<br>Deployed feed.eth in Block #5<br><br>Block #6 proposed by Node A<br>  data: feed.eth.report(<br>      source=&#39;independent-feed-a&#39;,<br>      eth_price_usd=2005.0<br>  )<br><br>  latest on feed.eth: $2,005/ETH from independent-feed-a<br>  if lending trusts only Block #6:<br>      ratio 1.67 -&gt; HEALTHY<br><br>Block #7 proposed by Node A<br>  data: feed.eth.report(<br>      source=&#39;malicious-feed&#39;,<br>      eth_price_usd=1200.0<br>  )<br><br>  latest on feed.eth: $1,200/ETH from malicious-feed<br>  if lending trusts only Block #7:<br>      ratio 1.00 -&gt; LIQUIDATE (wrong)<br><br>Block #8 proposed by Node A<br>  data: feed.eth.report(<br>      source=&#39;independent-feed-b&#39;,<br>      eth_price_usd=1995.0<br>  )<br><br>  latest on feed.eth: $1,995/ETH from independent-feed-b<br>  if lending trusts only Block #8:<br>      ratio 1.66 -&gt; HEALTHY</pre><p>The loan has not changed at all. Bob still has 10 ETH locked as collateral and still owes $12,000.</p><p>What changed was the number that the lending protocol treated as the price of ETH.</p><p>When the latest report says $2,005, Bob’s collateral ratio is 1.67 and the position is healthy. When the latest report suddenly says $1,200, the ratio falls to exactly 1.00 and the same liquidation rule says Bob should be liquidated. When another report of $1,995 arrives, the ratio climbs back to 1.66.</p><p>Nothing about the blockchain itself has gone wrong here. Every report was a valid transaction, every transaction was included in a valid block, and the PriceFeed stored exactly what it was instructed to store.</p><blockquote>The problem is that <strong>a valid piece of blockchain state is not necessarily a true statement about the outside world</strong>.</blockquote><p>So instead of trusting whichever report arrived last, one simple approach is the median: sort the values and take the one in the middle — yup, it’s that simple.</p><pre>class MedianOracle:<br>    def __init__(<br>        self,<br>        reports: list[PriceReport],<br>    ) -&gt; None:<br>        if not reports:<br>            raise ValueError(<br>                &quot;At least one price report is required.&quot;<br>            )<br><br>        self.reports = reports<br><br>    @property<br>    def price(self) -&gt; float:<br>        return statistics.median(<br>            report.eth_price_usd<br>            for report in self.reports<br>        )</pre><p>Now we can apply that rule to the three reports already stored in feed.eth.</p><pre>median_oracle = MedianOracle(feed.reports)<br><br>median_ratio = collateral_ratio(<br>    collateral_eth,<br>    debt_usd,<br>    median_oracle.price,<br>)<br><br>print(<br>    f&quot;Median of every report stored at {feed.address}: &quot;<br>    f&quot;${median_oracle.price:,.0f}/ETH&quot;<br>)<br><br>print(<br>    f&quot;If lending reads the median instead of the latest Block: &quot;<br>    f&quot;ratio {median_ratio:.2f} -&gt; HEALTHY&quot;<br>)<br><br>print(<br>    f&quot;Chain length: {len(chain.chain)}. &quot;<br>    &quot;The three reports are still sitting on their own Blocks.&quot;<br>)</pre><p>The median is $1,995, giving Bob a collateral ratio of 1.66.</p><p>Why median rather than average? <strong>Because the median is resistant to a single extreme outlier.</strong> With reports of $2,005, $1,200 and $1,995, the average is pulled down by the malicious report, while the median simply takes the middle value: $1,995.</p><p>This is a useful defence, but it is important to understand what it actually defended against.</p><p>It did not make the AMM more honest. The AMM was already being completely honest about its own state. If somebody trades a large amount of ETH into the pool, its reserves really do change, and its reserve ratio really does change with them.</p><p>The defence is instead about <strong>which information the lending protocol chooses to treat as authoritative</strong>.</p><h3>More oracles ≠ more truth</h3><p>The same problem appears in our toy example in another form. The source field looks reassuring because we have names such as independent-feed-a and independent-feed-b, but the blockchain has no idea whether those sources are actually independent.</p><p>The blockchain gives us a remarkably strong guarantee about the information it contains: nodes can agree on what was submitted and what the contracts did with it.</p><p>It does not give us a guarantee that somebody submitted the truth.</p><p>At this point, our little financial system has all the pieces needed to become dangerous. The AMM provides a price based on its reserves. The lending protocol uses a price to decide whether a loan is safe. The price feed provides another route for information to enter the system. Every individual component can behave exactly according to its rules while the overall system still produces a very bad outcome.</p><p>The next question is what happens when someone deliberately takes advantage of that gap.</p><p>Which is a story for another time.</p><p><em>Disclaimer: All opinions and interpretations are that of the writer, and not of MITB. I declare that I have full rights to use the contents published here, and nothing is plagiarized. I declare that this article is written by me and not with any generative AI tool such as ChatGPT. I declare that no data privacy policy is breached, and that any data associated with the contents here are obtained legitimately to the best of my knowledge. I agree not to make any changes without first seeking the editors’ approval. Any violations may lead to this article being retracted from the publication.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=340c20cbe672" width="1" height="1" alt=""><hr><p><a href="https://medium.com/mitb-for-all/introducing-smart-contracts-340c20cbe672">Introducing Smart Contracts</a> was originally published in <a href="https://medium.com/mitb-for-all">MITB For All</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Introducing mempools - The one time gossip pays off]]></title>
            <link>https://medium.com/mitb-for-all/introducing-mempools-the-one-time-gossip-pays-off-9fbc92701f70?source=rss----825f074cc537---4</link>
            <guid isPermaLink="false">https://medium.com/p/9fbc92701f70</guid>
            <category><![CDATA[blockchain]]></category>
            <category><![CDATA[mempool]]></category>
            <category><![CDATA[fintech]]></category>
            <dc:creator><![CDATA[Tituslhy]]></dc:creator>
            <pubDate>Sat, 05 Sep 2026 02:56:05 GMT</pubDate>
            <atom:updated>2026-09-05T02:56:03.782Z</atom:updated>
            <content:encoded><![CDATA[<h3>Introducing mempools</h3><h4>The one time gossip pays off</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3TviPmaI8yOX4v5_jtbtrQ.png" /></figure><p>In my <a href="https://medium.com/mitb-for-all/introducing-permissioned-chains-1d8a71f5b09a">previous article on permissioned chains</a>, we encountered the problem of race conditions.</p><p>In a system where transactions are constantly being proposed against shared state, two perfectly legitimate transactions can race against each other. One may change the state before the other reaches validation, making the second transaction stale. Multi-version concurrency control (MVCC) catches that conflict; the client can then re-execute against the latest state and try again.</p><p>That got me wondering: <strong>what happens in public chains, where participants may not even see the same information at the same time?</strong></p><p>Transactions and blocks have to propagate across a distributed network, and that propagation isn’t instantaneous. A transaction might reach one node before another. A block might arrive after a competing block has already been seen elsewhere. At any given moment, different participants can therefore have different views of what is happening.</p><p>So what does each participant actually know at any given moment — and how does the network eventually converge on a shared history?</p><p>This article explores how this gap is plugged on public blockchains.</p><p>As before, all my code can be found in <a href="https://github.com/tituslhy/Blockchain/blob/main/notebooks/5.%20mempools_and_oracles.ipynb">my GitHub repository.</a></p><h3>The idea of networks</h3><p><strong>“The blockchain”</strong> is <strong>not </strong>a single thing sitting somewhere on the internet. A blockchain is a <strong>sequence of blocks</strong>. But that sequence has to be stored, checked, and updated by computers participating in the blockchain network. These computers are called <strong>nodes</strong>.</p><p>A node is simply a computer running the blockchain’s software. It maintains its own local view of the blockchain and communicates with other nodes over the network.</p><p>The <strong>network</strong> is therefore the collection of nodes and the connections between them. The blockchain is the history those nodes are collectively maintaining.</p><p>You can think of it like a shared document, except there is no single document sitting on a central server. Instead, many participants keep their own copy and communicate with one another to keep those copies in agreement.</p><p>That distinction becomes important because the copies are not necessarily identical at every instant.</p><p>When a new transaction is created, it has to travel through the network before other nodes can learn about it. When a new block is created, it also has to propagate through the network. Network communication takes time, so different nodes can temporarily have different information. Conceptually:</p><pre>                 NETWORK<br>        ┌───────────┼───────────┐<br>        │           │           │<br>      NODE A      NODE B      NODE C<br>        │           │           │<br>     mempool     mempool     mempool<br>        │           │           │<br>        ▼           ▼           ▼<br>   candidate     candidate    candidate<br>   transactions  transactions transactions<br>        │           │           │<br>        └───────────┼───────────┘<br>                    │<br>              consensus /<br>             block creation<br>                    │<br>                    ▼<br>              NEW BLOCK<br>                    │<br>                    ▼<br>              propagated<br>              through network<br>                    │<br>        ┌───────────┼───────────┐<br>        ▼           ▼           ▼<br>      NODE A      NODE B      NODE C<br>        │           │           │<br>        └── blockchain history ┘</pre><h3>This is where <strong>mempools</strong> come in.</h3><p>A mempool is a node’s local collection of valid transactions that it has heard about but that have not yet been included in a block.</p><p>There is therefore no single, global mempool shared by the entire network.</p><p>Think of it like a group chat where everyone is receiving the same messages, but not necessarily at the same time or in the same order. Alice might have already received messages A, B, and C. Bob might have A and C but not B. Carol might have heard about B first.</p><p>They’re all participating in the same conversation. They just don’t have exactly the same view of it yet.</p><p>Our toy network models this deliberately. Each node gets its own mempool, and transactions propagate between nodes independently.</p><p>Assuming we are recording a list of transactions in a public blockchain:</p><pre>from dataclasses import dataclass <br><br>@dataclass(frozen=True)<br>class Transaction:<br>    tx_id: str<br>    description: str</pre><p>A network is then a set of nodes, each with its own mempool:</p><pre>class Network:<br>    &quot;&quot;&quot;A set of nodes, each holding its own local mempool.<br><br>    There is no shared, global mempool here on purpose: `broadcast`<br>    delivers a transaction to each peer independently and probabilistically,<br>    modelling how gossip reaches honest nodes at different times.<br>    &quot;&quot;&quot;<br><br>    def __init__(self, node_names: list[str], rng: random.Random) -&gt; None:<br>        &quot;&quot;&quot;Create one empty mempool per node.<br>        &quot;&quot;&quot;<br>        if not node_names:<br>            raise ValueError(&quot;A network needs at least one node.&quot;)<br>        self.node_names = list(node_names)<br>        self.rng = rng<br>        self.mempools: dict[str, dict[str, Transaction]] = {<br>            name: {} for name in node_names<br>        }<br><br>    def broadcast(self, transaction: Transaction, origin: str) -&gt; None:<br>        &quot;&quot;&quot;Deliver a transaction to its origin, then probabilistically to peers.<br>        &quot;&quot;&quot;<br>        if origin not in self.mempools:<br>            raise ValueError(f&quot;Unknown origin node: {origin}&quot;)<br>        self.mempools[origin][transaction.tx_id] = transaction<br>        for name in self.node_names:<br>            if name != origin and self.rng.random() &lt; 0.5:<br>                self.mempools[name][transaction.tx_id] = transaction<br><br>    def mempool_ids(self, node_name: str) -&gt; list[str]:<br>        &quot;&quot;&quot;Return a node&#39;s currently known transaction IDs, sorted.<br>        &quot;&quot;&quot;<br>        if node_name not in self.mempools:<br>            raise ValueError(f&quot;Unknown node: {node_name}&quot;)<br>        return sorted(self.mempools[node_name])</pre><p>The important ideas here are that:</p><ul><li>The network does not contain a single blockchain object</li><li>The network has no global mempool</li></ul><p>Instead, each node maintains its own local copy of the chain. The network is the communication layer through which nodes exchange transactions and blocks.</p><p>At some point, honest nodes may look like this:</p><pre>Alice<br>  blockchain: [B1, B2, B3]<br>  mempool:    [TX4, TX5]<br><br>Bob<br>  blockchain: [B1, B2, B3]<br>  mempool:    [TX4]<br><br>Carol<br>  blockchain: [B1, B2, B3]<br>  mempool:    [TX5, TX6]</pre><p>Their blockchain histories currently agree, but their mempools do not.</p><h3>Gossip: Useful precisely because it is imperfect</h3><p>Gossip is how transactions spread through the network. A node forwards information to its peers, which can then forward it to their peers, and so on.</p><p>It improves the chance that other nodes — including a future block proposer — receive a transaction. But it does not guarantee that every node receives it immediately, or even that every node has heard about it at the same moment.</p><p>Our code makes this imperfection explicit. We use a seeded random generator so the demonstration is repeatable. The 50% probability is deliberately artificial; it gives us a simple way to model transactions reaching honest nodes at different times.</p><p>Watch for the different transaction lists at each node, rather than treating any one node as having the network’s definitive mempool:</p><pre>nodes = [&quot;Node A&quot;, &quot;SketchyGuy-Node&quot;, &quot;Emma-Node&quot;, &quot;Farid-Node&quot;]<br>network = Network(nodes, random.Random(7))<br>transactions = [<br>    Transaction(&quot;tx1&quot;, &quot;Alice pays Bob&quot;),<br>    Transaction(&quot;tx2&quot;, &quot;Carol pays Titus&quot;),<br>    Transaction(&quot;tx3&quot;, &quot;Titus pays Alice&quot;),<br>    Transaction(&quot;tx4&quot;, &quot;Bob pays Carol&quot;),<br>]<br>origins = [&quot;Node A&quot;, &quot;SketchyGuy-Node&quot;, &quot;Emma-Node&quot;, &quot;Node A&quot;]<br><br>for transaction, origin in zip(transactions, origins):<br>    network.broadcast(transaction, origin)<br><br>print(&quot;Each node has its own mempool:&quot;)<br>for node in nodes:<br>    print(f&quot;  {node}: {network.mempool_ids(node)}&quot;)<br><br>mempool_sizes = [len(network.mempool_ids(node)) for node in nodes]<br>print(f&quot;Average local mempool size: {statistics.mean(mempool_sizes):.2f}&quot;)</pre><blockquote>Pause and predict: after four broadcasts, will every node have all four transaction IDs?</blockquote><p>Output:</p><pre>Each node has its own mempool:<br>  Node A: [&#39;tx1&#39;, &#39;tx2&#39;, &#39;tx3&#39;, &#39;tx4&#39;]<br>  SketchyGuy-Node: [&#39;tx1&#39;, &#39;tx2&#39;, &#39;tx4&#39;]<br>  Emma-Node: [&#39;tx1&#39;, &#39;tx3&#39;, &#39;tx4&#39;]<br>  Farid-Node: [&#39;tx2&#39;, &#39;tx3&#39;, &#39;tx4&#39;]<br>Average local mempool size: 3.25</pre><p>Node A has all four transaction IDs, while every other node is missing one.</p><p>Nothing has gone wrong. The nodes simply have <strong>different views because the transactions have not propagated to everyone yet</strong>.</p><p>And this distinction matters when we move from transactions to blocks.</p><p>A transaction being present in one node’s mempool does not mean that every node knows about it. Likewise, a transaction being known by many nodes does not mean that it has become part of the blockchain.</p><p>A transaction can be <strong>submitted</strong> to a node, <strong>propagated</strong> through gossip, <strong>included</strong> in a proposed block, and eventually <strong>confirmed</strong> as that block becomes part of the accepted chain.</p><p>Each stage depends on information reaching the relevant participants.</p><p>Which brings us to the next question:</p><p><strong>If a block proposer can only see the transactions that have reached its own node, what happens when it has an incomplete view of the network?</strong></p><h3>From transactions to block proposals</h3><p>Transactions sitting in mempools are not yet part of the blockchain. A node must turn these transactions into a block proposal.</p><p>In a <a href="https://medium.com/mitb-for-all/proof-of-stake-the-mining-free-blockchain-ddae684ffec5">proof-of-stake blockchain</a>, that job is assigned to a validator selected by the protocol. The proposer looks at the transactions available to it and constructs a candidate block.</p><p>And there is an important constraint here: <strong>A proposer can only choose from the information it has received.</strong></p><h4>Simply sending a transaction directly to the next scheduled proposer is not a great solution.</h4><p>The scheduled proposer might be delayed. It might be temporarily unavailable. It might refuse to relay the transaction. And even if that proposer receives it, the rest of the network still needs an opportunity to learn about and independently validate the resulting block.</p><h4><strong>Gossip solves a different problem.</strong></h4><p>Rather than trying to predict exactly which validator will propose the next block, a transaction can spread through the network so that many independently validating nodes have an opportunity to receive it.</p><p>That means a future proposer is more likely to have the transaction in its local mempool when its slot arrives. The question is:</p><blockquote><strong><em>“Which transactions does the proposer know about when it is time to build a block?”</em></strong></blockquote><h3>Introducing the idea of slots</h3><p>We now need one more piece of the puzzle: <strong>time</strong>.</p><p>Proof of stake divides time into discrete <strong>slots</strong>. For each slot, the protocol determines which eligible validator gets the opportunity to propose a block (like my elaboration in my <a href="https://medium.com/mitb-for-all/proof-of-stake-the-mining-free-blockchain-ddae684ffec5">previous article on PoS blockchains</a>).</p><p>All validators need to be able to independently determine who that proposer is. Otherwise, different validators could disagree about who was entitled to propose the block.</p><p>For this article, I do this deterministically in code. I’m deliberately simplifying the real mechanism here: what matters is that every validator can independently arrive at the same proposer assignment.</p><pre>def assign_proposer(<br>    validators: list[Validator], slot: int, epoch_seed: str<br>) -&gt; Validator:<br>    &quot;&quot;&quot;Deterministically pick a slot&#39;s proposer from a stake-weighted lottery.<br><br>    This hashes a documented ``epoch_seed`` and ``slot`` number so every<br>    validator can reproduce the same assignment independently -- a real<br>    protocol needs that reproducibility (a stand-in for VRF/RANDAO-style<br>    eligibility proofs), whereas per-call proposer selection does not.<br>    &quot;&quot;&quot;<br>    if not validators or sum(v.stake for v in validators) &lt;= 0:<br>        raise ValueError(&quot;Proposer assignment needs positive validator stake.&quot;)<br>    seed = hashlib.sha256(f&quot;{epoch_seed}:{slot}&quot;.encode()).digest()<br>    pick = int.from_bytes(seed, &quot;big&quot;) % sum(v.stake for v in validators)<br>    cumulative = 0<br>    for validator in validators:<br>        cumulative += validator.stake<br>        if pick &lt; cumulative:<br>            return validator<br>    raise RuntimeError(&quot;Unreachable proposer-selection state.&quot;)</pre><p>The epoch seed is shared, and every validator can run the same calculation to arrive at the same result.</p><blockquote>Caveat: Real proof-of-stake protocols use more sophisticated randomness and selection mechanisms. <a href="https://ethereum.org/developers/docs/consensus-mechanisms/pos/block-proposal/">Ethereum</a>, for example, uses RANDAO-based pseudo-random proposer selection, weighted by effective validator balance.</blockquote><p>And now we have all the pieces needed for the interesting part.</p><p>We have a network where nodes can have different local views, a gossip mechanism that takes time to propagate information, and a protocol that assigns a specific proposer to each slot.</p><p>So what happens if <strong>the protocol correctly selects the next proposer, but that proposer has not yet received the previous block?</strong></p><h3>A fork caused by delay, not dishonesty</h3><p>We will re-use our Block and Blockchain classes from the PoS article. This time, we’ll schedule different proposers while deliberately giving them <strong>different views of the chain</strong>.</p><p>The slot 2 proposer has not received Block #1, so it honestly builds on the latest tip it knows: Genesis. Both candidate blocks therefore point to Genesis.</p><pre>validators = [<br>    Validator(&quot;Node A&quot;, 100),<br>    Validator(&quot;SketchyGuy-Node&quot;, 80),<br>    Validator(&quot;Emma-Node&quot;, 70),<br>    Validator(&quot;Farid-Node&quot;, 50),<br>]<br>validators_by_name = {validator.name: validator for validator in validators}<br>epoch_seed = &quot;notebook-5&quot;<br><br>chain = Blockchain(validators)<br>genesis = chain.chain[0]<br>slot_1_proposer = assign_proposer(validators, 1, epoch_seed)<br>slot_2_proposer = assign_proposer(validators, 2, epoch_seed)<br>assert slot_1_proposer.name != slot_2_proposer.name<br><br>block_1 = chain.propose_candidate(<br>    &quot;, &quot;.join(network.mempool_ids(slot_1_proposer.name)), slot_1_proposer.name<br>)<br>proposal_time_views = {<br>    &quot;Node A&quot;: {genesis.hash},<br>    &quot;SketchyGuy-Node&quot;: {genesis.hash, block_1.hash},<br>    &quot;Emma-Node&quot;: {genesis.hash, block_1.hash},<br>    &quot;Farid-Node&quot;: {genesis.hash, block_1.hash},<br>}<br>assert block_1.hash not in proposal_time_views[slot_2_proposer.name]<br>block_2_alt = chain.propose_candidate(<br>    &quot;, &quot;.join(network.mempool_ids(slot_2_proposer.name)), slot_2_proposer.name<br>)<br><br>print(f&quot;Slot 1 proposer: {slot_1_proposer.name} creates Block #1.&quot;)<br>print(<br>    f&quot;Slot 2 proposer: {slot_2_proposer.name} has NOT received Block #1; &quot;<br>    &quot;it builds on Genesis.&quot;<br>)<br>print(&quot;\nPropagation fork (both arrows leave Genesis):&quot;)<br>print(&quot;          Genesis&quot;)<br>print(&quot;     +----+----+&quot;)<br>print(&quot;     |         |&quot;)<br>print(&quot;   Block #1    Block #2-alt&quot;)<br>print(f&quot;   {slot_1_proposer.name:&lt;12} {slot_2_proposer.name}&quot;)</pre><blockquote>Pause and predict: if the next proposer has not seen a valid earlier block, can deterministic proposer assignment alone prevent a fork?</blockquote><p>The output:</p><pre>Slot 1 proposer: Farid-Node creates Block #1.<br>Slot 2 proposer: Node A has NOT received Block #1; it builds on Genesis.<br><br>Propagation fork (both arrows leave Genesis):<br>          Genesis<br>     +----+----+<br>     |         |<br>   Block #1    Block #2-alt<br>   Farid-Node   Node A</pre><p>Farid-Node proposes Block #1, while Node A honestly proposes Block #2-alt from Genesis because its local view is delayed.</p><p><strong>The fork is caused by information asymmetry, not dishonest behaviour.</strong></p><p>This is an important distinction.</p><p>Farid-Node and Node A are two <em>different</em> validators, each honestly proposing for its own correctly assigned slot. Neither validator has signed two conflicting blocks for the same slot.</p><p>So, in this scenario, <strong>neither validator gets slashed</strong>. They simply proposed blocks based on different views of the latest chain tip.</p><p>The deterministic proposer schedule answered one question:</p><blockquote><strong><em>Who is supposed to propose?</em></strong></blockquote><p>It did not answer another:</p><blockquote><strong><em>What block does that proposer believe is the latest valid block?</em></strong></blockquote><p>And that second question depends on network propagation.</p><h3>Attestations: turning local views into votes</h3><p>We now have two competing blocks, both produced honestly:</p><ul><li><strong>Block #1</strong> was proposed by Farid-Node in slot 1.</li><li><strong>Block #2-alt</strong> was proposed by Node A in slot 2, because Node A had not yet received Block #1.</li></ul><p>The network now needs to decide which branch to follow.</p><p>This is where <strong>attestations</strong> come in.</p><p>Validators don’t magically acquire a global view of the network before voting. They attest to the chain they currently see. In our toy model, that means each validator votes for the candidate tip present in its local view.</p><p>This gives us an interesting consequence:</p><blockquote><strong><em>Different validators can vote for different blocks without any of them necessarily being dishonest.</em></strong></blockquote><p>At the moment of attestation, Block #1 has had longer to propagate. Block #2-alt is newer and, in our deliberately simplified scenario, has only reached its own proposer.</p><p>We therefore derive each validator’s vote directly from the blocks it has actually received rather than hand-picking the votes to produce a particular outcome.</p><p>The code below then assigns each vote the validator’s stake and adds the stake supporting each candidate.</p><pre>def attestation_weight(<br>    tip_hash: str,<br>    votes: dict[str, str],<br>    validators_by_name: dict[str, Validator],<br>) -&gt; int:<br>    &quot;&quot;&quot;Sum the stake of every validator whose vote names ``tip_hash``.&quot;&quot;&quot;<br>    unknown = set(votes) - set(validators_by_name)<br>    if unknown:<br>        raise ValueError(f&quot;Unknown attesters: {sorted(unknown)}&quot;)<br>    return sum(<br>        validators_by_name[name].stake<br>        for name, voted_hash in votes.items()<br>        if voted_hash == tip_hash<br>    )</pre><p>Our fork-choice function is intentionally tiny. It simply selects the candidate with the greatest attestation weight.</p><pre>def fork_choice_two_tips(<br>    candidate_tips: list[Block],<br>    votes: dict[str, str],<br>    validators_by_name: dict[str, Validator],<br>) -&gt; tuple[Block, dict[str, int]]:<br>    &quot;&quot;&quot;Pick the candidate tip with the most attestation-weighted stake.&quot;&quot;&quot;<br>    candidate_hashes = {block.hash for block in candidate_tips}<br>    invalid_votes = set(votes.values()) - candidate_hashes<br>    if invalid_votes:<br>        raise ValueError(&quot;An attestation names an unknown candidate tip.&quot;)<br>    weights = {<br>        block.hash: attestation_weight(block.hash, votes, validators_by_name)<br>        for block in candidate_tips<br>    }<br>    if len(set(weights.values())) != len(weights):<br>        raise ValueError(&quot;Teaching model requires an explicit tie-break rule.&quot;)<br>    winner_hash = max(weights, key=weights.get)<br>    return next(block for block in candidate_tips if block.hash == winner_hash), weights</pre><p>There is one deliberate simplification here.</p><p>A real blockchain’s fork-choice mechanism is considerably more sophisticated than “add up the stake and pick the largest number.” It has to account for things such as the protocol’s voting rules, chain structure, and tie-breaking behaviour.</p><p>But for this article, we only need one idea:</p><blockquote><strong><em>Fork choice gives weight to the different views held by validators.</em></strong></blockquote><p>Let’s see what happens with our actual network state.</p><pre># By attestation time, Block #1 (slot 1) has had longer to propagate, so every<br># node that already had it in `proposal_time_views` still has it. Block #2-alt<br># is brand new: only its own proposer has received it so far. No node holds<br># both, so each vote below is just &quot;the block this node has actually seen&quot; —<br># read out of `proposal_time_views`, not hand-picked to fit a story.<br>attestation_time_views = dict(proposal_time_views)<br>attestation_time_views[slot_2_proposer.name] = attestation_time_views[<br>    slot_2_proposer.name<br>] | {block_2_alt.hash}<br><br><br>def vote_for_known_tip(known_block_hashes: set[str]) -&gt; str:<br>    &quot;&quot;&quot;Return the block hash a validator with this local view would vote for.<br><br>    Prefers Block #1 whenever it is known -- in this scenario no validator<br>    ever holds both candidates at once, so this never needs a real<br>    tie-break rule between two simultaneously-known tips.<br>    &quot;&quot;&quot;<br>    return block_1.hash if block_1.hash in known_block_hashes else block_2_alt.hash<br><br><br>votes = {<br>    name: vote_for_known_tip(known_hashes)<br>    for name, known_hashes in attestation_time_views.items()<br>}<br><br>print(&quot;\nView-derived attestations:&quot;)<br>for name, voted_hash in votes.items():<br>    label = &quot;Block #1&quot; if voted_hash == block_1.hash else &quot;Block #2-alt&quot;<br>    print(f&quot;  {name} received {label} and votes for {label}.&quot;)<br><br>winner, weights = fork_choice_two_tips(<br>    [block_1, block_2_alt], votes, validators_by_name<br>)<br>winner_label = &quot;Block #1&quot; if winner.hash == block_1.hash else &quot;Block #2-alt&quot;<br>print(f&quot;Block #1 stake weight: {weights[block_1.hash]}&quot;)<br>print(f&quot;Block #2-alt stake weight: {weights[block_2_alt.hash]}&quot;)<br>print(f&quot;Canonical tip: {winner_label}&quot;)<br><br>chain.accept_candidate(winner)<br>valid, message = chain.is_valid()<br>print(<br>    f&quot;Chain length after fork resolution: {len(chain.chain)}; &quot;<br>    f&quot;valid? {valid} -- {message}&quot;<br>)</pre><p>The output:</p><pre>View-derived attestations:<br>  Node A received Block #2-alt and votes for Block #2-alt.<br>  SketchyGuy-Node received Block #1 and votes for Block #1.<br>  Emma-Node received Block #1 and votes for Block #1.<br>  Farid-Node received Block #1 and votes for Block #1.<br>Block #1 stake weight: 200<br>Block #2-alt stake weight: 100<br>Canonical tip: Block #1<br>Chain length after fork resolution: 2; valid? True -- Chain is valid.</pre><p>Block #1 wins because SketchyGuy-Node, Emma-Node, and Farid-Node already had it in their local view, contributing 80 + 70 + 50 = 200 stake, versus 100 stake from Node A, which has only its own Block #2-alt proposal.</p><p><strong>Fork choice follows the attestation weight behind each received view, not the block’s slot number.</strong></p><h3>Thinking about what we just learnt</h3><p>A transaction originates somewhere. Gossip carries it between nodes. Different nodes receive it at different times. A proposer builds a block from its local view. Other validators attest to the blocks they have seen. Fork choice then gives the network a mechanism for converging on one history.</p><p><strong>That is the connection between the network and the blockchain.</strong></p><p>The blockchain is the shared history. The network is the collection of nodes that propagate, validate, propose, and vote on that history.</p><p>At any given moment, individual nodes may have incomplete or even conflicting views. But through communication and consensus, those local views can eventually converge on a common chain.</p><p>In other words, we have been studying how a blockchain deals with <strong>incomplete and inconsistent information inside its own network</strong>.</p><p>But there is a more fundamental problem.</p><p>Suppose the transaction says:</p><pre>&quot;Team A won the football match 2–1.&quot;</pre><p>Our validators can check whether the transaction is properly formed and whether it was included in a valid block. But they cannot determine from the blockchain itself whether Team A actually won the match.</p><p>The network can agree on a fact. <strong>It cannot, by itself, discover whether that fact is true in the outside world.</strong></p><p>So how does information from outside the blockchain get into the network in the first place? And once it arrives, <strong>how can the blockchain trust it?</strong></p><p>That’s a rabbit hole for another time: <strong>oracles.</strong></p><p><em>Disclaimer: All opinions and interpretations are that of the writer, and not of MITB. I declare that I have full rights to use the contents published here, and nothing is plagiarized. I declare that this article is written by me and not with any generative AI tool such as ChatGPT. I declare that no data privacy policy is breached, and that any data associated with the contents here are obtained legitimately to the best of my knowledge. I agree not to make any changes without first seeking the editors’ approval. Any violations may lead to this article being retracted from the publication.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=9fbc92701f70" width="1" height="1" alt=""><hr><p><a href="https://medium.com/mitb-for-all/introducing-mempools-the-one-time-gossip-pays-off-9fbc92701f70">Introducing mempools - The one time gossip pays off</a> was originally published in <a href="https://medium.com/mitb-for-all">MITB For All</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Introducing permissioned chains]]></title>
            <link>https://medium.com/mitb-for-all/introducing-permissioned-chains-1d8a71f5b09a?source=rss----825f074cc537---4</link>
            <guid isPermaLink="false">https://medium.com/p/1d8a71f5b09a</guid>
            <category><![CDATA[python]]></category>
            <category><![CDATA[permissioned-blockchains]]></category>
            <category><![CDATA[blockchain]]></category>
            <category><![CDATA[fintech]]></category>
            <dc:creator><![CDATA[Tituslhy]]></dc:creator>
            <pubDate>Mon, 31 Aug 2026 02:39:22 GMT</pubDate>
            <atom:updated>2026-08-31T02:41:28.353Z</atom:updated>
            <content:encoded><![CDATA[<h4>A blockchain where the guest list matters</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dkv-5Lez5lAErm8uJxAxrQ.png" /></figure><p>In my previous articles, I covered blockchains that operate using (i) <a href="https://medium.com/mitb-for-all/understanding-blockchains-from-scratch-f14c454af32f">proof-of-work (PoW)</a>, aka mining, and (ii) <a href="https://medium.com/mitb-for-all/proof-of-stake-the-mining-free-blockchain-ddae684ffec5">proof-of-stake (PoS)</a>. In the <a href="https://medium.com/mitb-for-all/inside-opencerts-how-merkle-trees-verify-singapores-digital-diplomas-61f60d13700e">third article</a>, I extended the PoS blockchain with Merkle trees to explore how the same ideas can be used to create tamper-evident educational credentials.</p><p>All three articles have something in common: <strong>the blockchain is public and permissionless.</strong></p><p>In the PoW blockchain, miners race to perform computational work for the right to add the next block. In the PoS blockchain, proposers are randomly selected, weighted by the stake they put up. Other participants then independently verify that the proposed block follows the rules.</p><p>But nobody ever had to prove who they were. If &quot;Sketchy-Guy-Node” played by the rules, his block does get added to the blockchain as well. That makes sense when the whole point is to build a network that anyone can participate in.</p><p>But what if that isn’t what we want?</p><p>Imagine three banks running a shared settlement ledger. They don’t necessarily want random strangers joining the network. They don’t need a proof-of-work energy bill or a speculative token just so participants can stake it. And they probably don’t want <a href="https://www.investopedia.com/terms/1/51-attack.asp">&quot;51% of the CPU&quot; or </a><a href="https://www.investopedia.com/terms/1/51-attack.asp">&quot;51% of the stake&quot;</a> deciding the truth when the actual participants are a small, known, named group of institutions — institutions that would very much like a court to be able to determine <strong>who signed what</strong>.</p><p>This is where <strong>permissioned blockchains</strong> come in.</p><p>Instead of asking:</p><blockquote><strong>Who has earned the right to write the next block?</strong></blockquote><p>we can ask:</p><blockquote><strong>Who is allowed to participate, and whose approval is required for this transaction?</strong></blockquote><p>That seemingly small change gives us a very different kind of blockchain. In this article, we’ll build our own permissioned consortium chain, modelled loosely on <a href="https://hyperledger-fabric.readthedocs.io/en/latest/whatis.html"><strong>Hyperledger Fabric</strong></a>.</p><blockquote>Hyperledger Fabric is an open-source, enterprise-grade permissioned distributed ledger platform designed for situations where the participants in a blockchain network are <strong>known organizations rather than anonymous strangers</strong>. Unlike Bitcoin or Ethereum, participants have cryptographic identities, and policies can specify which organizations are allowed to perform particular actions or must approve particular transactions. Its modular architecture was designed with enterprise use cases such as banking, finance, insurance and supply chains in mind.</blockquote><p>And the architecture is quite different from the toy PoW and PoS chains we’ve built so far. Our Fabric-inspired chain will follow:</p><p><strong>execute → endorse → order → validate</strong></p><p>Multiple known organizations can independently execute a proposed transaction and cryptographically endorse its result. An ordering service then determines transaction order and packages transactions into blocks. Finally, peers validate that the required endorsements and other rules have been satisfied before committing the transaction to their ledgers.</p><p>Same broad goal — multiple parties maintaining a shared, tamper-evident history.</p><p><strong>Very different assumptions about who those parties are.</strong></p><p>As before, all my codes for this article can be found in my <a href="https://github.com/tituslhy/Blockchain/blob/main/notebooks/4.%20permissioned_chains.ipynb">GitHub repository.</a></p><h3>Writing our Membership Service — gives permission to even show up</h3><p>MembershipService is a deliberately tiny stand-in for one part of what Hyperledger Fabric handles through <a href="https://hyperledger-fabric.readthedocs.io/en/latest/msp.html"><strong>Membership Service Providers (MSPs)</strong></a>. In a real Fabric network, participants have cryptographic identities — typically X.509 certificates issued by trusted Certificate Authorities (CAs) — while MSP configuration defines which identities an organization recognizes and what roles those identities can play.</p><p>We’re going to collapse all of that machinery into a humble Python class 😂</p><p>No entry in the registry, no participation: Peer.endorse() checks is_registered() before doing anything else and throws if you’re not on the list.</p><p>This is the permissioned chain’s first and most fundamental departure from the previous articles:</p><blockquote><strong>Identity is a precondition, not an emergent property of computational effort or economic stake.</strong></blockquote><pre>import hashlib<br><br>def sha256(data: str) -&gt; str:<br>    &quot;&quot;&quot;Return the hex-encoded SHA-256 digest of a UTF-8 encoded string.&quot;&quot;&quot;<br>    return hashlib.sha256(data.encode()).hexdigest()<br><br><br>class MembershipService:<br>    def __init__(self) -&gt; None:<br>        &quot;&quot;&quot;Initialize an empty registry of member organizations.&quot;&quot;&quot;<br>        self.registered_orgs: set[str] = set()<br><br>    def register(self, org_name: str) -&gt; None:<br>        &quot;&quot;&quot;Add an organization to the registry, granting it participant identity.&quot;&quot;&quot;<br>        print(f&quot;  [MembershipService] Registered org: {org_name}&quot;)<br>        self.registered_orgs.add(org_name)<br><br>    def is_registered(self, org_name: str) -&gt; bool:<br>        &quot;&quot;&quot;Return True if the given organization is a registered participant.&quot;&quot;&quot;<br>        return org_name in self.registered_orgs</pre><h3>Writing the ledger’s state: a versioned key-value world, not just a list of blocks</h3><p>In this blockchain, multiple orgs can <em>concurrently</em> propose transactions against the same shared state, so we need to answer a sharper question than “what’s the current balance?” — we need to ask <strong>“what version of this key was I looking at when I computed my answer?”</strong></p><p>Our WorldState therefore tracks two things: the current value associated with each key, and the version of that key.</p><pre>from dataclasses import dataclass, field<br><br>@dataclass<br>class WorldState:<br>    &quot;&quot;&quot;Versioned key-value ledger state: current balances <br>    plus a per-key write version counter.&quot;&quot;&quot;<br><br>    balances: dict[str, float] = field(default_factory=dict)<br>    versions: dict[str, int] = field(default_factory=dict)<br><br>    def version_of(self, key: str) -&gt; int:<br>        &quot;&quot;&quot;Return the current version number of a key, or 0 if it <br>        has never been written.&quot;&quot;&quot;<br>        return self.versions.get(key, 0)<br><br><br>@dataclass<br>class ExecutionResult:<br>    &quot;&quot;&quot;The proposed effect of a chaincode run: what it wants to write, <br>    and the versions it read.&quot;&quot;&quot;<br><br>    write_set: dict[str, float]<br>    read_versions: dict[str, int]</pre><p>Hang onto version_of() — it&#39;s the hook the whole optimistic-concurrency story further down is built on.</p><p>ExecutionResult is what a chaincode run produces: a write_set (what it wants to change) and read_versions (what versions it <em>assumed</em> were current when it computed that write set). Neither is applied to WorldState yet — this is a <strong>proposal</strong>, not a commit.</p><h3>Chaincode: the business logic, run speculatively</h3><p>“Chaincode” is Fabric’s term for the business logic encoded in a smart contract. transfer_chaincode is about as simple as one gets: check a balance, then compute a new pair of balances.</p><p>The important part isn’t the arithmetic — it’s <strong>when this runs</strong>.</p><p>Executing this function does not touch the real ledger. It runs speculatively against the current state and produces an ExecutionResult: the values it wants to write, plus the versions of the values it read along the way.</p><p>An endorsing peer can then sign the result of that simulation.</p><p>For our toy implementation, we’ll make this especially visible: multiple peers independently execute the same proposal, hash the resulting write_set, and we’ll require those hashes to agree. Real Fabric carries considerably more information in its proposal responses and endorsements, but the intuition we’re trying to capture is the same:</p><pre>def transfer_chaincode(state: WorldState, frm: str, to: str, amount: float) -&gt; ExecutionResult:<br>    if state.balances.get(frm, 0) &lt; amount:<br>        raise ValueError(f&quot;Insufficient funds: {frm} has {state.balances.get(frm, 0)}, needs {amount}&quot;)<br>    return ExecutionResult(<br>        write_set={frm: state.balances.get(frm, 0) - amount, to: state.balances.get(to, 0) + amount},<br>        read_versions={frm: state.version_of(frm), to: state.version_of(to)},<br>    )</pre><p><strong>Execution produces a proposed state change; it does not commit that change.</strong></p><h3>And now: Execute → Endorse → Order → Validate</h3><p>Why bother separating execution from ordering?</p><p>Public and permissioned blockchains still share one awkward problem: multiple organizations that do not necessarily trust one another are trying to maintain a shared ledger.</p><p>Fabric tackles that problem by separating responsibilities.</p><p>First, endorsing peers <strong>simulate</strong> a proposed transaction and sign the resulting proposal response. The client collects enough endorsements to satisfy the transaction’s endorsement policy.</p><p>Only then does the transaction move to the ordering stage.</p><p>The ordering service has a deliberately narrower job: <strong>establish a consistent transaction order and package transactions into blocks.</strong> It does not execute the chaincode to decide what the transaction should do.</p><p>Our toy OrderingService.order() is therefore almost insultingly simple on purpose:</p><p>proposals in → ordered proposals out</p><pre>from collections.abc import Callable<br><br>@dataclass<br>class Endorsement:<br>    peer_name: str<br>    result_hash: str<br><br><br>@dataclass<br>class Peer:<br>    name: str<br>    membership: MembershipService<br><br>    def endorse(self, chaincode: Callable, state: WorldState, *args) -&gt; tuple[Endorsement, ExecutionResult]:<br>        if not self.membership.is_registered(self.name):<br>            raise PermissionError(f&quot;{self.name} is not a registered participant.&quot;)<br>        result = chaincode(state, *args)<br>        result_hash = sha256(json.dumps(result.write_set, sort_keys=True))<br>        return Endorsement(peer_name=self.name, result_hash=result_hash), result<br><br><br>@dataclass<br>class ProposedTransaction:<br>    description: str<br>    endorsements: list[Endorsement]<br>    execution_result: ExecutionResult<br><br><br>class OrderingService:<br>    def __init__(self, name: str) -&gt; None:<br>        self.name = name<br><br>    def order(self, proposals: list[ProposedTransaction]) -&gt; list[ProposedTransaction]:<br>        print(f&quot;  [{self.name}] Sequencing {len(proposals)} proposal(s).&quot;)<br>        return list(proposals)</pre><p>Real Fabric ordering services are distributed systems in their own right. Modern Fabric deployments can use a Raft-based ordering service so multiple ordering nodes agree on the sequence of transactions even if individual nodes fail.</p><p>And importantly, being ordered does <strong>not</strong> mean a transaction is valid.</p><p>That comes next.</p><h3>Finally, the ledger itself</h3><p>This block looks quite different from the PoW and PoS blocks we built earlier.</p><p>There’s no nonce, because nobody is mining. There’s no proposer or stake, because nobody is winning a validator lottery either. Instead, there’s an orderer field: not the winner of a competition, just a record of which ordering service sequenced these transactions into this block.</p><p>There’s another important difference.</p><p>In our earlier toy chains, we tended to ask whether an entire block was valid before accepting it.</p><p>Fabric separates those ideas more sharply:</p><blockquote><strong>A transaction can be ordered into a block and still fail validation afterward.</strong></blockquote><p>If that happens, the transaction remains part of the blockchain’s immutable history, but its write set is <strong>not</strong> applied to world state.</p><p>That distinction is important enough that we’ll model it explicitly.</p><pre>class Block:<br>    &quot;&quot;&quot;A tamper-evident block carrying a batch of ordered transactions, <br>    each pending its own validation code.&quot;&quot;&quot;<br><br>    def __init__(<br>        self,<br>        index: int,<br>        transactions: list[ProposedTransaction],<br>        previous_hash: str,<br>        orderer: str,<br>    ) -&gt; None:<br>        &quot;&quot;&quot;Create and hash a new block from an already-ordered batch of <br>        transactions.<br>        &quot;&quot;&quot;<br>        self.index = index<br>        self.timestamp = time.time()<br>        self.transactions = transactions<br>        self.previous_hash = previous_hash<br>        self.orderer = orderer<br><br>        # Validation happens after ordering.<br>        self.validation_codes = [&quot;NOT_VALIDATED&quot;] * len(transactions)<br><br>        self.hash = self.compute_hash()<br><br>    def compute_hash(self) -&gt; str:<br>        &quot;&quot;&quot;Compute this block&#39;s SHA-256 hash over its index, timestamp, <br>        transactions, previous hash, and orderer.&quot;&quot;&quot;<br>        tx_contents = [<br>            {<br>                &quot;description&quot;: tx.description,<br>                &quot;write_set&quot;: tx.execution_result.write_set,<br>                &quot;read_versions&quot;: tx.execution_result.read_versions,<br>                &quot;endorsements&quot;: [<br>                    {<br>                        &quot;peer_name&quot;: e.peer_name,<br>                        &quot;result_hash&quot;: e.result_hash,<br>                    }<br>                    for e in tx.endorsements<br>                ],<br>            }<br>            for tx in self.transactions<br>        ]<br><br>        block_contents = json.dumps(<br>            {<br>                &quot;index&quot;: self.index,<br>                &quot;timestamp&quot;: self.timestamp,<br>                &quot;transactions&quot;: tx_contents,<br>                &quot;previous_hash&quot;: self.previous_hash,<br>                &quot;orderer&quot;: self.orderer,<br>            },<br>            sort_keys=True,<br>        )<br>        return hashlib.sha256(block_contents.encode()).hexdigest()<br><br>    def __repr__(self) -&gt; str:<br>        &quot;&quot;&quot;Return a human-readable summary of the block, including <br>        per-transaction validation codes.&quot;&quot;&quot;<br>        return (<br>            f&quot;Block #{self.index}: &quot;<br>            f&quot;{len(self.transactions)} transaction(s), &quot;<br>            f&quot;validation={self.validation_codes}\n&quot;<br>        )</pre><h4>A very different looking blockchain</h4><p>Notice that the main event, validate_and_commit_block() below, happens <strong>after a block has already been created and appended to the chain</strong>.</p><p>That is a very different mental model from the is_valid() method in the previous articles. There, validation meant re-walking the blockchain and checking that the chain itself had not been tampered with.</p><p>Here, we’re asking a different question:</p><blockquote><strong>Which transactions inside this already-ordered block are actually allowed to change world state?</strong></blockquote><p>The block stays in the chain either way. Validation determines whether each transaction’s write set gets applied.</p><pre>class PermissionedBlockchain:<br>    &quot;&quot;&quot;A consortium ledger where blocks are cut by ordering, then <br>    validated (and applied to state) after the fact.&quot;&quot;&quot;<br><br>    def __init__(<br>        self,<br>        required_endorsers: set[str],<br>        min_endorsements: int,<br>        orderer_name: str,<br>    ) -&gt; None:<br>        &quot;&quot;&quot;Initialize the chain with an empty genesis block and an empty world <br>        state.<br>        &quot;&quot;&quot;<br>        self.required_endorsers = required_endorsers<br>        self.min_endorsements = min_endorsements<br>        self.orderer_name = orderer_name<br><br>        self.chain: list[Block] = [<br>            Block(<br>                index=0,<br>                transactions=[],<br>                previous_hash=&quot;0&quot; * 64,<br>                orderer=orderer_name,<br>            )<br>        ]<br><br>        self.state = WorldState()<br><br>    def add_ordered_block(<br>        self,<br>        transactions: list[ProposedTransaction],<br>    ) -&gt; Block:<br>        &quot;&quot;&quot;Cut a new block from an already-ordered batch of transactions and <br>        append it to the chain.<br><br>        This is the ordering-service side of the pipeline: it has no opinion<br>        on whether any transaction in the batch is actually valid.<br>        &quot;&quot;&quot;<br>        previous_block = self.chain[-1]<br><br>        block = Block(<br>            index=len(self.chain),<br>            transactions=transactions,<br>            previous_hash=previous_block.hash,<br>            orderer=self.orderer_name,<br>        )<br><br>        self.chain.append(block)<br><br>        print(<br>            f&quot;  [{self.orderer_name}] &quot;<br>            f&quot;Created Block #{block.index} with &quot;<br>            f&quot;{len(transactions)} transaction(s).&quot;<br>        )<br><br>        return block<br><br>    def validate_and_commit_block(self, block: Block) -&gt; list[bool]:<br>        &quot;&quot;&quot;Validate every transaction in a block and apply valid ones to world<br>        state.<br><br>        For each transaction, checks (in order): the endorsement policy is<br>        satisfied, a minimum number of endorsements was collected, all<br>        endorsers agree on the result, and no read key&#39;s version has changed<br>        since the transaction was endorsed (MVCC). The block&#39;s<br>        `validation_codes` are updated in place; the block itself is never<br>        removed or re-sequenced, even if every transaction in it turns out<br>        invalid -- only the *state-changing effect* of invalid transactions<br>        is withheld.<br>        &quot;&quot;&quot;<br>        results: list[bool] = []<br><br>        for i, tx in enumerate(block.transactions):<br>            print(f&quot;  Validating: {tx.description}&quot;)<br><br>            endorser_names = {e.peer_name for e in tx.endorsements}<br><br>            if not endorser_names.issuperset(self.required_endorsers):<br>                print(&quot;    INVALID: missing required endorsers&quot;)<br>                block.validation_codes[i] = &quot;INVALID_ENDORSEMENT_POLICY&quot;<br>                results.append(False)<br>                continue<br><br>            if len(tx.endorsements) &lt; self.min_endorsements:<br>                print(&quot;    INVALID: not enough endorsements&quot;)<br>                block.validation_codes[i] = &quot;INVALID_QUORUM&quot;<br>                results.append(False)<br>                continue<br><br>            if len({e.result_hash for e in tx.endorsements}) &gt; 1:<br>                print(&quot;    INVALID: endorsers disagree&quot;)<br>                block.validation_codes[i] = &quot;INVALID_ENDORSER_MISMATCH&quot;<br>                results.append(False)<br>                continue<br><br>            mvcc_conflict = False<br><br>            for key, read_version in tx.execution_result.read_versions.items():<br>                current_version = self.state.version_of(key)<br><br>                if current_version != read_version:<br>                    print(<br>                        f&quot;    INVALID: MVCC CONFLICT on &#39;{key}&#39; -- &quot;<br>                        f&quot;endorsed against version {read_version}, &quot;<br>                        f&quot;current is {current_version}&quot;<br>                    )<br>                    block.validation_codes[i] = &quot;INVALID_MVCC_CONFLICT&quot;<br>                    results.append(False)<br>                    mvcc_conflict = True<br>                    break<br><br>            if mvcc_conflict:<br>                continue<br><br>            for key, new_value in tx.execution_result.write_set.items():<br>                self.state.balances[key] = new_value<br>                self.state.versions[key] = self.state.version_of(key) + 1<br><br>            block.validation_codes[i] = &quot;VALID&quot;<br>            results.append(True)<br><br>            print(&quot;    VALID: write set committed to WorldState&quot;)<br><br>        return results</pre><p>This is where “permissioned” earns its keep:</p><ul><li><strong>add_ordered_block()</strong> does the <em>ordering service&#39;s</em> job: take whatever batch of transactions arrived, cut a Block around them, append it to the chain. It has zero opinion on whether any of those transactions are legitimate.</li><li><strong>validate_and_commit_block()</strong> does the <em>committer&#39;s</em> job: walk every transaction in that already-appended block and, for each one, check:</li></ul><ol><li><strong>Endorsement policy</strong> — did <em>all</em> required_endorsers actually sign off? (Here: both banks, always — a strict &quot;everyone must agree&quot; policy. Real Fabric policies can be richer, e.g. &quot;any 2 of 3&quot;.)</li><li><strong>Quorum</strong> — at least min_endorsements signatures, full stop.</li><li><strong>Agreement</strong> — do all endorsers’ result_hashes match? If two honest-looking endorsers computed <em>different</em> write sets, something is wrong (bug, malicious peer, or stale state) — mark it invalid rather than guess who&#39;s right.</li><li><strong>MVCC conflict check</strong> — for every key the transaction <em>read</em>, is state.version_of(key) still what the endorsers assumed? If someone else&#39;s transaction already landed and bumped that version, this proposal is stale — mark it invalid, no matter how honest it was.</li></ol><p>Only a transaction that passes all four gates gets its write set applied to WorldState, with its validation_codes entry set to &quot;VALID&quot;. Everything else gets a specific INVALID_* code — and stays on the chain anyway, permanently, as a record that it was ordered but failed validation.</p><p>The block itself is never rolled back or re-cut just because something inside it turned out to be invalid. That’s the whole point of separating <strong>“put these transactions in order”</strong> from <strong>“decide which of them may update world state.”</strong></p><p>Real Fabric follows the same broad principle: transaction validation happens after ordering, and an invalid transaction can remain recorded in the blockchain while its state changes are excluded from world state. A block does not have to disappear merely because transactions inside it fail validation.</p><h3>Then: what happens when two transactions collide?</h3><p>Gate #4 above is where <strong>optimistic concurrency control (OCC)</strong> lives.</p><p>“Optimistic” because nobody locks anything up front — every peer just executes hopefully, assuming nothing else will change the state before their transaction lands.</p><p>Most of the time that’s true, and we avoid the cost of locking.</p><p>When it isn’t, validate_and_commit_block() catches the stale read during validation, marks that transaction INVALID_MVCC_CONFLICT, and leaves it right where it is in the blockchain.</p><p>Nobody cheated. The transaction wasn’t necessarily malformed. <strong>The world simply changed between execution and validation.</strong></p><p>The client can respond with a shrug: re-run EXECUTE against whatever the state looks like now, collect fresh endorsements, get the new transaction ordered into a fresh block, and try again.</p><p>That’s what propose_with_retry() below demonstrates.</p><pre>def propose_with_retry(<br>    description: str,<br>    peers: list[Peer],<br>    chaincode: Callable,<br>    chain: PermissionedBlockchain,<br>    orderer: OrderingService,<br>    args: tuple,<br>    max_attempts: int = 3,<br>) -&gt; bool:<br>    &quot;&quot;&quot;The client-side retry loop -- what a real Fabric SDK does<br>    automatically. On an MVCC conflict, don&#39;t give up: re-run EXECUTE<br>    against whatever the CURRENT state now is, and try again. This is<br>    the &#39;optimistic&#39; half of optimistic concurrency control -- pay the<br>    retry cost only on an actual collision, not on every transaction.<br><br>    Every attempt still gets ordered into its own block and validated,<br>    even a failing one -- a rejected attempt leaves an INVALID block<br>    behind on the chain, it just never touches WorldState.<br>    &quot;&quot;&quot;<br>    for attempt in range(1, max_attempts + 1):<br>        print(f&quot;  Attempt {attempt}: {description} (fresh EXECUTE against current state)&quot;)<br>        endorsements: list[Endorsement] = []<br>        execution_result: ExecutionResult | None = None<br>        for peer in peers:<br>            endorsement, result = peer.endorse(chaincode, chain.state, *args)<br>            endorsements.append(endorsement)<br>            execution_result = result<br>        tx = ProposedTransaction(description, endorsements, execution_result)<br>        ordered = orderer.order([tx])<br>        block = chain.add_ordered_block(ordered)<br>        results = chain.validate_and_commit_block(block)<br>        if results[0]:<br>            return True<br>        print(&quot;    -&gt; stale, retrying...\n&quot;)<br>    print(f&quot;  Gave up after {max_attempts} attempts.&quot;)<br>    return False</pre><p>Time to give our permissioned chain a spin.</p><h3>Demo: The tale of three banks with one shared ledger</h3><p>The scenario: Bank-A, Bank-B, and Bank-C jointly run a settlement ledger. None of them trusts the other two&#39;s arithmetic, so the endorsement policy requires <strong>all three</strong> to independently compute and agree on every transaction before it commits. Nobody is mining. Nobody is staking. Everybody is, presumably, billing hours.</p><pre>membership = MembershipService()<br>membership.register(&quot;Bank-A&quot;)<br>membership.register(&quot;Bank-B&quot;)<br>membership.register(&quot;Bank-C&quot;)<br>peer_a = Peer(&quot;Bank-A&quot;, membership)<br>peer_b = Peer(&quot;Bank-B&quot;, membership)<br>peer_c = Peer(&quot;Bank-C&quot;, membership)<br>peers = [peer_a, peer_b, peer_c]<br><br>chain = PermissionedBlockchain(<br>    required_endorsers={&quot;Bank-A&quot;, &quot;Bank-B&quot;, &quot;Bank-C&quot;},<br>    min_endorsements=3,<br>    orderer_name=&quot;Shared-Ordering-Service&quot;,<br>)<br>orderer = OrderingService(&quot;Shared-Ordering-Service&quot;)<br>chain.state.balances = {&quot;Alice-Account&quot;: 1000.0, <br>                        &quot;Bob-Account&quot;: 500.0, <br>                        &quot;Carol-Account&quot;: 0.0}</pre><h4>Here comes the race condition</h4><p>Two clients prepare transactions at roughly the same time:</p><ul><li>Alice pays Bob <strong>$200</strong></li><li>Alice pays Carol <strong>$150</strong></li></ul><p>Both transactions are executed and endorsed before either one has changed WorldState, so both simulations see Alice-Account at <strong>version 0</strong>, with a balance of $1,000.</p><p>Let’s create both transactions first without ordering or committing either one:</p><pre>e_a1, res1 = peer_a.endorse(<br>    transfer_chaincode, chain.state,<br>    &quot;Alice-Account&quot;, &quot;Bob-Account&quot;, 200.0<br>)<br>e_b1, _ = peer_b.endorse(<br>    transfer_chaincode, chain.state,<br>    &quot;Alice-Account&quot;, &quot;Bob-Account&quot;, 200.0<br>)<br>e_c1, _ = peer_c.endorse(<br>    transfer_chaincode, chain.state,<br>    &quot;Alice-Account&quot;, &quot;Bob-Account&quot;, 200.0<br>)<br><br>tx1 = ProposedTransaction(<br>    &quot;Alice pays Bob $200&quot;,<br>    [e_a1, e_b1, e_c1],<br>    res1,<br>)<br><br>e_a2, res2 = peer_a.endorse(<br>    transfer_chaincode, chain.state,<br>    &quot;Alice-Account&quot;, &quot;Carol-Account&quot;, 150.0<br>)<br>e_b2, _ = peer_b.endorse(<br>    transfer_chaincode, chain.state,<br>    &quot;Alice-Account&quot;, &quot;Carol-Account&quot;, 150.0<br>)<br>e_c2, _ = peer_c.endorse(<br>    transfer_chaincode, chain.state,<br>    &quot;Alice-Account&quot;, &quot;Carol-Account&quot;, 150.0<br>)<br><br>tx2_stale_attempt = ProposedTransaction(<br>    &quot;Alice pays Carol $150&quot;,<br>    [e_a2, e_b2, e_c2],<br>    res2,<br>)</pre><p>At this point, <strong>nothing has been committed</strong>.</p><p>Both transactions have all three required endorsements, and because neither transaction has changed WorldStateyet, both were calculated against Alice-Account version 0.</p><p>But look at what they independently calculated:</p><p><strong>Transaction 1</strong></p><p>Alice: $1,000 → $800<br>Bob: $500 → $700</p><p><strong>Transaction 2</strong></p><p>Alice: $1,000 → $850<br>Carol: $0 → $150</p><p>Both calculations are perfectly correct <strong>given the state they saw when they executed</strong>.</p><p>The problem appears when we try to put them in order.</p><pre>print(&quot;=== ORDER + VALIDATE: Tx1 happens to get sequenced first ===\n&quot;)<br>ordered1 = orderer.order([tx1])<br>block1 = chain.add_ordered_block(ordered1)<br>chain.validate_and_commit_block(block1)</pre><p>The output:</p><pre>=== ORDER + VALIDATE: Tx1 happens to get sequenced first ===<br><br>  [Shared-Ordering-Service] Sequencing 1 proposal(s).<br>  [Shared-Ordering-Service] Created Block #1 with 1 transaction(s).<br>  Validating: Alice pays Bob $200<br>    VALID: write set committed to WorldState<br>[True]</pre><p>Now transaction 2 will fail because the WorldStatehas changed:</p><pre>print(&quot;\n=== Tx2&#39;s ORIGINAL (now-stale) endorsement finally reaches ORDER + VALIDATE ===\n&quot;)<br>ordered2 = orderer.order([tx2_stale_attempt])<br>block2 = chain.add_ordered_block(ordered2)<br>results = chain.validate_and_commit_block(block2)<br>print(f&quot;  First attempt succeeded? {results[0]}\n&quot;)</pre><p>The output:</p><pre>=== Tx2&#39;s ORIGINAL (now-stale) endorsement finally reaches ORDER + VALIDATE ===<br><br>  [Shared-Ordering-Service] Sequencing 1 proposal(s).<br>  [Shared-Ordering-Service] Created Block #2 with 1 transaction(s).<br>  Validating: Alice pays Carol $150<br>    INVALID: MVCC CONFLICT on &#39;Alice-Account&#39; -- endorsed against version 0, current is 1<br>  First attempt succeeded? False</pre><p>And there it is.</p><p>Transaction 2 was perfectly valid when it was executed and endorsed. But by the time it reached validation, Transaction 1 had already changed Alice-Account from version 0 to version 1.</p><p>Why not just apply Transaction 2 anyway?</p><p>Because its write set says Alice’s new balance should be <strong>$850</strong> — a number calculated from the old $1,000 balance.</p><p>But Alice is now actually at <strong>$800</strong>. Blindly applying that stale write set would overwrite $800 with $850, effectively making $50 of Alice’s first payment disappear.</p><p>So the validator doesn’t try to be clever. It simply asks:</p><p><strong>“You calculated this using version 0. Is version 0 still current?”</strong></p><p>No. Alice is now at version 1.</p><p>Transaction 2 is therefore marked INVALID_MVCC_CONFLICT. It stays recorded in Block #2, but its write set never touches WorldState.</p><p>Our client retries:</p><pre>print(&quot;=== Client retries automatically, re-executing against CURRENT state ===\n&quot;)<br>success = propose_with_retry(<br>    &quot;Alice pays Carol $150&quot;, peers, transfer_chaincode, chain, orderer,<br>    (&quot;Alice-Account&quot;, &quot;Carol-Account&quot;, 150.0),<br>)<br>print(f&quot;\n  Eventually succeeded? {success}\n&quot;)</pre><p>The output:</p><pre>=== Client retries automatically, re-executing against CURRENT state ===<br><br>  Attempt 1: Alice pays Carol $150 (fresh EXECUTE against current state)<br>  [Shared-Ordering-Service] Sequencing 1 proposal(s).<br>  [Shared-Ordering-Service] Created Block #3 with 1 transaction(s).<br>  Validating: Alice pays Carol $150<br>    VALID: write set committed to WorldState<br><br>  Eventually succeeded? True</pre><p>The final state:</p><pre>=== Final state ===<br>  Balances: {&#39;Alice-Account&#39;: 650.0, &#39;Bob-Account&#39;: 700.0, &#39;Carol-Account&#39;: 150.0}<br>  Versions: {&#39;Alice-Account&#39;: 2, &#39;Bob-Account&#39;: 1, &#39;Carol-Account&#39;: 1}<br>  Chain length: 4 blocks</pre><p>And now the numbers add up.</p><p>Alice started with $1,000, paid Bob $200, then successfully retried her $150 payment to Carol:</p><p><strong>$1,000 − $200 − $150 = $650</strong></p><p>Notice the versions too. Alice-Account changed twice, so it has reached version 2. Bob and Carol were each updated once, so both are at version 1.</p><p>And the chain contains <strong>four blocks</strong>:</p><ul><li>Block #0 — genesis</li><li>Block #1 — Alice → Bob $200: VALID</li><li>Block #2 — original Alice → Carol $150: INVALID_MVCC_CONFLICT</li><li>Block #3 — retried Alice → Carol $150: VALID</li></ul><p>Block #2 didn’t disappear just because its transaction failed.</p><p><strong>The blockchain remembers what happened. World state remembers what actually took effect.</strong></p><h3>Concluding thoughts</h3><p>A permissioned chain starts from a fundamentally different assumption than a public chain: <strong>we already know who is allowed to participate.</strong></p><p>That changes the mechanism.</p><p>In our toy public chains, PoW and PoS determine who gets to propose the next block — either by winning computational work or by being selected according to stake. In our permissioned chain, there is no mining race and no validator lottery. Known members execute transactions, endorse their results, an ordering service sequences them, and validators check whether those transactions are still valid when they reach the commit stage.</p><p>And that’s where things get interesting.</p><p>In a system where transactions are constantly being proposed against shared state, two perfectly legitimate transactions can race against each other. One may change the state before the other reaches validation, making the second transaction stale. MVCC catches that conflict; the client can then re-execute against the latest state and try again.</p><p>Which led me to another question.</p><p><strong>What happens on a public blockchain when different participants don’t see the same information at the same time?</strong></p><p>Transactions and blocks have to propagate across a distributed network, and that propagation isn’t instantaneous. A transaction might reach one node before another. A block might arrive after a competing block has already been seen elsewhere. So what does each participant actually know at any given moment — and how does the network eventually converge on a shared history?</p><p>That sent me down another rabbit hole.</p><p><strong>A story to be shared another time.</strong></p><p><em>Disclaimer: All opinions and interpretations are that of the writer, and not of MITB. I declare that I have full rights to use the contents published here, and nothing is plagiarized. I declare that this article is written by me and not with any generative AI tool such as ChatGPT. I declare that no data privacy policy is breached, and that any data associated with the contents here are obtained legitimately to the best of my knowledge. I agree not to make any changes without first seeking the editors’ approval. Any violations may lead to this article being retracted from the publication.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1d8a71f5b09a" width="1" height="1" alt=""><hr><p><a href="https://medium.com/mitb-for-all/introducing-permissioned-chains-1d8a71f5b09a">Introducing permissioned chains</a> was originally published in <a href="https://medium.com/mitb-for-all">MITB For All</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>