Skip to content

Commit

Permalink
Merge pull request #10 from EspressoSystems/hotshot-txns
Browse files Browse the repository at this point in the history
Fetch txns from hotshot
  • Loading branch information
nomaxg authored Nov 7, 2023
2 parents dd6345f + da65e98 commit 5fbdb1d
Show file tree
Hide file tree
Showing 13 changed files with 1,822 additions and 17 deletions.
148 changes: 148 additions & 0 deletions espresso/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package espresso

import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"

"github.com/ethereum/go-ethereum/log"
)

type Client struct {
baseUrl string
client *http.Client
log log.Logger
}

func NewClient(log log.Logger, url string) *Client {
if !strings.HasSuffix(url, "/") {
url += "/"
}
return &Client{
baseUrl: url,
client: http.DefaultClient,
log: log,
}
}

func (c *Client) FetchHeadersForWindow(ctx context.Context, start uint64, end uint64) (WindowStart, error) {
var res WindowStart
if err := c.get(ctx, &res, "availability/headers/window/%d/%d", start, end); err != nil {
return WindowStart{}, err
}
return res, nil
}

func (c *Client) FetchRemainingHeadersForWindow(ctx context.Context, from uint64, end uint64) (WindowMore, error) {
var res WindowMore
if err := c.get(ctx, &res, "availability/headers/window/from/%d/%d", from, end); err != nil {
return WindowMore{}, err
}
return res, nil
}

func (c *Client) FetchHeader(ctx context.Context, blockHeight uint64) (Header, error) {
var res Header
if err := c.get(ctx, &res, "availability/header/%d", blockHeight); err != nil {
return Header{}, err
}
return res, nil
}

func (c *Client) FetchTransactionsInBlock(ctx context.Context, block uint64, header *Header, namespace uint64) (TransactionsInBlock, error) {
var res NamespaceResponse
if err := c.get(ctx, &res, "availability/block/%d/namespace/%d", block, namespace); err != nil {
return TransactionsInBlock{}, err
}
return res.Validate(header, namespace)
}

type NamespaceResponse struct {
Proof *json.RawMessage `json:"proof"`
Transactions *[]Transaction `json:"transactions"`
}

// Validate a NamespaceResponse and extract the transactions.
// NMT proof validation is currently stubbed out.
func (res *NamespaceResponse) Validate(header *Header, namespace uint64) (TransactionsInBlock, error) {
if res.Proof == nil {
return TransactionsInBlock{}, fmt.Errorf("field proof of type NamespaceResponse is required")
}
if res.Transactions == nil {
return TransactionsInBlock{}, fmt.Errorf("field transactions of type NamespaceResponse is required")
}

// Check that these transactions are only and all of the transactions from `namespace` in the
// block with `header`.
// TODO this is a hack. We should use the proof from the response (`proof := NmtProof{}`).
// However, due to a simplification in the Espresso NMT implementation, where left and right
// boundary transactions not belonging to this namespace are included in the proof in their
// entirety, this proof can be quite large, even if this rollup has no large transactions in its
// own namespace. In production, we have run into issues where huge transactions from other
// rollups cause this proof to be so large, that the resulting PayloadAttributes exceeds the
// maximum size allowed for an HTTP request by OP geth. Since NMT proof validation is currently
// mocked anyways, we can subvert this issue in the short term without making the rollup any
// less secure than it already is simply by using an empty proof.
proof := NmtProof{}
if err := proof.Validate(header.TransactionsRoot, *res.Transactions); err != nil {
return TransactionsInBlock{}, err
}

// Extract the transactions.
var txs []Bytes
for i, tx := range *res.Transactions {
if tx.Vm != namespace {
return TransactionsInBlock{}, fmt.Errorf("transaction %d has wrong namespace (%d, expected %d)", i, tx.Vm, namespace)
}
txs = append(txs, tx.Payload)
}

return TransactionsInBlock{
Transactions: txs,
Proof: proof,
}, nil
}

