From 3ad2fc5c4af8bc236dc30512c5e9dab2ce3cf584 Mon Sep 17 00:00:00 2001 From: vyzo Date: Fri, 10 Jun 2022 14:47:19 +0300 Subject: [PATCH] feat: FVM Debug Dual Execution --- chain/gen/genesis/genesis.go | 2 +- chain/vm/fvm.go | 271 +++++++++++++++++++++++++++++++++++ chain/vm/vmi.go | 9 +- extern/filecoin-ffi | 2 +- node/bundle/util.go | 52 +++++++ 5 files changed, 332 insertions(+), 4 deletions(-) create mode 100644 node/bundle/util.go diff --git a/chain/gen/genesis/genesis.go b/chain/gen/genesis/genesis.go index 6304fb7f8..302770bd8 100644 --- a/chain/gen/genesis/genesis.go +++ b/chain/gen/genesis/genesis.go @@ -487,7 +487,7 @@ func VerifyPreSealedData(ctx context.Context, cs *store.ChainStore, sys vm.Sysca } vm, err := vm.NewVM(ctx, &vmopt) if err != nil { - return cid.Undef, xerrors.Errorf("failed to create NewLegacyVM: %w", err) + return cid.Undef, xerrors.Errorf("failed to create VM: %w", err) } for mi, m := range template.Miners { diff --git a/chain/vm/fvm.go b/chain/vm/fvm.go index 81b561f2e..f6a7a2b7f 100644 --- a/chain/vm/fvm.go +++ b/chain/vm/fvm.go @@ -4,9 +4,15 @@ import ( "bytes" "context" "fmt" + "io" "os" + "sort" + "sync" "time" + builtin7 "github.com/filecoin-project/specs-actors/v7/actors/builtin" + cbg "github.com/whyrusleeping/cbor-gen" + "github.com/ipfs/go-cid" cbor "github.com/ipfs/go-ipld-cbor" "golang.org/x/xerrors" @@ -16,6 +22,7 @@ import ( "github.com/filecoin-project/go-address" "github.com/filecoin-project/go-state-types/abi" "github.com/filecoin-project/go-state-types/exitcode" + "github.com/filecoin-project/go-state-types/manifest" "github.com/filecoin-project/lotus/blockstore" "github.com/filecoin-project/lotus/build" @@ -27,6 +34,7 @@ import ( "github.com/filecoin-project/lotus/chain/state" "github.com/filecoin-project/lotus/chain/types" "github.com/filecoin-project/lotus/lib/sigs" + "github.com/filecoin-project/lotus/node/bundle" ) var _ Interface = (*FVM)(nil) @@ -303,6 +311,162 @@ func NewFVM(ctx context.Context, opts *VMOpts) (*FVM, error) { }, nil } +func NewDebugFVM(ctx context.Context, opts *VMOpts) (*FVM, error) { + state, err := state.LoadStateTree(cbor.NewCborStore(opts.Bstore), opts.StateBase) + if err != nil { + return nil, err + } + + circToReport, err := opts.CircSupplyCalc(ctx, opts.Epoch, state) + if err != nil { + return nil, err + } + + baseBstore := opts.Bstore + overlayBstore := blockstore.NewMemorySync() + vmBstore := blockstore.NewTieredBstore(overlayBstore, baseBstore) + + fvmopts := &ffi.FVMOpts{ + FVMVersion: 0, + Externs: &FvmExtern{ + Rand: opts.Rand, + Blockstore: vmBstore, + lbState: opts.LookbackState, + base: opts.StateBase, + epoch: opts.Epoch, + }, + Epoch: opts.Epoch, + BaseFee: opts.BaseFee, + BaseCircSupply: circToReport, + NetworkVersion: opts.NetworkVersion, + StateBase: opts.StateBase, + Tracing: EnableDetailedTracing, + Debug: true, + } + + loadBundle := func(path string) (cid.Cid, error) { + return bundle.LoadBundleData(ctx, path, overlayBstore) + } + + loadManifest := func(mfCid cid.Cid) (manifest.ManifestData, error) { + return bundle.LoadManifestData(ctx, mfCid, overlayBstore) + } + + putMapping := func(ar map[cid.Cid]cid.Cid) (cid.Cid, error) { + var mapping xMapping + + mapping.redirects = make([]xRedirect, 0, len(ar)) + for from, to := range ar { + mapping.redirects = append(mapping.redirects, xRedirect{from: from, to: to}) + } + sort.Slice(mapping.redirects, func(i, j int) bool { + return bytes.Compare(mapping.redirects[i].from.Bytes(), mapping.redirects[j].from.Bytes()) < 0 + }) + + // Passing this as a pointer of structs has proven to be an enormous PiTA; hence this code. + cborStore := cbor.NewCborStore(overlayBstore) + mappingCid, err := cborStore.Put(context.TODO(), &mapping) + if err != nil { + return cid.Undef, err + } + + return mappingCid, nil + } + + av, err := actors.VersionForNetwork(opts.NetworkVersion) + if err != nil { + return nil, xerrors.Errorf("error determining actors version for network version %d: %w", opts.NetworkVersion, err) + } + + switch av { + case actors.Version7: + if bundle := os.Getenv("LOTUS_FVM_DEBUG_BUNDLE_V7"); bundle != "" { + mfCid, err := loadBundle(bundle) + if err != nil { + return nil, err + } + + mf, err := loadManifest(mfCid) + if err != nil { + return nil, err + } + + // create actor redirect mapping from the synthetic Cid to the debug code + actorRedirect := make(map[cid.Cid]cid.Cid) + fromMap := map[string]cid.Cid{ + "init": builtin7.InitActorCodeID, + "cron": builtin7.CronActorCodeID, + "account": builtin7.AccountActorCodeID, + "storagepower": builtin7.StoragePowerActorCodeID, + "storageminer": builtin7.StorageMinerActorCodeID, + "paymentchannel": builtin7.PaymentChannelActorCodeID, + "multisig": builtin7.MultisigActorCodeID, + "reward": builtin7.RewardActorCodeID, + "verifiedregistry": builtin7.VerifiedRegistryActorCodeID, + } + + for _, e := range mf.Entries { + from, ok := fromMap[e.Name] + if !ok { + return nil, xerrors.Errorf("error mapping %s for debug redirect: %w", e.Name, err) + } + actorRedirect[from] = e.Code + } + + if len(actorRedirect) > 0 { + mappingCid, err := putMapping(actorRedirect) + if err != nil { + return nil, xerrors.Errorf("error writing redirect mapping: %w", err) + } + fvmopts.ActorRedirect = mappingCid + } + } + + case actors.Version8: + if bundle := os.Getenv("LOTUS_FVM_DEBUG_BUNDLE_V8"); bundle != "" { + mfCid, err := loadBundle(bundle) + if err != nil { + return nil, err + } + + mf, err := loadManifest(mfCid) + if err != nil { + return nil, err + } + + // create actor redirect mapping + actorRedirect := make(map[cid.Cid]cid.Cid) + for _, e := range mf.Entries { + from, ok := actors.GetActorCodeID(actors.Version8, e.Name) + if !ok { + log.Warnf("unknown actor %s", e.Name) + continue + } + + actorRedirect[from] = e.Code + } + + if len(actorRedirect) > 0 { + mappingCid, err := putMapping(actorRedirect) + if err != nil { + return nil, xerrors.Errorf("error writing redirect mapping: %w", err) + } + fvmopts.ActorRedirect = mappingCid + } + } + } + + fvm, err := ffi.CreateFVM(fvmopts) + + if err != nil { + return nil, err + } + + return &FVM{ + fvm: fvm, + }, nil +} + func (vm *FVM) ApplyMessage(ctx context.Context, cmsg types.ChainMsg) (*ApplyRet, error) { start := build.Clock.Now() vmMsg := cmsg.VMMessage() @@ -427,3 +591,110 @@ func (vm *FVM) ApplyImplicitMessage(ctx context.Context, cmsg *types.Message) (* func (vm *FVM) Flush(ctx context.Context) (cid.Cid, error) { return vm.fvm.Flush() } + +type dualExecutionFVM struct { + main *FVM + debug *FVM +} + +var _ Interface = (*dualExecutionFVM)(nil) + +func NewDualExecutionFVM(ctx context.Context, opts *VMOpts) (Interface, error) { + main, err := NewFVM(ctx, opts) + if err != nil { + return nil, err + } + + debug, err := NewDebugFVM(ctx, opts) + if err != nil { + return nil, err + } + + return &dualExecutionFVM{ + main: main, + debug: debug, + }, nil +} + +func (vm *dualExecutionFVM) ApplyMessage(ctx context.Context, cmsg types.ChainMsg) (ret *ApplyRet, err error) { + var wg sync.WaitGroup + + wg.Add(2) + + go func() { + defer wg.Done() + ret, err = vm.main.ApplyMessage(ctx, cmsg) + }() + + go func() { + defer wg.Done() + if _, err := vm.debug.ApplyMessage(ctx, cmsg); err != nil { + log.Errorf("debug execution failed: %w", err) + } + }() + + wg.Wait() + return ret, err +} + +func (vm *dualExecutionFVM) ApplyImplicitMessage(ctx context.Context, msg *types.Message) (ret *ApplyRet, err error) { + var wg sync.WaitGroup + + wg.Add(2) + + go func() { + defer wg.Done() + ret, err = vm.main.ApplyImplicitMessage(ctx, msg) + }() + + go func() { + defer wg.Done() + if _, err := vm.debug.ApplyImplicitMessage(ctx, msg); err != nil { + log.Errorf("debug execution failed: %s", err) + } + }() + + wg.Wait() + return ret, err +} + +func (vm *dualExecutionFVM) Flush(ctx context.Context) (cid.Cid, error) { + return vm.main.Flush(ctx) +} + +// Passing this as a pointer of structs has proven to be an enormous PiTA; hence this code. +type xRedirect struct{ from, to cid.Cid } +type xMapping struct{ redirects []xRedirect } + +func (m *xMapping) MarshalCBOR(w io.Writer) error { + scratch := make([]byte, 9) + if err := cbg.WriteMajorTypeHeaderBuf(scratch, w, cbg.MajArray, uint64(len(m.redirects))); err != nil { + return err + } + + for _, v := range m.redirects { + if err := v.MarshalCBOR(w); err != nil { + return err + } + } + + return nil +} + +func (r *xRedirect) MarshalCBOR(w io.Writer) error { + scratch := make([]byte, 9) + + if err := cbg.WriteMajorTypeHeaderBuf(scratch, w, cbg.MajArray, uint64(2)); err != nil { + return err + } + + if err := cbg.WriteCidBuf(scratch, w, r.from); err != nil { + return xerrors.Errorf("failed to write cid field from: %w", err) + } + + if err := cbg.WriteCidBuf(scratch, w, r.to); err != nil { + return xerrors.Errorf("failed to write cid field from: %w", err) + } + + return nil +} diff --git a/chain/vm/vmi.go b/chain/vm/vmi.go index 9b7faf2b1..925def68c 100644 --- a/chain/vm/vmi.go +++ b/chain/vm/vmi.go @@ -4,9 +4,8 @@ import ( "context" "os" - "github.com/ipfs/go-cid" - "github.com/filecoin-project/go-state-types/network" + cid "github.com/ipfs/go-cid" "github.com/filecoin-project/lotus/chain/types" ) @@ -25,11 +24,17 @@ var useFvmForMainnetV15 = os.Getenv("LOTUS_USE_FVM_TO_SYNC_MAINNET_V15") == "1" func NewVM(ctx context.Context, opts *VMOpts) (Interface, error) { if opts.NetworkVersion >= network.Version16 { + if os.Getenv("LOTUS_FVM_DEBUG") == "1" { + return NewDualExecutionFVM(ctx, opts) + } return NewFVM(ctx, opts) } // Remove after v16 upgrade, this is only to support testing and validation of the FVM if useFvmForMainnetV15 && opts.NetworkVersion >= network.Version15 { + if os.Getenv("LOTUS_FVM_DEBUG") == "1" { + return NewDualExecutionFVM(ctx, opts) + } return NewFVM(ctx, opts) } diff --git a/extern/filecoin-ffi b/extern/filecoin-ffi index 943e33574..e87bffeaf 160000 --- a/extern/filecoin-ffi +++ b/extern/filecoin-ffi @@ -1 +1 @@ -Subproject commit 943e33574dcacd940edff0cf414c82e656bdaeb3 +Subproject commit e87bffeaf690c18d656d636bdab46f0ffa90593b diff --git a/node/bundle/util.go b/node/bundle/util.go new file mode 100644 index 000000000..b13d3125f --- /dev/null +++ b/node/bundle/util.go @@ -0,0 +1,52 @@ +package bundle + +import ( + "context" + "os" + + "golang.org/x/xerrors" + + "github.com/filecoin-project/go-state-types/manifest" + "github.com/filecoin-project/lotus/blockstore" + "github.com/filecoin-project/lotus/chain/actors/adt" + + "github.com/ipfs/go-cid" + cbor "github.com/ipfs/go-ipld-cbor" + car "github.com/ipld/go-car" +) + +func LoadBundleData(ctx context.Context, bundle string, bs blockstore.Blockstore) (cid.Cid, error) { + f, err := os.Open(bundle) + if err != nil { + return cid.Undef, xerrors.Errorf("error opening debug bundle: %w", err) + } + defer f.Close() //nolint + + hdr, err := car.LoadCar(ctx, bs, f) + if err != nil { + return cid.Undef, xerrors.Errorf("error loading debug bundle: %w", err) + } + + return hdr.Roots[0], nil +} + +func LoadManifestData(ctx context.Context, mfCid cid.Cid, bs blockstore.Blockstore) (manifest.ManifestData, error) { + adtStore := adt.WrapStore(ctx, cbor.NewCborStore(bs)) + + var mf manifest.Manifest + var mfData manifest.ManifestData + + if err := adtStore.Get(ctx, mfCid, &mf); err != nil { + return mfData, xerrors.Errorf("error reading debug manifest: %w", err) + } + + if err := mf.Load(ctx, adtStore); err != nil { + return mfData, xerrors.Errorf("error loading debug manifest: %w", err) + } + + if err := adtStore.Get(ctx, mf.Data, &mfData); err != nil { + return mfData, xerrors.Errorf("error fetching manifest data: %w", err) + } + + return mfData, nil +}