merge stateUI

cleaning up
This commit is contained in:
Alon Muroch 2014-09-09 15:46:10 +03:00
parent b2ac8fc47b
commit 41efe75aec
11 changed files with 641 additions and 28 deletions

View File

@ -2,8 +2,11 @@ package org.ethereum.core;
import static org.ethereum.util.ByteUtil.EMPTY_BYTE_ARRAY;
import static org.ethereum.crypto.HashUtil.EMPTY_DATA_HASH;
import org.ethereum.util.RLP;
import org.ethereum.util.RLPList;
import org.spongycastle.util.Arrays;
import org.spongycastle.util.encoders.Hex;
import java.math.BigInteger;
@ -113,4 +116,16 @@ public class AccountState {
}
return rlpEncoded;
}
public String toString() {
String ret = "Nonce: " + this.getNonce().toString() + "\n" +
"Balance: " + Denomination.toFriendlyString(getBalance()) + "\n";
if(this.getStateRoot()!= null && !Arrays.areEqual(this.getStateRoot(), EMPTY_BYTE_ARRAY))
ret += "State Root: " + Hex.toHexString(this.getStateRoot()) + "\n";
if(this.getCodeHash() != null && !Arrays.areEqual(this.getCodeHash(), EMPTY_DATA_HASH))
ret += "Code Hash: " + Hex.toHexString(this.getCodeHash());
return ret;
}
}

View File

@ -31,4 +31,33 @@ public enum Denomination {
private static BigInteger newBigInt(int value) {
return BigInteger.valueOf(10).pow(value);
}
public static String toFriendlyString(BigInteger value) {
if(value.compareTo(DOUGLAS.value()) == 1 || value.compareTo(DOUGLAS.value()) == 0) {
return Float.toString(value.divide(DOUGLAS.value()).floatValue()) + " DOUGLAS";
}
else if(value.compareTo(EINSTEIN.value()) == 1 || value.compareTo(EINSTEIN.value()) == 0) {
return Float.toString(value.divide(EINSTEIN.value()).floatValue()) + " EINSTEIN";
}
else if(value.compareTo(ETHER.value()) == 1 || value.compareTo(ETHER.value()) == 0) {
return Float.toString(value.divide(ETHER.value()).floatValue()) + " ETHER";
}
else if(value.compareTo(FINNY.value()) == 1 || value.compareTo(FINNY.value()) == 0) {
return Float.toString(value.divide(FINNY.value()).floatValue()) + " FINNY";
}
else if(value.compareTo(SZABO.value()) == 1 || value.compareTo(SZABO.value()) == 0) {
return Float.toString(value.divide(SZABO.value()).floatValue()) + " SZABO";
}
else if(value.compareTo(SHANNON.value()) == 1 || value.compareTo(SHANNON.value()) == 0) {
return Float.toString(value.divide(SHANNON.value()).floatValue()) + " SHANNON";
}
else if(value.compareTo(BABBAGE.value()) == 1 || value.compareTo(BABBAGE.value()) == 0) {
return Float.toString(value.divide(BABBAGE.value()).floatValue()) + " BABBAGE";
}
else if(value.compareTo(ADA.value()) == 1 || value.compareTo(ADA.value()) == 0) {
return Float.toString(value.divide(ADA.value()).floatValue()) + " ADA";
}
else
return Float.toString(value.divide(WEI.value()).floatValue()) + " WEI";
}
}

View File