func (c *Client) get(ctx context.Context, out any, format string, args ...any) error {
url := c.baseUrl + fmt.Sprintf(format, args...)

c.log.Debug("get", "url", url)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
c.log.Error("failed to build request", "err", err, "url", url)
return err
}
res, err := c.client.Do(req)
if err != nil {
c.log.Error("error in request", "err", err, "url", url)
return err
}
defer res.Body.Close()

if res.StatusCode != 200 {
// Try to get the response body to include in the error message, as it may have useful
// information about why the request failed. If this call fails, the response will be `nil`,
// which is fine to include in the log, so we can ignore errors.
body, _ := io.ReadAll(res.Body)
c.log.Error("request failed", "err", err, "url", url, "status", res.StatusCode, "response", string(body))
return fmt.Errorf("request failed with status %d", res.StatusCode)
}

// Read the response body into memory before we unmarshal it, rather than passing the io.Reader
// to the json decoder, so that we still have the body and can inspect it if unmarshalling
// failed.
body, err := io.ReadAll(res.Body)
if err != nil {
c.log.Error("failed to read response body", "err", err, "url", url)
return err
}
if err := json.Unmarshal(body, out); err != nil {
c.log.Error("failed to parse body as json", "err", err, "url", url, "response", string(body))
return err
}
c.log.Debug("request completed successfully", "url", url, "res", res, "body", string(body), "out", out)
return nil
}
182 changes: 182 additions & 0 deletions espresso/commit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package espresso

import (
"bytes"
"encoding/binary"
"fmt"
"io"
"unicode/utf8"

"github.com/ethereum/go-ethereum/crypto"
)

type Commitment [32]byte

func CommitmentFromUint256(n *U256) (Commitment, error) {
var bytes [32]byte

bigEndian := n.Bytes()
if len(bigEndian) > 32 {
return Commitment{}, fmt.Errorf("integer out of range for U256 (%d)", n)
}

// `n` might have fewer than 32 bytes, if the commitment starts with one or more zeros. Pad out
// to 32 bytes exactly, adding zeros at the beginning to be consistent with big-endian byte
// order.
if len(bigEndian) < 32 {
zeros := make([]byte, 32-len(bigEndian))
bigEndian = append(zeros, bigEndian...)
}

for i, b := range bigEndian {
// Bytes() returns the bytes in big endian order, but HotShot encodes commitments as
// U256 in little endian order, so we populate the bytes in reverse order.
bytes[31-i] = b
}
return bytes, nil
}

func (c Commitment) Uint256() *U256 {
var bigEndian [32]byte
for i, b := range c {
// HotShot interprets the commitment as a little-endian integer. `SetBytes` takes the bytes
// in big-endian order, so we populate the bytes in reverse order.
bigEndian[31-i] = b
}
return NewU256().SetBytes(bigEndian)
}

func (c Commitment) Equals(other Commitment) bool {
return bytes.Equal(c[:], other[:])
}

type RawCommitmentBuilder struct {
hasher crypto.KeccakState
}

func NewRawCommitmentBuilder(name string) *RawCommitmentBuilder {
b := new(RawCommitmentBuilder)
b.hasher = crypto.NewKeccakState()
return b.ConstantString(name)
}

// Append a constant string to the running hash.
//
// WARNING: The string `s` must be a constant. This function does not encode the length of `s` in
// the hash, which can lead to domain collisions when different strings with different lengths are
// used depending on the input object.
func (b *RawCommitmentBuilder) ConstantString(s string) *RawCommitmentBuilder {
// The commitment scheme is only designed to work with UTF-8 strings. In the reference
// implementation, written in Rust, all strings are UTF-8, but in Go we have to check.
if !utf8.Valid([]byte(s)) {
panic(fmt.Sprintf("ConstantString must only be called with valid UTF-8 strings: %v", s))
}

if _, err := io.WriteString(b.hasher, s); err != nil {
panic(fmt.Sprintf("KeccakState Writer is not supposed to fail, but it did: %v", err))
}

// To denote the end of the string and act as a domain separator, include a byte sequence which
// can never appear in a valid UTF-8 string.
invalidUtf8 := []byte{0xC0, 0x7F}
return b.FixedSizeBytes(invalidUtf8)
}

