forked from OffchainLabs/nitro
-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #10 from EspressoSystems/hotshot-txns
Fetch txns from hotshot
- Loading branch information
Showing
13 changed files
with
1,822 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.