A simulator for blockchain-based local energy markets.
- vagrant >= 2.0
- vagrant-disksize
- virtualbox >= 6.0
If you use Homebrew, pick up the latest versions of all of the above like so:
brew cask install virtualbox vagrant
vagrant plugin install vagrant-disksize
Clone this repo:
git clone [email protected]:kchristidis/island.git
cd into the trace
directory within the island
repo, and download 04-final-trace-2013.csv
(link) there.
cd into the root directory of island
:
vagrant up && vagrant ssh
When inside the VM:
make all
When the simulation is over, the metrics of interest are captured in the output
folder in the island
repo.
To enable debugging mode, set schema.StagingLevel
to Debug
before running the simulation.
The simulated local energy market consists of trace.IDCount
households. It runs for schema.TraceLength
slots.
Households are equipped with solar panels, and therefore may produce their own energy. They will sell that excess energy to a neighbor, if there is demand; otherwise they will sell it back to the grid for a lower price. We do not consider a residential energy storage mechanism.
Energy is exchanged within the market using a closed-book double auction mechanism.
Each household is represented in that auction by a bidder
.
On every slot, bidders place a buy
offer if their energy needs are larger than their energy generation for that slot; they place a sell
offer if the opposite applies. All bids are encrypted. The bidders are rational; the prices they pick for their bids are greater than the low price that the grid is offering to them for their surplus, and smaller than the high price that the grid is selling energy to them for. For a given slot, every bidder picks the price for their bid randomly within that price interval for a given slot.
At the end of the slot, a market clearing price is calculated (using the dauction library).
The simulation engine tracks the performance of the market; how much energy was bought by the grid and at which price, how much energy was sold to the grid and at which price, how much energy was traded within the market and at which market clearing price. It also tracks the performance of the underlying transaction management platform for a given experiment type; count of late transactions, problematic decryptions, size of blocks, etc. All of this information is persisted in files that are produced at the end of each run.
The trace for every simulation tracks trace.IDCount
households across trace.RowCount
slots.
The expectation is that every slot should capture:
trace.Gen
: the power that is generated by the house's solar paneltrace.Grid
: the power that the house draws from the grid, or the power that it sends back to the grid if the value is negativetrace.Use
: the aggregate oftrace.Gen
andtrace.Grid
trace.Lo
: the lower bound for the market clearing price; this is how much the grid is offering for whatever the solar panel-carrying houses generate.trace.Hi
: the upper bound for the market clearing price; this is how much the grid provider is selling its electricity to the households for.
All power values should correspond to average real power over the slot in kW.
All prices should be denominated cents per kWh.
See kchristidis/island-input repo for a sample trace.
A bidder
represents a household in the local energy market. They place a buy
bid for slot N
if their projected energy usage (the trace.Use
value) for that slot is positive. They place a sell
bid if their projected energy production (trace.Gen
) is positive.
N.B. that this means that the prosumer tries to sell all of their output; instead of using it to satisfy their own needs first. This assumption does not necessarily work in favor of the individual generator (as they may end up bying energy for a higher price than they one they sell their own production for), but it contributes positively to the welfare of the local market. In practice, we do this here because we want to increase the opportunities for matching bids, as we wish to evaluate the trade clearing performance of the platform.
On a given slot, each bidder can a place of maximum of one buy
and one sell
bid.
In Experiments 1 and 3, bidders encrypt their bids with their own unique private key per slot; as such, they are expected to post their decryption key (postKey
) when the PostKey
phase in that slot begins.
In Experiment 2, we introduce the concept of a regulator
. For a given slot, all bids are encrypted using the regulator's public key. At the end of the slot, the regulator posts their private key to allow the decryption of the posted bids for that slot, and the calculation of the market clearing price. See the "Types of experiments" section for more info.
The blocknotifier
checks the ledger height every schema.SleepDuration
. If the new height corresponds to a new a slot, it notifies the slotnotifier
. The blocknotifier
also invokes a dummy Clock
method on the smart contract every schema.ClockPeriod
seconds so as to ensure a continuous stream of blocks. We need this because the passage of blocks is how the agents in the simulation track time.
The slotnotifier
is notified by the blocknotifier
when a new slot should be triggered, and notifies all subscribed agents (bidders and the regulator) of that event. For experiments with a separate PostKey
phase, a second slotnotifier
is used to signal the beginning of that phase in a given slot.
The statscollector
thread receives block statistics from the blocknotifier
(what is the size of a block), and transaction statistics by the agents (what kind of transaction was invoked, what was its type, result, and end-to-end latency). At the end of the run, it queries the smart contract for slot statistics, and prints all statistics to files in the output
folder; see the "Parsing the results" section for more info.
The contract encodes the primitives necessary to run the double auction. It exposes the following methods:
buy
andsell
: In Experiment 1, it persists the encrypted bid in a key (data slice) in the contract's key-value store that is common for all bids of that type (i.e.buy
orsell
) in that slot. In Experiments 2 and 3, it persists the encrypted bid in a key that is unique per bid in that slot.postKey
: In Experiment 1, it persists the private key for a given bid in a key that is common for all private keys in that slot. In Experiment 3, it persists the private key for a given bid in a data slice that is unique per private key in that slot. In Experiment 2, this method is not invoked; the private key will be posted by the regulator on themarkEnd
call.markEnd
: It is invoked by at the beginning of slotN
to mark the end of slotN-1
. In Experiment 2, the regulator uses that call to post the private key that decrypts all bids posted in slotN-1
, so that every market participant can calculate the market clearing price locally.
For exposition across all experiments, we use this markEnd
method to calculate the market clearing price (decode all the posted bids for slot N-1
, create bid collections for buyers and sellers, calculate the market clearing price, post that value in the chaincode's key-value store); this is not necessary; across all experiments, the market participants are in a position to calculate the market clearing price for a slot locally, after the end of that slot.
If a chaincode invocation fails, it will be retried schema.RetryCount
times, for a total of up to schema.RetryCount + 1
times.
The wait time between retries follows an exponential backoff algorithm for better flow control; concretely, we wait anywhere from [0, schema.Alpha * 2 ^ (currentAttemptNumber))
blocks.
Each experiment runs for schema.TraceLength
slots.
A slot consists of schema.BlocksPerSlot
blocks. For those experiments with a PostKey
phase (i.e. experiments 1 and 3), this phase begins schema.BlockOffset
blocks into the slot.
A block is cut every schema.BatchTimeout
seconds, or every Orderer.BatchSize.MaxMessageCount
messages; whichever comes first.
The block notifier process checks the ledger for blocks every schema.SleepDuration
seconds.
We design these along two axes: encryption keys for the posted bids, and data model (data slices) for the smart contract.
In Experiments 1 and 3, bidders encrypt their bids using their own keys. In Experiment 2, they encrypt using the public key of the regulator for that slot.
In Experiment 1, bidders post all of their buy offers for a given slot in the same key in the contract's key-value store. Ditto for sell offers, or postKey
transactions. As a result, we expect contention and MVCC read conflicts. In order to mitigate this contention somewhat, bidders always backoff exponentially even before their first attempt to post. This experiment is meant to demonstrate what can happen if the contract is not set up correctly, i.e. we expect it to be the most sub-optimal approach of the lot. In Experiments 2 and 3, every transaction updates a key in the contract's key-value store that is unique to that transaction.
To change the experiment that the simulation executes, modify schema.ExpNum
.
schema.BlocksPerSlot
schema.BlockOffset
schema.RetryCount
schema.Alpha
schema.BatchTimeout
schema.BlocksPerslot
schema.BlockOffset
The program writes three files in the output
folder:
- block-indexed stats:
exp-MM-run-NN-block.csv
- slot-indexed stats:
exp-MM-run-NN-slot.csv
- transaction-indexed stats:
exp-MM-run-NN-tran.csv
Where:
MM
identifies the experiment the simulator is performing; seeschema.ExpNum
.NN
identifies the iteration of this experiment.
If you're running experiment 1 then, the first iteration of the simulation should produce the following files:
exp-01-run-01-block.csv
exp-01-run-01-slot.csv
exp-01-run-01-tran.csv
As the file extension suggests, these are comma-separated value (CSV) files.
Values for each row in the *-block.csv
file (type of value [in brackets]):
block_num
[integer] (index): the block under inspection.size_kib
[float]: the size of the block in kibibytes.
slot_num
[integer] (index): the slot under inspectionbfg_qty_kwh
[float]: energy bought from the grid in kWhbfg_ppu_c_per_kWh
[float]: price per unit for energy bought from the grid in US cents per kWhstg_qty_kwh
[float]: energy sold to the grid (kWh)stg_ppu_c_per_kWh
[float]: price per unit for energy sold to the grid (US cents per kWh)dmi_qty_kwh
[float]: energy needs met internally, i.e. from trading within the microgrid (kWh)dmi_ppu_c_per_kWh
[float]: price per unit for energy needs met internal (US cents per kWh)late_cnt_all
[integer]: count of late transactions, i.e.buy
,sell
, orpostKey
transactions that are received after a slot is marked as over (with amarkEnd
call); it is the sum oflate_cnt_buy
,late_cnt_sell
, andlate_decrs
late_cnt_buy
[integer]: count of latebuy
transactionslate_cnt_sell
[integer]: count of latesell
transactionslate_decrs
[integer]: count of latepostKey
transactionsprob_iters
[integer]: count of problematic iterations; this may occur when we attempt to iterate over the keys in the contract's key-value store with a partial composite keyprob_marshals
[integer]: count of problematic serializations and deserializations. This counter is incremented if an error occurs when (a) the contract attempts to serialize the JSON blob that it will send back to the invoker as a response, or (b) when the contract attempts to deserialize the call arguments, or a marshalled value in the contract's key-value store.prob_decrs
[integer]: count of problematic decryption attempts; these can happen during themarkEnd
call when we attempt to retrieve a serialized PEM-encoded private key, deserialize said key, decode said key, or decode the bid that is encrypted with said key.prob_bid_calcs
[integer]: count of problematic attempts to calculate the market clearing price during themarkEnd
callprob_keys
[integer]: count of problematic attempts to interact with a key in the contract's key-value store, i.e. read from it or write to it; it is the sum ofprob_gets
andprob_puts
prob_gets
[integer]: count of problematic attempts to read a key from the contract's key-value storeprob_puts
[integer]: count of problematic attempts to write a key to the contract's key-value store
For practitioners that wish to understand the exact context under which a slot counter is incremented, see the fields in the MetricsOutput
struct in chaincode/schema.go
and grep the codebase for them.
tx_id
[string] (index): the transaction under inspectionlatency_ms
[integer]: the end-to-end latency of the transaction, as observed by the client; timer starts right before the client invokes the smart contract method; timer ends when the contract response is received.tx_type
[string]: the type of the transaction; allowed values arebuy
,sell
,postKey
, andmarkEnd
.attempt
[intger]: the attempt for this particular transaction; a transaction can be attempted up toschema.RetryCount
times.tx_status
[string]: the result of the transaction; allowed values aresuccess
, or the specific error that the invocation returned.
This repo began its life as a fork of the heroes-service repo. Experiments 2-3 make use of the composite keys iteration, initially demonstrated in the high-throughput Fabric sample. All Vagrant-related files were adapted from the Fabric repo.
Contributions are welcome. Fork this repo and submit a pull request.