Rippled Audit

Modifying The Ledger

Given the server handling of client connections, the P2P network, and Ledger Management/Consensus, the next missing piece in the client to server workflow is storing submitted transactions via the internal TX queue and applying them to the open ledger. This is accomplished via a fairly simple sequence of method calls, originating from one of two places depending on if a client is informing a validator of a new transaction or if a peer is relaying a transaction accross the network:

To understand what happens from here, we must explore how the ledger is represented in memory and how modifications are made to it.

rippled implements a heirarchy of Serialzable Types used to represent entities in the ledger and store them on disk. At the base of the heirarchy is STBase (where ST = Serialized Type) defining the common fields and interface for serialization. Above this are representations of various primitive and custom data structures (STInteger, STObject), collections (STArray), Ledger-based data structures (STAccount, STAmount), and Ledger Data itself (STLedgerEntry). STLedgerEntry, deriving from STObject and used to store various types of data in the ledger, is instantiated and referenced extensively in many other places in the application logic, and is aliased to SLE.

The Ledger Class itself, introduced in the last section maintains a set of two SHAMaps for Ledger Entity States and Transactions (recall in the Node/SHAMap Store Section, SHAMaps are tree based databases driven by one of several persistent backends), and implements the logic to store the SLEs in the DB.

Ledger SHAMap / SLE Adaptor- src/ripple/app/ledger/Ledger.cpp

525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
  void
  Ledger::rawErase(std::shared_ptr<SLE> const& sle)
  {
      if (! stateMap_->delItem(sle->key()))
          LogicError("Ledger::rawErase: key not found");
  }

  void
  Ledger::rawInsert(std::shared_ptr<SLE> const& sle)
  {
      Serializer ss;
      sle->add(ss);
      auto item = std::make_shared<
          SHAMapItem const>(sle->key(),
              std::move(ss));
      // VFALCO NOTE addGiveItem should take ownership
      if (! stateMap_->addGiveItem(
              std::move(item), false, false))
          LogicError("Ledger::rawInsert: key already exists");
  }

  void
  Ledger::rawReplace(std::shared_ptr<SLE> const& sle)
  {
      Serializer ss;
      sle->add(ss);
      auto item = std::make_shared<
          SHAMapItem const>(sle->key(),
              std::move(ss));
      // VFALCO NOTE updateGiveItem should take ownership
      if (! stateMap_->updateGiveItem(
              std::move(item), false, false))
          LogicError("Ledger::rawReplace: key not found");
  }

  void
  Ledger::rawTxInsert (uint256 const& key,
      std::shared_ptr<Serializer const
          > const& txn, std::shared_ptr<
              Serializer const> const& metaData)
  {
      assert (metaData);

      // low-level - just add to table
      Serializer s(txn->getDataLength () +
          metaData->getDataLength () + 16);
      s.addVL (txn->peekData ());
      s.addVL (metaData->peekData ());
      auto item = std::make_shared<
          SHAMapItem const> (key, std::move(s));
      if (! txMap().addGiveItem
              (std::move(item), true, true))
          LogicError("duplicate_tx: " + to_string(key));
  }

Also recall from the previous section that the top level Application class manages a list of Ledgers via the LedgerMaster and LedgerHistory classes. In addition to this Application holds onto an instance of OpenLedger, a central class used in conjunction with Readable/Writable Views on the Ledger, whose lifecycle is managed as part of the concensus process as detailed below:

We can see the OpenLedger & LastClosedLedger being updated in NetworkOPsImp::switchLastClosedLedger and LedgerMaster::switchLCL.

NetworkOPs::switchLastClosedLedger - src/ripple/app/misc/NetworkOPs.cpp

1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
void NetworkOPsImp::switchLastClosedLedger (
    std::shared_ptr<Ledger const> const& newLCL)
{
    // set the newLCL as our last closed ledger -- this is abnormal code
    JLOG(m_journal.error()) <<
        "JUMP last closed ledger to " << newLCL->info().hash;

    clearNeedNetworkLedger ();

    // Update fee computations.
    app_.getTxQ().processClosedLedger(app_, *newLCL, true);

    // Caller must own master lock
    {
        // Apply tx in old open ledger to new
        // open ledger. Then apply local tx.

        auto retries = m_localTX->getTxSet();
        auto const lastVal =
            app_.getLedgerMaster().getValidatedLedger();
        boost::optional<Rules> rules;
        if (lastVal)
            rules.emplace(*lastVal, app_.config().features);
        else
            rules.emplace(app_.config().features);
        app_.openLedger().accept(app_, *rules,
            newLCL, OrderedTxs({}), false, retries,
                tapNONE, "jump",
                    [&](OpenView& view, beast::Journal j)
                    {
                        // Stuff the ledger with transactions from the queue.
                        return app_.getTxQ().accept(app_, view);
                    });
    }

    m_ledgerMaster.switchLCL (newLCL);

LedgerMaster::switchLCL - src/ripple/app/ledger/impl/LedgerMaster.cpp

254
255
256
257
258
259
260
261
262
263
264
265
266
267
void
LedgerMaster::switchLCL(std::shared_ptr<Ledger const> const& lastClosed)
{
    assert (lastClosed);
    if(! lastClosed->isImmutable())
        LogicError("mutable ledger in switchLCL");

    if (lastClosed->open())
        LogicError ("The new last closed ledger is open!");

    {
        ScopedLockType ml (m_mutex);
        mClosedLedger.set (lastClosed);
    }

As seen in the diagram at the top of the page, the current OpenLedger's modify method is invoked to process new transactions as they come in, which dispatches to a callback, passing it the OpenView instance corresponding to the OpenLedger. This view is passed to TxQ::apply and ultimately used to construct an ApplyView instance which is passed to the specific Transaction's doApply method to actually modify ledger data structures. This process can be seen in the following diagram:

Through this process, transactions have both read and write access to Entities on the Open Ledger so as to validate state and make modifications.

In the next section we will look at the prerequisites enforced before transactions can modify the ledger.