@ -75,10 +75,14 @@ public class RepositoryImpl implements Repository {
* @See loadBlockchain() to update the stateRoot
*/
public RepositoryImpl() {
chainDB = new DatabaseImpl("blockchain");
detailsDB = new DatabaseImpl("details");
this("blockchain", "details", "state");
}
public RepositoryImpl(String blockChainDbName, String detailsDbName, String stateDbName) {
chainDB = new DatabaseImpl(blockChainDbName);
detailsDB = new DatabaseImpl(detailsDbName);
contractDetailsDB = new TrackDatabase(detailsDB);
stateDB = new DatabaseImpl("state");
stateDB = new DatabaseImpl(stateDbName);
worldState = new Trie(stateDB.getDb());
accountStateDB = new TrackTrie(worldState);
}
@ -479,6 +483,9 @@ public class RepositoryImpl implements Repository {
}
}
public DBIterator getContractDetailsDBIterator() {
return detailsDB.iterator();
}
public boolean isClosed(){
return chainDB == null;

View File

@ -696,6 +696,37 @@ public class Program {
}
}
public static String stringify(byte[] code, int index, String result) {
if(code == null || code.length == 0)
return result;
OpCode op = OpCode.code(code[index]);
byte[] continuedCode = null;
switch(op){
case PUSH1: case PUSH2: case PUSH3: case PUSH4: case PUSH5: case PUSH6: case PUSH7: case PUSH8:
case PUSH9: case PUSH10: case PUSH11: case PUSH12: case PUSH13: case PUSH14: case PUSH15: case PUSH16:
case PUSH17: case PUSH18: case PUSH19: case PUSH20: case PUSH21: case PUSH22: case PUSH23: case PUSH24:
case PUSH25: case PUSH26: case PUSH27: case PUSH28: case PUSH29: case PUSH30: case PUSH31: case PUSH32:
result += ' ' + op.name() + ' ';
int nPush = op.val() - OpCode.PUSH1.val() + 1;
byte[] data = Arrays.copyOfRange(code, index+1, index + nPush + 1);
result += new BigInteger(data).toString() + ' ';
continuedCode = Arrays.copyOfRange(code, index + nPush + 1, code.length);
break;
default:
result += ' ' + op.name();
continuedCode = Arrays.copyOfRange(code, index + 1, code.length);
break;
}
return stringify(continuedCode, 0, result);
}
public void addListener(ProgramListener listener) {
this.listener = listener;
}

View File

@ -24,7 +24,7 @@ public class ProgramInvokeMockImpl implements ProgramInvoke {
}
public ProgramInvokeMockImpl() {
this.repository = new RepositoryImpl();
this.repository = new RepositoryImpl("blockchainMoc", "detailsMoc", "stateMoc");
this.repository.createAccount(Hex.decode(ownerAddress));
}

View File

@ -0,0 +1,135 @@
package org.ethereum.gui;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Image;
import java.awt.Toolkit;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.table.AbstractTableModel;
import org.ethereum.core.Account;
import org.ethereum.core.AccountState;
import org.ethereum.core.Denomination;
import org.ethereum.crypto.HashUtil;
import org.ethereum.manager.WorldManager;
import org.iq80.leveldb.DBIterator;
import org.spongycastle.util.Arrays;
import org.spongycastle.util.encoders.Hex;
public class AccountsListWindow extends JFrame {
private JTable tblAccountsDataTable;
private AccountsDataAdapter adapter;
public AccountsListWindow() {
java.net.URL url = ClassLoader.getSystemResource("ethereum-icon.png");
Toolkit kit = Toolkit.getDefaultToolkit();
Image img = kit.createImage(url);
this.setIconImage(img);
setTitle("Accounts List");
setSize(700, 500);
setLocation(50, 180);
setResizable(false);
JPanel panel = new JPanel();
getContentPane().add(panel);
tblAccountsDataTable = new JTable();
adapter = new AccountsDataAdapter(new ArrayList<DataClass>());
tblAccountsDataTable.setModel(adapter);
JScrollPane scrollPane = new JScrollPane(tblAccountsDataTable);
scrollPane.setPreferredSize(new Dimension(680,490));
panel.add(scrollPane);
loadAccounts();
}
private void loadAccounts() {
new Thread(){
@Override
public void run(){
DBIterator i = WorldManager.getInstance().getRepository().getContractDetailsDBIterator();
while(i.hasNext()) {
DataClass dc = new DataClass();
dc.address = i.next().getKey();
AccountState state = WorldManager.getInstance().getRepository().getAccountState(dc.address);
dc.accountState = state;
adapter.addDataPiece(dc);
}
}
}.start();
}
private class AccountsDataAdapter extends AbstractTableModel {
List<DataClass> data;
final String[] columns = new String[]{ "Account", "Balance", "Is Contract"};
public AccountsDataAdapter(List<DataClass> data) {
this.data = data;
}
public void addDataPiece(DataClass d) {
data.add(d);
this.fireTableRowsInserted(Math.min(data.size() - 2, 0), data.size() - 1);
}
@Override
public int getRowCount() {
return data.size();
}
@Override
public int getColumnCount() {
return 3;
}
@Override
public String getColumnName(int column) {
return columns[column];
}
@Override
public boolean isCellEditable(int row, int column) { // custom isCellEditable function
return column == 0? true:false;
}
@Override
public Object getValueAt(int rowIndex, int columnIndex) {
if(columnIndex == 0) {
return Hex.toHexString(data.get(rowIndex).address);
}
else if(columnIndex == 1 ){
if(data.get(rowIndex).accountState != null) {
return Denomination.toFriendlyString(data.get(rowIndex).accountState.getBalance());
}
return "---";
}
else {
if(data.get(rowIndex).accountState != null) {
if(!Arrays.areEqual(data.get(rowIndex).accountState.getCodeHash(), HashUtil.EMPTY_DATA_HASH))
return "Yes";
}
return "No";
}
}
}
private class DataClass {
public byte[] address;
public AccountState accountState;
}
}

