diff --git a/.github/workflows/seq-e2e-tests.yml b/.github/workflows/seq-e2e-tests.yml index 4d581e6..a8907c3 100644 --- a/.github/workflows/seq-e2e-tests.yml +++ b/.github/workflows/seq-e2e-tests.yml @@ -3,7 +3,7 @@ name: SEQ e2e tests on: push: branches: - - '*' + - main pull_request: types: [labeled,synchronize,reopened] diff --git a/actions/consts.go b/actions/consts.go index 42b867f..672c310 100644 --- a/actions/consts.go +++ b/actions/consts.go @@ -5,9 +5,11 @@ package actions // Note: Registry will error during initialization if a duplicate ID is assigned. We explicitly assign IDs to avoid accidental remapping. const ( - TransferID uint8 = 0 - MsgID uint8 = 1 - AuctionID uint8 = 2 + TransferID uint8 = 0 + MsgID uint8 = 1 + AuctionID uint8 = 2 + DACertID uint8 = 3 + SetSettledToBNonceID uint8 = 4 ) const ( @@ -16,7 +18,8 @@ const ( EpochExitComputeUnits = 10 AuctionComputeUnits = 4 - MsgComputeUnits = 15 + MsgComputeUnits = 15 + DACertComputeUnits = 1 MaxSymbolSize = 8 MaxMemoSize = 256 diff --git a/actions/da_cert.go b/actions/da_cert.go new file mode 100644 index 0000000..0cdfa07 --- /dev/null +++ b/actions/da_cert.go @@ -0,0 +1,121 @@ +package actions + +import ( + "context" + "errors" + "fmt" + + "github.com/AnomalyFi/hypersdk/chain" + "github.com/AnomalyFi/hypersdk/codec" + "github.com/AnomalyFi/hypersdk/consts" + "github.com/AnomalyFi/hypersdk/state" + "github.com/AnomalyFi/nodekit-seq/storage" + "github.com/AnomalyFi/nodekit-seq/types" + "github.com/ava-labs/avalanchego/ids" +) + +const ( + CertLimit = 2048 // 2KB +) + +const ( + CelestiaDA = iota +) + +var _ chain.Action = (*DACertificate)(nil) + +type DACertificate struct { + Cert *types.DACertInfo `json:"cert"` +} + +func (*DACertificate) GetTypeID() uint8 { + return DACertID +} + +func (cert *DACertificate) StateKeys(_ codec.Address, _ ids.ID) state.Keys { + return state.Keys{ + string(storage.DACertToBNonceKey()): state.All, + string(storage.ToBNonceEpochKey(cert.Cert.Epoch)): state.All, + string(storage.DACertChunkIDKey(cert.Cert.ChainID, cert.Cert.BlockNumber)): state.All, + string(storage.DACertIndexKey(cert.Cert.ToBNonce)): state.All, + string(storage.DACertByChunkIDKey(cert.Cert.ChunkID)): state.All, + } +} + +func (*DACertificate) StateKeysMaxChunks() []uint16 { + return []uint16{ + storage.DACertificateChunks, storage.DACertificateChunks, + } +} + +func (cert *DACertificate) Execute( + ctx context.Context, + rules chain.Rules, + mu state.Mutable, + _ int64, + _ uint64, + actor codec.Address, + _ ids.ID, +) ([][]byte, error) { + if !isWhiteListed(rules, actor) { + return nil, ErrNotWhiteListed + } + + // TODO: clear cert layer may be needed but should be no influence as in rare case Arcadia may go down + // and we need to clear everything after the highestSettledToBNonce + // add current chunk to chunk layer at ToBNonce + err := storage.AddDACertToLayer(ctx, mu, cert.Cert.ToBNonce, cert.Cert.ChunkID) + if err != nil && !errors.Is(err, storage.ErrCertExists) { // allow override + return nil, fmt.Errorf("failed to add cert chunk layer: %w", err) + } + + // store the cert and the index + if err := storage.SetDACertChunkID(ctx, mu, cert.Cert.ChainID, cert.Cert.BlockNumber, cert.Cert.ChunkID); err != nil { + return nil, fmt.Errorf("failed to set cert chunk id: %w", err) + } + + if err := storage.SetDACert(ctx, mu, cert.Cert.ChunkID, cert.Cert); err != nil { + return nil, fmt.Errorf("failed to save cert: %w", err) + } + // set the lowest ToBNonce for the epoch + if err := storage.SetToBNonceAtEpoch(ctx, mu, cert.Cert.Epoch, cert.Cert.ToBNonce); err != nil { + return nil, fmt.Errorf("failed to set tobnonce of epoch: %w", err) + } + + return nil, nil +} + +func (*DACertificate) ComputeUnits(codec.Address, chain.Rules) uint64 { + return DACertComputeUnits +} + +func (cert *DACertificate) Size() int { + return consts.ByteLen + cert.Cert.Size() +} + +func (cert *DACertificate) Marshal(p *codec.Packer) { + cert.Cert.Marshal(p) +} + +func UnmarshalDACertificate(p *codec.Packer) (chain.Action, error) { + var cert DACertificate + certInfo, err := types.UnmarshalCertInfo(p) + if err != nil { + return nil, err + } + cert.Cert = certInfo + return &cert, p.Err() +} + +func (*DACertificate) ValidRange(chain.Rules) (int64, int64) { + // Returning -1, -1 means that the action is always valid. + return -1, -1 +} + +func (*DACertificate) NMTNamespace() []byte { + return DefaultNMTNamespace +} + +func (*DACertificate) UseFeeMarket() bool { + return false +} diff --git a/actions/set_settled_tobnonce.go b/actions/set_settled_tobnonce.go new file mode 100644 index 0000000..e4d921f --- /dev/null +++ b/actions/set_settled_tobnonce.go @@ -0,0 +1,100 @@ +package actions + +import ( + "context" + "fmt" + + "github.com/AnomalyFi/hypersdk/chain" + "github.com/AnomalyFi/hypersdk/codec" + "github.com/AnomalyFi/hypersdk/consts" + "github.com/AnomalyFi/hypersdk/state" + "github.com/AnomalyFi/nodekit-seq/storage" + "github.com/ava-labs/avalanchego/ids" +) + +var _ chain.Action = (*SetSettledToBNonce)(nil) + +type SetSettledToBNonce struct { + ToBNonce uint64 `json:"tobNonce"` + Reset bool `json:"reset"` +} + +func (*SetSettledToBNonce) GetTypeID() uint8 { + return SetSettledToBNonceID +} + +func (*SetSettledToBNonce) StateKeys(_ codec.Address, _ ids.ID) state.Keys { + return state.Keys{ + string(storage.DACertToBNonceKey()): state.All, + } +} + +func (*SetSettledToBNonce) StateKeysMaxChunks() []uint16 { + return []uint16{ + storage.DACertficateToBNonceChunks, storage.DACertficateToBNonceChunks, + } +} + +func (sst *SetSettledToBNonce) Execute( + ctx context.Context, + rules chain.Rules, + mu state.Mutable, + _ int64, + _ uint64, + actor codec.Address, + _ ids.ID, +) ([][]byte, error) { + if !isWhiteListed(rules, actor) { + return nil, ErrNotWhiteListed + } + + // store highest ToBNonce if there's new one + tobNonce, err := storage.GetDACertToBNonce(ctx, mu) + if err != nil { + return nil, fmt.Errorf("failed to get tob nonce: %w", err) + } + if !sst.Reset && sst.ToBNonce <= tobNonce { + // no-op + return nil, nil + } + + // set the new tob nonce + if err := storage.SetDACertToBNonce(ctx, mu, sst.ToBNonce); err != nil { + return nil, fmt.Errorf("failed to store tob nonce: %w", err) + } + + return nil, nil +} + +func (*SetSettledToBNonce) ComputeUnits(codec.Address, chain.Rules) uint64 { + return DACertComputeUnits +} + +func (*SetSettledToBNonce) Size() int { + return consts.Uint64Len + consts.BoolLen +} + +func (sst *SetSettledToBNonce) Marshal(p *codec.Packer) { + p.PackUint64(sst.ToBNonce) + p.PackBool(sst.Reset) +} + +func UnmarshalSetSettledToBNonce(p *codec.Packer) (chain.Action, error) { + var sst SetSettledToBNonce + sst.ToBNonce = p.UnpackUint64(false) + sst.Reset = p.UnpackBool() + return &sst, p.Err() +} + +func (*SetSettledToBNonce) ValidRange(chain.Rules) (int64, int64) { + // Returning -1, -1 means that the action is always valid. + return -1, -1 +} + +func (*SetSettledToBNonce) NMTNamespace() []byte { + return DefaultNMTNamespace +} + +func (*SetSettledToBNonce) UseFeeMarket() bool { + return false +} diff --git a/cmd/seq-cli/cmd/action.go b/cmd/seq-cli/cmd/action.go index 1c3b6f0..e9b157d 100644 --- a/cmd/seq-cli/cmd/action.go +++ b/cmd/seq-cli/cmd/action.go @@ -104,7 +104,7 @@ var sequencerMsgCmd = &cobra.Command{ }, } -var rollupCmd = &cobra.Command{ +var rollupRegisterCmd = &cobra.Command{ Use: "rollup-register", RunE: func(*cobra.Command, []string) error { ctx := context.Background() @@ -219,3 +219,23 @@ var updateArcadiaURL = &cobra.Command{ return valCLI.UpdateArcadiaURL(str) }, } + +var resetToBNonce = &cobra.Command{ + Use: "reset-tobnonce", + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := cmd.Context() + _, _, factory, cli, scli, tcli, err := handler.DefaultActor() + if err != nil { + return err + } + + _, err = sendAndWait(ctx, []chain.Action{ + &actions.SetSettledToBNonce{ + ToBNonce: 0, + Reset: true, + }, + }, cli, scli, tcli, factory, priorityFee, true) + + return err + }, +} diff --git a/cmd/seq-cli/cmd/rollup.go b/cmd/seq-cli/cmd/rollup.go new file mode 100644 index 0000000..61cfcd7 --- /dev/null +++ b/cmd/seq-cli/cmd/rollup.go @@ -0,0 +1,64 @@ +package cmd + +import ( + "fmt" + "strconv" + + "github.com/spf13/cobra" +) + +var rollupCmd = &cobra.Command{ + Use: "rollupCmd", + RunE: func(*cobra.Command, []string) error { + return ErrMissingSubcommand + }, +} + +var getCertCmd = &cobra.Command{ + Use: "get-cert [chainID] [height]", + PreRunE: func(cmd *cobra.Command, args []string) error { + if len(args) != 2 { + return ErrInvalidArgs + } + + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + _, _, _, _, _, tcli, err := handler.DefaultActor() + if err != nil { + return err + } + + chainID := args[0] + blockNumber, err := strconv.Atoi(args[1]) + if err != nil { + return err + } + + cert, err := tcli.GetCertByChainInfo(ctx, chainID, uint64(blockNumber)) + if err != nil { + return err + } + fmt.Printf("cert info: %+v\n", cert) + return nil + }, +} + +var getToBNonceCmd = &cobra.Command{ + Use: "get-nonce", + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + _, _, _, _, _, tcli, err := handler.DefaultActor() + if err != nil { + return err + } + + cert, err := tcli.GetHighestSettledToBNonce(ctx) + if err != nil { + return err + } + fmt.Printf("cert info: %+v\n", cert) + return nil + }, +} diff --git a/cmd/seq-cli/cmd/root.go b/cmd/seq-cli/cmd/root.go index bbe81f7..fd65f54 100644 --- a/cmd/seq-cli/cmd/root.go +++ b/cmd/seq-cli/cmd/root.go @@ -56,6 +56,7 @@ func init() { actionCmd, spamCmd, prometheusCmd, + rollupCmd, ) rootCmd.PersistentFlags().StringVar( &dbPath, @@ -161,9 +162,10 @@ func init() { actionCmd.AddCommand( transferCmd, sequencerMsgCmd, - rollupCmd, + rollupRegisterCmd, auctionCmd, updateArcadiaURL, + resetToBNonce, ) // spam @@ -223,6 +225,12 @@ func init() { prometheusCmd.AddCommand( generatePrometheusCmd, ) + + // rollup + rollupCmd.AddCommand( + getCertCmd, + getToBNonceCmd, + ) } func Execute() error { diff --git a/config/config.go b/config/config.go index d784966..b6f853c 100644 --- a/config/config.go +++ b/config/config.go @@ -195,9 +195,10 @@ func (c *Config) Loaded() bool { return c.loaded } func (c *Config) GetETHL1RPC() string { return c.ETHRPCAddr } func (c *Config) GetETHL1WS() string { return c.ETHWSAddr } func (c *Config) GetArcadiaURL() string { return c.ArcadiaURL } -func (*Config) GetValServerConfig() *rpc.JSONRPCValServerConfig { +func (c *Config) GetValServerConfig() *rpc.JSONRPCValServerConfig { return &rpc.JSONRPCValServerConfig{ - DerivePort: true, + DerivePort: c.ValRPCConfig.DerivePort, + Port: c.ValRPCConfig.Port, } } diff --git a/controller/resolutions.go b/controller/resolutions.go index ab3ed72..5f315d9 100644 --- a/controller/resolutions.go +++ b/controller/resolutions.go @@ -5,6 +5,7 @@ package controller import ( "context" + "fmt" hactions "github.com/AnomalyFi/hypersdk/actions" "github.com/AnomalyFi/hypersdk/codec" @@ -14,6 +15,7 @@ import ( "github.com/AnomalyFi/nodekit-seq/genesis" rollupregistry "github.com/AnomalyFi/nodekit-seq/rollup_registry" "github.com/AnomalyFi/nodekit-seq/storage" + "github.com/AnomalyFi/nodekit-seq/types" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/trace" "github.com/ava-labs/avalanchego/utils/logging" @@ -95,3 +97,43 @@ func (c *Controller) NetworkID() uint32 { func (c *Controller) ChainID() ids.ID { return c.snowCtx.ChainID } + +func (c *Controller) GetHighestSettledToBNonceFromState(ctx context.Context) (uint64, error) { + return storage.GetDAToBNonceFromState(ctx, c.inner.ReadState) +} + +func (c *Controller) GetCertByChunkIDFromState(ctx context.Context, chunkID ids.ID) (*types.DACertInfo, error) { + return storage.GetDACertByChunkIDFromState(ctx, c.inner.ReadState, chunkID) +} + +func (c *Controller) GetCertByChainInfoFromState(ctx context.Context, chainID string, blockNumber uint64) (*types.DACertInfo, error) { + chunkID, err := storage.GetDACertChunkIDFromState(ctx, c.inner.ReadState, chainID, blockNumber) + if err != nil { + return nil, err + } + if chunkID == ids.Empty { + return nil, nil + } + return storage.GetDACertByChunkIDFromState(ctx, c.inner.ReadState, chunkID) +} + +func (c *Controller) GetCertsByToBNonceFromState(ctx context.Context, tobNonce uint64) ([]*types.DACertInfo, error) { + chunkIDs, err := storage.GetDACertChunkIDsFromState(ctx, c.inner.ReadState, tobNonce) + if err != nil { + return nil, fmt.Errorf("cannot get chunkIDs from state: %w", err) + } + + ret := make([]*types.DACertInfo, 0, len(chunkIDs)) + for i := 0; i < len(chunkIDs); i++ { + cert, err := storage.GetDACertByChunkIDFromState(ctx, c.inner.ReadState, chunkIDs[i]) + if err != nil { + return nil, fmt.Errorf("cannot get cert by chunkID: %s: %w", chunkIDs[i].String(), err) + } + ret = append(ret, cert) + } + return ret, nil +} + +func (c *Controller) GetLowestToBNonceAtEpochFromState(ctx context.Context, epoch uint64) (uint64, error) { + return storage.GetToBNonceAtEpochFromState(ctx, c.inner.ReadState, epoch) +} diff --git a/go.mod b/go.mod index f1a045c..c06a5cf 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/AnomalyFi/nodekit-seq go 1.21.12 require ( - github.com/AnomalyFi/hypersdk v0.9.7-arcadia.16 + github.com/AnomalyFi/hypersdk v0.9.7-arcadia.22 github.com/ava-labs/avalanche-network-runner v1.7.4-rc.0 github.com/ava-labs/avalanchego v1.11.10 github.com/ethereum/go-ethereum v1.13.14 diff --git a/go.sum b/go.sum index 81db740..82aba48 100644 --- a/go.sum +++ b/go.sum @@ -39,8 +39,8 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7 filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek= filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= -github.com/AnomalyFi/hypersdk v0.9.7-arcadia.16 h1:bwkpHWSs/fzelJp97iU+npoS5KAGcz48EloGSD23Jdc= -github.com/AnomalyFi/hypersdk v0.9.7-arcadia.16/go.mod h1:0Vj2PdwSFN7pat4Sno39IfmtOiv/gO9mxZXyRKnoKtI= +github.com/AnomalyFi/hypersdk v0.9.7-arcadia.22 h1:8C7R6sy6oZZratzS7oouvoHNpV/Vjok1NLGPEVp56Sg= +github.com/AnomalyFi/hypersdk v0.9.7-arcadia.22/go.mod h1:0Vj2PdwSFN7pat4Sno39IfmtOiv/gO9mxZXyRKnoKtI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= diff --git a/registry/registry.go b/registry/registry.go index fb94274..873adc1 100644 --- a/registry/registry.go +++ b/registry/registry.go @@ -26,6 +26,8 @@ func init() { consts.ActionRegistry.Register((&actions.RollupRegistration{}).GetTypeID(), actions.UnmarshalRollupRegister, false), consts.ActionRegistry.Register((&actions.EpochExit{}).GetTypeID(), actions.UnmarshalEpochExit, false), consts.ActionRegistry.Register((&actions.Auction{}).GetTypeID(), actions.UnmarshalAuction, false), + consts.ActionRegistry.Register((&actions.DACertificate{}).GetTypeID(), actions.UnmarshalDACertificate, false), + consts.ActionRegistry.Register((&actions.SetSettledToBNonce{}).GetTypeID(), actions.UnmarshalSetSettledToBNonce, false), // When registering new auth, ALWAYS make sure to append at the end. consts.AuthRegistry.Register((&auth.ED25519{}).GetTypeID(), auth.UnmarshalED25519, false), consts.AuthRegistry.Register((&auth.BLS{}).GetTypeID(), auth.UnmarshalBLS, false), diff --git a/rpc/dependencies.go b/rpc/dependencies.go index 48f657f..1dbc302 100644 --- a/rpc/dependencies.go +++ b/rpc/dependencies.go @@ -13,6 +13,7 @@ import ( "github.com/AnomalyFi/nodekit-seq/archiver" "github.com/AnomalyFi/nodekit-seq/genesis" rollupregistry "github.com/AnomalyFi/nodekit-seq/rollup_registry" + "github.com/AnomalyFi/nodekit-seq/types" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/trace" "github.com/ava-labs/avalanchego/utils/logging" @@ -41,4 +42,9 @@ type Controller interface { // TODO: update this to only have read permission Archiver() *archiver.ORMArchiver RollupRegistry() *rollupregistry.RollupRegistry + GetHighestSettledToBNonceFromState(ctx context.Context) (uint64, error) + GetCertByChunkIDFromState(ctx context.Context, chunkID ids.ID) (*types.DACertInfo, error) + GetCertByChainInfoFromState(ctx context.Context, chainID string, blockNumber uint64) (*types.DACertInfo, error) + GetCertsByToBNonceFromState(ctx context.Context, tobNonce uint64) ([]*types.DACertInfo, error) + GetLowestToBNonceAtEpochFromState(ctx context.Context, epoch uint64) (uint64, error) } diff --git a/rpc/jsonrpc_client.go b/rpc/jsonrpc_client.go index 13d3f73..daeb2ce 100644 --- a/rpc/jsonrpc_client.go +++ b/rpc/jsonrpc_client.go @@ -335,6 +335,74 @@ func (cli *JSONRPCClient) GetBuilder(ctx context.Context, epoch uint64) (*[]byte return &resp.BuilderPubKey, err } +func (cli *JSONRPCClient) GetHighestSettledToBNonce(ctx context.Context) (uint64, error) { + resp := new(types.GetHighestSettledToBNonceReply) + err := cli.requester.SendRequest( + ctx, + "getHighestSettledToBNonce", + nil, + resp, + ) + return resp.ToBNonce, err +} + +func (cli *JSONRPCClient) GetCertByChunkID(ctx context.Context, chunkID ids.ID) (*types.DACertInfo, error) { + resp := new(types.GetCertByChunkIDReply) + args := &types.GetCertByChunkIDArgs{ + ChunkID: chunkID, + } + err := cli.requester.SendRequest( + ctx, + "getCertByChunkID", + args, + resp, + ) + return resp.Cert, err +} + +func (cli *JSONRPCClient) GetCertByChainInfo(ctx context.Context, chainID string, blockNumber uint64) (*types.DACertInfo, error) { + resp := new(types.GetCertByChainInfoReply) + args := &types.GetCertByChainInfoArgs{ + ChainID: chainID, + BlockNumber: blockNumber, + } + err := cli.requester.SendRequest( + ctx, + "getCertByChainInfo", + args, + resp, + ) + return resp.Cert, err +} + +func (cli *JSONRPCClient) GetCertsByToBNonce(ctx context.Context, tobNonce uint64) ([]*types.DACertInfo, error) { + resp := new(types.GetCertsByToBNonceReply) + args := &types.GetCertsByToBNonceArgs{ + ToBNonce: tobNonce, + } + err := cli.requester.SendRequest( + ctx, + "getCertsByToBNonce", + args, + resp, + ) + return resp.Certs, err +} + +func (cli *JSONRPCClient) GetEpochLowestToBNonce(ctx context.Context, epoch uint64) (uint64, error) { + resp := new(types.GetLowestToBNonceAtEpochReply) + args := &types.GetLowestToBNonceAtEpochArgs{ + Epoch: epoch, + } + err := cli.requester.SendRequest( + ctx, + "getEpochLowestToBNonce", + args, + resp, + ) + return resp.ToBNonce, err +} + var _ chain.Parser = (*Parser)(nil) type Parser struct { diff --git a/rpc/jsonrpc_server.go b/rpc/jsonrpc_server.go index ae5eb97..8e7a6b2 100644 --- a/rpc/jsonrpc_server.go +++ b/rpc/jsonrpc_server.go @@ -436,6 +436,61 @@ func (j *JSONRPCServer) GetValidRollupsAtEpoch(_ *http.Request, args *types.GetR return nil } +func (j *JSONRPCServer) GetHighestSettledToBNonce(req *http.Request, _ *struct{}, reply *types.GetHighestSettledToBNonceReply) error { + ctx, span := j.c.Tracer().Start(req.Context(), "Server.GetHighestSettledToBNonce") + defer span.End() + tobNonce, err := j.c.GetHighestSettledToBNonceFromState(ctx) + if err != nil { + return err + } + reply.ToBNonce = tobNonce + return nil +} + +func (j *JSONRPCServer) GetCertByChunkID(req *http.Request, args *types.GetCertByChunkIDArgs, reply *types.GetCertByChunkIDReply) error { + ctx, span := j.c.Tracer().Start(req.Context(), "Server.GetCertByChunkID") + defer span.End() + cert, err := j.c.GetCertByChunkIDFromState(ctx, args.ChunkID) + if err != nil { + return err + } + reply.Cert = cert + return nil +} + +func (j *JSONRPCServer) GetCertByChainInfo(req *http.Request, args *types.GetCertByChainInfoArgs, reply *types.GetCertByChainInfoReply) error { + ctx, span := j.c.Tracer().Start(req.Context(), "Server.GetCertByChainInfo") + defer span.End() + cert, err := j.c.GetCertByChainInfoFromState(ctx, args.ChainID, args.BlockNumber) + if err != nil { + return err + } + reply.Cert = cert + return nil +} + +func (j *JSONRPCServer) GetCertsByToBNonce(req *http.Request, args *types.GetCertsByToBNonceArgs, reply *types.GetCertsByToBNonceReply) error { + ctx, span := j.c.Tracer().Start(req.Context(), "Server.GetCertsByToBNonce") + defer span.End() + certs, err := j.c.GetCertsByToBNonceFromState(ctx, args.ToBNonce) + if err != nil { + return err + } + reply.Certs = certs + return nil +} + +func (j *JSONRPCServer) GetEpochLowestToBNonce(req *http.Request, args *types.GetLowestToBNonceAtEpochArgs, reply *types.GetLowestToBNonceAtEpochReply) error { + ctx, span := j.c.Tracer().Start(req.Context(), "Server.GetEpochLowestToBNonce") + defer span.End() + tobNonce, err := j.c.GetLowestToBNonceAtEpochFromState(ctx, args.Epoch) + if err != nil { + return err + } + reply.ToBNonce = tobNonce + return nil +} + var _ chain.Parser = (*ServerParser)(nil) type ServerParser struct { diff --git a/scripts/run.sh b/scripts/run.sh index b9b4641..3181741 100755 --- a/scripts/run.sh +++ b/scripts/run.sh @@ -202,7 +202,7 @@ cat < "${TMPDIR}"/seqvm.config }, "valRPCConfig": { "derivePort": true, - "port": 0 + "port": 9652 } } EOF diff --git a/storage/arcadia.go b/storage/arcadia.go index 854f5d6..6f696d5 100644 --- a/storage/arcadia.go +++ b/storage/arcadia.go @@ -22,10 +22,7 @@ func StoreArcadiaBidInfo( bidderSignature []byte, // 96 bytes ) error { k := ArcadiaBidKey(epoch) - v := make([]byte, 8+48+96) - binary.BigEndian.PutUint64(v[:8], bidPrice) - copy(v[8:], bidderPublicKey) - copy(v[8+48:], bidderSignature) + v := hactions.PackArcadiaAuctionWinner(bidPrice, bidderPublicKey, bidderSignature) return mu.Insert(ctx, k, v) } diff --git a/storage/da_cert.go b/storage/da_cert.go new file mode 100644 index 0000000..a9e1241 --- /dev/null +++ b/storage/da_cert.go @@ -0,0 +1,305 @@ +package storage + +import ( + "context" + "encoding/binary" + "errors" + "slices" + + "github.com/AnomalyFi/hypersdk/codec" + "github.com/AnomalyFi/hypersdk/consts" + "github.com/AnomalyFi/hypersdk/state" + "github.com/AnomalyFi/nodekit-seq/types" + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/ids" +) + +// for highest stored DACert ToBNonce +func DACertToBNonceKey() []byte { + k := make([]byte, 1+consts.Uint16Len) + k[0] = DACertToBNoncePrefix + binary.BigEndian.PutUint16(k[1:], DACertficateToBNonceChunks) + return k +} + +func SetDACertToBNonce( + ctx context.Context, + mu state.Mutable, + tobNonce uint64, +) error { + k := DACertToBNonceKey() + return setBalance(ctx, mu, k, tobNonce) +} + +func GetDACertToBNonce( + ctx context.Context, + im state.Immutable, +) (uint64, error) { + k := DACertToBNonceKey() + nonce, _, err := innerGetBalance(im.GetValue(ctx, k)) + return nonce, err +} + +// Used to serve RPC queries +func GetDAToBNonceFromState( + ctx context.Context, + f ReadState, +) (uint64, error) { + k := DACertToBNonceKey() + values, errs := f(ctx, [][]byte{k}) + nonce, _, err := innerGetBalance(values[0], errs[0]) + return nonce, err +} + +// for mapping epoch to the lowest tobnonce in that epoch, this can give a rough range of chunks +// for rollups/sidecar to traverse through at derivation pipeline +// for highest stored DACert ToBNonce +func ToBNonceEpochKey(epoch uint64) []byte { + k := make([]byte, 1+consts.Uint64Len+consts.Uint16Len) + k[0] = EpochToBNoncePrefix + binary.BigEndian.PutUint64(k[1:], epoch) + binary.BigEndian.PutUint16(k[1+consts.Uint64Len:], EpochToBNonceChunks) + return k +} + +func SetToBNonceAtEpoch( + ctx context.Context, + mu state.Mutable, + epoch uint64, + tobNonce uint64, +) error { + k := ToBNonceEpochKey(epoch) + _, exists, err := innerGetBalance(mu.GetValue(ctx, k)) + if err != nil { + return err + } + if exists { + return nil + } + return setBalance(ctx, mu, k, tobNonce) +} + +func GetToBNonceAtEpoch( + ctx context.Context, + im state.Immutable, + epoch uint64, +) (uint64, error) { + k := ToBNonceEpochKey(epoch) + nonce, _, err := innerGetBalance(im.GetValue(ctx, k)) + return nonce, err +} + +// Used to serve RPC queries +func GetToBNonceAtEpochFromState( + ctx context.Context, + f ReadState, + epoch uint64, +) (uint64, error) { + k := ToBNonceEpochKey(epoch) + values, errs := f(ctx, [][]byte{k}) + nonce, exists, err := innerGetBalance(values[0], errs[0]) + if !exists { + return 0, ErrToBNonceNotExistsForEpoch + } + return nonce, err +} + +// for cert indexes +func DACertIndexKey(tobNonce uint64) []byte { + k := make([]byte, 1+consts.Uint64Len+consts.Uint16Len) + k[0] = DACertIndexPrefix + binary.BigEndian.PutUint64(k[1:], tobNonce) + binary.BigEndian.PutUint16(k[1+consts.Uint64Len:], DACertficateIndexChunks) + return k +} + +func AddDACertToLayer( + ctx context.Context, + mu state.Mutable, + tobNonce uint64, + chunkID ids.ID, +) error { + k := DACertIndexKey(tobNonce) + var chunkIDs []ids.ID + value, err := mu.GetValue(ctx, k) + switch err { + case database.ErrNotFound: + chunkIDs = make([]ids.ID, 0) + case nil: + chunkIDs, err = UnpackIDs(value) + if err != nil { + return err + } + default: + return err + } + + if slices.ContainsFunc(chunkIDs, func(c ids.ID) bool { + return chunkID.Compare(c) == 0 + }) { + return ErrCertExists + } + + chunkIDs = append(chunkIDs, chunkID) + raw, err := PackIDs(chunkIDs) + if err != nil { + return err + } + return mu.Insert(ctx, k, raw) +} + +func GetDACertChunkIDs( + ctx context.Context, + im state.Immutable, + tobNonce uint64, +) ([]ids.ID, error) { + k := DACertIndexKey(tobNonce) + value, err := im.GetValue(ctx, k) + if errors.Is(err, database.ErrNotFound) { + return nil, nil + } else if err != nil { + return nil, err + } + return UnpackIDs(value) +} + +// Used to serve RPC queries +func GetDACertChunkIDsFromState( + ctx context.Context, + f ReadState, + tobNonce uint64, +) ([]ids.ID, error) { + k := DACertIndexKey(tobNonce) + values, errs := f(ctx, [][]byte{k}) + err := errs[0] + if errors.Is(err, database.ErrNotFound) { + return nil, nil + } else if err != nil { + return nil, err + } + return UnpackIDs(values[0]) +} + +// for individual cert +func DACertByChunkIDKey(chunkID ids.ID) []byte { + k := make([]byte, 1+ids.IDLen+consts.Uint16Len) + k[0] = DACertPrefix + copy(k[1:1+ids.IDLen], chunkID[:]) + binary.BigEndian.PutUint16(k[1+ids.IDLen:], DACertificateChunks) + return k +} + +func GetDACertByChunkID( + ctx context.Context, + im state.Immutable, + chunkID ids.ID, +) (*types.DACertInfo, error) { + k := DACertByChunkIDKey(chunkID) + value, err := im.GetValue(ctx, k) + if err != nil { + return nil, err + } + + p := codec.NewReader(value, types.CertInfoSizeLimit) + return types.UnmarshalCertInfo(p) +} + +// Used to serve RPC queries +func GetDACertByChunkIDFromState( + ctx context.Context, + f ReadState, + chunkID ids.ID, +) (*types.DACertInfo, error) { + k := DACertByChunkIDKey(chunkID) + values, errs := f(ctx, [][]byte{k}) + if errors.Is(errs[0], database.ErrNotFound) { + return nil, types.ErrCertNotExists + } + if errs[0] != nil { + return nil, errs[0] + } + p := codec.NewReader(values[0], types.CertInfoSizeLimit) + return types.UnmarshalCertInfo(p) +} + +func SetDACert( + ctx context.Context, + mu state.Mutable, + chunkID ids.ID, + cert *types.DACertInfo, +) error { + k := DACertByChunkIDKey(chunkID) + p := codec.NewWriter(128, types.CertInfoSizeLimit) + cert.Marshal(p) + if p.Err() != nil { + return p.Err() + } + raw := p.Bytes() + return mu.Insert(ctx, k, raw) +} + +// for mapping to chunkID +func DACertChunkIDKey(chainID string, blockNumber uint64) []byte { + k := make([]byte, 1+len(chainID)+consts.Uint64Len+consts.Uint16Len) + k[0] = DACertChunkIDPrefix + copy(k[1:], []byte(chainID)) + binary.BigEndian.PutUint64(k[1+len(chainID):], blockNumber) + binary.BigEndian.PutUint16(k[1+len(chainID)+consts.Uint64Len:], DACertificateChunkIDChunks) + return k +} + +func GetDACertChunkID( + ctx context.Context, + im state.Immutable, + chainID string, + blockNumber uint64, +) (ids.ID, error) { + ret := ids.ID{} + k := DACertChunkIDKey(chainID, blockNumber) + value, err := im.GetValue(ctx, k) + if err != nil { + return ids.Empty, err + } + + p := codec.NewReader(value, ids.IDLen) + p.UnpackID(true, &ret) + return ret, p.Err() +} + +// Used to serve RPC queries +func GetDACertChunkIDFromState( + ctx context.Context, + f ReadState, + chainID string, + blockNumber uint64, +) (ids.ID, error) { + ret := ids.ID{} + k := DACertChunkIDKey(chainID, blockNumber) + values, errs := f(ctx, [][]byte{k}) + if errors.Is(errs[0], database.ErrNotFound) { + return ids.Empty, nil + } + if errs[0] != nil { + return ids.Empty, errs[0] + } + p := codec.NewReader(values[0], ids.IDLen) + p.UnpackID(true, &ret) + return ret, p.Err() +} + +func SetDACertChunkID( + ctx context.Context, + mu state.Mutable, + chainID string, + blockNumber uint64, + chunkID ids.ID, +) error { + k := DACertChunkIDKey(chainID, blockNumber) + p := codec.NewWriter(ids.IDLen, ids.IDLen) + p.PackID(chunkID) + if err := p.Err(); err != nil { + return err + } + raw := p.Bytes() + return mu.Insert(ctx, k, raw) +} diff --git a/storage/errors.go b/storage/errors.go index 506103e..b4d6b62 100644 --- a/storage/errors.go +++ b/storage/errors.go @@ -5,4 +5,9 @@ package storage import "errors" -var ErrInvalidBalance = errors.New("invalid balance") +var ( + ErrInvalidBalance = errors.New("invalid balance") + ErrCertExists = errors.New("cert exists for chunk layer") + ErrLowestToBNonceExistsForEpoch = errors.New("lowest tob nonce exists for epoch") + ErrToBNonceNotExistsForEpoch = errors.New("lowest tob nonce not exists for epoch") +) diff --git a/storage/rollup_info.go b/storage/rollup_info.go index e849acd..f3b587a 100644 --- a/storage/rollup_info.go +++ b/storage/rollup_info.go @@ -3,7 +3,6 @@ package storage import ( "context" "errors" - "fmt" hactions "github.com/AnomalyFi/hypersdk/actions" "github.com/AnomalyFi/hypersdk/codec" @@ -69,7 +68,6 @@ func SetRollupInfo( return err } infoBytes := p.Bytes() - fmt.Printf("key: %s, value: %s\n", string(k), string(infoBytes)) return mu.Insert(ctx, k, infoBytes) } diff --git a/storage/storage.go b/storage/storage.go index 4e75634..957cfba 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -21,16 +21,27 @@ const ( txPrefix = 0x0 // stateDB - balancePrefix = 0x0 - heightPrefix = 0x4 - timestampPrefix = 0x5 - feePrefix = 0x6 - blockPrefix = 0x9 - feeMarketPrefix = 0xa + balancePrefix = 0x0 + heightPrefix = 0x4 + timestampPrefix = 0x5 + feePrefix = 0x6 + blockPrefix = 0x9 + feeMarketPrefix = 0xa + DACertToBNoncePrefix = 0xb0 + EpochToBNoncePrefix = 0xb1 + DACertIndexPrefix = 0xb2 + DACertPrefix = 0xb3 + DACertChunkIDPrefix = 0xb4 ) +// 64 bytes per chunk, this constrict the value size for the corresponding keys const ( - BalanceChunks uint16 = 1 + BalanceChunks uint16 = 1 + DACertficateToBNonceChunks uint16 = 1 + EpochToBNonceChunks uint16 = 1 + DACertficateIndexChunks uint16 = 64 // 4KiB + DACertificateChunks uint16 = 64 // 4KiB + DACertificateChunkIDChunks uint16 = 1 ) var ( diff --git a/storage/utils.go b/storage/utils.go index 714b330..79642db 100644 --- a/storage/utils.go +++ b/storage/utils.go @@ -4,6 +4,7 @@ import ( hactions "github.com/AnomalyFi/hypersdk/actions" "github.com/AnomalyFi/hypersdk/codec" "github.com/AnomalyFi/hypersdk/consts" + "github.com/ava-labs/avalanchego/ids" ) func PackNamespaces(namespaces [][]byte) ([]byte, error) { @@ -34,3 +35,28 @@ func UnpackEpochs(raw []byte) ([]uint64, error) { return epochs, p.Err() } + +func PackIDs(ids []ids.ID) ([]byte, error) { + p := codec.NewWriter(1024, consts.NetworkSizeLimit) + p.PackInt(len(ids)) + for _, id := range ids { + p.PackID(id) + } + + return p.Bytes(), p.Err() +} + +func UnpackIDs(raw []byte) ([]ids.ID, error) { + p := codec.NewReader(raw, consts.NetworkSizeLimit) + numIDs := p.UnpackInt(false) + ret := make([]ids.ID, 0, numIDs) + for i := 0; i < numIDs; i++ { + id := ids.ID{} + p.UnpackID(true, &id) + if err := p.Err(); err != nil { + return nil, err + } + ret = append(ret, id) + } + return ret, nil +} diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go index 9b0f682..bd831b0 100644 --- a/tests/e2e/e2e_test.go +++ b/tests/e2e/e2e_test.go @@ -1310,53 +1310,13 @@ var _ = ginkgo.Describe("[Test]", func() { err = instances[0].wsCli.RegisterTx(tx) require.NoError(err) txID, txErr, result, err := instances[0].wsCli.ListenTx(ctx) - require.Equal(tx.ID(), txID) require.NoError(err) + require.Equal(tx.ID(), txID) require.NoError(txErr) require.False(result.Success) require.Contains(string(result.Error), "not authorized") }) - // with previous changes focused on devops ease, this test is irrelevant now. - // ginkgo.It("test already existing namespace in rollup registration", func() { - // ctx := context.Background() - // ctx, cancel := context.WithTimeout(ctx, 20*time.Second) - // defer cancel() - // tpriv, err := bls.GeneratePrivateKey() - // require.NoError(err) - - // pubKey := bls.PublicFromPrivateKey(tpriv) - // seqAddress := auth.NewBLSAddress(pubKey) - - // currEpoch, err := instances[0].cli.GetCurrentEpoch() - // require.NoError(err) - // txActions := []chain.Action{&actions.RollupRegistration{ - // Info: hactions.RollupInfo{ - // Namespace: []byte("nkit"), - // FeeRecipient: seqAddress, - // AuthoritySEQAddress: seqAddress, - // SequencerPublicKey: pubKeyDummy, - // StartEpoch: currEpoch + 5, - // }, - // Namespace: []byte("nkit"), - // OpCode: actions.CreateRollup, - // }} - // parser, err := instances[0].tcli.Parser(ctx) - // require.NoError(err) - // _, tx, _, err := instances[0].cli.GenerateTransaction(ctx, parser, txActions, factory, 0) - // require.NoError(err) - // hutils.Outf("{{green}}txID of submitted data:{{/}}%s\n", tx.ID().String()) - - // err = instances[0].wsCli.RegisterTx(tx) - // require.NoError(err) - // txID, txErr, result, err := instances[0].wsCli.ListenTx(ctx) - // require.Equal(tx.ID(), txID) - // require.NoError(err) - // require.NoError(txErr) - // require.False(result.Success) - // require.Contains(string(result.Error), "namespace already registered") - // }) - var currEpoch uint64 ginkgo.It("test create epoch exit for rollups", func() { @@ -1462,6 +1422,58 @@ var _ = ginkgo.Describe("[Test]", func() { require.False(result.Success) require.Contains(string(result.Error), "namespace is not registered") }) + + ginkgo.It("test da cert can be submitted & storage methods are correct", func() { + ctx := context.Background() + ctx, cancel := context.WithTimeout(ctx, 20*time.Second) + defer cancel() + cert := &types.DACertInfo{ + DAType: actions.CelestiaDA, + Epoch: 1, + BlockNumber: 100, + ChainID: "0xb096", + ToBNonce: 10, + ChunkID: ids.GenerateTestID(), + Cert: []byte("someblobid"), + } + txActions := []chain.Action{ + &actions.DACertificate{ + Cert: cert, + }, + &actions.SetSettledToBNonce{ + ToBNonce: cert.ToBNonce, + }, + } + parser, err := instances[0].tcli.Parser(ctx) + require.NoError(err) + _, tx, _, err := instances[0].cli.GenerateTransaction(ctx, parser, txActions, factory, 0) + require.NoError(err) + hutils.Outf("{{green}}txID of submitted DA cert:{{/}}%s\n", tx.ID().String()) + err = instances[0].wsCli.RegisterTx(tx) + require.NoError(err) + txID, txErr, result, err := instances[0].wsCli.ListenTx(ctx) + require.NoError(err) + require.NoError(txErr) + require.Equal(tx.ID(), txID) + require.Empty(result.Error) + require.True(result.Success) + + // check state by rpc methods + lowestToBNonce, err := instances[0].tcli.GetEpochLowestToBNonce(context.TODO(), cert.Epoch) + require.NoError(err) + require.Equal(cert.ToBNonce, lowestToBNonce) + certByChunkID, err := instances[0].tcli.GetCertByChunkID(context.TODO(), cert.ChunkID) + require.NoError(err) + require.Equal(cert, certByChunkID) + certs, err := instances[0].tcli.GetCertsByToBNonce(context.TODO(), cert.ToBNonce) + require.NoError(err) + require.Equal(1, len(certs)) + require.Equal(cert, certs[0]) + certByChainInfo, err := instances[0].tcli.GetCertByChainInfo(context.TODO(), cert.ChainID, cert.BlockNumber) + require.NoError(err) + require.Equal(cert, certByChainInfo) + }) + switch mode { case modeTest: hutils.Outf("{{yellow}}skipping bootstrap and state sync tests{{/}}\n") diff --git a/types/consts.go b/types/consts.go new file mode 100644 index 0000000..eab9903 --- /dev/null +++ b/types/consts.go @@ -0,0 +1,8 @@ +package types + +import "errors" + +var ( + ErrCertChunkIDNotExists = errors.New("cert chunk id not exists") + ErrCertNotExists = errors.New("cert not exists") +) diff --git a/types/da_cert.go b/types/da_cert.go new file mode 100644 index 0000000..63134c9 --- /dev/null +++ b/types/da_cert.go @@ -0,0 +1,56 @@ +package types + +import ( + "github.com/AnomalyFi/hypersdk/codec" + "github.com/AnomalyFi/hypersdk/consts" + "github.com/ava-labs/avalanchego/ids" +) + +const ( + ToBChainID = "tob" + ToBBlockNumber = 0 + + CertInfoSizeLimit = 2048 +) + +type DACertInfo struct { + DAType uint8 `json:"daType"` + ChainID string `json:"chainID"` + BlockNumber uint64 `json:"blockNumber"` // for tob, this is the tob nonce + Epoch uint64 `json:"epoch"` + ToBNonce uint64 `json:"tobNonce"` + ChunkID ids.ID `json:"chunkID"` + IsPlaceHolder bool `json:"isPlaceHolder"` + Cert []byte `json:"cert"` +} + +func (info *DACertInfo) Size() int { + return consts.ByteLen + codec.StringLen(info.ChainID) + 3*consts.Uint64Len + ids.IDLen + codec.BytesLen(info.Cert) +} + +func (info *DACertInfo) Marshal(p *codec.Packer) { + p.PackByte(info.DAType) + p.PackString(info.ChainID) + p.PackUint64(info.BlockNumber) + p.PackUint64(info.ToBNonce) + p.PackUint64(info.Epoch) + p.PackID(info.ChunkID) + p.PackBool(info.IsPlaceHolder) + p.PackBytes(info.Cert) +} + +func UnmarshalCertInfo(p *codec.Packer) (*DACertInfo, error) { + ret := new(DACertInfo) + ret.DAType = p.UnpackByte() + ret.ChainID = p.UnpackString(true) + ret.BlockNumber = p.UnpackUint64(false) + ret.ToBNonce = p.UnpackUint64(false) + ret.Epoch = p.UnpackUint64(false) + p.UnpackID(false, &ret.ChunkID) + ret.IsPlaceHolder = p.UnpackBool() + p.UnpackBytes(CertInfoSizeLimit, !ret.IsPlaceHolder, &ret.Cert) + if err := p.Err(); err != nil { + return nil, err + } + return ret, nil +} diff --git a/types/types.go b/types/types.go index 42ebabd..cd6a463 100644 --- a/types/types.go +++ b/types/types.go @@ -255,3 +255,40 @@ type GetRollupsInfoAtEpochArgs struct { type GetRollupsInfoAtEpochReply struct { Rollups []*hactions.RollupInfo `json:"rollups"` } + +type GetHighestSettledToBNonceReply struct { + ToBNonce uint64 `json:"tobNonce"` +} + +type GetCertByChunkIDArgs struct { + ChunkID ids.ID `json:"chunkID"` +} + +type GetCertByChunkIDReply struct { + Cert *DACertInfo `json:"cert"` +} + +type GetCertByChainInfoArgs struct { + ChainID string `json:"chainID"` + BlockNumber uint64 `json:"blockNumber"` +} + +type GetCertByChainInfoReply struct { + Cert *DACertInfo `json:"cert"` +} + +type GetCertsByToBNonceArgs struct { + ToBNonce uint64 `json:"tobNonce"` +} + +type GetCertsByToBNonceReply struct { + Certs []*DACertInfo `json:"certs"` +} + +type GetLowestToBNonceAtEpochArgs struct { + Epoch uint64 `json:"epoch"` +} + +type GetLowestToBNonceAtEpochReply struct { + ToBNonce uint64 `json:"tobNonce"` +}