// Include a named field of another committable type.
func (b *RawCommitmentBuilder) Field(f string, c Commitment) *RawCommitmentBuilder {
return b.ConstantString(f).FixedSizeBytes(c[:])
}

func (b *RawCommitmentBuilder) OptionalField(f string, c *Commitment) *RawCommitmentBuilder {
b.ConstantString(f)

// Encode a 0 or 1 to separate the nil domain from the non-nil domain.
if c == nil {
b.Uint64(0)
} else {
b.Uint64(1)
b.FixedSizeBytes((*c)[:])
}

return b
}

// Include a named field of type `uint256` in the hash.
func (b *RawCommitmentBuilder) Uint256Field(f string, n *U256) *RawCommitmentBuilder {
return b.ConstantString(f).Uint256(n)
}

// Include a value of type `uint256` in the hash.
func (b *RawCommitmentBuilder) Uint256(n *U256) *RawCommitmentBuilder {
bytes := make([]byte, 32)
n.FillBytes(bytes)

// `FillBytes` uses big endian byte ordering, but the Espresso commitment scheme uses little
// endian, so we need to reverse the bytes.
for i, j := 0, len(bytes)-1; i < j; i, j = i+1, j-1 {
bytes[i], bytes[j] = bytes[j], bytes[i]
}

return b.FixedSizeBytes(bytes)
}

// Include a named field of type `uint64` in the hash.
func (b *RawCommitmentBuilder) Uint64Field(f string, n uint64) *RawCommitmentBuilder {
return b.ConstantString(f).Uint64(n)
}

// Include a value of type `uint64` in the hash.
func (b *RawCommitmentBuilder) Uint64(n uint64) *RawCommitmentBuilder {
bytes := make([]byte, 8)
binary.LittleEndian.PutUint64(bytes, n)
return b.FixedSizeBytes(bytes)
}

// Include a named field of fixed length in the hash.
//
// WARNING: Go's type system cannot express the requirement that `bytes` is a fixed size array of
// any size. The best we can do is take a dynamically sized slice. However, this function uses a
// fixed-size encoding; namely, it does not encode the length of `bytes` in the hash, which can lead
// to domain collisions when this function is called with a slice which can have different lengths
// depending on the input object.
//
// The caller must ensure that this function is only used with slices whose length is statically
// determined by the type being committed to.
func (b *RawCommitmentBuilder) FixedSizeField(f string, bytes Bytes) *RawCommitmentBuilder {
return b.ConstantString(f).FixedSizeBytes(bytes)
}

// Append a fixed size byte array to the running hash.
//
// WARNING: Go's type system cannot express the requirement that `bytes` is a fixed size array of
// any size. The best we can do is take a dynamically sized slice. However, this function uses a
// fixed-size encoding; namely, it does not encode the length of `bytes` in the hash, which can lead
// to domain collisions when this function is called with a slice which can have different lengths
// depending on the input object.
//
// The caller must ensure that this function is only used with slices whose length is statically
// determined by the type being committed to.
func (b *RawCommitmentBuilder) FixedSizeBytes(bytes Bytes) *RawCommitmentBuilder {
b.hasher.Write(bytes)
return b
}

// Include a named field of dynamic length in the hash.
func (b *RawCommitmentBuilder) VarSizeField(f string, bytes Bytes) *RawCommitmentBuilder {
return b.ConstantString(f).VarSizeBytes(bytes)
}

// Include a byte array whose length can be dynamic to the running hash.
func (b *RawCommitmentBuilder) VarSizeBytes(bytes Bytes) *RawCommitmentBuilder {
// First commit to the length, to prevent length extension and domain collision attacks.
b.Uint64(uint64(len(bytes)))
b.hasher.Write(bytes)
return b
}

func (b *RawCommitmentBuilder) Finalize() Commitment {
var comm Commitment
bytes := b.hasher.Sum(nil)
copy(comm[:], bytes)
return comm
}
Loading

0 comments on commit 5fbdb1d

Please sign in to comment.