Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fetch txns from hotshot #10

Merged
merged 7 commits into from
Nov 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
sveitser marked this conversation as resolved.
Show resolved Hide resolved

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
Loading