View File

@ -3,6 +3,7 @@ package org.ethereum.gui;
import org.ethereum.core.Block;
import org.ethereum.core.Transaction;
import org.ethereum.db.RepositoryImpl;
import org.ethereum.facade.Repository;
import org.ethereum.manager.WorldManager;
import org.ethereum.vm.*;
import org.spongycastle.util.encoders.Hex;
@ -10,6 +11,7 @@ import org.spongycastle.util.encoders.Hex;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
@ -29,41 +31,39 @@ public class ProgramPlayDialog extends JPanel implements ActionListener,
private JTextArea console;
private JSlider stepSlider;
private ProgramInvoke pi;
public ProgramPlayDialog(byte[] code) {
outputList = new ArrayList<String>();
VM vm = new VM();
ProgramInvoke pi = new ProgramInvokeMockImpl();
Program program = new Program(code, pi);
program.addListener(this);
program.fullTrace();
vm.play(program);
doGUI();
this(code, new ProgramInvokeMockImpl(), null);
}
public ProgramPlayDialog(byte[] code, Transaction tx, Block lastBlock) {
this(code,
ProgramInvokeFactory.createProgramInvoke(tx,
lastBlock,
WorldManager.getInstance().getRepository()),
WorldManager.getInstance().getRepository());
}
outputList = new ArrayList<String>();
public ProgramPlayDialog(byte[] code, ProgramInvoke programInvoke, RepositoryImpl tractRepository) {
pi = programInvoke;
outputList = new ArrayList<String>();
VM vm = new VM();
RepositoryImpl tractRepository = WorldManager.getInstance().getRepository().getTrack();
Program program = new Program(code ,
ProgramInvokeFactory.createProgramInvoke(tx, lastBlock, tractRepository));
Program program = new Program(code, programInvoke);
program.addListener(this);
program.fullTrace();
vm.play(program);
tractRepository.rollback();
if(tractRepository != null)
tractRepository.rollback();
doGUI();
}
public void doGUI() {
setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));
//Create the slider.
@ -136,7 +136,7 @@ public class ProgramPlayDialog extends JPanel implements ActionListener,
*/
public static void createAndShowGUI(byte[] runCode, Transaction tx, Block lastBlock) {
ProgramPlayDialog ppd;
final ProgramPlayDialog ppd;
if (tx != null)
ppd = new ProgramPlayDialog(runCode, tx, lastBlock);
else{
@ -158,12 +158,21 @@ public class ProgramPlayDialog extends JPanel implements ActionListener,
//Add content to the window.
frame.add(ppd, BorderLayout.CENTER);
// close event
frame.addWindowListener(new java.awt.event.WindowAdapter() {
@Override
public void windowClosing(java.awt.event.WindowEvent windowEvent) {
ppd.pi.getRepository().close();
}
});
//Display the window.
frame.pack();
frame.setVisible(true);
ppd.setFocus();
}
@Override
public void output(String out) {
outputList.add(out);

View File

@ -0,0 +1,353 @@
package org.ethereum.gui;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Image;
import java.awt.TextArea;
import java.awt.Toolkit;
import javax.swing.JFrame;
import javax.swing.JPanel;
import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.math.BigInteger;
import java.util.Map;
import javax.swing.Box;
import javax.swing.ImageIcon;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.JButton;
import javax.swing.JToggleButton;
import javax.swing.SwingUtilities;
import javax.swing.table.AbstractTableModel;
import org.ethereum.core.AccountState;
import org.ethereum.core.Block;
import org.ethereum.core.Transaction;
import org.ethereum.db.ContractDetails;
import org.ethereum.manager.WorldManager;
import org.ethereum.vm.DataWord;
import org.ethereum.vm.OpCode;
import org.ethereum.vm.Program;
import org.ethereum.vm.ProgramInvoke;
import org.ethereum.vm.ProgramInvokeFactory;
import org.ethereum.vm.Program.ProgramListener;
import org.spongycastle.util.encoders.DecoderException;
import org.spongycastle.util.encoders.Hex;
import java.awt.Component;
import java.awt.FlowLayout;
public class StateExplorerWindow extends JFrame{
private ToolBar toolBar = null;
private JTextField txfAccountAddress;
private WindowTextArea txaPrinter;
private JButton btnPlayCode;
private AccountsListWindow accountsListWindow;
ProgramPlayDialog codePanel;
private JTable tblStateDataTable;
private StateDataTableModel dataModel;
String[] dataTypes = {"String", "Hex", "Number"};
public StateExplorerWindow(ToolBar toolBar) {
this.toolBar = toolBar;
java.net.URL url = ClassLoader.getSystemResource("ethereum-icon.png");
Toolkit kit = Toolkit.getDefaultToolkit();
Image img = kit.createImage(url);
this.setIconImage(img);
setTitle("State Explorer");
setSize(700, 530);
setLocation(50, 180);
setResizable(false);
/*
* top search panel
*/
JPanel panel = new JPanel();
getContentPane().add(panel);
Box horizontalBox = Box.createHorizontalBox();
panel.add(horizontalBox);
java.net.URL imageURL = ClassLoader.getSystemResource("buttons/list.png");
ImageIcon image = new ImageIcon(imageURL);
JToggleButton btnListAccounts = new JToggleButton("");
btnListAccounts.setIcon(image);
btnListAccounts.setContentAreaFilled(true);
btnListAccounts.setToolTipText("Serpent Editor");
btnListAccounts.setBackground(Color.WHITE);
btnListAccounts.setBorderPainted(false);
btnListAccounts.setFocusPainted(false);
btnListAccounts.setCursor(new Cursor(Cursor.HAND_CURSOR));
btnListAccounts.addItemListener(new ItemListener() {
@Override
public void itemStateChanged(ItemEvent e) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
if(accountsListWindow == null)
accountsListWindow = new AccountsListWindow();
accountsListWindow.setVisible(true);
}
});
}
});
horizontalBox.add(btnListAccounts);
txfAccountAddress = new JTextField();
horizontalBox.add(txfAccountAddress);
txfAccountAddress.setColumns(30);
JButton btnSearch = new JButton("Search");
horizontalBox.add(btnSearch);
btnSearch.addMouseListener(new MouseAdapter(){
@Override
public void mouseClicked(MouseEvent e) {
searchAccount(txfAccountAddress.getText());
}
});
btnPlayCode = new JButton("Play Code");
horizontalBox.add(btnPlayCode);
btnPlayCode.addMouseListener(new MouseAdapter(){
@Override
public void mouseClicked(MouseEvent e) {
byte[] code = WorldManager.getInstance().getRepository().getCode(Hex.decode(txfAccountAddress.getText()));
if(code != null)
ProgramPlayDialog.createAndShowGUI(code, null, null);
}
});
/*
* center text panel
*/
JPanel centerPanel = new JPanel();
panel.add(centerPanel);
txaPrinter = new WindowTextArea();
centerPanel.add(txaPrinter);
/*
* bottom data panel
*/
// data type choice boxes
Box Hbox = Box.createHorizontalBox();
panel.add(Hbox);
Box VBox1 = Box.createVerticalBox();
JLabel l1 = new JLabel("Key Encoding");
l1.setAlignmentX(Component.CENTER_ALIGNMENT);
JComboBox cmbKey = new JComboBox(dataTypes);
cmbKey.setSelectedIndex(1);
cmbKey.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
JComboBox cmb = (JComboBox) e.getSource();
DataEncodingType t = DataEncodingType.getTypeFromString((String) cmb.getSelectedItem());
dataModel.setKeyEncoding(t);
}
});
VBox1.add(l1);
VBox1.add(cmbKey);
Box VBox2 = Box.createVerticalBox();
VBox2.setAlignmentX(LEFT_ALIGNMENT);
JLabel l2 = new JLabel("Value Encoding");
l2.setAlignmentX(Component.CENTER_ALIGNMENT);
JComboBox cmbValue = new JComboBox(dataTypes);
cmbValue.setSelectedIndex(1);
cmbValue.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
JComboBox cmb = (JComboBox) e.getSource();
DataEncodingType t = DataEncodingType.getTypeFromString((String) cmb.getSelectedItem());
dataModel.setValueEncoding(t);
}
});
VBox2.add(l2);
VBox2.add(cmbValue);
Hbox.add(VBox1);
JPanel spacer = new JPanel();
FlowLayout flowLayout = (FlowLayout) spacer.getLayout();
flowLayout.setHgap(100);
Hbox.add(spacer);
Hbox.add(VBox2);
// table
tblStateDataTable = new JTable();
dataModel = new StateDataTableModel();
tblStateDataTable.setModel(dataModel);
JScrollPane scrollPane = new JScrollPane(tblStateDataTable);
scrollPane.setPreferredSize(new Dimension(600,200));
panel.add(scrollPane);
}
private void searchAccount(String accountStr){
txaPrinter.clean();
byte[] add = null;
try { add = Hex.decode(txfAccountAddress.getText()); }
catch(DecoderException ex) { return; }
txaPrinter.println(accountDetailsString(add, dataModel));
}
private String accountDetailsString(byte[] account, StateDataTableModel dataModel){
String ret = "";
// 1) print account address
ret = "Account: " + Hex.toHexString(account) + "\n";
//2) print state
AccountState state = WorldManager.getInstance().getRepository().getAccountState(account);
if(state != null)
ret += state.toString() + "\n";
//3) print storage
ContractDetails contractDetails = WorldManager.getInstance().getRepository().getContractDetails(account);
if(contractDetails != null) {
Map<DataWord, DataWord> accountStorage = contractDetails.getStorage();
dataModel.setData(accountStorage);
}
//4) code print
byte[] code = WorldManager.getInstance().getRepository().getCode(account);
if(code != null) {
ret += "\n\nCode:\n";
ret += Program.stringify(code, 0, "");
}
return ret;
}
private class StateDataTableModel extends AbstractTableModel {
Map<DataWord, DataWord> data;
DataEncodingType keyEncodingType = DataEncodingType.HEX;
DataEncodingType valueEncodingType = DataEncodingType.HEX;
String[] columns = new String[]{ "Key", "Value"};
public StateDataTableModel() { }
public StateDataTableModel(Map<DataWord, DataWord> initData) {
setData(initData);
}
public void setData(Map<DataWord, DataWord> initData) {
data = initData;
fireTableDataChanged();
}
public void setKeyEncoding(DataEncodingType type) {
keyEncodingType = type;
fireTableDataChanged();
}
public void setValueEncoding(DataEncodingType type) {
valueEncodingType = type;
fireTableDataChanged();
}
@Override
public String getColumnName(int column) {
return columns[column];
}
@Override
public int getRowCount() {
return data == null? 0:data.size();
}
@Override
public int getColumnCount() {
return columns.length;
}
@Override
public Object getValueAt(int rowIndex, int columnIndex) {
DataWord key = (DataWord) this.data.keySet().toArray()[rowIndex];
if(columnIndex == 0) {
return getDataWithEncoding(key.getData(), keyEncodingType);
}
else {
DataWord value = this.data.get(key);
return getDataWithEncoding(value.getData(), valueEncodingType);
}
}
private String getDataWithEncoding(byte[] data, DataEncodingType enc) {
switch(enc) {
case STRING:
return new String(data);
case HEX:
return Hex.toHexString(data);
case NUMBER:
return new BigInteger(data).toString();
}
return data.toString();
}
}
private enum DataEncodingType{
STRING,
HEX,
NUMBER;
static public DataEncodingType getTypeFromString(String value) {
switch(value){
case "String":
return STRING;
case "Hex":
return HEX;
case "Number":
return NUMBER;
}
return STRING;
}
}
private class WindowTextArea extends TextArea {
public WindowTextArea() {
super();
}
public void println(String txt) {
setText(getText() + txt + "\n");
}
public void clean() {
setText("");
}
}
public static void main(String[] args) {
// Start all Swing applications on the EDT.
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new StateExplorerWindow(null).setVisible(true);
}
});
}
}

View File

@ -28,12 +28,14 @@ public class ToolBar extends JFrame {
private BlockChainTable blockchainWindow = null;
private WalletWindow walletWindow = null;
private SerpentEditor serpentEditor = null;
private StateExplorerWindow stateExplorerWindow = null;
JToggleButton editorToggle;
JToggleButton logToggle;
JToggleButton peersToggle;
JToggleButton chainToggle;
JToggleButton walletToggle;
JToggleButton stateExplorer;
public ToolBar() throws HeadlessException {
@ -95,6 +97,9 @@ public class ToolBar extends JFrame {
java.net.URL imageURL_5 = ClassLoader.getSystemResource("buttons/wallet.png");
ImageIcon image_5 = new ImageIcon(imageURL_5);
java.net.URL imageURL_6 = ClassLoader.getSystemResource("buttons/stateExplorer.png");
ImageIcon image_6 = new ImageIcon(imageURL_6);
editorToggle = new JToggleButton("");
editorToggle.setIcon(image_1);
editorToggle.setContentAreaFilled(true);
@ -223,11 +228,40 @@ public class ToolBar extends JFrame {
}
);
stateExplorer = new JToggleButton();
stateExplorer.setIcon(image_6);
stateExplorer.setToolTipText("State Explorer");
stateExplorer.setContentAreaFilled(true);
stateExplorer.setBackground(Color.WHITE);
stateExplorer.setBorderPainted(false);
stateExplorer.setFocusPainted(false);
stateExplorer.setCursor(new Cursor(Cursor.HAND_CURSOR));
stateExplorer.addItemListener(
new ItemListener() {
@Override
public void itemStateChanged(ItemEvent e) {
if (e.getStateChange() == ItemEvent.SELECTED) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
if (stateExplorerWindow == null)
stateExplorerWindow = new StateExplorerWindow(ToolBar.this);
stateExplorerWindow.setVisible(true);
}
});
} else if (e.getStateChange() == ItemEvent.DESELECTED) {
stateExplorerWindow.setVisible(false);
}
}
}
);
cp.add(editorToggle);
cp.add(logToggle);
cp.add(peersToggle);
cp.add(chainToggle);
cp.add(walletToggle);
cp.add(stateExplorer);
Ethereum ethereum = UIEthereumManager.ethereum;

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 927 B